Building A RESTful CRUD API with Nest.js, TypeORM & PostgreSQL: A Step-by-Step Guide
Published: 2023-12-20 23:02:05

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](https://docs.nestjs.com/cli/overview).

1. Install Nest.js globally:

	```sh
	npm i -g @nestjs/cli
	```

2. Scaffold the project:

	```sh
	nest new goal-tracker-nestjs
	```

3. Navigate to your directory that you just created with the following:

	```sh
	cd goal-tracker-nestjs
	```

4. Start the server

	```sh
	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.

1. Open `main.ts` in `/src` and add the following:

	```js
	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.

1. Let's create our controllers. To create a new controller type the following command:

	```sh
	nest generate controller goals
	```

2. Now we need to update our `goals.controller.ts` file to the following:

	```javascript
	import { Controller, Delete, Get, Patch, Post } from '@nestjs/common';
	
	@Controller('goals')
	export class GoalsController {
		@Get()
		findAll() {}
		
		@Get()
		findOne() {}
		
		@Post()
		create() {}
		
		@Patch()
		update() {}
		
		@Delete()
		remove() {}
	}
	```

3. Now we need to register this controller with Nest.js. Add `GoalsController` to the **controllers** array.

	```javascript
	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 {}
	```

4. You are done, you will now get successful responses back from the API if you send a `GET`, `POST`, `PATCH` or `DELETE` to `http://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.

1. Update our `goals.controller.ts` file to the following:

	```javascript
	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 like, there are also some other use cases such as data validation, security, etc.

1. Update our `goals.controller.ts` file to the following:

	```javascript
	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:

1. Update our `goals.controller.ts` file to the following:

	```javascript
	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)

1. Let's create our Create DTO. Create a new file called `create-goal.dto.ts` in the directory `/src/goals/dtos`

2. Now we need to update our `create-goal.dto.ts` file to the following:

	```javascript
	export class CreateGoalDto {
	  name: string;
	  priority: string;
	  status: string;
	  createdAt: string;
	  updatedAt: string;
	}
	```

2. Now we need to update our `goals.controller.ts` file to the following:

	```javascript
	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;
	  }
	
	  // ...
	}
	```

&nbsp;

### Update Payload (Update)

1. Install @nestjs/mapped-types:

	```sh
	npm i @nestjs/mapped-types
	```

2. Let's create our Update DTO. Create a new file called `update-goal.dto.ts` in the directory `/src/goals/dtos`

3. Now we need to update our `update-goal.dto.ts` file to the following:

	```javascript
	import { PartialType } from '@nestjs/mapped-types';
	import { CreateGoalDto } from './create-goal.dto';
	
	// Pulls types from CreateGoalDto into UpdateGoalDto
	export class UpdateGoalDto extends PartialType(CreateGoalDto) {}
	```

4. To make our code a bit more cleaner, lets combine the Classes together into a single file with the following:
   
	```javascript
	// index.ts
	import { CreateGoalDto } from './create-goal.dto';
	import { UpdateGoalDto } from './update-goal.dto';
	
	export { CreateGoalDto, UpdateGoalDto };
	```


5. Now we need to update our `goals.controller.ts` file to the following:

	```javascript
	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)

1. Before we create our entity let's create **Enums** for our `priority` and `status` type. Create a new file called `priority.enum.ts` and another called `status.enum.ts` in the directory `/src/goals/enums`
   
2. Now we need to update our `priority.enum.ts` to the following:

	```javascript
	// priority.enum.ts
	export enum Priority {
	  LOW = 'Low',
	  MEDIUM = 'Medium',
	  HIGH = 'High',
	}
	```

3. Now we need to update our `status.enum.ts`  to the following:

	```javascript
	// status.enum.ts
	export enum Status {
	  PENDING = 'Pending',
	  IN_PROGRESS = 'In Progress',
	  COMPLETED = 'Completed',
	}
	```

4. To make our code a bit more cleaner, lets combine the Enums together into a single file with the following:
   
	```javascript
	// index.ts
	import { Priority } from './priority.enum';
	import { Status } from './status.enum';
	
	export { Priority, Status };
	```


5. Now we need to update our `create-goal.dto.ts` file to the following:

	```javascript
	import { Priority, Status } from '../enums';
	
	export class CreateGoalDto {
	  name: string;
	  priority: Priority;
	  status: Status;
	  createdAt: string;
	  updatedAt: string;
	}
	```


6. Let's create our Entity. Create a new file called `goal.entity.ts` in the directory `/src/goals/entities`

7. Now we need to update our `goal.entity.ts` file to the following:

	```javascript
	import { Priority, Status } from '../enums';
	
	export class Goal {
	  id: number;
	  name: string;
	  priority: Priority;
	  status: Status;
	  createdAt: Date;
	  updatedAt: Date;
	}
	```


8. Now we need to update our `goals.controller.ts` file to the following:

	```javascript
	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));
	  }
	}
	```

9. 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

1. Install **TypeORM** and the database you want to use, in this case we will use **PostgreSQL (pg)**:

	```bash
	npm install @nestjs/typeorm typeorm pg
	```

&nbsp;

### Creating Our Database

1. Open your favourite database management tool, in this case I will be using pgAdmin.
   
2. Right-Click **Databases** > **Create** > **Database...**
   
3. Name you database whatever you like, we will be using `goaltracker-db`
   
4. Now, Click **Save**

&nbsp;

### Connecting To Our Database

1. Let's create **two** database configurations, one for **production** called `orm.config.prod.ts` and the other for **development** called `orm.config.ts` in the directory `/src/config`
   
2. Now we need to update `orm.config.prod.ts` to the following:

	```javascript
	// 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
	  }),
	);
	```

3. Now we need to update `orm.config.ts` to the following:

	```javascript
	// 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,
	  }),
	);
	```

4. Install **@nestjs/config** since we will be needing it next:
   
	```bash
	npm install @nestjs/config
	```

5. Now, in our `app.module.ts` we need to update our file to the following to use our configuration:

	```javascript
	// 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 {}
	```

6. 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:

	```javascript
	// 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;
	}
	```

7. 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)

1. Update our `goals.controller.ts` file to the following:

	```javascript
	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);
	  }
	}
	```

2. 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.

&nbsp;

### Code

If you want to refer to the code you can do so [here](https://github.com/sleithdylan/goal-tracker-nestjs).

&nbsp;

### Thanks for reading!

Have a question? Connect with me via [Twitter](https://twitter.com/dylansleith) or send me a message at [hello@dylansleith.com](mailto:hello@dylansleith.com)