DEV Community

Cover image for From Beginner to Pro: Setting Up a TypeScript NestJS Backend with Prisma
Rajat Parihar
Rajat Parihar

Posted on

From Beginner to Pro: Setting Up a TypeScript NestJS Backend with Prisma

From Beginner to Pro: Setting Up a TypeScript NestJS Backend with Prisma

A complete, production-ready guide to building scalable backend applications with NestJS, TypeScript, and Prisma ORM

NestJS TypeScript Prisma PostgreSQL

πŸ“‹ Table of Contents


πŸš€ Why This Stack?

NestJS

  • Enterprise-grade architecture with dependency injection
  • Built on Express/Fastify - familiar, battle-tested foundations
  • TypeScript-first - catch bugs before runtime
  • Microservices ready - scale as you grow
  • Amazing ecosystem - authentication, GraphQL, WebSockets, etc.

Prisma

  • Type-safe database access - autocomplete for your queries
  • Auto-generated migrations - version control your database schema
  • Multi-database support - PostgreSQL, MySQL, MongoDB, SQLite
  • Prisma Studio - visual database browser included
  • Performance - optimized queries out of the box

TypeScript

  • Reduced bugs - 15% fewer bugs in production (Microsoft Research)
  • Better refactoring - rename with confidence
  • Self-documenting code - types as documentation
  • IDE superpowers - IntelliSense everywhere

The result? A backend that's maintainable, scalable, and a joy to develop.


βœ… Prerequisites

Before you begin, ensure you have:

  • Node.js (v18+) - Download here
  • npm or yarn - Comes with Node.js
  • PostgreSQL (v12+) - Download here or use Docker
  • Git - Download here
  • Basic knowledge of:
    • JavaScript/TypeScript
    • REST APIs
    • SQL (helpful but not required)

Quick Environment Check

node --version  # Should be v18 or higher
npm --version   # Should be 8 or higher
psql --version  # Should be 12 or higher
Enter fullscreen mode Exit fullscreen mode

πŸ“ Project Structure

nestjs-prisma-backend/
β”œβ”€β”€ prisma/
β”‚   β”œβ”€β”€ schema.prisma          # Database schema definition
β”‚   β”œβ”€β”€ migrations/            # Database migration history
β”‚   └── seed.ts               # Seed data for development
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ auth/                 # Authentication module
β”‚   β”‚   β”œβ”€β”€ auth.controller.ts
β”‚   β”‚   β”œβ”€β”€ auth.service.ts
β”‚   β”‚   β”œβ”€β”€ auth.module.ts
β”‚   β”‚   β”œβ”€β”€ dto/              # Data Transfer Objects
β”‚   β”‚   β”œβ”€β”€ guards/           # Auth guards
β”‚   β”‚   └── strategies/       # Passport strategies
β”‚   β”œβ”€β”€ users/                # Users module
β”‚   β”‚   β”œβ”€β”€ users.controller.ts
β”‚   β”‚   β”œβ”€β”€ users.service.ts
β”‚   β”‚   β”œβ”€β”€ users.module.ts
β”‚   β”‚   └── dto/
β”‚   β”œβ”€β”€ posts/                # Posts module (example)
β”‚   β”‚   β”œβ”€β”€ posts.controller.ts
β”‚   β”‚   β”œβ”€β”€ posts.service.ts
β”‚   β”‚   β”œβ”€β”€ posts.module.ts
β”‚   β”‚   └── dto/
β”‚   β”œβ”€β”€ common/               # Shared utilities
β”‚   β”‚   β”œβ”€β”€ decorators/       # Custom decorators
β”‚   β”‚   β”œβ”€β”€ filters/          # Exception filters
β”‚   β”‚   β”œβ”€β”€ guards/           # Global guards
β”‚   β”‚   β”œβ”€β”€ interceptors/     # Response interceptors
β”‚   β”‚   └── pipes/            # Validation pipes
β”‚   β”œβ”€β”€ config/               # Configuration
β”‚   β”‚   β”œβ”€β”€ database.config.ts
β”‚   β”‚   └── app.config.ts
β”‚   β”œβ”€β”€ prisma/               # Prisma service
β”‚   β”‚   β”œβ”€β”€ prisma.service.ts
β”‚   β”‚   └── prisma.module.ts
β”‚   β”œβ”€β”€ app.module.ts         # Root module
β”‚   β”œβ”€β”€ app.controller.ts     # Root controller
β”‚   β”œβ”€β”€ app.service.ts        # Root service
β”‚   └── main.ts              # Application entry point
β”œβ”€β”€ test/                     # E2E tests
β”‚   β”œβ”€β”€ app.e2e-spec.ts
β”‚   └── jest-e2e.json
β”œβ”€β”€ .env                      # Environment variables
β”œβ”€β”€ .env.example             # Example environment file
β”œβ”€β”€ .eslintrc.js             # ESLint configuration
β”œβ”€β”€ .prettierrc              # Prettier configuration
β”œβ”€β”€ docker-compose.yml       # Docker setup (optional)
β”œβ”€β”€ Dockerfile               # Production Docker image
β”œβ”€β”€ nest-cli.json            # NestJS CLI configuration
β”œβ”€β”€ package.json             # Dependencies
β”œβ”€β”€ tsconfig.json            # TypeScript configuration
└── README.md                # You are here!
Enter fullscreen mode Exit fullscreen mode

⚑ Quick Start

Get up and running in under 5 minutes:

# 1. Clone or create new project
npx @nestjs/cli new nestjs-prisma-backend
cd nestjs-prisma-backend

# 2. Install Prisma
npm install prisma @prisma/client
npm install -D prisma

# 3. Initialize Prisma
npx prisma init

# 4. Set up environment variables
cp .env.example .env
# Edit .env with your database credentials

# 5. Create your schema (see Database Configuration section)

# 6. Run migrations
npx prisma migrate dev --name init

# 7. Generate Prisma Client
npx prisma generate

# 8. Start development server
npm run start:dev

# πŸŽ‰ Visit http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ Step-by-Step Setup

1. Create NestJS Project

# Install NestJS CLI globally
npm install -g @nestjs/cli

# Create new project
nest new nestjs-prisma-backend

# Choose your package manager (npm/yarn/pnpm)
cd nestjs-prisma-backend
Enter fullscreen mode Exit fullscreen mode

2. Install Dependencies

# Prisma ORM
npm install @prisma/client
npm install -D prisma

# Configuration
npm install @nestjs/config

# Validation
npm install class-validator class-transformer

# Authentication (we'll add this later)
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt

# Security
npm install helmet
npm install @nestjs/throttler

# Documentation
npm install @nestjs/swagger swagger-ui-express
Enter fullscreen mode Exit fullscreen mode

3. Initialize Prisma

npx prisma init
Enter fullscreen mode Exit fullscreen mode

This creates:

  • prisma/schema.prisma - Your database schema
  • .env - Environment variables file

4. Configure Environment Variables

Create .env file:

# Database
DATABASE_URL="postgresql://username:password@localhost:5432/nestjs_db?schema=public"

# Application
PORT=3000
NODE_ENV=development

# JWT (for authentication)
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=7d

# CORS
CORS_ORIGIN=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

Create .env.example (commit this to git):

# Database
DATABASE_URL="postgresql://username:password@localhost:5432/database_name?schema=public"

# Application
PORT=3000
NODE_ENV=development

# JWT
JWT_SECRET=change-me-in-production
JWT_EXPIRES_IN=7d

# CORS
CORS_ORIGIN=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

πŸ—„οΈ Database Configuration

Step 1: Define Your Schema

Edit prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String?
  password  String
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@map("users")
}

model Post {
  id          String   @id @default(uuid())
  title       String
  content     String?
  published   Boolean  @default(false)
  author      User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId    String
  tags        Tag[]
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@map("posts")
  @@index([authorId])
}

model Tag {
  id    String @id @default(uuid())
  name  String @unique
  posts Post[]

  @@map("tags")
}

enum Role {
  USER
  ADMIN
  MODERATOR
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Migration

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

This will:

  • Create the database if it doesn't exist
  • Generate SQL migration files
  • Apply migrations to your database
  • Generate Prisma Client

Step 3: Create Prisma Service

Create src/prisma/prisma.service.ts:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
    console.log('βœ… Database connected');
  }

  async onModuleDestroy() {
    await this.$disconnect();
    console.log('❌ Database disconnected');
  }

  // Helper method for transactions
  async executeTransaction<T>(fn: (prisma: PrismaClient) => Promise<T>): Promise<T> {
    return this.$transaction(fn);
  }
}
Enter fullscreen mode Exit fullscreen mode

Create src/prisma/prisma.module.ts:

import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
Enter fullscreen mode Exit fullscreen mode

Step 4: Register Prisma Module

Update src/app.module.ts:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    PrismaModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ Building Your First API

Step 1: Generate Resource

nest generate resource users
Enter fullscreen mode Exit fullscreen mode

Choose:

  • Transport layer: REST API
  • Generate CRUD entry points: Yes

Step 2: Create DTOs

Create src/users/dto/create-user.dto.ts:

import { IsEmail, IsNotEmpty, IsString, MinLength, IsOptional, IsEnum } from 'class-validator';
import { Role } from '@prisma/client';

export class CreateUserDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @MinLength(8, { message: 'Password must be at least 8 characters long' })
  password: string;

  @IsOptional()
  @IsEnum(Role)
  role?: Role;
}
Enter fullscreen mode Exit fullscreen mode

Create src/users/dto/update-user.dto.ts:

import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { IsOptional, IsString } from 'class-validator';

export class UpdateUserDto extends PartialType(CreateUserDto) {
  @IsOptional()
  @IsString()
  name?: string;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement Service

Update src/users/users.service.ts:

import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async create(createUserDto: CreateUserDto) {
    // Check if user already exists
    const existingUser = await this.prisma.user.findUnique({
      where: { email: createUserDto.email },
    });

    if (existingUser) {
      throw new ConflictException('User with this email already exists');
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    // Create user
    const user = await this.prisma.user.create({
      data: {
        ...createUserDto,
        password: hashedPassword,
      },
      select: {
        id: true,
        email: true,
        name: true,
        role: true,
        createdAt: true,
        updatedAt: true,
      },
    });

    return user;
  }

  async findAll() {
    return this.prisma.user.findMany({
      select: {
        id: true,
        email: true,
        name: true,
        role: true,
        createdAt: true,
        updatedAt: true,
      },
    });
  }

  async findOne(id: string) {
    const user = await this.prisma.user.findUnique({
      where: { id },
      select: {
        id: true,
        email: true,
        name: true,
        role: true,
        posts: {
          select: {
            id: true,
            title: true,
            published: true,
            createdAt: true,
          },
        },
        createdAt: true,
        updatedAt: true,
      },
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    return user;
  }

  async findByEmail(email: string) {
    return this.prisma.user.findUnique({
      where: { email },
    });
  }

  async update(id: string, updateUserDto: UpdateUserDto) {
    // Check if user exists
    await this.findOne(id);

    // If password is being updated, hash it
    if (updateUserDto.password) {
      updateUserDto.password = await bcrypt.hash(updateUserDto.password, 10);
    }

    const user = await this.prisma.user.update({
      where: { id },
      data: updateUserDto,
      select: {
        id: true,
        email: true,
        name: true,
        role: true,
        createdAt: true,
        updatedAt: true,
      },
    });

    return user;
  }

  async remove(id: string) {
    // Check if user exists
    await this.findOne(id);

    await this.prisma.user.delete({
      where: { id },
    });

    return { message: 'User deleted successfully' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Install bcrypt:

npm install bcrypt
npm install -D @types/bcrypt
Enter fullscreen mode Exit fullscreen mode

Step 4: Update Controller

Update src/users/users.controller.ts:

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  HttpCode,
  HttpStatus,
  UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Enable Validation

Update src/main.ts:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import * as helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global validation pipe
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // Strip properties that don't have decorators
      forbidNonWhitelisted: true, // Throw error if non-whitelisted properties exist
      transform: true, // Auto-transform payloads to DTO instances
    }),
  );

  // Security
  app.use(helmet());

  // CORS
  app.enableCors({
    origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
    credentials: true,
  });

  // Global prefix
  app.setGlobalPrefix('api/v1');

  const port = process.env.PORT || 3000;
  await app.listen(port);

  console.log(`πŸš€ Application is running on: http://localhost:${port}`);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Step 6: Test Your API

# Start the server
npm run start:dev

# Create a user (using curl or Postman)
curl -X POST http://localhost:3000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "name": "John Doe",
    "password": "securepassword123"
  }'

# Get all users
curl http://localhost:3000/api/v1/users

# Get specific user
curl http://localhost:3000/api/v1/users/{user-id}
Enter fullscreen mode Exit fullscreen mode

πŸ” Authentication & Authorization

Step 1: Generate Auth Module

nest generate module auth
nest generate service auth
nest generate controller auth
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Auth DTOs

Create src/auth/dto/login.dto.ts:

import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class LoginDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

Create src/auth/dto/register.dto.ts:

import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class RegisterDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @MinLength(8)
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement Auth Service

Update src/auth/auth.service.ts:

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import { UsersService } from '../users/users.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
    private usersService: UsersService,
  ) {}

  async register(registerDto: RegisterDto) {
    const user = await this.usersService.create(registerDto);
    const token = this.generateToken(user.id, user.email);

    return {
      user,
      access_token: token,
    };
  }

  async login(loginDto: LoginDto) {
    const user = await this.usersService.findByEmail(loginDto.email);

    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const isPasswordValid = await bcrypt.compare(loginDto.password, user.password);

    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const token = this.generateToken(user.id, user.email);

    return {
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
      },
      access_token: token,
    };
  }

  async validateUser(userId: string) {
    return this.usersService.findOne(userId);
  }

  private generateToken(userId: string, email: string): string {
    const payload = { sub: userId, email };
    return this.jwtService.sign(payload);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create JWT Strategy

Create src/auth/strategies/jwt.strategy.ts:

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from '../auth.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: any) {
    const user = await this.authService.validateUser(payload.sub);

    if (!user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Create JWT Guard

Create src/auth/guards/jwt-auth.guard.ts:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Enter fullscreen mode Exit fullscreen mode

Step 6: Create Custom Decorator

Create src/common/decorators/current-user.decorator.ts:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);
Enter fullscreen mode Exit fullscreen mode

Step 7: Update Auth Module

Update src/auth/auth.module.ts:

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Step 8: Update Auth Controller

Update src/auth/auth.controller.ts:

import { Controller, Post, Body, HttpCode, HttpStatus, Get, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { CurrentUser } from '../common/decorators/current-user.decorator';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  @HttpCode(HttpStatus.CREATED)
  register(@Body() registerDto: RegisterDto) {
    return this.authService.register(registerDto);
  }

  @Post('login')
  @HttpCode(HttpStatus.OK)
  login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }

  @Get('me')
  @UseGuards(JwtAuthGuard)
  getProfile(@CurrentUser() user: any) {
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 9: Protect Routes

Update src/users/users.controller.ts to add authentication:

import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../common/decorators/current-user.decorator';

@Controller('users')
@UseGuards(JwtAuthGuard) // Protect all routes
export class UsersController {
  // ... existing code

  @Get('profile')
  getProfile(@CurrentUser() user: any) {
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 10: Test Authentication

# Register a new user
curl -X POST http://localhost:3000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "name": "Test User",
    "password": "password123"
  }'

# Login
curl -X POST http://localhost:3000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123"
  }'

# Use the returned token to access protected routes
curl http://localhost:3000/api/v1/auth/me \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Testing

Unit Tests

Create src/users/users.service.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { PrismaService } from '../prisma/prisma.service';

describe('UsersService', () => {
  let service: UsersService;
  let prisma: PrismaService;

  const mockPrismaService = {
    user: {
      create: jest.fn(),
      findMany: jest.fn(),
      findUnique: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    },
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('findAll', () => {
    it('should return an array of users', async () => {
      const mockUsers = [
        { id: '1', email: 'test@example.com', name: 'Test User' },
      ];

      mockPrismaService.user.findMany.mockResolvedValue(mockUsers);

      const result = await service.findAll();
      expect(result).toEqual(mockUsers);
      expect(prisma.user.findMany).toHaveBeenCalled();
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Run tests:

npm run test          # Unit tests
npm run test:watch    # Watch mode
npm run test:cov      # Coverage report
Enter fullscreen mode Exit fullscreen mode

E2E Tests

Create test/users.e2e-spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('Users (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe());
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/api/v1/users (POST)', () => {
    return request(app.getHttpServer())
      .post('/api/v1/users')
      .send({
        email: 'test@example.com',
        name: 'Test User',
        password: 'password123',
      })
      .expect(201)
      .expect((res) => {
        expect(res.body).toHaveProperty('id');
        expect(res.body.email).toBe('test@example.com');
      });
  });

  it('/api/v1/users (GET)', () => {
    return request(app.getHttpServer())
      .get('/api/v1/users')
      .expect(200)
      .expect((res) => {
        expect(Array.isArray(res.body)).toBe(true);
      });
  });
});
Enter fullscreen mode Exit fullscreen mode

Run e2e tests:

npm run test:e2e
Enter fullscreen mode Exit fullscreen mode

🚒 Deployment

Docker Setup

Create Dockerfile:

# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
COPY prisma ./prisma/

RUN npm ci

COPY . .

RUN npm run build

# Production stage
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
COPY prisma ./prisma/

RUN npm ci --only=production

COPY --from=builder /app/dist ./dist

EXPOSE 3000

CMD ["sh", "-c", "npx prisma migrate deploy && node dist/main"]
Enter fullscreen mode Exit fullscreen mode

Create docker-compose.yml:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: nestjs_db
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data

  app:
    build: .
    ports:
      - '3000:3000'
    environment:
      DATABASE_URL: postgresql://postgres:postgres@postgres:5432/nestjs_db?schema=public
      JWT_SECRET: your-production-secret-change-this
      NODE_ENV: production
    depends_on:
      - postgres

volumes:
  postgres_data:
Enter fullscreen mode Exit fullscreen mode

Run with Docker:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Deployment Checklist

  • [ ] Set strong JWT_SECRET in production
  • [ ] Use environment-specific .env files
  • [ ] Enable HTTPS/SSL
  • [ ] Set up proper CORS origins
  • [ ] Enable rate limiting
  • [ ] Set up logging (Winston, Pino)
  • [ ] Add health check endpoint
  • [ ] Set up monitoring (Sentry, DataDog)
  • [ ] Configure reverse proxy (Nginx, Caddy)
  • [ ] Set up CI/CD pipeline
  • [ ] Database backups configured
  • [ ] Review Prisma connection pooling

πŸ’Ž Best Practices

1. Error Handling

Create src/common/filters/http-exception.filter.ts:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Internal server error';

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Logging

Create src/common/interceptors/logging.interceptor.ts:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const now = Date.now();

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const { statusCode } = response;
        const delay = Date.now() - now;

        this.logger.log(`${method} ${url} ${statusCode} - ${delay}ms`);
      }),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

3. API Documentation with Swagger

Update src/main.ts:

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Swagger configuration
  const config = new DocumentBuilder()
    .setTitle('NestJS Prisma API')
    .setDescription('API documentation for NestJS + Prisma backend')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api/docs', app, document);

  await app.listen(3000);
  console.log(`πŸ“š API Documentation: http://localhost:3000/api/docs`);
}
Enter fullscreen mode Exit fullscreen mode

4. Rate Limiting

Install throttler:

npm install @nestjs/throttler
Enter fullscreen mode Exit fullscreen mode

Update app.module.ts:

import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot([{
      ttl: 60000, // 1 minute
      limit: 10,  // 10 requests per minute
    }]),
    // ... other imports
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

5. Database Seeding

Create prisma/seed.ts:

import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';

const prisma = new PrismaClient();

async function main() {
  // Create admin user
  const hashedPassword = await bcrypt.hash('admin123', 10);

  const admin = await prisma.user.upsert({
    where: { email: 'admin@example.com' },
    update: {},
    create: {
      email: 'admin@example.com',
      name: 'Admin User',
      password: hashedPassword,
      role: 'ADMIN',
    },
  });

  console.log('βœ… Seed data created:', { admin });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
Enter fullscreen mode Exit fullscreen mode

Add to package.json:

{
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run seed:

npx prisma db seed
Enter fullscreen mode Exit fullscreen mode

πŸ› Troubleshooting

Common Issues

1. Prisma Client Not Generated

# Generate Prisma Client
npx prisma generate
Enter fullscreen mode Exit fullscreen mode

2. Migration Failed

# Reset database (WARNING: deletes all data)
npx prisma migrate reset

# Or create a new migration
npx prisma migrate dev --name fix_migration
Enter fullscreen mode Exit fullscreen mode

3. TypeScript Errors

# Clear cache and rebuild
rm -rf dist node_modules package-lock.json
npm install
npm run build
Enter fullscreen mode Exit fullscreen mode

4. Database Connection Issues

Check your .env file:

# Correct format
DATABASE_URL="postgresql://username:password@localhost:5432/database_name"

# Common mistakes:
# ❌ Missing quotes
# ❌ Wrong port
# ❌ Incorrect schema name
Enter fullscreen mode Exit fullscreen mode

5. JWT Authentication Not Working

  • Verify JWT_SECRET is set in .env
  • Check token format: Bearer <token>
  • Ensure JwtAuthGuard is applied correctly
  • Verify user exists in database

πŸ“š Resources

Official Documentation

Learning Resources

Tools

Community


πŸ‘¨β€πŸ’» Author

Rajat


⭐ Support

If this guide helped you, please consider:

  • Sharing it with other developers
  • Following for more content

Happy Coding! πŸš€

Made with ❀️ by Rajat

Top comments (0)