Menu
Whitepaper
Book a demo
There are two kinds of Node.js backend frameworks.

Adonis JS Controller Validator

Scroll for more
Scroll for more

Opinions Are Not a Framework Bug

There are two kinds of Node.js backend frameworks.

The first kind hands you a router and a req object and steps back. Here is the HTTP primitive. Everything else is your problem. Express is the canonical example. Fastify, Hono, many others. Capable, flexible, beloved. The blank canvas.

The second kind hands you a worldview. Not just a router but conventions for validation, ORM, authentication, queuing, configuration. It tells you how to structure controllers, what validators look like, how to handle errors. It has opinions. Strong ones. AdonisJS is in this camp.

We chose AdonisJS. Not despite its opinions, but because of them.

Here is the thing about opinions in a codebase: they are decisions that have already been made. When a framework is opinionated, you do not spend a sprint evaluating validation libraries. You do not spend a month debating error handling patterns. You do not hold five discussions about how controllers should be structured. The framework held them for you, years ago, with the benefit of having seen a hundred projects make the wrong call. You inherit the conclusion.

The cost is flexibility. The gain is a codebase that doesn't become a jungle.

We have built on Express. We have built on Fastify. We know what the blank canvas looks like three years into a project with a rotating team. It looks like four different validation approaches living in four different parts of the codebase. It looks like some endpoints that return { error: "..." } and others that return { message: "...", code: 500 } and a third format no one can explain. It looks like the senior developer's approach in one module and the new hire's approach in another and a Stack Overflow answer in a third. The jungle grows one reasonable decision at a time.

AdonisJS gives you VineJS for validation. Not "here is how you could do validation." Here is how you do validation. request.validateUsing(schema). That's the API. One API.

And this is where it gets interesting.

The Type Pipeline

When you call await request.validateUsing(storeValidator), VineJS does not just check that the request body is valid. It returns a fully typed result. The schema definition on the validator side produces a concrete TypeScript type. data is not any. data is not unknown. data is the exact shape you declared — inferred by the TypeScript compiler from the schema definition.

This is the pipe.

async store({ request }: HttpContext) { const data = await request.validateUsing(storeValidator) // data: { title: string, content: string, published: boolean } // not any. not unknown. the actual shape. const post = await PostService.create(data) return this.successResponse<Post>(post) }

data flows into your service fully typed. Your service returns a typed model. You return successResponse<Post> — the generic parameter is an explicit annotation that the response envelope carries a Post. Your frontend, which shares types, receives this envelope and knows exactly what's inside it.

The entire vertical — from the validator schema definition to the React component rendering the data — is typed. Not approximately typed. Exactly typed. The type information travels through pipes that the framework built, and it arrives on the other side intact.

This is not an accident of AdonisJS. It is a consequence of its opinions. The opinion that validation is done through validateUsing. The opinion that responses go through successResponse<T>. The opinion that errors come from a centralized AppErrors catalog. Each opinion is a pipe fitting. Together they form a pipeline.

But a pipeline is only valuable if it is complete. A single break — one method that skips validateUsing, one response that drops its generic parameter — and the type information leaks. The endpoint returns any. The frontend receives unknown. Someone writes a cast, the cast is wrong, a bug appears that the type system should have caught.

We had nineteen breaks.

The Audit

We reviewed every AdonisJS controller in the backend. Nineteen endpoints where request.body() or request.only() was used in place of validateUsing. Raw user input flowing directly into business logic.

The security implications were obvious — potential paths for injection, mass assignment, parameter pollution. But the type implications were equally bad. Every one of those nineteen methods was a hole in the pipeline. The validated, typed data that VineJS was supposed to produce never materialized. The type information did not flow forward. The service received untyped data. The response typing was guesswork.

We fixed all nineteen. Then we wrote the conventions down: validateUsing for any method that reads the request. successResponse<T> with an explicit generic. AppErrors for error returns. Not preferences — requirements.

The document was thorough. The document was correct. The document would be forgotten.

Making the Pipe Unforgeable

adonis-controller-validator is a TypeScript static analysis tool. It walks the AST of an AdonisJS project using ts-morph, resolves only the controller methods that are actually bound to routes, and checks three things.

Does the method use request? Then it must call validateUsing. No validateUsing, no pass. This is where the type pipeline starts. A method that reads raw request data without validation is not just a security vulnerability — it is a type rupture. The inferred type that VineJS would have produced never exists.

Does the method return a success response? Then it must be typed. successResponse(data) fails. successResponse<User[]>(data) passes. Without the generic parameter, the response contract is invisible. The frontend receives any where it should receive a concrete type.

Does the method return an error response? Then it must use AppErrors. Not { status: 400, message: "Bad request" }. Not a plain string. A named constant from a centralized catalog that every consumer of the API can depend on.

Three rules. Three points in the pipeline. The tool exits with code 1 on violations, which means it belongs in CI and it stays there.

What we found when we added it to CI was not the nineteen violations we already knew about. Those were fixed. What we found was that the tool caught three new ones in the first two weeks — methods that would have been fine under the old regime, where "correct" meant "it works when you test it." Under the new regime, "correct" means the type information flows from entry to exit.

The AdonisJS opinions set up the pipeline. The tool makes sure no one punches holes in it.

Why This Matters Beyond Security

The security angle is real. Unvalidated request data is an attack surface. But the deeper value of enforcing this pattern is about what you gain from a typed full-stack.

When every controller method validates with VineJS and types its responses, the frontend is not guessing. It is not reading documentation that may be out of date. It is not trusting that the field user.email will be a string and not null. The types say it. The compiler checks it. The linter enforces the contract on the backend side so the frontend contract can be trusted.

We are a small team. We cannot afford the kind of integration bugs that come from a backend developer changing a response shape and forgetting to update the frontend, or adding a nullable field that was previously never null. We also cannot afford the cognitive overhead of maintaining a separate API documentation layer that someone has to keep synchronized.

The type pipeline eliminates the synchronization problem. When the pipe is complete and enforced, a change to the validator schema propagates through the TypeScript compiler. The frontend breaks at compile time. You fix it before it ships.

That is the actual value of choosing an opinionated framework and then actually following its opinions. Not just clean code. Not just fewer bugs. A codebase where the type system does work that would otherwise require a person.

The opinions are not a limitation. They are infrastructure.

7 min read
by Leo Blondel
Share this post on :
Copy Link
X
Linkedin
Newsletter subscription
Related Papers
Let’s build what’s next, together.
Close