Building A RESTful CRUD API with Nest.js, TypeORM & PostgreSQL: A Step-by-Step Guide
Embark on a personal exploration of building a robust REST API with Nest.js, TypeORM, and PostgreSQL. Throughout this series, I'll guide you through the essentials, from setting up a Nest.js project to connecting to PostgreSQL and implementing RESTful endpoints. We'll delve into the intricacies of database operations, whether you're an experienced developer or just starting, join me on this journey to master the powerful combination of Nest.js, TypeORM, and PostgreSQL, enhancing your backend development skills along the way!
RESTful API Standards
Routes | Description |
---|---|
GET /api/v1/goals | Get goals |
GET /api/v1/goals/1 | Get goal with id of 1 |
POST /api/v1/goals | Add a goal |
PUT /api/v1/goals/1 | Update goal with id of 1 |
PATCH /api/v1/goals/1 | Partially Update goal with id of 1 |
DELETE /api/v1/goals/1 | Delete goal with id of 1 |
Setting Up Your Development Environment
Before we start building our API, we first scaffold the project with the Nest CLI.
Install Nest.js globally:
npm i -g @nestjs/cli
Scaffold the project:
nest new goal-tracker-nestjs
Navigate to your directory that you just created with the following:
cd goal-tracker-nestjs
Start the server
npm run start:dev
Setting A Global Prefix For Our API
Before we jump into creating our API, lets quickly add a prefix to it.
Open
main.ts
in/src
and add the following:import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api/v1'); // Adds prefix await app.listen(3000); } bootstrap();
Creating Our Controllers
Controllers are responsible for handling incoming requests and returning responses to the client.
Let's create our controllers. To create a new controller type the following command:
nest generate controller goals
Now we need to update our
goals.controller.ts
file to the following:import { Controller, Delete, Get, Patch, Post } from '@nestjs/common'; @Controller('goals') export class GoalsController { @Get() findAll() {} @Get() findOne() {} @Post() create() {} @Patch() update() {} @Delete() remove() {} }
Now we need to register this controller with Nest.js. Add
GoalsController
to the controllers array.import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { GoalsController } from './goals/goals.controller'; @Module({ imports: [], controllers: [AppController, GoalsController], providers: [AppService], }) export class AppModule {}
You are done, you will now get successful responses back from the API if you send a
GET
,POST
,PATCH
orDELETE
tohttp://localhost:3000/api/v1/goals
.
Adding Route Parameters To Our Controllers
We would want to add Route Parameters if we want to have dynamic routes and to extract values from the URL.
Update our
goals.controller.ts
file to the following:import { Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; @Controller('goals') export class GoalsController { @Get() findAll() {} // Example #1 - Value @Get(':id') findOne(@Param('id') id) { return id; // 1 } // Example #2 - JSON Object @Get(':id') findOne(@Param() id) { return id; // { // "id": "1" // } } @Post() create() {} @Patch(':id') update(@Param('id') id) {} @Delete(':id') remove(@Param('id') id) {} }
Adding The Request Body
We need to create our Goal Model/Schema in order to describe how our data is going to look, there are also some other use cases such as data validation, security, etc.
Update our
goals.controller.ts
file to the following:import { Body, Controller, Delete, Get, Param, Patch, Post, } from '@nestjs/common'; @Controller('goals') export class GoalsController { // ... @Post() create(@Body() input) { return input; } @Patch(':id') update(@Param('id') id, @Body() input) {} // ... }
Adding The Responses & Status Codes
By default Nest.js adds the HTTP Codes for you but if you want to give a specific Controller a HTTP Code you can do so with the decorator @HttpCode(204)
. We are going to update the current API with the following:
Update our
goals.controller.ts
file to the following:import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, } from '@nestjs/common'; @Controller('goals') export class GoalsController { @Get() findAll() { return [ { id: 1, name: 'Goal 1' }, { id: 2, name: 'Goal 2' }, { id: 3, name: 'Goal 3' }, ]; } @Get(':id') findOne(@Param('id') id) { return { id: 1, name: 'Goal 1', }; } @Post() create(@Body() input) { return input; } @Patch(':id') update(@Param('id') id, @Body() input) { return input; } @Delete(':id') @HttpCode(204) remove(@Param('id') id) {} }
Creating Our Data Transfer Objects (DTO)
We use Data Transfer Objects (DTO) for defining the input properties and their types upfront.
Request Payload (Create)
Let's create our Create DTO. Create a new file called
create-goal.dto.ts
in the directory/src/goals/dtos
Now we need to update our
create-goal.dto.ts
file to the following:export class CreateGoalDto { name: string; priority: string; status: string; createdAt: string; updatedAt: string; }
Now we need to update our
goals.controller.ts
file to the following:import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, } from '@nestjs/common'; import { CreateGoalDto } from './dtos/create-goal.dto'; @Controller('goals') export class GoalsController { // ... @Post() create(@Body() input: CreateGoalDto) { return input; } // ... }
Update Payload (Update)
Install @nestjs/mapped-types:
npm i @nestjs/mapped-types
Let's create our Update DTO. Create a new file called
update-goal.dto.ts
in the directory/src/goals/dtos
Now we need to update our
update-goal.dto.ts
file to the following:import { PartialType } from '@nestjs/mapped-types'; import { CreateGoalDto } from './create-goal.dto'; // Pulls types from CreateGoalDto into UpdateGoalDto export class UpdateGoalDto extends PartialType(CreateGoalDto) {}
To make our code a bit more cleaner, lets combine the Classes together into a single file with the following:
// index.ts import { CreateGoalDto } from './create-goal.dto'; import { UpdateGoalDto } from './update-goal.dto'; export { CreateGoalDto, UpdateGoalDto };
Now we need to update our
goals.controller.ts
file to the following:import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, } from '@nestjs/common'; import { CreateGoalDto, UpdateGoalDto } from './dtos/index'; @Controller('goals') export class GoalsController { // ... @Patch(':id') update(@Param('id') id, @Body() input: UpdateGoalDto) { return input; } // ... }
Example of Our API Working In A Session (Without Being Connected To A Database)
Before we create our entity let's create Enums for our
priority
andstatus
type. Create a new file calledpriority.enum.ts
and another calledstatus.enum.ts
in the directory/src/goals/enums
Now we need to update our
priority.enum.ts
to the following:// priority.enum.ts export enum Priority { LOW = 'Low', MEDIUM = 'Medium', HIGH = 'High', }
Now we need to update our
status.enum.ts
to the following:// status.enum.ts export enum Status { PENDING = 'Pending', IN_PROGRESS = 'In Progress', COMPLETED = 'Completed', }
To make our code a bit more cleaner, lets combine the Enums together into a single file with the following:
// index.ts import { Priority } from './priority.enum'; import { Status } from './status.enum'; export { Priority, Status };
Now we need to update our
create-goal.dto.ts
file to the following:import { Priority, Status } from '../enums'; export class CreateGoalDto { name: string; priority: Priority; status: Status; createdAt: string; updatedAt: string; }
Let's create our Entity. Create a new file called
goal.entity.ts
in the directory/src/goals/entities
Now we need to update our
goal.entity.ts
file to the following:import { Priority, Status } from '../enums'; export class Goal { id: number; name: string; priority: Priority; status: Status; createdAt: Date; updatedAt: Date; }
Now we need to update our
goals.controller.ts
file to the following:import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, } from '@nestjs/common'; import { CreateGoalDto, UpdateGoalDto } from './dtos/index'; import { Goal } from './entities/goal.entity'; import { Priority, Status } from './enums/index'; @Controller('goals') export class GoalsController { private goals: Goal[] = [ { id: 1, name: 'Learn tRPC', priority: Priority.LOW, status: Status.PENDING, createdAt: new Date(), updatedAt: new Date(), }, { id: 2, name: 'Learn Nest.js', priority: Priority.HIGH, status: Status.IN_PROGRESS, createdAt: new Date(), updatedAt: new Date(), }, ]; // GET /api/v1/goals @Get() findAll() { return this.goals; } // GET /api/v1/goals/:id @Get(':id') findOne(@Param('id') id) { const goal = this.goals.find((goal) => goal.id === parseInt(id)); return goal; } // POST /api/v1/goals @Post() create(@Body() input: CreateGoalDto) { const goal = { ...input, createdAt: new Date(input.createdAt), updatedAt: new Date(input.updatedAt), id: this.goals.length + 1, }; this.goals.push(goal); } // PATCH /api/v1/goals/:id @Patch(':id') update(@Param('id') id, @Body() input: UpdateGoalDto) { const index = this.goals.findIndex((goal) => goal.id === parseInt(id)); this.goals[index] = { ...this.goals[index], ...input, createdAt: input.createdAt ? new Date(input.createdAt) : this.goals[index].createdAt, updatedAt: input.updatedAt ? new Date(input.updatedAt) : this.goals[index].updatedAt, }; return this.goals[index]; } // DELETE /api/v1/goals/:id @Delete(':id') @HttpCode(204) remove(@Param('id') id) { this.goals = this.goals.filter((goal) => goal.id !== parseInt(id)); } }
You can make the following requests to each endpoint and the responses will be cached in each session to simulate a real working API.
Setting Up & Connecting To Our Database
Installing TypeORM & PostgreSQL
Install TypeORM and the database you want to use, in this case we will use PostgreSQL (pg):
npm install @nestjs/typeorm typeorm pg
Creating Our Database
Open your favourite database management tool, in this case I will be using pgAdmin.
Right-Click Databases > Create > Database...
Name you database whatever you like, we will be using
goaltracker-db
Now, Click Save
Connecting To Our Database
Let's create two database configurations, one for production called
orm.config.prod.ts
and the other for development calledorm.config.ts
in the directory/src/config
Now we need to update
orm.config.prod.ts
to the following:// orm.config.prod.ts import { registerAs } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { Goal } from 'src/goals/entities/goal.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ type: 'postgres', host: '<YOUR_HOST>', port: 5432, username: '<YOUR_PRODUCTION_DATABASE_USERNAME>', password: '<YOUR_PRODUCTION_DATABASE_PASSWORD>', database: 'goaltracker-db', entities: [Goal], synchronize: false, // Disable this always in production }), );
Now we need to update
orm.config.ts
to the following:// orm.config.ts import { registerAs } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { Goal } from 'src/goals/entities/goal.entity'; export default registerAs( 'orm.config', (): TypeOrmModuleOptions => ({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: '<YOUR_DATABASE_PASSWORD>', database: 'goaltracker-db', entities: [Goal], synchronize: true, }), );
Install @nestjs/config since we will be needing it next:
npm install @nestjs/config
Now, in our
app.module.ts
we need to update our file to the following to use our configuration:// app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import ormConfig from './config/orm.config'; import ormConfigProd from './config/orm.config.prod'; import { Goal } from './goals/entities/goal.entity'; import { GoalsController } from './goals/goals.controller'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [ormConfig], expandVariables: true, }), TypeOrmModule.forRootAsync({ useFactory: process.env.NODE_ENV !== 'production' ? ormConfig : ormConfigProd, }), TypeOrmModule.forFeature([Goal]), ], controllers: [AppController, GoalsController], providers: [AppService], }) export class AppModule {}
Finally, we will need to mark our Goal Class as an Entity and the properties as Columns so that we can define the structure of our data. Update
goal.entity.ts
to the following:// goal.entity.ts import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; import { Priority, Status } from '../enums'; @Entity() export class Goal { @PrimaryGeneratedColumn('uuid') id: number; @Column() name: string; @Column({ type: 'enum', enum: Priority, default: Priority.LOW, }) priority: Priority; @Column({ type: 'enum', enum: Status, default: Status.PENDING, }) status: Status; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }
Restart the server and you should see your table appear in your database with all of the columns.
Example of Our API Working (While Connected To A Database)
Update our
goals.controller.ts
file to the following:import { Body, Controller, Delete, Get, HttpCode, NotFoundException, Param, Patch, Post, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateGoalDto, UpdateGoalDto } from './dtos/index'; import { Goal } from './entities/goal.entity'; @Controller('goals') export class GoalsController { // Dependency Injection constructor( @InjectRepository(Goal) private readonly repository: Repository<Goal>, ) {} // GET /api/v1/goals @Get() async findAll() { const goals = await this.repository.find(); return { success: true, count: goals.length, data: goals }; } // GET /api/v1/goals/:id @Get(':id') async findOne(@Param('id') id) { const goal = await this.repository.findOneBy({ id }); if (!goal) { throw new NotFoundException(); } return { success: true, data: goal }; } // POST /api/v1/goals @Post() async create(@Body() input: CreateGoalDto) { const goal = await this.repository.save({ ...input, createdAt: input.createdAt, updatedAt: input.updatedAt, }); return { success: true, data: goal }; } // PATCH /api/v1/goals/:id @Patch(':id') async update(@Param('id') id, @Body() input: UpdateGoalDto) { const goal = await this.repository.findOneBy({ id }); if (!goal) { throw new NotFoundException(); } const data = await this.repository.save({ ...goal, ...input, createdAt: input.createdAt ?? goal.createdAt, updatedAt: input.updatedAt ?? goal.updatedAt, }); return { success: true, data }; } // DELETE /api/v1/goals/:id @Delete(':id') @HttpCode(204) async remove(@Param('id') id) { const goal = await this.repository.findOneBy({ id }); if (!goal) { throw new NotFoundException(); } await this.repository.remove(goal); } }
You can make the following requests to each endpoint and your database will populate with data.
In summary, the use of Nest.js, TypeORM, and PostgreSQL facilitates the creation of a robust and scalable REST API. Nest.js provides a modular structure, TypeORM streamlines database interactions, and PostgreSQL ensures efficiency. This powerful combination enables developers to build high-performance APIs that adhere to best practices, fostering reliable and maintainable backend systems.
Code
If you want to refer to the code you can do so here.
Thanks for reading!
Have a question? Connect with me via Twitter or send me a message at hello@dylansleith.com