Building Type-Safe APIs with NestJS and Prisma
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
- Always use DTOs for input validation
- Include relations explicitly with Prisma’s
include - Use partial types for update operations (
Partial<CreateUserDto>) - Handle errors with NestJS exception filters
- Enable strict mode in TypeScript config
- 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: '[email protected]', 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!