Skip to content

Part 8.5 — Backend Architecture: Controllers vs “Resolvers” in NestJS

Site Console Site Console
3 min read Updated Feb 9, 2026 Backend Development 0 comments

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:

  1. Controller

    • parses input

    • validates DTOs

    • defines intent

  2. Service

    • contains business logic

    • coordinates data access

  3. 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:

  • UsersController

  • PostsController

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

Leave a comment

Sign in to leave a comment.

Comments