Day#2: Designing a Reliable Backend: Lessons from Early Infrastructure Work

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.

mariusz
mariusz
20 posts
2 followers

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 HttpException cases
  • Database errors (QueryFailedError)
  • Generic Error objects
  • 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 ValidationPipe made input handling predictable and safe
  • Unified AllExceptionsFilter enabled 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.


Opublikowano

w

,

przez

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *