Build a Pet Management REST API
Build a NestJS REST API with Prisma ORM and PostgreSQL for a vet clinic.
Introduction
30 Second Summary
Every veterinary clinic needs a way to track which pets belong to which owners. Behind the scenes, that means a backend API that stores, retrieves, and updates patient records reliably.
In this project, you will build a Pet Management REST API using TypeScript, NestJS, Prisma, and PostgreSQL. You will create endpoints for managing pet owners and their pets, complete with input validation and relational data.
What You'll Build
A fully functional REST API for a veterinary clinic that handles pet patient registration and owner management, running locally with full CRUD operations.
By the end of this project, you'll have:
- A NestJS backend with modular architecture including Prisma, Owners, and Pets modules.
- A PostgreSQL database with a properly migrated Prisma schema containing Owner and Pet models linked by a one-to-many relationship.
- Full CRUD REST API endpoints with input validation, tested and verified via Postman.
- Secret Mission: Add a vaccination tracking endpoint with date-based filtering.
Are there any prerequisites?
This project assumes basic understanding of backend concepts like controllers, services, middleware, and ORMs. If you have experience with PHP/Laravel, you will find the patterns here map directly to what you already know.
You should also be familiar with REST APIs and relational databases. No prior TypeScript or NestJS experience is required.
Before We Start
Before We Start...
Install Node.js and Set Up the Project
Building a REST API requires three foundational pieces: a runtime to execute your code, a framework to structure it, and a database to store data.
In this step, you will install Node.js as the runtime, scaffold a NestJS project as the framework, and spin up PostgreSQL in Docker as the database.
In this step, get ready to:
- Install Node.js v24 LTS and verify it works.
- Scaffold a NestJS project using the CLI.
- Start a PostgreSQL database in a Docker container.
Install Node.js v24 LTS
Node.js is the JavaScript runtime that powers NestJS. You need version 20 or newer to run NestJS projects.
- Open your browser and navigate to the Node.js download page:
- https://nodejs.org/en/download
- Click the Windows Installer (.msi) option for 64-bit.
- Run the downloaded .msi file and follow the installation wizard.
- Accept the default settings on each screen and click Next until the installation completes.
What does this installer do?
The Node.js MSI installer adds both node (the JavaScript runtime) and npm (the package manager) to your system PATH. This means you can run both commands from any terminal window after installation.
After the installer finishes, verify it worked.
- Open a new terminal window by pressing the Windows key, typing Terminal, and pressing Enter.
- Check your Node.js version by running:
node --version
- Check your npm version by running:
npm --version
✔️ I see v24.x for Node and a version number for npm
Node.js is ready to go. Move on to the next substep.
ⓧ I see a version lower than 24
You have an older version of Node.js installed. Download and run the v24 LTS installer from the link above to upgrade. The installer replaces your existing version automatically.
- After upgrading, close and reopen your terminal, then run node --version again to confirm it shows v24.
ⓧ Command not found
The installer did not add Node.js to your PATH, or you need to restart your terminal.
- Close your terminal completely and open a new one.
- Run node --version again.
- If it still fails, re-run the MSI installer and make sure the Add to PATH option is checked.
Scaffold the NestJS project
The NestJS CLI generates a fully structured project with controllers, services, modules, and TypeScript configuration already wired up. This is similar to how php artisan scaffolds Laravel projects.
- Install the NestJS CLI globally by running:
npm install -g @nestjs/cli
What does this command do?
The -g flag installs the package globally, meaning the nest command becomes available in any terminal window. You only need to do this once per machine.
Permission error on install?
If you see an EACCES or permissions error, try running the command in an elevated terminal. Right-click Terminal in your Start menu and select Run as administrator.
Help me fix this permissions error.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your NestJS project by running this command:
nest new pet-management-api --package-manager npm --strict
What do these flags do?
The --package-manager npm flag tells the CLI to use npm (skipping the package manager prompt). The --strict flag enables strict TypeScript checking, which catches more bugs at compile time.
The CLI downloads dependencies and scaffolds your project. This takes about a minute.
- Once it finishes, move into the project folder by running:
cd pet-management-api
- Confirm the project files exist by running:
ls
You should see files like package.json, tsconfig.json, nest-cli.json, and a src/ folder in the output.
Nest command not found?
Close and reopen your terminal so it picks up the newly installed nest command. If it still fails, verify the global install worked by running npm list -g @nestjs/cli.
Help me troubleshoot the nest command not being found.
Start PostgreSQL in Docker
Your API needs a database to store owners and pets. Running PostgreSQL inside a Docker container keeps it isolated from your system and easy to reset.
Why Docker for the database?
Installing PostgreSQL directly on your machine creates background services and config files that are hard to clean up. A Docker container gives you a fresh database in one command, and you can delete it without leaving traces.
- Make sure Docker Desktop is running. Press the Windows key, type Docker Desktop, and press Enter to open it.
- Wait until the Docker Desktop status bar shows Engine running (green icon in the bottom-left corner).
Now you will create a docker-compose.yml file that defines your PostgreSQL container. This file makes it easy to start, stop, and recreate the database with a single command.
- Open the pet-management-api project in VS Code by running:
code .
- In the VS Code Explorer sidebar (left panel), right-click in the empty space below the existing files.
- Select New File.
- Type docker-compose.yml as the filename and press Enter.
- Paste the following configuration into the new file:
version: "3.8"
services:
postgres:
image: postgres:16
container_name: pet-postgres
environment:
POSTGRES_USER: petadmin
POSTGRES_PASSWORD: petpass123
POSTGRES_DB: pet_management
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
What does this configuration do?
- image: postgres:16 pulls the official PostgreSQL 16 image from Docker Hub.
- The environment section sets the database username, password, and database name.
- ports: "5432:5432" maps the container's PostgreSQL port to your machine so your app can connect.
- The pgdata volume persists your data even if you stop or restart the container.
- Save docker-compose.yml by pressing Ctrl+S.
- Open the VS Code integrated terminal by pressing Ctrl+`.
- Start the PostgreSQL container by running:
docker compose up -d
- Verify the container is running by running:
docker ps
You should see a row showing pet-postgres with a status of Up and port 5432 mapped.
Container not starting?
If you see a port conflict error, another service is already using port 5432. Check if you have a local PostgreSQL installation running and stop it, or change the port mapping in docker-compose.yml to "5433:5432".
If Docker says the daemon is not running, make sure Docker Desktop is open and shows Engine running.
Help me troubleshoot my PostgreSQL Docker container not starting.
✔️ Awesome, I've got everything!
Great. Double check you have saved docker-compose.yml and that docker ps shows your container running.
ⓧ I'd like to double check the full code
Here is the complete docker-compose.yml file. Make sure yours matches exactly:
version: "3.8"
services:
postgres:
image: postgres:16
container_name: pet-postgres
environment:
POSTGRES_USER: petadmin
POSTGRES_PASSWORD: petpass123
POSTGRES_DB: pet_management
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Your development environment is ready. Next up, you will connect Prisma to your PostgreSQL database and define the schema for owners and pets.
Configure Prisma and Define the Database Schema
Your NestJS project is scaffolded and PostgreSQL is running in Docker. Now you need a way for your TypeScript code to talk to that database.
In this step, you will install Prisma, define your database schema with Owner and Pet models, and run your first migration to create the actual tables in PostgreSQL.
In this step, get ready to:
- Install Prisma and initialize it with a PostgreSQL connection.
- Define Owner and Pet models with a one-to-many relationship.
- Run your first database migration and verify the tables exist.
Install Prisma and connect to PostgreSQL
Prisma is an ORM (Object-Relational Mapper) that lets you define your database schema in a dedicated file and generates a fully typed client for querying your data. If you have used Eloquent in Laravel, Prisma fills the same role but with stronger type safety.
- Install the Prisma CLI as a dev dependency by running this command:
npm install prisma --save-dev
- Install the Prisma Client package (the runtime library your app uses to query the database) by running:
npm install @prisma/client
- Initialize Prisma in your project by running:
npx prisma init --datasource-provider postgresql
What does this command do?
This creates two new files in your project:
- prisma/schema.prisma is where you define your database models (like Laravel migrations + Eloquent models combined).
- .env stores your database connection string.
- Confirm the files were created by running:
ls prisma/
You should see schema.prisma listed in the output.
Now you need to point Prisma at your Docker PostgreSQL container. The connection string tells Prisma which user, password, host, port, and database to use.
- Open the .env file in the root of your pet-management-api folder.
- Replace the entire contents of the file with this connection string:
DATABASE_URL="postgresql://petadmin:petpass123@localhost:5432/pet_management?schema=public"
What does this connection string mean?
- petadmin:petpass123 matches the POSTGRES_USER and POSTGRES_PASSWORD you set when creating the Docker container.
- localhost:5432 is the host and port where PostgreSQL is listening (mapped from Docker).
- pet_management is the database name you specified with POSTGRES_DB.
- Save .env.
Port 5432 already in use?
If you have a local PostgreSQL installation running on port 5432, it will conflict with the Docker container. Stop the local PostgreSQL service, or change the port mapping in your Docker container to -p 5433:5432 and update the connection string to use port 5433.
Help me resolve a port conflict with PostgreSQL on port 5432.
Define the Owner and Pet models
Your veterinary clinic needs to track two things: pet owners and their pets. One owner can have many pets, but each pet belongs to exactly one owner. This is a one-to-many relationship.
- Open prisma/schema.prisma in VS Code.
- Replace the entire contents of the file with the following schema:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Owner model - represents a pet owner at the vet clinic
model Owner {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
phone String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
pets Pet[] // One owner can have many pets
}
// Pet model - represents a pet patient
model Pet {
id Int @id @default(autoincrement())
name String
species String
breed String? // Optional - not all pets have a known breed
age Int? // Optional - age might be unknown
weight Float? // Optional - weight might not be recorded yet
ownerId Int
owner Owner @relation(fields: [ownerId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
What does this schema do?
This schema defines two database tables and the relationship between them:
- @id @default(autoincrement()) creates an auto-incrementing primary key (like Laravel's $table->id()).
- @unique on email prevents duplicate owner registrations.
- String? and Int? make fields nullable (optional data the clinic might not have yet).
- pets Pet[] on the Owner model defines the "one" side of the relationship.
- @relation(fields: [ownerId], references: [id]) on the Pet model creates the foreign key linking each pet to its owner.
- Save prisma/schema.prisma.
Seeing red squiggly lines in the schema file?
VS Code might not recognize .prisma files by default. This is purely cosmetic and does not affect the migration. You can install the Prisma VS Code extension from the Extensions sidebar for syntax highlighting.
Help me fix Prisma schema errors in VS Code.
Run the migration and verify the tables
Your schema file describes what you want the database to look like. A migration translates that description into actual SQL commands that create the tables. This is the same concept as php artisan migrate in Laravel.
- Make sure your Docker container is still running. Verify by running:
docker ps
You should see pet-postgres listed with status "Up". If it is not running, restart it with docker start pet-postgres.
- Run your first migration by executing this command:
npx prisma migrate dev --name init
What does this command do?
- prisma migrate dev creates a new migration file with the SQL needed to create your tables.
- --name init gives the migration a descriptive name (you will see a folder called prisma/migrations/ with a timestamped subfolder).
- It also runs prisma generate automatically, which creates the typed client your app will use to query the database.
You should see output that includes "Your database is now in sync with your schema" and "Generated Prisma Client".
Seeing a connection error?
If you see "Can't reach database server", make sure the Docker container is running with docker ps. If it is not listed, restart it with docker start pet-postgres. Also double-check that your .env file has the correct DATABASE_URL with no typos in the username, password, or port.
Help me fix a Prisma connection error to PostgreSQL.
- Verify the tables were created by opening Prisma Studio:
npx prisma studio
This opens a visual database browser in your web browser at http://localhost:5555. You should see both the Owner and Pet models listed. They are empty for now, but the tables exist.
- Press Ctrl+C in your terminal to stop Prisma Studio when you are done exploring.
✔️ Awesome, I've got everything!
Great. Double check you have saved both .env and prisma/schema.prisma.
ⓧ I'd like to double check the full code
Here is the full .env file:
DATABASE_URL="postgresql://petadmin:petpass123@localhost:5432/pet_management?schema=public"
Here is the full prisma/schema.prisma file:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Owner {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
phone String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
pets Pet[]
}
model Pet {
id Int @id @default(autoincrement())
name String
species String
breed String?
age Int?
weight Float?
ownerId Int
owner Owner @relation(fields: [ownerId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Your database is set up and your schema is migrated. Next up, you will build the NestJS service layer that connects your application code to this database through Prisma.
Create the Prisma Service and Owners Module
Your database schema is ready, but the NestJS application has no way to talk to it yet. You need a service layer that bridges Prisma and NestJS's dependency injection system.
In this step, you'll create a shared Prisma service, then build the complete Owners module with a controller, service, and validated DTOs. This mirrors the Controller-Service-FormRequest pattern from Laravel.
In this step, get ready to:
- Create a PrismaService and PrismaModule for database access.
- Build the Owners module with full CRUD endpoints.
- Add input validation using DTOs and class-validator.
Create the Prisma service and module
NestJS uses dependency injection to share services across your application. To let any module access the database, you'll wrap Prisma in a NestJS service with proper lifecycle hooks.
- Create a new folder called prisma inside your src/ directory.
- Create a file called prisma.service.ts inside src/prisma/ and paste the following code:
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();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
What does this code do?
PrismaService extends PrismaClient, which means it inherits all of Prisma's database query methods (like .owner.create() and .pet.findMany()).
The onModuleInit hook connects to the database when NestJS starts up. The onModuleDestroy hook disconnects cleanly when the app shuts down. The @Injectable() decorator registers this class as a NestJS provider that can be injected anywhere.
- Save prisma.service.ts.
- Create a second file called prisma.module.ts inside src/prisma/ and paste the following code:
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
What does this code do?
The @Global() decorator makes PrismaService available to every module in your app without needing to import PrismaModule individually each time. The exports array lets other modules inject PrismaService through NestJS's dependency injection.
- Save prisma.module.ts.
You won't see a visible change yet. The next chunk uses this module when we wire up the Owners feature.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files in the src/prisma/ folder.
ⓧ I'd like to double check the full code
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();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
Generate and implement the Owners module
NestJS provides a CLI generator that scaffolds a full CRUD resource in seconds. It creates the module, controller, service, DTOs, and entity files automatically.
- Generate the Owners resource by running this command:
nest generate resource owners --no-spec
What prompts will I see?
The CLI will ask two questions:
- "What transport layer do you use?" Select REST API.
- "Would you like to generate CRUD entry points?" Select Yes.
- Verify the output shows files created inside src/owners/ and that app.module.ts was updated.
Command not found?
Make sure you're running the command from inside your pet-management-api project folder. If you see a "nest: command not found" error, the NestJS CLI may not be installed globally. Reinstall it with:
npm install -g @nestjs/cli
help me fix the nest generate resource command
The generated service has placeholder methods that return static strings. You'll now replace it with real database logic using PrismaService.
- Open src/owners/owners.service.ts and replace all of its content with the following:
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateOwnerDto } from './dto/create-owner.dto';
import { UpdateOwnerDto } from './dto/update-owner.dto';
@Injectable()
export class OwnersService {
constructor(private prisma: PrismaService) {}
// Create a new owner record in the database
async create(createOwnerDto: CreateOwnerDto) {
return this.prisma.owner.create({
data: createOwnerDto,
});
}
// Retrieve all owners with their pets included
async findAll() {
return this.prisma.owner.findMany({
include: { pets: true },
});
}
// Find a single owner by ID, throw 404 if not found
async findOne(id: number) {
const owner = await this.prisma.owner.findUnique({
where: { id },
include: { pets: true },
});
if (!owner) {
throw new NotFoundException(`Owner with ID ${id} not found`);
}
return owner;
}
// Update an existing owner's data
async update(id: number, updateOwnerDto: UpdateOwnerDto) {
await this.findOne(id);
return this.prisma.owner.update({
where: { id },
data: updateOwnerDto,
});
}
// Delete an owner from the database
async remove(id: number) {
await this.findOne(id);
return this.prisma.owner.delete({
where: { id },
});
}
}
What does this code do?
- The constructor injects PrismaService, giving the service access to all database models.
- findAll and findOne use Prisma's include option to eagerly load each owner's pets in a single query.
- findOne throws a NotFoundException (HTTP 404) when no owner matches the given ID. The update and remove methods call findOne first to ensure the owner exists before modifying data.
- Save owners.service.ts.
- Open src/owners/owners.controller.ts and replace all of its content with the following:
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe } from '@nestjs/common';
import { OwnersService } from './owners.service';
import { CreateOwnerDto } from './dto/create-owner.dto';
import { UpdateOwnerDto } from './dto/update-owner.dto';
@Controller('owners')
export class OwnersController {
constructor(private readonly ownersService: OwnersService) {}
// POST /owners - create a new owner
@Post()
create(@Body() createOwnerDto: CreateOwnerDto) {
return this.ownersService.create(createOwnerDto);
}
// GET /owners - list all owners
@Get()
findAll() {
return this.ownersService.findAll();
}
// GET /owners/:id - get one owner by ID
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.ownersService.findOne(id);
}
// PATCH /owners/:id - update an owner
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() updateOwnerDto: UpdateOwnerDto) {
return this.ownersService.update(id, updateOwnerDto);
}
// DELETE /owners/:id - remove an owner
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.ownersService.remove(id);
}
}
What does this code do?
- @Controller('owners') sets the base route prefix. All endpoints in this class are under /owners.
- ParseIntPipe automatically converts the :id URL parameter from a string to a number and returns a 400 error if it is not a valid integer.
- @Body() extracts the request body and maps it to the DTO type, which will be validated once you enable the ValidationPipe.
- Save owners.controller.ts.
You won't see a change yet. The next chunk adds input validation so the endpoints reject invalid data.
Seeing red squiggly lines in your editor?
Your editor may show import errors for the DTO files because you haven't updated them yet. This is expected and will resolve in the next substep.
help me understand the TypeScript errors in my owners module
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateOwnerDto } from './dto/create-owner.dto';
import { UpdateOwnerDto } from './dto/update-owner.dto';
@Injectable()
export class OwnersService {
constructor(private prisma: PrismaService) {}
async create(createOwnerDto: CreateOwnerDto) {
return this.prisma.owner.create({
data: createOwnerDto,
});
}
async findAll() {
return this.prisma.owner.findMany({
include: { pets: true },
});
}
async findOne(id: number) {
const owner = await this.prisma.owner.findUnique({
where: { id },
include: { pets: true },
});
if (!owner) {
throw new NotFoundException(`Owner with ID ${id} not found`);
}
return owner;
}
async update(id: number, updateOwnerDto: UpdateOwnerDto) {
await this.findOne(id);
return this.prisma.owner.update({
where: { id },
data: updateOwnerDto,
});
}
async remove(id: number) {
await this.findOne(id);
return this.prisma.owner.delete({
where: { id },
});
}
}
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe } from '@nestjs/common';
import { OwnersService } from './owners.service';
import { CreateOwnerDto } from './dto/create-owner.dto';
import { UpdateOwnerDto } from './dto/update-owner.dto';
@Controller('owners')
export class OwnersController {
constructor(private readonly ownersService: OwnersService) {}
@Post()
create(@Body() createOwnerDto: CreateOwnerDto) {
return this.ownersService.create(createOwnerDto);
}
@Get()
findAll() {
return this.ownersService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.ownersService.findOne(id);
}
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() updateOwnerDto: UpdateOwnerDto) {
return this.ownersService.update(id, updateOwnerDto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.ownersService.remove(id);
}
}
import { Module } from '@nestjs/common';
import { OwnersService } from './owners.service';
import { OwnersController } from './owners.controller';
@Module({
controllers: [OwnersController],
providers: [OwnersService],
})
export class OwnersModule {}
Add validation DTOs and enable the validation pipe
DTOs (Data Transfer Objects) define the shape of incoming request data. Combined with class-validator decorators, they automatically reject invalid requests before your service code runs.
- Install the validation packages by running this command:
npm install class-validator class-transformer
- Verify the install completes without errors.
- Open src/owners/dto/create-owner.dto.ts and replace all of its content with the following:
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class CreateOwnerDto {
@IsEmail()
email: string;
@IsString()
@IsNotEmpty()
firstName: string;
@IsString()
@IsNotEmpty()
lastName: string;
@IsString()
@IsNotEmpty()
phone: string;
}
What does this code do?
Each decorator validates a specific rule. @IsEmail() ensures the email field contains a valid email address. @IsString() confirms the value is a string. @IsNotEmpty() rejects empty strings.
If a request fails any check, NestJS automatically returns a 400 Bad Request response with details about which fields failed. This is the same concept as Laravel's Form Request validation.
- Save create-owner.dto.ts.
- Open src/owners/dto/update-owner.dto.ts and replace all of its content with the following:
import { PartialType } from '@nestjs/mapped-types';
import { CreateOwnerDto } from './create-owner.dto';
export class UpdateOwnerDto extends PartialType(CreateOwnerDto) {}
What is PartialType?
PartialType takes your CreateOwnerDto and creates a new class where every field is optional. This means PATCH requests can send only the fields they want to update. It is the same concept as Laravel's sometimes() validation rule.
- Save update-owner.dto.ts.
- Open src/main.ts and replace all of its content with the following:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable global validation for all incoming requests
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
await app.listen(3000);
}
bootstrap();
What do these ValidationPipe options do?
- whitelist: true strips any properties that are not defined in the DTO. Extra fields get silently removed.
- forbidNonWhitelisted: true goes further and throws a 400 error if unknown properties are sent.
- transform: true automatically converts plain JSON objects into DTO class instances so the decorators can run.
- Save main.ts.
- Open src/app.module.ts and replace all of its content with the following:
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { OwnersModule } from './owners/owners.module';
import { PetsModule } from './pets/pets.module';
@Module({
imports: [PrismaModule, OwnersModule, PetsModule],
})
export class AppModule {}
What does this code do?
This replaces the default AppModule (which included AppController and AppService) with a clean module that only imports the three feature modules. PrismaModule provides database access globally. OwnersModule handles owner endpoints. PetsModule will handle pet endpoints in the next step.
Why does the file reference PetsModule?
PetsModule does not exist yet. Your editor will show a red underline on that import. This is expected. You will create PetsModule in the next step, and the error will disappear then.
- Save app.module.ts.
Seeing other TypeScript errors?
If you see errors beyond the expected PetsModule import error, check that you saved all four files: create-owner.dto.ts, update-owner.dto.ts, main.ts, and app.module.ts. Also confirm that class-validator and class-transformer installed successfully by checking your package.json for both entries.
help me fix TypeScript errors in my NestJS project
✔️ Awesome, I've got everything!
Great. Make sure all files are saved before continuing.
ⓧ I'd like to double check the full code
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class CreateOwnerDto {
@IsEmail()
email: string;
@IsString()
@IsNotEmpty()
firstName: string;
@IsString()
@IsNotEmpty()
lastName: string;
@IsString()
@IsNotEmpty()
phone: string;
}
import { PartialType } from '@nestjs/mapped-types';
import { CreateOwnerDto } from './create-owner.dto';
export class UpdateOwnerDto extends PartialType(CreateOwnerDto) {}
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
await app.listen(3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { OwnersModule } from './owners/owners.module';
import { PetsModule } from './pets/pets.module';
@Module({
imports: [PrismaModule, OwnersModule, PetsModule],
})
export class AppModule {}
Your Owners module is complete with full CRUD operations and input validation. Next up, you'll build the Pets module, wire up the owner-pet relationship in the API responses, and test everything with Postman.
Build the Pets Module with Owner Relationship
Your Owners module is live with full CRUD operations and input validation. Every pet in a veterinary system belongs to someone, so the next step is building the Pets module that links each pet record to its owner.
In this step, you will generate the Pets resource, implement type-safe CRUD operations with the owner relationship, and test the full API flow in Postman.
In this step, get ready to:
- Generate the Pets resource and implement the PetsService with owner relationship.
- Create validated DTOs and implement the PetsController.
- Test all endpoints in Postman to verify the one-to-many relationship.
Generate the Pets resource and implement the service
Just like you did with Owners, the NestJS CLI can scaffold the entire Pets module in one command. This creates the module, controller, service, and DTO files with CRUD boilerplate ready to customize.
- Generate the Pets resource by running this command in your terminal:
nest generate resource pets --no-spec
- When prompted for the transport layer, select REST API.
- When asked whether to generate CRUD entry points, enter yes.
What does this command do?
The --no-spec flag skips generating test files so you can focus on the application code. The CLI creates src/pets/ with a module, controller, service, and DTO files.
- You should see output confirming the creation of files inside src/pets/.
Seeing a conflict with app.module.ts?
If the CLI reports that app.module.ts already imports PetsModule, that is expected. Your app.module.ts from the previous step already includes the import. The CLI will skip that update.
help me resolve the NestJS CLI conflict
Now you need to replace the generated boilerplate in the service file with your actual Prisma-backed implementation. The PetsService follows the same pattern as OwnersService, but every query includes the owner relation so pet responses always contain their owner's details.
- Open src/pets/pets.service.ts and replace its entire contents with the following code:
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePetDto } from './dto/create-pet.dto';
import { UpdatePetDto } from './dto/update-pet.dto';
@Injectable()
export class PetsService {
constructor(private prisma: PrismaService) {}
// Create a new pet and return it with owner details
async create(createPetDto: CreatePetDto) {
return this.prisma.pet.create({
data: createPetDto,
include: { owner: true },
});
}
// Retrieve all pets with their owners
async findAll() {
return this.prisma.pet.findMany({
include: { owner: true },
});
}
// Find a single pet by ID, throw if not found
async findOne(id: number) {
const pet = await this.prisma.pet.findUnique({
where: { id },
include: { owner: true },
});
if (!pet) {
throw new NotFoundException(`Pet with ID ${id} not found`);
}
return pet;
}
// Update pet fields and return the updated pet with owner
async update(id: number, updatePetDto: UpdatePetDto) {
await this.findOne(id);
return this.prisma.pet.update({
where: { id },
data: updatePetDto,
include: { owner: true },
});
}
// Delete a pet by ID
async remove(id: number) {
await this.findOne(id);
return this.prisma.pet.delete({
where: { id },
});
}
}
What does this code do?
Every method uses include: { owner: true } to eagerly load the owner relationship. This means any response about a pet automatically includes the owner object nested inside it.
The findOne method throws a NotFoundException if the pet does not exist. The update and remove methods call findOne first to verify the pet exists before modifying it.
- Save src/pets/pets.service.ts.
Seeing red underlines on the DTO imports?
The imports for CreatePetDto and UpdatePetDto may show errors because you have not updated those files yet. This resolves in the next substep.
help me fix TypeScript import errors in my PetsService
✔️ Awesome, I've got everything!
Great. Make sure you have saved src/pets/pets.service.ts before continuing.
ⓧ I'd like to double check the full code
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePetDto } from './dto/create-pet.dto';
import { UpdatePetDto } from './dto/update-pet.dto';
@Injectable()
export class PetsService {
constructor(private prisma: PrismaService) {}
async create(createPetDto: CreatePetDto) {
return this.prisma.pet.create({
data: createPetDto,
include: { owner: true },
});
}
async findAll() {
return this.prisma.pet.findMany({
include: { owner: true },
});
}
async findOne(id: number) {
const pet = await this.prisma.pet.findUnique({
where: { id },
include: { owner: true },
});
if (!pet) {
throw new NotFoundException(`Pet with ID ${id} not found`);
}
return pet;
}
async update(id: number, updatePetDto: UpdatePetDto) {
await this.findOne(id);
return this.prisma.pet.update({
where: { id },
data: updatePetDto,
include: { owner: true },
});
}
async remove(id: number) {
await this.findOne(id);
return this.prisma.pet.delete({
where: { id },
});
}
}
Create the DTOs and implement the controller
DTOs define the shape of data your API accepts. For pets, some fields are required (name, species, ownerId) while others are optional (breed, age, weight). Validation decorators enforce these rules automatically.
- Open src/pets/dto/create-pet.dto.ts and replace its entire contents with the following code:
import { IsInt, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
export class CreatePetDto {
// Required: the pet's name
@IsString()
@IsNotEmpty()
name: string;
// Required: cat, dog, bird, etc.
@IsString()
@IsNotEmpty()
species: string;
// Optional: specific breed
@IsString()
@IsOptional()
breed?: string;
// Optional: age in years
@IsInt()
@IsOptional()
age?: number;
// Optional: weight in kg
@IsNumber()
@IsOptional()
weight?: number;
// Required: links this pet to an existing owner
@IsInt()
ownerId: number;
}
What does this code do?
Each decorator validates incoming request data before it reaches your service. @IsOptional() means the field can be omitted entirely. The ? on the TypeScript type matches this for type safety.
The ownerId field is required with no @IsOptional() decorator. Every pet must belong to an owner when created.
- Save src/pets/dto/create-pet.dto.ts.
- Open src/pets/dto/update-pet.dto.ts and replace its entire contents with the following code:
import { PartialType } from '@nestjs/mapped-types';
import { CreatePetDto } from './create-pet.dto';
export class UpdatePetDto extends PartialType(CreatePetDto) {}
What is PartialType?
PartialType takes your CreatePetDto and makes every field optional. This lets PATCH requests send only the fields that need updating. It is the same pattern you used for UpdateOwnerDto.
- Save src/pets/dto/update-pet.dto.ts.
Now implement the controller that maps HTTP requests to your service methods. The PetsController follows the same structure as OwnersController.
- Open src/pets/pets.controller.ts and replace its entire contents with the following code:
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe } from '@nestjs/common';
import { PetsService } from './pets.service';
import { CreatePetDto } from './dto/create-pet.dto';
import { UpdatePetDto } from './dto/update-pet.dto';
@Controller('pets')
export class PetsController {
constructor(private readonly petsService: PetsService) {}
// POST /pets - register a new pet
@Post()
create(@Body() createPetDto: CreatePetDto) {
return this.petsService.create(createPetDto);
}
// GET /pets - list all pets
@Get()
findAll() {
return this.petsService.findAll();
}
// GET /pets/:id - get a single pet by ID
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.petsService.findOne(id);
}
// PATCH /pets/:id - update a pet's fields
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() updatePetDto: UpdatePetDto) {
return this.petsService.update(id, updatePetDto);
}
// DELETE /pets/:id - remove a pet
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.petsService.remove(id);
}
}
What does this code do?
The @Controller('pets') decorator sets the base route to /pets. Each method decorator (@Get(), @Post(), etc.) maps an HTTP verb to that method.
ParseIntPipe automatically converts the :id URL parameter from a string to a number. If someone sends a non-numeric ID like /pets/abc, NestJS returns a 400 Bad Request before your code even runs.
- Save src/pets/pets.controller.ts.
Seeing TypeScript errors in your editor?
Make sure you saved all three files: create-pet.dto.ts, update-pet.dto.ts, and pets.controller.ts. TypeScript resolves imports across files, so unsaved changes can trigger false errors.
help me fix TypeScript errors in my Pets module files
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved: pets.service.ts, create-pet.dto.ts, update-pet.dto.ts, and pets.controller.ts.
ⓧ I'd like to double check the full code
import { IsInt, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
export class CreatePetDto {
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@IsNotEmpty()
species: string;
@IsString()
@IsOptional()
breed?: string;
@IsInt()
@IsOptional()
age?: number;
@IsNumber()
@IsOptional()
weight?: number;
@IsInt()
ownerId: number;
}
import { PartialType } from '@nestjs/mapped-types';
import { CreatePetDto } from './create-pet.dto';
export class UpdatePetDto extends PartialType(CreatePetDto) {}
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe } from '@nestjs/common';
import { PetsService } from './pets.service';
import { CreatePetDto } from './dto/create-pet.dto';
import { UpdatePetDto } from './dto/update-pet.dto';
@Controller('pets')
export class PetsController {
constructor(private readonly petsService: PetsService) {}
@Post()
create(@Body() createPetDto: CreatePetDto) {
return this.petsService.create(createPetDto);
}
@Get()
findAll() {
return this.petsService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.petsService.findOne(id);
}
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() updatePetDto: UpdatePetDto) {
return this.petsService.update(id, updatePetDto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.petsService.remove(id);
}
}
Test all endpoints in Postman
Your code is complete. Time to start the server and verify the entire flow works end to end.
- Start the NestJS development server by running this command:
npm run start:dev
- You should see output ending with Nest application successfully started and the app listening on port 3000.
Seeing a connection error on startup?
Make sure your PostgreSQL Docker container is still running. Run docker ps to check. If it is not listed, restart it by running:
docker start pet-postgres
help me fix the NestJS database connection error on startup
Open Postman and work through the following requests to verify the full pet management flow.
- Create a new request in Postman.
- Set the method to POST and the URL to http://localhost:3000/owners.
- Click the Body tab, select raw, and choose JSON from the dropdown.
- Paste this JSON body:
{
"email": "sarah@example.com",
"firstName": "Sarah",
"lastName": "Johnson",
"phone": "555-0101"
}
- Click Send.
- You should see a 201 Created response with the owner object including an id field (likely 1).
Why create an owner first?
Pets require a valid ownerId foreign key. You need at least one owner in the database before you can register a pet.
- Create a new request with the method set to POST and the URL to http://localhost:3000/pets.
- In the Body tab (raw JSON), paste this:
{
"name": "Bella",
"species": "Dog",
"breed": "Golden Retriever",
"age": 3,
"weight": 28.5,
"ownerId": 1
}
- Click Send.
- You should see a 201 Created response containing the pet object with a nested owner object inside it.
The nested owner object in the response confirms the one-to-many relationship is working. Prisma joined the data automatically.
- Set the method to GET and the URL to http://localhost:3000/pets/1.
- Click Send.
- You should see Bella's full profile with Sarah's owner details nested inside.
- Set the method to PATCH and the URL to http://localhost:3000/pets/1.
- In the body, paste this JSON to update the pet's weight:
{
"weight": 29.0
}
- Click Send.
- You should see the updated pet with weight now showing 29.0.
- Set the method to DELETE and the URL to http://localhost:3000/pets/1.
- Click Send.
- You should see the deleted pet object returned in the response.
- Verify deletion by sending a GET request to http://localhost:3000/pets/1.
- You should see a 404 response with Pet with ID 1 not found.
Getting a 500 error when creating a pet?
Check that your ownerId in the request body matches an actual owner ID in your database. If you did not create the owner first, or used a different ID, Prisma throws a foreign key constraint error.
Also confirm that your JSON body uses correct field names (ownerId not owner_id).
help me debug the 500 error when creating a pet in my NestJS API
Your Pet Management API is fully operational. You have proven that owners and pets are properly linked through a one-to-many relationship, with full CRUD support on both resources. Next up, you will take on the Secret Mission and extend your API with vaccination tracking.
Secret mission
Add Vaccination Tracking
Your Pet Management API handles basic pet profiles. But a real veterinary clinic needs to track vaccinations and know which pets are due for their next shot. Can you extend your API with a Vaccination model and date-based filtering?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. There are no cloud costs or recurring charges. The only resource consuming system memory is the Docker container running PostgreSQL.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- PostgreSQL Docker container (pet-postgres).
- Docker volume for PostgreSQL data (pgdata).
- NestJS project folder (pet-management-api).
- Global NestJS CLI package (@nestjs/cli).
Keep everything running
No action needed. Choose this if you are still actively building on this project or plan to continue immediately.
- Your PostgreSQL container stays running and accessible on port 5432.
- Your NestJS app can be started anytime with npm run start:dev from the project folder.
- The Docker container uses a small amount of memory while running.
Pause - I'll come back to this later
Stop the Docker container to free up memory. Your data is preserved in the Docker volume and will be there when you return.
- Stop the PostgreSQL container by running:
docker stop pet-postgres
- When you are ready to resume, restart the container by running:
docker start pet-postgres
- Your database tables and data will still be there.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your database data and project files.
- Stop and remove the PostgreSQL container by running:
docker stop pet-postgres
docker rm pet-postgres
- Remove the Docker volume that stored your database data by running:
docker volume rm pet-management-api_pgdata
- Delete the project folder by running:
cd ~/Desktop
rm -rf pet-management-api
- Optionally, uninstall the NestJS CLI globally by running:
npm uninstall -g @nestjs/cli
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional Pet Management REST API using NestJS, Prisma, and PostgreSQL.
You've learned how to:
- Architect a NestJS backend with modular separation of concerns using modules, controllers, services, and DTOs.
- Model one-to-many relationships in a Prisma schema and run type-safe database queries against PostgreSQL.
- Build full CRUD REST API endpoints with input validation using class-validator decorators and test them with Postman.
- Secret Mission: Add vaccination tracking with date-based query filtering, proving you can extend the system independently.
Ready to quiz yourself?