Part 8.5 — Backend Architecture: Controllers vs “Resolvers” in NestJS
Once you understand GraphQL’s mental model, a surprising realization follows:
You may already be building “resolvers” — you’re just calling them controllers.
This post connects the dots between GraphQL resolver architecture and clean NestJS REST architecture, so you can design APIs that scale without rewriting your stack.
1. What a Resolver Really Does
A resolver answers a single question:
“Given this input, how do I produce this piece of data?”
It does not:
own HTTP details
manage persistence directly
care about transport
It coordinates:
validation
authorization
data access
response shaping
That description fits a well-designed NestJS controller method perfectly.
2. The NestJS Request Flow (Clean Version)
In a disciplined NestJS app:
Controller
parses input
validates DTOs
defines intent
Service
contains business logic
coordinates data access
Prisma
talks to PostgreSQL
executes queries
This mirrors GraphQL exactly—just at endpoint granularity instead of field granularity.
3. Controllers as Endpoint-Level Resolvers
Consider this controller:
@Get(':id')
getUser(
@Param('id', ParseIntPipe) id: number,
) {
return this.usersService.findById(id);
}This method:
receives arguments
enforces type boundaries
resolves a response shape
That’s a resolver.
The only difference is scope:
GraphQL resolves fields
REST resolves endpoints
The architectural responsibility is the same.
4. Why REST Resolvers Are Often Cleaner
GraphQL resolvers are:
numerous
field-scoped
harder to trace
REST resolvers (controllers) are:
fewer
endpoint-scoped
easier to debug
easier to cache
For most products, endpoint-level resolution is the sweet spot.
5. Services Are Where Resolver Logic Belongs
Never put logic in controllers.
Bad:
@Get(':id')
async getUser(@Param('id') id: number) {
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) throw new NotFoundException();
return user;
}Good:
@Get(':id')
getUser(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findById(id);
}Services:
are testable
are reusable
mirror GraphQL resolver functions
Controllers stay thin.
6. DTOs Replace GraphQL Types
GraphQL forces you to define response types explicitly.
REST should too.
export class UserResponseDto {
id: number;
name: string;
}Returning DTOs:
stabilizes contracts
decouples database models
documents intent
This is one of the biggest quality upgrades you can make to REST APIs.
7. Input DTOs = Resolver Arguments
GraphQL arguments map cleanly to DTOs:
export class UpdateUserDto {
name: string;
}@Patch(':id')
updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateUserDto,
) {
return this.usersService.update(id, dto);
}Validation here is equivalent to schema-level validation in GraphQL.
8. Authorization Belongs at the Boundary
GraphQL often struggles with field-level auth complexity.
REST has a simpler rule:
authorize at endpoint boundaries
Use:
guards
decorators
role checks
This keeps authorization:
understandable
testable
observable
Field-level auth is rarely worth the cost.
9. Avoid “God Controllers”
Just like “God resolvers,” controllers can grow too large.
Split by:
resource
responsibility
Good:
UsersControllerPostsController
Bad:
AppController
One controller = one resource family.
10. Composition Happens in Services, Not Controllers
If one endpoint needs multiple data sources:
async getDashboard(userId: number) {
const [user, stats] = await Promise.all([
this.usersService.findById(userId),
this.statsService.getForUser(userId),
]);
return { user, stats };
}This is resolver composition—clean, explicit, testable.
Avoid chaining controllers or HTTP calls internally.
11. Debugging: REST Has the Upper Hand
With REST:
every request is inspectable
logs map to URLs
metrics map to endpoints
GraphQL:
hides intent inside queries
complicates observability
For teams without deep tooling, REST remains easier to operate.
12. The Key Architectural Insight
GraphQL didn’t invent resolver architecture.
It formalized it.
NestJS already gives you:
controllers (entry points)
services (resolver logic)
DTOs (types)
Prisma (data source)
You don’t need a new protocol to apply these ideas.
13. What Comes Next
You now understand:
why controllers are resolvers
how DTOs replace GraphQL types
where logic and auth belong
Related
Part 8.4 — Client-Driven Data with REST: Avoiding Over-Fetching
Over-fetching is a client problem as much as a server problem. This post shows how React apps can drive data needs intentionally—without turning REST APIs into guesswork.
Part 8.3 — Designing REST APIs That Feel Like GraphQL
You don’t need GraphQL to give clients control. This post shows how to design REST APIs that feel flexible, intentional, and predictable—without sacrificing debuggability or caching.
Part 8.2 — GraphQL Mental Model: Schemas, Types & Resolvers (Conceptual)
You don’t need GraphQL to benefit from GraphQL thinking. This post explains schemas, types, and resolvers—and how they map directly to clean REST architecture.
Comments