Validation in NestJS works best when DTOs describe transport input, services own business rules, and transformation is deliberate.
From my notebook
Where this came from
I keep returning to this subject because validation bugs often arrive disguised as database or service errors. The fix is usually to make the HTTP boundary honest: query parameters are strings, frontend types are not runtime guarantees, and DTOs should not double as persistence models.
My short checklist
Validation problems in NestJS usually come from mixing three different concerns: transport input, domain rules, and persistence shape. A DTO should describe what the API accepts. It should not become the database model, the domain entity, and the UI contract at the same time.
Query parameters arrive as strings. Route params arrive as strings. JSON bodies can contain unexpected types even when the frontend is typed. TypeScript does not protect runtime boundaries. If transformation is enabled, it should be enabled deliberately and tested with real HTTP-shaped values, not only with already-created class instances.
I prefer DTOs that make required fields, optional fields, nested objects, and arrays obvious. If an endpoint accepts a filter object, the allowed filter fields should be visible in the DTO instead of passed as a loose object. This protects the database layer from accidental broad queries and makes API behavior easier to document.
If page and limit are strings at the controller boundary, converting them deep inside a service makes behavior harder to reason about. The controller layer or a dedicated pipe should normalize transport values before business logic runs. Then services can work with real numbers, dates, booleans, and enums.
Happy-path validation tests are not enough. I test missing fields, wrong types, empty strings, oversized arrays, invalid enum values, and nested object failures. These cases catch the bugs that otherwise show up as database errors, confusing 500 responses, or silent incorrect filters.
Good validation is not about adding decorators everywhere. It is about drawing a clean line between the outside world and the system. Once that line is reliable, the rest of the backend becomes simpler to test and safer to change.