Building a CRUD API with NestJS

Sanjay R - Oct 6 - - Dev Community

Hello guys! In this article, we can make crud APIs in NestJs. if you are unfamiliar with NestJS, let me tell you shortly.

NestJS is a framework for NodeJS to build server-side applications, under the hood by default NestJS uses the express, if required we can also change the platform to Fastify. NestJS provides an abstraction to these frameworks like Express and Fastify.

We can set up the Nest with cli, it will generate the files and basic code. but we don't need that we can set it up from scratch.

npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata

run the above command to initialize the project.

this initializes the project, also we need to install additional libraries

npm install @nestjs/platform-express @nestjs/mongoose ts-node typescript nodemon

in the package.json add the scripts to run the server.

"scripts": {
    "start": "ts-node main.ts",
    "dev": "nodemon --exec ts-node main.ts"
 },
Enter fullscreen mode Exit fullscreen mode

we use the ts-node to run the typescript files directly on the node.

Let's start the project,

Project structure: this is my folder structure

CRUD/
│
├── node_modules/
│
├── schemas/
│   └── todo.schema.ts
│
├── todo/
│   ├── todo.controller.ts
│   ├── todo.module.ts
│   ├── todo.service.ts
│   └── todo.types.ts
│
├── .gitignore
├── app.module.ts
├── main.ts
├── package-lock.json
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

before that, we need to config in the tsconfig.json

create a file tsconfig.json and add the following,

the experimentalDecorators and emitDecoratorMetadata allow us to use the Decorators in the ts, if you are unfamiliar with the decorators, let me tell you the high-level definition, "decorators are used to modifying the behavior of a class without modifying the underlying code".

create a main.ts and app.module.ts

In the app.module.ts

import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { TodoModule } from "./todo/todo.module";

@Module({
  imports: [
    MongooseModule.forRoot("mongodb://127.0.0.1:27017/nest"),
    TodoModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

In the main.ts

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function Server() {
  const app = await NestFactory.create(AppModule);    
  await app.listen(4000);    //server listen on port 4000
}

Server();
Enter fullscreen mode Exit fullscreen mode

The good thing about NestJS is the dependency injection, we can configure the dependency in the module, and inject it across the app and the instance will be created automatically by the nest.

let's define a schema for the database in the todo.schema.ts

import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";

export type TodoDoc = HydratedDocument<Todo>;

@Schema()
export class Todo {
  @Prop({ required: true })
  name!: string;

  @Prop({ required: true })
  content!: string;
}

export const TodoSchema = SchemaFactory.createForClass(Todo);
Enter fullscreen mode Exit fullscreen mode

we can also define the schema without decorators, as we do in the express application but let's try with the decorators.

Let's create a separate directory for called todo, here we will handle the service, controller, and module regarding todos.

In the todo/todo.module.ts

import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { Todo, TodoSchema } from "../schemas/todo.schema";
import { TodoController } from "./todo.controller";
import { TodoService } from "./todo.service";

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Todo.name, schema: TodoSchema }]),
  ],
  controllers: [],
  providers: [],
})
export class TodoModule {}
Enter fullscreen mode Exit fullscreen mode

The MongooseModule.forFeature() registers the schema in NestJS's dependency injection system, making the TodoModel available throughout the app. For now, let's keep the controller's array and the provider's array empty, once we define the service and controllers we can add it here.

Let's make a service, create a file in todo/todo.service.ts

import { Model } from "mongoose";
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Todo } from "../schemas/todo.schema";
import { todo } from "./todo.types";

@Injectable()
export class TodoService {
  constructor(@InjectModel(Todo.name) private todoModel: Model<Todo>) {}
  async create(data: todo): Promise<Todo> {
    try {
      const result = new this.todoModel(data);
      return await result.save();
    } catch (error) {
      throw error;
    }
  }

  async getAll(): Promise<Todo[]> {
    try {
      const data = await this.todoModel.find();
      if (!data) {
        throw new Error("No data");
      }
      return data;
    } catch (error) {
      throw error;
    }
  }

  async deleteTodo(id: string): Promise<Todo> {
    try {
      const data = await this.todoModel.findByIdAndDelete(id);
      if (!data) {
        throw new Error("error in deletion");
      }
      return data;
    } catch (error) {
      throw error;
    }
  }

  async updateTodo(id: string, upDate: todo): Promise<Todo> {
    try {
      const data = await this.todoModel.findByIdAndUpdate(id, upDate, {
        new: true,
      });
      if (!data) {
        throw new Error("can't update");
      }
      return data;
    } catch (error) {
      throw error;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

These are the crud services for our API.

  • @Injectable(): This is a decorator, that makes our service to be used in the controller, with injection. NestJS provides a powerful Dependency Injection system that automatically resolves and manages dependencies across the application. The @Injectable() decorator marks a class as a provider, and NestJS injects these services where needed without requiring the manual instantiation.
constructor(@InjectModel(Todo.name) private todoModel: Model<Todo>) {}

//the above like Inject the model into our service, so we can use the Mongo methods in it.
Enter fullscreen mode Exit fullscreen mode

remaining are just regular methods for crud operations. As we see that every method returns a Promise<Todo>, it will resolve the Todo structure promise.

Now, update the todo.module.ts Providers array,

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Todo.name, schema: TodoSchema }]),
  ],
  controllers: [],
  providers: [TodoService],
})
Enter fullscreen mode Exit fullscreen mode

Now the TodoService is available for injection, we will inject it into the controllers and use those methods.

Let's Now create a controller todo/todo.controller.ts

import {
  Body,
  Controller,
  Get,
  Post,
  Req,
  Res,
  Param,
  Delete,
  Patch,
} from "@nestjs/common";
import { Request, Response } from "express";
import { TodoService } from "./todo.service";
import { todo } from "./todo.types";

@Controller("todo")
export class TodoController {
  constructor(private todoService: TodoService) {}

//create a new data
  @Post("/create")
  async create(@Body() body: todo, @Res() res: Response) {
    const data = body;
    try {
      const result = await this.todoService.create(data);
      res.status(200).json(result);
    } catch (error) {
      return res.status(500).json({ error: (error as Error).message });
    }
  }

//get all the data
  @Get()
  async getAll(@Res() res: Response) {
    try {
      const result = await this.todoService.getAll();
      return res.status(200).json(result);
    } catch (error) {
      res.status(500).json({ error: (error as Error).message });
    }
  }

//delete a record
  @Delete("/delete/:id")
  async deleteTodo(@Param("id") id: string, @Res() res: Response) {
    try {
      const result = await this.todoService.deleteTodo(id);
      res.status(200).json(result);
    } catch (error) {
      res.status(500).json({ error: (error as Error).message });
    }
  }

//update the record
  @Patch("/update/:id")
  async updateTodo(
    @Param("id") id: string,
    @Body() body: todo,
    @Res() res: Response
  ) {
    try {
      const result = await this.todoService.updateTodo(id, body);
      res.status(200).json(result);
    } catch (error) {
      res.status(500).json({ error: (error as Error).message });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Controllers will handle the request and response.

with the @Controller decorator, it indicates that the below class will handle the incoming requests. when we give the @Controller("todo") the API will prefixed with todo, Now we can write a code for curd routes.

As we can see the Request and Response are from the express, we are using the @Req and @Res decorators with the Request and Response types.

Now we can handle the request and the response like we handle in express.

Now update the todo.module.ts

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Todo.name, schema: TodoSchema }]),
  ],
  controllers: [TodoController],
  providers: [TodoService],
})
Enter fullscreen mode Exit fullscreen mode

these are the API endpoints

--   Get: localhost:4000/todo/
--  POST: localhost:4000/todo/create
-- Patch: localhost:4000/todo/update/:id
--Delete: localhost:4000/todo/delete/:id
Enter fullscreen mode Exit fullscreen mode

You can test it in the insomnia or postman

For the full code check out my repo: nestjs-crud

if there are any mistakes, comment below

Thank You!!!

. . . . . . . . . . . . . . . . .