Full Stack Development Series Part 2: Creating a REST API with NestJS
Learn the basics of NestJS, create a shared library for data structures, and implement a REST API
Welcome back! In part 2 of this series, I'll cover some basics of NestJS, establish the purpose of this application, demonstrate how shared data structures can be used, and implement a proper REST API.
Other posts in this series:
- An Introduction
- Part 1: Getting Started With Nx, Angular, and NestJS
- Part 2: Creating a REST API (this post)
- Part 3: Connecting Angular to a REST API
If you want to skip ahead to the code, you can checkout out the repository: wgd3/full-stack-todo@part-02
What's This App Going To Do?
Our full stack application is going to...
Serve a to-do list! Not the most exciting thing in the world, I know, but it's the perfect way to demonstrate all layers of a full-stack application. To that end, our first step is going to be establishing data structures that both the client
and server
apps will use.
Sharing Is Caring
Nx has some excellent documentation on their philosophy behind directory structure and shared folders. Following their lead, let's create our first library:
~/git/full-stack-todo | main
> npx nx generate @nx/js:library domain \
--directory=shared \
--importPath=@fst/shared/domain \
--skipBabelrc \
--standaloneConfig \
--tags=scope:shared,type:domain
Explaining what just happened
This command just did a lot of work for us. It automatically:
- Created the
libs/shared
directory - Instantiated a new library under
libs/shared/domain
with all the config files necessary for an individual library - Updated the
tsconfig.base.json
file with a new import path (this allows us to not use relative imports in other libraries) - Added tags to the
project.json
for the library. More on tags soon.
With a shared library in place, we can create our first interface:
// libs/shared/domain/src/lib/models/todo.interface.ts
export interface ITodo {
id: string;
title: string;
description: string;
completed: boolean;
}
There are a couple of conventions I follow in my projects:
- One file per interface
- Files that export an interface should use the naming scheme
<name>.interface.ts
. This is extremely useful when tools accept a blob pattern to lookup files, and you can specify exactly what files you want. And it's easier to read in my opinion. - Interface names should be prefaced with an
I
for easier identification throughout the codebase
While updating this library, let's remove two auto-generated files that aren't needed:
~/git/full-stack-todo | main
> rm libs/shared/domain/src/lib/shared-domain*
remove libs/shared/domain/src/lib/shared-domain.spec.ts? y
remove libs/shared/domain/src/lib/shared-domain.ts? y
And lastly, update the index.ts
file:
Our to-do interface is now available anywhere in the application!
Building Blocks
I've mentioned before that NestJS was built similarly to Angular in terms of data flow and structure. If you're new to NestJS, let's review some common terms before moving on to the CRUD REST API.
Name | Purpose |
---|---|
Controller | Similar to a component in the Angular world, controllers are the "user-facing" part of the framework. This is where API routes are established |
Service | Very similar to Angular services, this is typically where data lookup/manipulation occurs. Controllers should delegate business logic to services as much as possible |
Pipe | Like Angular, pipes are meant for validating or manipulating data during the request. In this article we'll show a "global" pipe that's used for validating JSON payloads |
Module | Modules act as you might expect - they are pluggable groupings of controllers, services, and other providers. We'll use a feature module to manage our first controller and service, and the main AppModule will simply import the feature module |
CRUD REST API
Time to write some actual NestJS-specific code. We'll use the Nx generator to create a new feature library:
~/git/full-stack-todo | main
> npx nx generate @nx/nest:library feature-todo \
--directory=server \
--controller \
--importPath=@fst/server/feature-todo \
--service \
--strict \
--tags=scope:server,type:feature
Explaining The Command
Another great generator from Nx, @nx/nest:library
sets up a fresh library with some needed components:
ServerFeatureTodoController
will host our API routesServerFeatureTodoService
will handle our dataServerFeatureTodoModule
will be imported byAppModule
soon
The command also used a new tag: type:feature
with a small scope scope:server
. I promise we'll get to tags before the end of this post.
Our first controller:
Before we add a route, there needs to be some form of a data store. A later article will address the TypeORM integration, so for now let's use a BehaviorSubject:
tsconfig.base.json
- in particular when new import paths are created. I manually typed out the import above, because the autocomplete import pulled from libs/shared/domain
and that resulted in red squigglies. Once I corrected the import path, I used the Restart TS Command to refresh the available paths. On a Mac, Shift+Space+P
launches the command prompt, and you can start typing to find the command.I'm using a BehaviorSubject to store an empty array initially, and made it private so that no other class can directly access the BehaviorSubject - we're forced to add methods to the service for altering data. With a "data store" in place (yes, it's ephemeral, it'll reset every time the server
app launches), we can start adding routes to the controller.
Our first routes! NestJS uses decorators to signal which methods should be used as API routes, and what type of HTTP request is performed on that route. We're specifying 2 GET routes, one for all of the to-dos, and one for a particular to-do.
I'm sure you're wondering why I left the getOne()
method in the service, even though the IDE is complaining. The find()
method isn't guaranteed to return a to-do - if no matching id
is found, then it will return undefined
. Let's fix this:
If todo
is undefined here, we use NestJS' Built-in HTTP exceptions to return a 404 to the user.
One last step before we're able to use the API: adding our module to the AppModule:
And with that, we can now see our feature module and it's API routes in Nest's logger:
~/git/full-stack-todo | main
> nx serve server
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [NestFactory] Starting Nest application...
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [InstanceLoader] AppModule dependencies initialized +15ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [InstanceLoader] ServerFeatureTodoModule dependencies initialized +0ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [RoutesResolver] AppController {/api}: +8ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [RouterExplorer] Mapped {/api, GET} route +3ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [RoutesResolver] ServerFeatureTodoController {/api/server-feature-todo}: +0ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [RouterExplorer] Mapped {/api/server-feature-todo, GET} route +1ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [RouterExplorer] Mapped {/api/server-feature-todo/:id, GET} route +1ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG [NestApplication] Nest application successfully started +6ms
[Nest] 6607 - 02/10/2023, 2:03:54 PM LOG π Application is running on: http://localhost:3333/api
Interacting With The API
Great, the routes are there - now how do we use them? There are plenty of tools out there such as the well-known Postman, but I'm going to stick to the command line for now. We'll add Swagger docs (a UI for your API that's automatically generated) in a later post.
I used Homebrew to install httpie, and can now make GET requests against the routes:
Empty arrays are nothing to get excited about, so I'm going to hard code a to-do item in my service:
Now I can make that API call again and see my hard-coded item:
~/git/full-stack-todo | main
> http localhost:3333/api/server-feature-todo
HTTP/1.1 200 OK
[
{
"completed": false,
"description": "Yes, this is foreshadowing a POST route introduction",
"id": "something-something-dark-side",
"title": "Add a route to create todo items!"
}
]
Checking Off The To-do List
We have a way to retrieve data, but we also need to be able to create, update, and delete to-do items. Pivoting for a moment, I'd like to review the various HTTP request methods and how they're supposed to be used with a REST API:
Verb | Description |
---|---|
GET | Retrieves a representation of the resource at the specified URI |
POST | creates a new resource at the specified URI |
PUT | replaces an existing resource at the specified URI, or creates it if it doesn't exist |
PATCH | updates part or all properties of the resource at the specified URI |
DELETE | deletes the resource at the specified URI |
Since I've added a to-do for a POST route, that's what I'll add next.
There's a small problem with this code though - once a database is involved, the id
property of a to-do will be created automatically (provided you're using auto-incrementing primary keys). We don't want users manually specifying the id
, or creating a to-do item marked complete: true
. And it has to have a title! All of these issues can be addressed by creating a Data Transfer Object with validation.
Data Transfer Objects and Payload Validation
Since our POST route is using an interface for the @Body
decorator, there's no real enforcement of the data structure - this just tells the compiler and IDE that data should look that way in the request. Switching to a class-based DTO pattern (and telling NestJS to validate payloads) will avoid this issue. Before we create the DTO, install a couple of helper packages that we'll use for validation:
~/git/full-stack-todo | main
> npm i -S class-validator class-transformer
Now let's create a DTO for the ITodo
objects:
Our POST route needs to be updated to use this class:
And the IDE is now complaining because create(data)
is not passing in the expected ITodo
- so the service should be updated as well:
One last file to update before we can test the validation:
And the result:
Library Types and Tags
Now to address the "library types" I've referred to a few times. Nx has a really good starting point for this so I won't type it all out here. It is important that tags are being properly used from the start, however, as it can be a hassle to go back through dozens of libraries to make sure they're all in check. Here's the current state of our apps and libs:
~/git/full-stack-todo | main
> grep tags {libs,apps}/**/project.json
libs/server/feature-todo/project.json: "tags": ["scope:server", "type:feature"]
libs/shared/domain/project.json: "tags": ["scope:shared", "type:domain"]
apps/client-e2e/project.json: "tags": [],
apps/client/project.json: "tags": ["type:app", "scope:client"]
apps/server/project.json: "tags": ["scope:server","type:app"]
The .eslintrc.json
file does not have any boundaries defined for these tags, so let's update that:
This does not address the type
tags that we've added to libraries, but for now we'll make sure that server
and client
don't get any code mixed up. And at the moment, our linting passes!
~/git/full-stack-todo | main
> nx run-many --target=lint --all
β nx run shared-domain:lint (3s)
β nx run server-feature-todo:lint (3s)
β nx run client:lint (3s)
β nx run server:lint (2s)
β nx run client-e2e:lint (2s)
β nx run server-e2e:lint (2s)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
> NX Successfully ran target lint for 6 projects (5s)
Final Notes
We now officially have a REST API for our to-do items! In the tagged version of the repository for this part, you'll notice that I've fleshed out the remaining CRUD routes for to-do items, updated the service, and added DTOs as needed. That code can be found here: wgd3/full-stack-todo@part-02
In the next post, we'll add Swagger documentation to our routes, improve the DTOs and shared interfaces, and add tests; all done in an effort to have a bulletproof API before the Angular client starts using it. Stay tuned and thanks for reading!
Updates
May 8, 2023: Thanks to @rostag for pointing out that the generate
commands I originally posted should be updated! They are now in sync with the proper syntax for Nx 16.