Building a NestJS CRUD App: Part 1

This is the first post in a two-part series where we will explore when, how, and why we should use NestJS. This post covers how to build a CRUD application that talks to a PostgreSQL database.

My goal this month is to learn NestJS, one of the technologies on my list to explore this year. I've spent some time digging in and am really excited to share what I've learned so far.

NestJS is a Node.js framework for building server-side web-applications. Generally speaking, a web-application has two components: server code that lives on a host somewhere (server-side application) and a client or user-interface (client-side application) that communicates with the server using a protocol, such as HTTP. A server-side application usually has some kind of backend for data storage, such as a database. There are already several ways to build a server-side application with Node.js: you could use the builtin HTTP module, Express or Koa, or Fastify.

While NestJS borrows ideas from these libraries (it even uses Express internally), it stands out because it takes a stance on how some of the key components of the application should work. When you create a NestJS project, you are automatically opted-in to static typing, tooling for code generation, linters, formatters, and testing, among other things. Most importantly, NestJS provides you with patterns for building server-side web-applications in a scalable and maintainable way.

In this tutorial, we will learn the basics of NestJS by using it to build a CRUD Todo application that talks to a PostgreSQL database. All code for this tutorial can be found in the following repository: https://github.com/taylorreis/nestjs-todo-server.

Prerequisites

This tutorial requires Node.js and PostgreSQL. Links to download and install this software can be found below. Feel free to download the current version of each of them, but for reference, I am using Node.js v14.15.0 and PostgreSQL v13.1. Older versions may not work correctly with this tutorial.

Once Node.js is installed, we will need the NestJS command line tools (CLI) package to build our project. I have installed this globally on my machine with Node's builtin package manager NPM using the following command:

> npm i -g @nestjs/cli
Copy to clipboard icon

Finally, I have created a React client that we can use to talk to our NestJS code. Feel free to fork or download the source code from the Github repository below:

Getting Started

Now that we have our tooling installed, we can start building our application. Our first step will be to create our NestJS App with the CLI tools. Use the following command:

> nest new todo-server
Copy to clipboard icon

This command will ask you to select a package manager (NPM is fine) and generate a bunch of project resources. When NestJS is done setting up the project, open the folder that was created (todo-server) in your favorite editor. Congratulations, you've just generated code for your first NestJS app!

Application Structure

In the screenshot below we can see everything that NestJS generated for us with the previous command.

todo-server project opened in VS Code

Let's dive in here. Working top to bottom, we have the following:

  • .vscode/ This snuck in here and wasn't generated by NestJS. This is for vscode workspace settings.
  • node_modules/ The node modules that have been installed by default. These dependencies are defined in package.json.
  • src/ Contains all of our application source code, along with unit tests. Files ordered by relevance:
    • main.ts Our top-level file that when run (this will actually be compiled by TypeScript into a javascript file that we will run), starts listening for HTTP requests on port 3000. This file will import and invoke (call) the app module that we export.
    • app.module.ts Our root module. NestJS uses modules to help organize our code and enforce seperation of concerns. Later on, we'll create a new module for Todo objects. If you look at this file, you'll notice that it takes in a controller and service through its @Module() decorator. More on this later!
    • app.controller.ts The root module's controller. This is what we use to expose HTTP routes. Each method in the controller class is typically given a decorator corresponding to the HTTP method (GET, PUT, POST, DELETE, etc...) required to call it. We can define as many of these as we want, and they will automatically be picked up for routing as long as we register the controller with the module file. Notice that the controller takes a service in its constructor.
    • app.service.ts The root module's service. These are called providers at the module level and are injected into controllers that ask for them. While the controllers provide application routing (think GET https://todos.com/todos), our service providers are responsible for doing the lifting to execute whatever type of operation against our data that the controller needs. This might be something like querying a database table for records, sending an email, or invoking some other type of service.
    • app.controller.spec.ts A file containing unit tests for the controller. Unit tests are responsible for testing isolated blocks of logic in our application. We won't spend as much time covering unit-tests in this post, but know that NestJS makes these tools available to you out of the box.
  • test/ Contains higher-level (not unit-tests) test suites (end-to-end tests). Again, I won't spend much time here in this post.
    • app.e2e-spec.ts Application end-to-end test for the one route created by the controller above.
    • jest-e2e.json A configuration file that Jest will use when running the end-to-end tests.
  • .eslintrc.js ESLint configurations setup to work with TypeScript.
  • .gitignore Did I mention that NestJS initializes new projects as git repos? Yep, it does. This file tells git which files and file patterns it can ignore. Files included in here will either be sensitive (environment variables like a database password), or generated, like a build directory.
  • .prettierrc Prettier configuration file. Prettier is a very popular code formatter. When multiple people contribute to a codebase, the code will often be written in several different styles. Code formatters are generally helpful because they create a consistent style across your codebase that makes it easier for developers to read and work in. Additionally, using a code formatter allows you to stop worrying about formatting your own code - simply setup the formatter to run everytime your save.
  • nest-cli.json NestJS CLI configuration file.
  • package-lock.json Our package.json lock file. This ensures that when we don't have an exact dependency version specified, we will still have dependency version consistency across installations.
  • package.json All of our npm information. This includes metadata about the project itself, dependencies, scripts to run, build, and test our application, and various other configurations (jest has configurations set here).
  • README.md A markdown file containing information about your project. Write docs about what your app does here!
  • tsconfig.build.ts A configuration file defining how to build our TypeScript project.
  • tsconfig.ts A configuration file defining the root of our TypeScript project and options for compiling.

As noted before, NestJS takes care of setting up a lot of tooling for you. Setting up each of these in every project can be frustrating and incredibly time-consuming. Automating the setup saves time and helps you focus on the logic of your application. NestJS gives us control of these tools by creating scripts in the package.json file. In this post, I will only be using a single script: start:dev. This script is responsible for building our project (compiling the TypeScript) and watching for future changes to files in the src directory. When a file changes, it will automatically rebuild and reload the application for you. We run this script by navigating to the project directory and running the following in a terminal:

> npm run start:dev
Copy to clipboard icon

NestJS Modules

Let's take a quick look at our app module:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

As we can see, a module is simply a class that uses the @Module decorator, which takes metadata about the module. Modules are the core building blocks of NestJS. Under the hood, NestJS uses modules to build a graph of our application in order to support dependency injection. It does this by starting with a single root module and resolving imports in the modules we have defined.

Each module takes a metadata object that can have four different types of entities: imports, controllers, providers, and exports. Let's take a look at each of these:

  • imports An array of the modules that we would like to import and use in this module. The modules that we import like this will need to register their providers as exports to access them in this module.
  • controllers An array of controllers in this module. As a reminder, controllers expose our application's external-facing API (via rest, graphql, or other means).
  • providers An array of providers used by this module. Providers expose lower-level functionality that controllers will call into. Providers are often injected into our Controllers, but can also be injected into other providers. We will demonstrate this later.
  • exports An array of the providers we would like to export from this module. Once we have added a provider to this array, we can use it in another module by importing this module.

When we generated our NestJS application earlier, a root module called app was created and registered for us in the NestFactory.create function call in main.ts. Our module simply leverages a single controller and service provider. While we could put all of our code into the files contained by this module, NestJS encourages developers to build many modules to organize and encapsulate our code. We will explore this later.

We can generate new modules with the following command:

> nest generate module <module_name>
Copy to clipboard icon

Running this command will create a new directory in src named with whatever you provided for <module_name>. Additionally, this command will take care of registering the new module with the root module.

Creating a CRUD Module

Since we want to build an app that handles todos, we should generate a new module specifically for them. Before we jump in, let's do a quick recap of the things this module will need:

  • A controller, providing an interface for creating, reading, updating, and deleting todo objects
  • A service provider for performing these operations against a database
  • A module to register the controller and provider

The operations that the controller will need to perform are commonly known as CRUD: Create, Read, Update, and Delete. We could create a new module with the command described in the last section, but lets instead explore another command designed specifically for CRUD modules. Run the following command to create out todos CRUD module (hit enter to select the default option for the choices it gives you - refer to the output below):

> nest generate resource todos
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/todos/todos.controller.spec.ts (566 bytes)
CREATE src/todos/todos.controller.ts (890 bytes)
CREATE src/todos/todos.module.ts (247 bytes)
CREATE src/todos/todos.service.spec.ts (453 bytes)
CREATE src/todos/todos.service.ts (609 bytes)
CREATE src/todos/dto/create-todo.dto.ts (30 bytes)
CREATE src/todos/dto/update-todo.dto.ts (169 bytes)
CREATE src/todos/entities/todo.entity.ts (21 bytes)
UPDATE src/app.module.ts (312 bytes)

After that completes, we need to install an additional NestJS package to support some of the created resources:

> npm i --save @nestjs/mapped-types
Copy to clipboard icon

As we can see, NestJS has created a bunch of resources for us again! Open the new todos directory and check it out. Not only has it created a new module with a controller and provider, but it has also provided a dto (data transfer object) directory containing classes that we can use to represent what a todo object look like in transit (what fields will we expect a todo object to have in a create request? what about when we want to update an existing todo?), as well as an entities directory providing a class that describes the shape of a full Todo object. Later in this post, we will replace the dto and entities with models generated by Prisma.

Open up todos.controller.ts and you should see the following:

import { Controller, Get, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { TodosService } from './todos.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}
@Get()
findAll() {
return this.todosService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.todosService.findOne(+id);
}
@Put(':id')
update(@Param('id') id: string, @Body() updateTodoDto: UpdateTodoDto) {
return this.todosService.update(+id, updateTodoDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.todosService.remove(+id);
}
}

There's a lot going on in here so let's discuss the interesting parts. First, notice all of the decorators (things starting with '@'). The controller passes the todos string to the @Controller decorator to namespace all of these REST endpoints defined by the methods with the /todos prefix: http://localhost/todos. We also see that each controller method has an HTTP method decorator indicating the HTTP method (e.g. GET or POST) required to call that controller method. We can also see that some of the methods have decorated arguments (@Param and @Body). This gives us an easy way to tell NestJS that we want to access a query parameter (the id parameter in this case), or the request body, in that method. Secondly, notice that the controller class has five methods: all of the CRUD operations were created for us, including two different ways to read our todos (get all todos vs. get a single todo by id). Lastly notice that the TodosService class is injected into the controller and called in each method.

If we look into our corresponding todos.service.ts file, we will see that all of the methods exposed by the TodosService class return a static string value right now.

Let's see these new endpoints in action - run the development start command:

> npm run start:dev
[11:24:15 AM] Starting compilation in watch mode...

[11:24:21 AM] Found 0 errors. Watching for file changes.

[Nest] 80657   - 02/06/2021, 11:24:22 AM   [NestFactory] Starting Nest application...
[Nest] 80657   - 02/06/2021, 11:24:22 AM   [InstanceLoader] AppModule dependencies initialized +128ms
[Nest] 80657   - 02/06/2021, 11:24:22 AM   [InstanceLoader] TodosModule dependencies initialized +1ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RoutesResolver] AppController {}: +28ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RouterExplorer] Mapped {, GET} route +6ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RoutesResolver] TodosController {/todos}: +0ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RouterExplorer] Mapped {/todos, POST} route +4ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RouterExplorer] Mapped {/todos, GET} route +3ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RouterExplorer] Mapped {/todos/:id, GET} route +2ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RouterExplorer] Mapped {/todos/:id, PUT} route +228ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [RouterExplorer] Mapped {/todos/:id, DELETE} route +12ms
[Nest] 80657   - 02/06/2021, 11:24:23 AM   [NestApplication] Nest application successfully started +6ms  

Your output should look similar to mine. Notice the routes that NestJS has resolved and their corresponding controllers. Let's test some of them. Open another terminal and run the following cURL commands:

> curl http://localhost:3000/todos
This action returns all todos
> curl http://localhost:3000/todos
This action returns a #1 todos

Note that by default, cURL will make a GET request to the url provided. Based off the output, we can see that our controller and service are working as expected and return the strings that are hardcoded. Congratulations, you've built your todos interface!

Connecting to a PostgreSQL Database with Prisma

As part of this project, I'd like to connect to a database so that my todos can be presisted and accessed on any machine that runs the client. We will be storing our todos in a table in PostgreSQL and accessing them with Prisma.

Prisma is a really powerful ORM tool that was designed to accelerate the development of database-facing TypeScript applications. Like NestJS, it has rich tooling that helps developers with everything from defining their data models, to creating a database client that you can use in your code, to generating the types that you'll need in you TypeScript application, to actually writing SQL files and running them as database migrations. In this post, we will barely scratch the surface of Prisma; I plan on doing a deep-dive in a later post.

To start working with PostgreSQL and Prisma, we will need to make sure that our database server is up and running. We will use psql shell (this should have been installed with PostgreSQL), but feel free to start PostgreSQL another way if you prefer. When opening psql, it should ask for the following information (you will probably need to create a password):

Server [localhost]: 
Database [postgres]: 
Port [5432]: 
Username [postgres]: 
Password for user postgres: 

Entering the default values for these fields should be fine, just make sure you note what these values are since we will need them later. Once the database server starts successfully, we can add Prisma by installing it in our project with the following command:

> npm install --save-dev prisma
Copy to clipboard icon

After that completes, we can start working with the Prisma CLI tooling. Initialize Prisma with the following command:

> npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma.
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql or sqlite.
3. Run prisma introspect to turn your database schema into a Prisma data model.
4. Run prisma generate to install Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

As you can see from the output, Prisma has generated a couple of new resources for us. First, it has added a directory called /prisma and a file called schema.prisma. This file is where we will define our Prisma data models. Additionally, Prisma has created a .env file that contains a DATABASE_URL environment variable. Prisma will use this value to connect to our database, so let's update it with our values. Update your .env file by replacing the credential values that are specified below in angle brackets with the database credentials you took note of above.

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables

# Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="postgresql://<username>:<password>@localhost:5432/<database_name>?schema=public"

Important: When working with files like .env that contain secrets (credentials, API keys, etc...), make sure that git knows to ignore them by adding these files to .gitignore. You can find more information on how to use .gitignore here.

Now that we are ready to connect to our database, let's define our todo data model. Copy the following into your prisma/schema.prisma file:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Todo {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
description String
}

Our todo model is defined at the bottom. The model is incredibly simple for this tutorial, only including an autoincremented ID, autogenerated created and updated timestamps, and a text description field where we store the todo action (e.g. "Walk dog"). Note that the syntax of this file is agnostic to the query language we use. Learn more about the Prisma data models here.

At this point, we have a database running and a model defined in Prisma, but our database still doesn't know anything about our model yet. We need to use Prisma to generate a SQL file to run against our database in order to create a table holding the fields we have defined in our model. We do this by running Prisma's migrate command, as shown below:

> npx prisma migrate dev --name init --preview-feature
Copy to clipboard icon

When you run this command, Prisma will read your schema.prisma file, check for existing migrations and any changes that the database needs to be aware of, and generate a SQL file with the changes you've made (named with the argument following the --name flag) that will be run against the database you've specified in .env. After running this command, check the prisma directory and you should see a new directory called migrations. Inside, you should see a folder with the following format: <timestamp>_<migration_name>. Open the SQL file inside and you should see that Prisma has generated a PostgreSQL query to create a table for our Todo object.

This command also generated and installed a client module for you to use that contains TypeScript models based off of the Todo model you defined. This is awesome because it means that we don't have to define any additional types in our application and can use the Prisma client directly!

Using the Prisma Client

Now that we have generated all of our database and Prisma resources, it's time to use them in our application. Let's start by generating a new NestJS module with a service provider:

> nest generate module prisma && nest generate service prisma
Copy to clipboard icon

This module and service will be responsible for providing our Prisma client to any other modules in the application that need it. Go ahead and update the new src/prisma/prisma.service.ts file with the following code, which will take care of setting up the Prisma client for our application:

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();
}
}

Next, we need to update our new prisma module to export this service so that other modules can use it:

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService], // export our service for other modules to use here
})
export class PrismaModule {}

Then, we can update src/todos/todos.module.ts to import our new Prisma module, which exposes our Prisma client:

import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { TodosService } from './todos.service';
import { TodosController } from './todos.controller';
@Module({
imports: [PrismaModule], // import the prisma module here
controllers: [TodosController],
providers: [TodosService]
})
export class TodosModule {}

Finally! We can start using the Prisma client in our todos service! Let's go ahead and update src/todos/todos.service.ts:

// Import our types from the generated prisma client
import { Prisma } from '@prisma/client';
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class TodosService {
// inject the PrismaService into TodosService
constructor(private readonly prisma: PrismaService) {}
create(todo: Prisma.TodoCreateInput) {
return this.prisma.todo.create({
data: todo,
});
}
findAll() {
return this.prisma.todo.findMany();
}
findOne(id: number) {
return this.prisma.todo.findUnique({
where: {
id,
},
});
}
update(id: number, todo: Prisma.TodoUpdateInput) {
return this.prisma.todo.update({
where: {
id,
},
data: todo,
});
}
remove(id: number) {
return this.prisma.todo.delete({
where: {
id,
},
});
}
}

Notice how clean it is to use Prisma here. Instead of writing out queries in our code, Prisma has generated a bunch of functions that are based off of the Todo model that we defined in prisma.schema. The best part is that the Prisma client knows which fields are required for specific operations. An example of this is the create function, which takes in Prisma.TodoCreateInput as input. This type simply requires that the object has a description in it, since that's the only non-null field that isn't autogenerated. Combining these Prisma-generated TypeScript models with our database queries helps enforce the contracts of our data models throughout our application, preventing bugs from creeping in.

Let's go ahead and update src/todos/todos.controller.ts by replacing the DTO references with the types that Prisma generated for us based off of our model:

import {
Controller,
Get,
Post,
Body,
Put,
Param,
Delete,
} from '@nestjs/common';
// import the Prisma client to access our generated types
import { Prisma } from '@prisma/client';
import { TodosService } from './todos.service';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
// notice the Prisma type that was created: Prisma.TodoCreateInput
@Post()
create(@Body() todo: Prisma.TodoCreateInput) {
return this.todosService.create(todo);
}
@Get()
findAll() {
return this.todosService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.todosService.findOne(+id);
}
// again, Prisma has generated a useful type for us to describe the shape of our update data
@Put(':id')
update(@Param('id') id: string, @Body() todo: Prisma.TodoUpdateInput) {
return this.todosService.update(+id, todo);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.todosService.remove(+id);
}
}

Testing our Application

At this point, we have a fully functioning backend service that allows us to interact with a database to create, read, update, and delete todo objects.

Our final question is: how do we test that everything works correctly? While we could (and should) write end to end tests, we will use the todos web client that you downloaded as a prerequisite for this post to verify our backend's behavior.

We have one last consideration before doing so, though. CORS will prevent our web client from interacting with our backend application since they will be running on different ports. We therefore need to configure our server to send special headers in its responses to allow it to communicate with the client.

Update src/main.ts to look like the following (note the new options argument that has been added to NestFactory.create):

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
cors: true, // passing true here tells the server to reflect the request origin in the response
});
await app.listen(3000);
}
bootstrap();

With that out of the way, we can start playing with what we have built. Make sure our todos backend application is running by running:

> npm run start:dev
Copy to clipboard icon

In another terminal, navigate to the web client that you downloaded and run the following commands to build and start the client:

> npm i && npm start
Copy to clipboard icon

Once this finishes, it should open the client in a web browser. You should see the following:

todos web client

Go ahead and open the developer tools (Chrome, Firefox), navigate to the network tab, and filter for XHR requests. Start interacting with the client by clicking 'Create Todo' and saving something. You should see XHR requests being made to localhost:3000/todos. Additionally, you should see your todos populated in the web client. From here, you can use the client to perform all of the same operations on the todos that you have built in your backend. Here's what mine looks like:

todos web client with network tab

🎉 Congratulations, you've built a NestJS application that talks to a database! 🎉

Stay tuned for the next post where we will explore how to add authentication and authorization to our application.