All articles
Tutorial

Building a REST API with NestJS, Prisma, and PostgreSQL

A complete tutorial for building a production-ready REST API using NestJS, Prisma ORM, and PostgreSQL β€” from schema design to running endpoints.

March 24, 202611 min read

NestJS with Prisma and PostgreSQL is one of the most productive backend stacks available in 2026. NestJS provides structure and dependency injection, Prisma handles database access with type safety, and PostgreSQL gives you a battle-tested relational database. Here's how to build a complete REST API with this stack.

Project Setup

Create a new NestJS project and install Prisma:

npm i -g @nestjs/cli
nest new my-api
cd my-api
npm install prisma @prisma/client
npx prisma init

This creates a prisma/schema.prisma file and a .env with a placeholder DATABASE_URL.

Configure the Database Connection

Update .env with your PostgreSQL connection string:

DATABASE_URL="postgresql://postgres:password@localhost:5432/myapi_db"

If you don't have PostgreSQL installed, the quickest way is Docker:

docker run --name my-postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=myapi_db -p 5432:5432 -d postgres:16

Define Your Schema

Open prisma/schema.prisma and define your models. Let's build a simple task management API:

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

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

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  tasks     Task[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Task {
  id          String     @id @default(cuid())
  title       String
  description String?
  priority    Priority   @default(MEDIUM)
  status      TaskStatus @default(TODO)
  dueDate     DateTime?
  userId      String
  user        User       @relation(fields: [userId], references: [id])
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt
}

enum Priority {
  LOW
  MEDIUM
  HIGH
}

enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
}

Run the migration to create the tables:

npx prisma migrate dev --name init

Create a Prisma Service

Create a shared Prisma service that the rest of your application injects:

// 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();
  }
}
// src/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

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

Build the Tasks Module

// src/tasks/tasks.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateTaskDto } from './dto/create-task.dto';

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

  async findAll(userId: string) {
    return this.prisma.task.findMany({
      where: { userId },
      orderBy: { createdAt: 'desc' },
    });
  }

  async findOne(id: string, userId: string) {
    const task = await this.prisma.task.findFirst({
      where: { id, userId },
    });
    if (!task) throw new NotFoundException('Task not found');
    return task;
  }

  async create(userId: string, dto: CreateTaskDto) {
    return this.prisma.task.create({
      data: { ...dto, userId },
    });
  }

  async update(id: string, userId: string, dto: Partial<CreateTaskDto>) {
    await this.findOne(id, userId); // throws if not found
    return this.prisma.task.update({
      where: { id },
      data: dto,
    });
  }

  async remove(id: string, userId: string) {
    await this.findOne(id, userId);
    return this.prisma.task.delete({ where: { id } });
  }
}

Validation with DTOs

Install validation packages and use class-validator decorators:

npm install class-validator class-transformer
// src/tasks/dto/create-task.dto.ts
import { IsString, IsOptional, IsEnum, IsDateString } from 'class-validator';
import { Priority, TaskStatus } from '@prisma/client';

export class CreateTaskDto {
  @IsString()
  title: string;

  @IsString()
  @IsOptional()
  description?: string;

  @IsEnum(Priority)
  @IsOptional()
  priority?: Priority;

  @IsDateString()
  @IsOptional()
  dueDate?: string;
}

Enable validation globally in main.ts:

app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

Testing Your Endpoints

Once running (npm run start:dev), your API is available at http://localhost:3000:

POST /tasks          β†’ Create a task
GET  /tasks          β†’ List all tasks
GET  /tasks/:id      β†’ Get one task
PATCH /tasks/:id     β†’ Update a task
DELETE /tasks/:id    β†’ Delete a task

Skip the Setup With PromptForge

This tutorial covered the basics of a NestJS + Prisma + PostgreSQL setup. A production application also needs authentication, rate limiting, error handling, Swagger documentation, Docker configuration, and CI/CD. Setting all of that up from scratch takes 1–2 days.

PromptForge generates all of it β€” the schema, services, controllers, auth module, Dockerfile, and documentation β€” from a single prompt in under five minutes. Try it for free.

Ready to build your SaaS with AI?

Generate a complete NestJS + Prisma backend from a single prompt β€” free to try.

Start for free