跪拜 Guibai
← Back to the summary

NestJS Controller, Service, Module: The Three Buckets Every Route Hits

This is the third article in the Nest First Steps series.

In the first two articles, we completed project setup and database connection. In this article, we will understand the core design of NestJS—the three-layer architecture.

To understand it from a frontend perspective:

  • Controller is like routing (equivalent to router.js in frontend), defining "which URL corresponds to which handler function"
  • Service is like utility functions (equivalent to utils/ in frontend), encapsulating specific business logic
  • Module is like file organization (equivalent to index.js aggregate exports in frontend), grouping related code together

Once you understand these three layers, you can read the code structure of a NestJS project and know "where to write the code."

1. Three-Layer Architecture Diagram

Let's first use a diagram to clearly see the data flow of the three-layer architecture:

graph LR
    A[&#34;Frontend Request<br>GET /users&#34;] --> B[&#34;Controller Layer<br>app.controller.ts<br>@Get('users')&#34;]
    B --> C[&#34;Service Layer<br>app.service.ts<br>getUserList()&#34;]
    C --> D[&#34;Prisma Layer<br>prisma.service.ts<br>user.findMany()&#34;]
    D --> E[&#34;Database<br>Neon PostgreSQL<br>User Table&#34;]
    E --> F[&#34;Return Data<br>User List JSON&#34;]
    F --> G[&#34;Frontend<br>Render Page&#34;]

    style A fill:#e1f5fe,stroke:#01579b
    style B fill:#fff3e0,stroke:#e65100
    style C fill:#e8f5e9,stroke:#1b5e20
    style D fill:#f3e5f5,stroke:#4a148c
    style E fill:#fff9c4,stroke:#f57f17
    style F fill:#e1f5fe,stroke:#01579b
    style G fill:#e1f5fe,stroke:#01579b

What does this diagram tell us?

Step Who Does It What They Do
1 Frontend Initiates HTTP request GET /users
2 Controller Receives request, calls Service
3 Service Handles business logic, calls Prisma to query database
4 Prisma Executes SQL, fetches data from database
5 Service Returns data to Controller
6 Controller Wraps data as JSON and returns to frontend
7 Frontend Receives JSON, renders page

2. Controller Layer: Routing and Request Handling

Frontend analogy: Controller is like router.js in frontend, defining "which URL corresponds to which handler function"

2.1 What is a Controller?

In NestJS, the responsibilities of a Controller are:

  1. Receive HTTP requests (GET, POST, PATCH, DELETE, etc.)
  2. Parse request parameters (URL parameters, request body, query parameters, etc.)
  3. Call Service to handle business
  4. Return response to client

2.2 Walkthrough of Our Existing Controller Code

// ────────────────────────────────────────────────────────────
// 1. Import Dependencies
// ────────────────────────────────────────────────────────────

// Controller: marks this class as a controller
// Get, Post, Patch, Delete: HTTP method decorators
// Body, Param: parameter decorators for getting data from requests
import {
  Controller,
  Get,
  Post,
  Patch,
  Delete,
  Body,
  Param,
} from '@nestjs/common';

// Import Service (business logic layer)
import { AppService, UserService } from './app.service.js';

// ────────────────────────────────────────────────────────────
// 2. Controller Class
// ────────────────────────────────────────────────────────────

// @Controller() decorator: tells NestJS this class is a controller
// If a prefix is written like @Controller('api'), all routes become /api/xxx
@Controller()
export class AppController {
  // Constructor: dependency injection of Service
  // Equivalent to "the front desk is equipped with a walkie-talkie to directly contact the back office"
  constructor(
    private readonly appService: AppService,
    private readonly userService: UserService,
  ) {}

  // ──────────────────────────────────────────────────────────
  // Route: GET /
  // ──────────────────────────────────────────────────────────

  // @Get() decorator: handles GET requests
  // No path argument means root path: /
  // Equivalent to frontend route: { path: '/', component: ... }
  @Get()
  getHello(): string {
    // Calls Service method, returns result
    return this.appService.getHello();
  }

  // ──────────────────────────────────────────────────────────
  // Route: GET /users
  // ──────────────────────────────────────────────────────────

  // @Get('users'): handles GET /users requests
  // Equivalent to frontend route: { path: '/users', component: ... }
  @Get('users')
  async getUsers() {
    // Calls UserService's getUserList method
    // Service layer queries database for all users
    return this.userService.getUserList();
  }

  // ──────────────────────────────────────────────────────────
  // Route: GET /users/:id
  // ──────────────────────────────────────────────────────────

  // @Get('users/:id'): handles requests like GET /users/1
  // :id is a dynamic parameter, can match /users/1, /users/2, etc.
  @Get('users/:id')
  async getUser(@Param('id') id: string) {
    // @Param('id'): extracts id parameter from URL
    // For example, request GET /users/1 → id = '1'
    // Then passes to Service, converted to number type
    return this.userService.getUserDetail(Number(id));
  }

  // ──────────────────────────────────────────────────────────
  // Route: POST /users
  // ──────────────────────────────────────────────────────────

  // @Post('users'): handles POST /users requests
  // Used for creating new users
  @Post('users')
  async createUser(@Body() body: { email: string; name: string }) {
    // @Body(): extracts entire body object from request body
    // When frontend POSTs /users, it sends JSON in request body
    // Directly passes to Service for processing
    return this.userService.createUser(body);
  }

  // ──────────────────────────────────────────────────────────
  // Route: PATCH /users/:id
  // ──────────────────────────────────────────────────────────

  // @Patch('users/:id'): handles requests like PATCH /users/1
  // Used for partial update of user information
  @Patch('users/:id')
  async updateUser(
    @Param('id') id: string,
    @Body() body: { email?: string; name?: string },
  ) {
    // Takes id from URL, fields to update from body
    // Passes to Service to execute update
    return this.userService.updateUser(Number(id), body);
  }

  // ──────────────────────────────────────────────────────────
  // Route: DELETE /users/:id
  // ──────────────────────────────────────────────────────────

  @Delete('users/:id')
  async deleteUser(@Param('id') id: string) {
    return this.userService.deleteUser(Number(id));
  }
}

2.3 Summary of Common Controller Decorators

Decorator Purpose Example
@Controller() Marks class as controller @Controller('api') → all routes prefixed with /api
@Get() Handles GET requests @Get('list') → GET /list
@Post() Handles POST requests @Post() → POST /
@Patch() Handles PATCH requests @Patch(':id') → PATCH /:id
@Delete() Handles DELETE requests @Delete(':id') → DELETE /:id
@Param() Gets URL path parameters @Param('id') id → extracts 1 from /users/1
@Query() Gets URL query parameters @Query('page') page → extracts 2 from ?page=2
@Body() Gets request body @Body() body → gets entire JSON body

2.4 Frontend Comparison Table

Frontend Concept NestJS Equivalent
router.js defining routes @Get(), @Post() decorators
useParams() getting URL params @Param() decorator
useSearchParams() getting query params @Query() decorator
fetch() body @Body() decorator

3. Service Layer: Business Logic

Frontend analogy: Service is like utils/ or api/ folders in frontend, encapsulating specific business logic

3.1 What is a Service?

The responsibilities of a Service are:

  1. Handle specific business logic
  2. Operate the database (via Prisma)
  3. Call external APIs
  4. Process data transformation

Simply put: Service is the "back office"—after Controller receives an order (request), it tells Service "go do the work," and Service returns the result (data) after finishing.

3.2 Why Have a Service Layer?

Without Service With Service
All code written in Controller Controller only handles routing and parameter parsing
Same business logic duplicated across multiple Controllers One Service reused by multiple Controllers
Changing business logic requires changing Controller, changing routes also requires changing Controller Changing business logic only changes Service, Controller unchanged

Core principle: Controller only handles "receiving" and "returning," Service only handles "doing."

3.3 Walkthrough of Our Existing Service Code

// ────────────────────────────────────────────────────────────
// 1. Import Dependencies
// ────────────────────────────────────────────────────────────

// Injectable: marks class as injectable via dependency injection
import { Injectable } from '@nestjs/common';

// PrismaService: database operation service
import { PrismaService } from './prisma.service.js';

// UserModel: type generated by Prisma, used for type checking
import type { UserModel } from './generated/prisma/models.js';

// ────────────────────────────────────────────────────────────
// 2. AppService: Application Basic Service
// ────────────────────────────────────────────────────────────

@Injectable()
export class AppService {
  // This is a synchronous method, directly returns a string
  getHello(): string {
    return 'Hello World!';
  }
}

// ────────────────────────────────────────────────────────────
// 3. UserService: User Business Service
// ────────────────────────────────────────────────────────────

@Injectable()
export class UserService {
  // Constructor: injects PrismaService
  // Equivalent to "the kitchen is equipped with a fridge (database) to directly get ingredients"
  constructor(private readonly prisma: PrismaService) {}
  
  // The methods below are called by Controller
  // Methods are responsible for "going to the database" to query, edit, or delete users

  // ──────────────────────────────────────────────────────────
  // Business Method: Query All Users
  // ──────────────────────────────────────────────────────────
  async getUserList(): Promise<UserModel[]> {
    return this.prisma.user.findMany();
  }

  // ──────────────────────────────────────────────────────────
  // Business Method: Query Single User by ID
  // ──────────────────────────────────────────────────────────

  async getUserDetail(id: number): Promise<UserModel | null> {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }

  // ──────────────────────────────────────────────────────────
  // Business Method: Create User
  // ──────────────────────────────────────────────────────────

  async createUser(data: { email: string; name: string }): Promise<UserModel> {
    return this.prisma.user.create({
      data,
    });
  }

  // ──────────────────────────────────────────────────────────
  // Business Method: Update User
  // ──────────────────────────────────────────────────────────

  async updateUser(
    id: number,
    data: { email?: string; name?: string },
  ): Promise<UserModel> {
    return this.prisma.user.update({
      where: { id },
      data,
    });
  }

  // ──────────────────────────────────────────────────────────
  // Business Method: Delete User
  // ──────────────────────────────────────────────────────────

  async deleteUser(id: number): Promise<UserModel> {
    return this.prisma.user.delete({
      where: { id },
    });
  }
}

3.4 Prisma Common Methods Quick Reference

When operating the database in Service, the most commonly used are these 5 methods provided by Prisma:

Prisma Method Purpose Corresponding SQL
findMany() Query multiple records SELECT * FROM "User"
findUnique() Query single record by unique field SELECT * FROM "User" WHERE id = ?
create() Create a record INSERT INTO "User" ...
update() Update a record UPDATE "User" SET ... WHERE id = ?
delete() Delete a record DELETE FROM "User" WHERE id = ?

Other commonly used methods:

Prisma Method Purpose
findFirst() Query first record matching conditions
count() Count number of records
upsert() Update if exists, create if not
deleteMany() Delete multiple records
updateMany() Update multiple records

3.5 Frontend Comparison Table

Frontend Concept NestJS Equivalent
utils/ utility functions Methods in Service
api/ interface encapsulation Calling Prisma in Service
Actions in state management Business methods in Service

4. Module Layer: Organizing Code

Frontend analogy: Module is like index.js aggregate exports in frontend, organizing related code together

4.1 What is a Module?

The responsibilities of a Module are:

  1. Organize Controller and Service together
  2. Register all classes that can be dependency injected
  3. Import functionality from other modules

Simply put: Module is the "company organizational chart"—it tells NestJS: which front desks (Controllers) and which back offices (Services) are in this module.

4.2 Walkthrough of Our Existing Module Code

// ────────────────────────────────────────────────────────────
// 1. Import Dependencies
// ────────────────────────────────────────────────────────────

// Module: marks class as a module
import { Module } from '@nestjs/common';

// Controller: front desk responsible for handling HTTP requests
import { AppController } from './app.controller.js';

// Service: back office responsible for business logic
import { AppService, UserService } from './app.service.js';

// Database service
import { PrismaService } from './prisma.service.js';

// ────────────────────────────────────────────────────────────
// 2. Root Module AppModule
// ────────────────────────────────────────────────────────────

@Module({
  // imports: import other modules (none yet)
  // Equivalent to "referencing external company departments"
  imports: [],

  // controllers: register controllers (front desks)
  // Tells NestJS which front desk receptionists this module has
  controllers: [AppController],

  // providers: register services (back offices)
  // Tells NestJS which back offices this module has
  // All classes registered in providers can be "dependency injected" into other classes
  providers: [
    AppService,     // Basic service
    UserService,    // User business service
    PrismaService,  // Database service
  ],
})
export class AppModule {}

4.3 Three Core Properties of Module

Property Role Analogy
imports Import functionality from other modules "Borrowing resources from other departments"
controllers Register controllers (routes) "Front desk receptionist list"
providers Register services (injectable) "Back office employee list"

Hands-On Practice: Query User by Email

5.1 Add Method in Service

Add in UserService of src/app.service.ts:

// ──────────────────────────────────────────────────────────
// Business Method: Query Single User by Email
// ──────────────────────────────────────────────────────────

// Corresponding route: GET /users/email/:email
// Example: GET /users/email/[email protected]
async getUserByEmail(email: string): Promise<UserModel | null> {
  // Prisma executes SQL: SELECT * FROM "User" WHERE email = ?;
  return this.prisma.user.findUnique({
    where: { email },
  });
}

5.2 Add Route in Controller

Add in AppController of src/app.controller.ts:

// ──────────────────────────────────────────────────────────
// Route: GET /users/email/:email
// ──────────────────────────────────────────────────────────

@Get('users/email/:email')
async getUserByEmail(@Param('email') email: string) {
  return this.userService.getUserByEmail(email);
}

5.3 Test

Visit in browser: http://localhost:3000/users/email/[email protected]

image.png

6. Summary

Three-Layer Responsibilities in One Sentence

Layer One-Sentence Summary Frontend Analogy
Controller Defines URLs and receives requests Route configuration
Service Writes specific business logic Utility functions / API encapsulation
Module Assembles Controller and Service together Module export aggregation

Code Organization Principle

Controller receives request → hands to Service for processing → returns result to client

Remember:

Welcome to follow, let's move from React/Vue to full-stack together.

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

Liora_Yvonne

Very detailed explanation, great. You can check out my open-source project, it's a NestJS practical project. https://github.com/liangy0323/ly-fullstack