Back to Articles
January 10, 2025 12 min

Building Type-Safe APIs with NestJS and Prisma

#NestJS
#TypeScript
#Tutorial

Building Type-Safe APIs with NestJS and Prisma

Type safety is crucial for building maintainable applications. In this guide, I’ll show you how to achieve end-to-end type safety using NestJS and Prisma.

Why NestJS + Prisma?

NestJS provides:

  • TypeScript-first framework
  • Dependency injection
  • Built-in validation
  • Modular architecture

Prisma provides:

  • Type-safe database client
  • Auto-generated types
  • Database migrations
  • Intuitive query API

Together, they create a fully type-safe stack from database to API.

Project Setup

1. Initialize NestJS Project

npm i -g @nestjs/cli
nest new my-api
cd my-api

2. Install Prisma

npm install prisma @prisma/client
npm install -D prisma
npx prisma init

3. Define Your Schema

Edit prisma/schema.prisma:

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

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

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
}

4. Run Migration

npx prisma migrate dev --name init
npx prisma generate

This generates TypeScript types for your database!

Creating a Prisma Service

Create src/prisma/prisma.service.ts:

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

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

Register in src/prisma/prisma.module.ts:

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

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Creating DTOs with Validation

Install class-validator:

npm install class-validator class-transformer

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

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

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

  @IsString()
  @MinLength(2)
  name: string;

  @IsOptional()
  @IsString()
  bio?: string;
}

Building the Users Service

Create src/users/users.service.ts:

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User, Prisma } from '@prisma/client';

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

  async create(createUserDto: CreateUserDto): Promise<User> {
    return this.prisma.user.create({
      data: createUserDto,
    });
  }

  async findAll(): Promise<User[]> {
    return this.prisma.user.findMany({
      include: { posts: true },
    });
  }

  async findOne(id: number): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { id },
      include: { posts: true },
    });
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
    return this.prisma.user.update({
      where: { id },
      data: updateUserDto,
    });
  }

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

Notice how TypeScript knows the exact shape of User and validates your queries!

Building the Controller

Create src/users/users.controller.ts:

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  ParseIntPipe,
} 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()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

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

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

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

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id);
  }
}

Global Validation Pipe

Enable validation globally in src/main.ts:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
  
  await app.listen(3000);
}
bootstrap();

Error Handling

import { NotFoundException } from '@nestjs/common';

async findOne(id: number): Promise<User> {
  const user = await this.prisma.user.findUnique({
    where: { id },
  });

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

  return user;
}

Benefits of This Approach

End-to-end type safety - Database → Service → Controller → Response ✅ Auto-completion - IDE knows all available fields and methods ✅ Compile-time errors - Catch mistakes before runtime ✅ Automatic validation - DTOs validated automatically ✅ Database migrations - Version-controlled schema changes ✅ Generated documentation - Swagger integration available

Best Practices

  1. Always use DTOs for input validation
  2. Include relations explicitly with Prisma’s include
  3. Use partial types for update operations (Partial<CreateUserDto>)
  4. Handle errors with NestJS exception filters
  5. Enable strict mode in TypeScript config
  6. Use Prisma Studio for database exploration (npx prisma studio)

Testing

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;

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

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

  it('should create a user', async () => {
    const dto = { email: 'test@example.com', name: 'Test User' };
    const result = await service.create(dto);
    expect(result.email).toBe(dto.email);
  });
});

Conclusion

NestJS + Prisma provides a robust foundation for building type-safe APIs. The combination ensures:

  • Fewer runtime errors
  • Better developer experience
  • Easier refactoring
  • Self-documenting code

Start your next project with this stack and experience the benefits of full type safety!