# Scalable REST API Architecture with NestJS, Prisma, Swagger, & Docker: How To.

## Introduction

In today's rapidly evolving tech landscape, building robust, scalable, and maintainable backend services is a critical skill for developers. This article explores how to leverage modern technologies like NestJS, Docker, Swagger, and Prisma to create a production-ready REST API. You will learn the core fundamentals of NestJS and best practices while referencing code from [this repository](https://github.com/jideabdqudus/task-manager-api).

Modern applications require architectures that facilitate easy maintenance, testing, and scaling. NestJS, a progressive Node.js framework, provides an excellent foundation with its modular, TypeScript-based approach. Combined with Prisma for database operations, Swagger for API documentation, and Docker for containerization, you have a powerful stack for building server-side applications.

To follow step-by-step you can refer to the fully functional application available in this repository.

```bash
git clone https://github.com/jideabdqudus/task-manager-api.git
```

## Stack Overview

1. ### NestJS
    
    NestJS is a progressive Node.js framework inspired by Angular's architecture. It uses TypeScript and follows object-oriented programming principles, making it ideal for building scalable server-side applications. Its modular structure encourages the separation of concerns, leading to more maintainable code. ([Check out Nest](https://nestjs.com/))
    
2. ### Prisma
    
    Prisma is a next-generation ORM (Object-Relational Mapping) that significantly simplifies database access. With its type-safe database client and schema-based approach, Prisma ensures that database operations are both secure and predictable. ([Check out Prisma](https://www.prisma.io/))
    
3. ### Swagger
    
    Swagger (OpenAPI) is a powerful tool for documenting and testing APIs. It provides interactive documentation that makes it easier for front-end developers and API consumers to understand and use your endpoints. ([Check out Swagger](https://swagger.io/))
    
4. ### Docker
    
    Docker enables consistent application deployment across different environments through containerization. By packaging your application and its dependencies into containers, you ensure that it runs the same way in development, testing, and production. ([Check out Docker](https://www.docker.com/))
    

## Architecture

Whether you’re new to NestJS or looking to expand your skills, this article provides step-by-step instructions to build a full-featured API.

The task management API follows a modular architecture:

```bash
src/
├── auth/             # Authentication module
├── tasks/            # Task management module
├── user/             # User management module
├── database/         # Database module with Prisma service
├── app.module.ts     # Main application module
└── main.ts           # Application entry point
```

This structure separates the application into domain-specific modules, each handling specific business logic. Each module contains controllers (handling HTTP requests), services/providers (implementing business logic), and DTOs (Data Transfer Objects).

The final result of your application should look like this:

![Swagger docs showing the endpoints.](https://cdn.hashnode.com/res/hashnode/image/upload/v1744327308984/b406ba41-ee7e-4c43-ba1b-d9172da85a11.png align="center")

## **Prerequisites**

This tutorial is designed to be beginner-friendly, but it would be helpful if you’re familiar with these:

* Basic NestJS
    
* TypeScript
    
* Docker
    

Make sure you have the following installed before you begin:

* **Node.js** (v16 or later)
    
* **Node Package Manager**
    
* **Docker & Docker Compose**
    
* **PostgreSQL** (or your preferred database)
    

## Step 1: Setting Up Your NestJS Project

Create a new NestJS project using the CLI:

```bash
npm i -g @nestjs/cli # run if you don't have the NestJS CLI installed already
nest new task-manager-api
cd task-manager-api
```

This command scaffolds a basic NestJS application with all essential configurations. The CLI will prompt you to choose a package manager (npm or yarn). Select your preference, and NestJS will scaffold a basic project structure for you. This initial structure provides the foundation for the application.

As said earlier, Nest follows a modular architecture inspired by Angular. The file structure is of format:

* **Modules**: Organize application components into logical units
    
* **Controllers**: Handle HTTP requests and return responses
    
* **Services**: Contain business logic used by controllers
    
* **Providers**: Injectable components that can be shared across the application
    

The `src` directory is where the essential bits of the application would be and the majority of the codebase. On initializing the application, you’d find some key files in there.

* **src/main.ts:** Entry point that bootstraps the application
    
* **src/app.module.ts**: Root module that imports and organizes all other modules
    
* **src/app.controller.ts**: Basic controller handling routes/endpoints
    
* **src/app.service.ts**: Contains business logic used by the controller
    

Feel free to remove the `app.controller.spec.ts`, `./app.service`, and `./app.controller` files to emulate a fresh codebase.

To start the app, you can run `npm run start:dev`

[⭐️ View Codebase](https://github.com/jideabdqudus/task-manager-api/tree/initial-app-strucutre)

---

## Step 2: **Setting Up the Database with Prisma**

Prisma provides a clean, type-safe interface to the PostgreSQL database.

1. **Install Prisma:**
    
    ```bash
    npm install @prisma/client
    npm install --save-dev prisma
    ```
    
2. **Initialize Prisma:**
    
    ```bash
    npx prisma init
    ```
    
    This creates a `prisma/` directory with a `schema.prisma` file. Update the file to define models for **User** and **Task**.
    
    ```typescript
    // prisma/schema.prisma
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
    
    model User {
      id        Int     @id @default(autoincrement())
      email     String  @unique
      password  String
      name      String
      tasks Task[]
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }
    
    model Task {
      id          Int      @id @default(autoincrement())
      title       String
      description String?
      status      Status   @default(PENDING)
      priority    Priority @default(MEDIUM)
      dueDate     DateTime?
      category    String?
      labels      String?
      ownerId     Int
      owner       User     @relation(fields: [ownerId], references: [id])
      createdAt   DateTime @default(now())
      updatedAt   DateTime @updatedAt
    }
    enum Status {
      PENDING
      IN_PROGRESS
      COMPLETED
    }
    
    enum Priority {
      LOW
      MEDIUM
      HIGH
    }
    ```
    
3. **Migrate the Database:**
    
    Set your `DATABASE_URL` in a `.env` file and run:
    
    ```bash
    npx prisma migrate dev --name init
    ```
    
    To find out how to create your Database URL, there are helpful guides based on the service you’d like to use ([Supabase](https://supabase.com/docs/guides/database/prisma), [Neon](https://neon.tech/docs/guides/prisma) etc.)
    

This schema defines the data models along with their relationships. Prisma generates a type-safe client from this schema, providing methods for CRUD operations with full TypeScript support.

Based on your chosen service, you should see the tables after migration.

![Supabase Database Table](https://cdn.hashnode.com/res/hashnode/image/upload/v1744508411911/5b6d8633-2713-4e24-a6df-d2d025410d01.png align="center")

[⭐️ View Codebase](https://github.com/jideabdqudus/task-manager-api/tree/prisma-integration)

---

## Step 3: **Create Database Service**

Let’s create a Database service to handle the Primsa operations. It makes it easy to interact with the database through the application.

You can create services, modules, and controllers directly through your CLI

```bash
nest g module     <name>  # generates a module only
nest g service    <name>  # generates a service only  
nest g controller <name>  # generates a controller only
nest g resource   <name>  # generates all 3 of the above
```

You can, therefore, generate the database service and module (no controller).

```typescript
// src/database.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class DatabaseService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
```

```typescript
// src/database.module.ts
import { Module, Global } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Global()
@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}
```

Ordinarily, the newly created services and modules get imported into the `app.module.ts` file. So ensure they are found there.

[⭐️ View Codebase](https://github.com/jideabdqudus/task-manager-api/tree/database-resource)

---

## Step 4: **Configure Swagger**

The application uses Swagger to automatically generate interactive API documentation. You can configure Swagger in the `main.ts` file.

1. **Install Swagger:**
    
    ```bash
    npm install --save @nestjs/swagger swagger-ui-express
    ```
    
2. **Update** `main.ts` **file:**
    
    ```typescript
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const config = new DocumentBuilder()
        .setTitle('Tasks API')
        .setDescription('Task Management API')
        .addBearerAuth({ // Validation for Protected Routes
          type: 'http',
          name: 'JWT',
          scheme: 'bearer',
          bearerFormat: 'JWT',
        })
        .setVersion('1.0')
        .addTag('tasks')
        .build();
      const document = SwaggerModule.createDocument(app, config);
      SwaggerModule.setup('api-docs', app, document);
      await app.listen(process.env.PORT ?? 3000);
    }
    bootstrap();
    ```
    
    This setup creates an interactive documentation interface at the `/api-docs` endpoint, where developers can explore and test the API.
    
    [⭐️ View Codebase](https://github.com/jideabdqudus/task-manager-api/tree/swagger-configuration)
    

---

## Step 5: **RESTful Endpoints**

Before advancing into this section, it’s important you follow with the [codebase](https://github.com/jideabdqudus/task-manager-api/tree/rest-endpoints) at this point as this would be an overview of what to expect.

In NestJS, when building a feature like task-management, you'll typically work with four main components: Modules, Controllers, Services, and DTOs. Let's dive deep into how these components interact and the development flow for creating a complete resource.

1. **Modules**
    
    In NestJS, **modules** are the organizational units that encapsulate related functionality. They help maintain a clean, organized codebase as applications grow in complexity. Each feature (like Tasks) typically has its own module.
    
    Here's the possible structure of the TaskModule:
    
    ```typescript
    // src/tasks.module.ts
    import { Module } from '@nestjs/common';
    
    @Module({
      imports: [DatabaseModule], // Import dependencies from other modules
      controllers: [TasksController], // Register controllers
      providers: [TasksService], // Register services
      exports: [TasksService], // Export services for use in other modules
    })
    export class TaskModule {}
    ```
    
    This decorator-based configuration tells NestJS:
    
    * Which other modules this module depends on
        
    * Which controllers handle the HTTP requests
        
    * Which providers (services) implement the business logic
        
    * Which providers should be available to other modules
        
2. **Controllers**
    
    **Controllers** are responsible for handling incoming HTTP requests and returning responses to the client. They define routes and use decorators to specify HTTP methods (GET, POST, etc.). Controllers depend on services to perform the actual business logic.
    
    Here's your task controller with detailed annotations:
    
    ```typescript
    import {
      Controller,
      Get,
      Post,
      Body,
      Patch,
      Param,
      Delete,
    } from '@nestjs/common';
    import {
      ApiTags,
      ApiOperation,
      ApiResponse,
      ApiBearerAuth,
    } from '@nestjs/swagger';
    import { CreateTaskDto } from './dto/create-task.dto';
    import { UpdateTaskDto } from './dto/update-task.dto';
    import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
    
    @ApiTags('tasks') // Swagger tag for grouping endpoints in documentation
    @Controller('tasks') // Base route prefix (/api/tasks)
    @UseGuards(JwtAuthGuard) // Authentication guard applied to all endpoints
    export class TasksController {
      constructor(private readonly tasksService: TasksService) {} // Dependency injection
    
      @Post() // HTTP POST method for creating resources
      @ApiOperation({ summary: 'Create a new task' }) // Swagger documentation
      @ApiResponse({
        status: 201,
        description: 'The task has been successfully created.',
      })
      @ApiBearerAuth() // Indicates authentication requirement in Swagger
      create(@Request() req, @Body() createTaskDto: CreateTaskDto) {
        // Extract user ID from JWT token payload
        return this.tasksService.create({
          ...createTaskDto,
          ownerId: (req as { user: { userId: number } }).user.userId,
        });
      }
    
      @Get() // HTTP GET method for retrieving resources
      @ApiOperation({ summary: 'Get all tasks' })
      @ApiResponse({
        status: 200,
        description: 'The tasks have been successfully retrieved.',
      })
      @ApiBearerAuth()
      findAll(@Request() req: unknown) {
        // Return only tasks owned by the authenticated user
        return this.tasksService.findAllByUser(
          (req as { user: { userId: number } }).user.userId,
        );
      }
    
      @Get(':id') // Dynamic route parameter
      @ApiOperation({ summary: 'Get a task by id' })
      @ApiResponse({
        status: 200,
        description: 'The task has been successfully retrieved.',
      })
      @ApiBearerAuth()
      findOne(@Request() req: unknown, @Param('id') id: string) {
        // Convert string ID to number with the + operator
        return this.tasksService.findOne(
          +id,
          (req as { user: { userId: number } }).user,
        );
      }
    
      @Patch(':id') // HTTP PATCH for partial updates
      @ApiOperation({ summary: 'Update a task' })
      @ApiResponse({
        status: 200,
        description: 'The task has been successfully updated.',
      })
      @ApiBearerAuth()
      update(
        @Request() req: unknown,
        @Param('id') id: string,
        @Body() updateTaskDto: UpdateTaskDto,
      ) {
        return this.tasksService.update(
          +id,
          updateTaskDto,
          (req as { user: { userId: number } }).user,
        );
      }
    
      @Delete(':id') // HTTP DELETE for removing resources
      @ApiOperation({ summary: 'Delete a task' })
      @ApiResponse({
        status: 200,
        description: 'The task has been successfully deleted.',
      })
      @ApiBearerAuth()
      remove(@Request() req: unknown, @Param('id') id: string) {
        return this.tasksService.remove(
          +id,
          (req as { user: { userId: number } }).user,
        );
      }
    }
    ```
    
    Notice how each method in the controller:
    
    1. Uses specific HTTP method decorators (`@Get()`, `@Post()`, etc).
        
    2. Accepts parameters from various sources (`@Body()`, `@Param()`, `@Request()`)
        
    3. Delegates the actual business logic to the injected service
        
    4. Includes Swagger documentation for API explorability
        
3. **Services**
    
    **Services** implement the business logic and are responsible for data storage and retrieval. They abstract the database operations and provide a clean interface for controllers. In the architecture, services interact with the Prisma client to perform database operations.
    
    Here's a simplified view of the task service:
    
    ```typescript
    import {
      Injectable,
      NotFoundException,
      ForbiddenException,
    } from '@nestjs/common';
    
    @Injectable() // Makes the service available for dependency injection
    export class TasksService {
      constructor(private databaseService: DatabaseService) {} // Inject the Prisma client service
    
      async create(createTaskDto: CreateTaskDto & { ownerId: number }) {
        return this.databaseService.task.create({
          data: createTaskDto,
        });
      }
    
      async findAllByUser(userId: number) {
        return this.databaseService.task.findMany({
          where: { ownerId: userId },
          orderBy: { updatedAt: 'desc' },
        });
      }
    
      async findOne(id: number, user: { userId: number }) {
        const task = await this.databaseService.task.findUnique({
          where: { id },
        });
    
        if (!task) {
          throw new NotFoundException(`Task with ID ${id} not found`);
        }
    
        // Authorization check
        if (task.ownerId !== user.userId) {
          throw new ForbiddenException('You can only access your own tasks');
        }
    
        return task;
      }
    
      // Update and delete methods follow a similar pattern
    }
    ```
    
    The service layer is where business rules, validations, and authorization checks should be implemented. The `TasksService` ensures that users can only access, modify, or delete their own tasks.
    
4. **DTOs (Data Transfer Objects)**
    
    **DTOs** define the shape of data for a specific API operation. They provide type safety and validation through decorators, ensuring that incoming requests conform to expected formats.
    
    The CreateTaskDto:
    
    ```typescript
    import { ApiProperty } from '@nestjs/swagger';
    import {
      IsNotEmpty,
      IsString,
      IsOptional,
      IsEnum,
      IsISO8601,
    } from 'class-validator';
    import { Status, Priority } from '@prisma/client';
    
    export class CreateTaskDto {
      @ApiProperty() // Swagger documentation
      @IsString() // Validation: must be a string
      @IsNotEmpty() // Validation: cannot be empty
      title: string;
    
      @ApiProperty({ required: false })
      @IsString()
      @IsOptional() // Marks the field as optional
      description?: string;
    
      @ApiProperty({ enum: Status, default: Status.PENDING })
      @IsEnum(Status) // Validation: must be one of the enum values
      @IsOptional()
      status?: Status;
    
      @ApiProperty({ enum: Priority, default: Priority.MEDIUM })
      @IsEnum(Priority)
      @IsOptional()
      priority?: Priority;
    
      @ApiProperty({ required: false })
      @IsDateString() // Validates ISO date string format
      @IsOptional()
      dueDate?: string;
    
      @ApiProperty({ required: false })
      @IsString()
      @IsOptional()
      category?: string;
    
      @ApiProperty({ required: false })
      @IsString()
      @IsOptional()
      labels?: string;
    }
    ```
    
    UpdateTaskDto extends from a partial version of CreateTaskDto, making all fields optional for updates. Basically, what this does is take a copy of the CreateTaskDto whilst making its fields optional. You can decide to extend it further depending on the needs of your application:
    
    ```typescript
    export class UpdateTaskDto extends PartialType(CreateTaskDto) {}
    ```
    
    [⭐️ View Codebase](https://github.com/jideabdqudus/task-manager-api/tree/rest-endpoints)
    

---

## **Understanding the building blocks of Nest**

As you can tell, when developing a new feature or resource in NestJS, a typical workflow would be:

1. **Define the Data Model**: Start with the Prisma schema to define the database model for your resource (in our case, the Task model).
    
2. **Generate DTOs**: Create Data Transfer Objects to define the shapes of requests and responses (CreateTaskDto, UpdateTaskDto).
    
3. **Create the Service**: Implement the business logic that interacts with the database via Prisma (TasksService).
    
4. **Build the Controller**: Define the API endpoints that map to service methods (TasksController).
    
5. **Configure the Module**: Wire everything together in a module (TaskModule).
    
6. **Add Swagger Documentation**: Add API documentation using decorators wherever necessary.
    
7. **Implement Authentication/Authorization**: Add guards and strategies for protecting routes.
    
8. **Write Tests**: Create unit and integration tests to verify the functionality.
    

This modular approach allows developers to work on different parts of the feature independently and promotes code reusability and separation of concerns.

---

## Step 6: **Containerization with Docker**

Docker ensures that the application runs consistently across environments. Our multi-stage Dockerfile optimizes for both development and production:

```dockerfile
# Dockerfile

# Development stage
FROM node:20-alpine AS development
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS production
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . .
COPY --from=development /usr/src/app/dist ./dist
CMD ["node", "dist/main"]
```

This approach:

1. Creates a development image with all dependencies for building
    
2. Creates a production image with only production dependencies
    
3. Copies the built application from the development stage to the production image
    

Combined with docker-compose, this setup allows for easy orchestration of the API alongside its PostgreSQL database.

Next, you’ll need to create a `docker-compose.yml` file

```bash
touch docker-compose.yml     # creates docker-compose.yml file
```

You can then add the following code to the created file

```yaml
version: '3.8'
services:
  postgres:
    image: postgres:13.5
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: nest_task
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - '5432:5432'
volumes:
  postgres-data:
```

To start the container, run `docker compose up` . Ensure you have docker installed before running this on your machine. By now, you should have the PostgreSQL container running. To stop the container, you can run `docker compose down`.

[⭐️ View Codebase](https://github.com/jideabdqudus/task-manager-api/tree/containerization-with-docker)

---

## **Conclusion**

Building a scalable REST API involves combining the right technologies with solid architectural principles. NestJS provides an excellent foundation with its modular structure and TypeScript support. Prisma simplifies database operations with its type-safe client. Swagger automates API documentation, making it easier for others to use your API. Finally, Docker ensures consistent deployment across environments.

This approach results in an API that is not only powerful and flexible but also maintainable and testable. As your application grows, the modular architecture allows for easy extension without compromising stability.

By following the patterns demonstrated in this project, you can build professional-grade APIs that scale with your business needs.

Don't hesitate to explore the repository further, experiment with the code, and refer to the official NestJS documentation for a comprehensive understanding of the framework's capabilities. Happy coding!

[⭐️ View Final Code](https://github.com/jideabdqudus/task-manager-api)  
  
👉🏾 [**Learn more about me**](https://www.abdulqudus.com/)

👉🏾 [**Connect on LinkedIn**](https://www.linkedin.com/in/jideabdqudus/)

👉🏾 [**Subscribe to my blog**](https://abdulqudus.com/newsletter)
