Building new software from scratch means making foundational decisions early — decisions that shape how maintainable, scalable, and consistent the system will be.
In this post, I’ll walk through two technical areas that caused friction in my early backend work — and how I resolved them.
1. Input validation: obvious in theory, messy in practice
I started by wiring up the backend (NestJS + TypeScript + ESM + Turbo monorepo), and one of the first things I stumbled upon was data validation.
I had DTOs defined using class-validator, but validation was inconsistent. Sometimes invalid payloads reached the business logic. Sometimes they didn’t. Sometimes I forgot to add a local ValidationPipe.
The solution was to configure a global ValidationPipe in main.ts:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
This ensures that:
- Only expected properties are accepted (
whitelist) - Unknown fields trigger an error (
forbidNonWhitelisted) - Strings are converted to numbers, dates, etc. as needed (
transform) - All controller inputs are consistently validated
2. Consistent error messages: not optional if you’re building UI too
Next problem: inconsistent error responses. Default NestJS exceptions return different shapes depending on where the error came from. Sometimes it’s a string. Sometimes it’s an array. Sometimes there’s no code to act on.
To fix this, I defined a standard API error format:
{
"statusCode": 400,
"error": "Bad Request",
"message": "Field 'email' is required",
"code": "VALIDATION_EMAIL_REQUIRED",
"details": {
"field": "email",
"reason": "required"
}
}
Then I updated the existing AllExceptionsFilter to enforce this format across:
- All
HttpExceptioncases - Database errors (
QueryFailedError) - Generic
Errorobjects - Any unknown exceptions
I also made sure to register the filter globally in the application’s entry point:
import { AllExceptionsFilter } from '@my-lib/filters/all-exceptions.filter';
app.useGlobalFilters(new AllExceptionsFilter());
Now the frontend(s) — whether human- or machine-facing — always receive predictable and localizable feedback. Better UX, better debuggability.
Summary
What looked like small backend details turned out to be foundational:
- Global
ValidationPipemade input handling predictable and safe - Unified
AllExceptionsFilterenabled consistent error contracts - Registering the filter globally ensured it covered all exceptions, app-wide
These changes also helped prevent bugs and saved time when building early UI integrations — without them, I would’ve spent hours chasing undefined fields and stringified stack traces.
Tomorrow: creating the first real endpoints, wired up to type-safe DTOs and ready for actual users.

Dodaj komentarz