Throughout this course we will gradually build a Node.js Express API. As we progress elements, techniques and libraries will be added to the project.
The end goal is to build a CRUD (create, read, update, delete) API using modern Node.js practices with proper validation, authentication, and database integration.
Almost time to dive in!
Since we won't be diving very deep into the basics of Node.js or Express it can be useful for inexperienced developers to checkout a Node.js course on Udemy.
It's a great course covering all the basics. Some sections are more interesting than others for our purposes.
The most interesting sections are:
- 3 - Node.js Module System (Notes App)
- 6 - Asynchronous Node.js (Weather App)
- 7 - Web Servers (Weather App)
- 9 - Application Deployment (Weather App)
- 11 - REST APIs and Mongoose (Task App)
- 13 - Sorting, Pagination, and Filtering (Task App)
Rather than explicitly watching the entire course up front I would recommend to find a section that is of interest whenever something is not clear to you.
Express is a fast and small Node.js web framework. We will be using express to serve our API and define our routes.
The first step is to create a very small example API to get a hold of some main express routing concepts.
- Create a new directory and open it in vscode
- Open the terminal and run the following command:
pnpm init- Add express as a dependency to the project:
pnpm add express-
Add files
-
src/app.js
Create a class to initialize ahost(express app) in the constructor and add a method to start listening on port 3000.
That should give you something like this:// src/app.js const express = require("express"); class App { constructor() { // Init server this.host = express(); } listen() { this.host.listen(3000, () => { console.info(`🚀 http://localhost:3000`); console.info(`========================`); }); } } module.exports = { App };
-
src/server.js
This is actually just a simple, almost empty entrypoint that will be starting initializing the app class. It should look like this:// src/server.js const { App } = require("./app.js"); const app = new App(); app.listen();
-
Running the server
Add an NPM script in the package.json that runs the server:Now you can run the server by simply executing
pnpm startfrom your terminal.Since we are using CommonJS require syntax (CJS), this should work directly without any additional configuration. CommonJS is the default module system for Node.js, so our require statements will work out of the box.
// package.json "scripts": { "start": "node src/server.js" },
Currently this is quite useless though as we don't have any API code to run yet.
-
Normally on every code change you'd have to stop and run the start command again so the code would be recompiled. In our old project we use nodemon to watch for code changes and restart the server whenever something changes. More info on nodemon can be found here
For this project we can use node --watch to achieve the same thing.
// package.json
"scripts": {
"start": "node ./src/server.js",
"dev": "node --watch ./src/server.js"
},From now on you can run pnpm dev to start the server in watch mode.
So whenever you make a change to the code the server will restart automatically.
With express it's very easy to create a simple endpoint.
The host in app.js has methods to define a route. Add a get route on the root.
You can simply define it in the constructor of App.
The handler (callback) can be called with 3 arguments. request, response
and next. Use the response to send a "Hello world" string when the route is
hit.
this.host.get("/", (req, res, next) => {
res.send("Hello World!");
});Fetch the endpoint with curl
curl -X GET http://localhost:3000To learn more about this, checkout the Udemy course on section 7 - 43. Hello Express!
CRUD endpoints (create, read, update, delete) are the most common type of endpoint, definitely for a REST API.
Let's build sample CRUD endpoints for users.
Instead of extending app.js infinitely you should create a maintainable structure.
src/controllers(holds all routes and handlers)src/controllers/users(or any other CRUD) -src/controllers/users/user.route.js(the route definition) -src/controllers/users/handlers(the handlers, logic to be executed by the routes)
To simply create everything you can run the following commands:
mkdir src/controllers
mkdir src/controllers/users
mkdir src/controllers/users/handlers
touch src/controllers/users/handlers/create.handler.js
touch src/controllers/users/handlers/delete.handler.js
touch src/controllers/users/handlers/get.handler.js
touch src/controllers/users/handlers/getList.handler.js
touch src/controllers/users/handlers/update.handler.js
touch src/controllers/users/handlers/user.store.js
touch src/controllers/users/user.route.jsThen you have a result like this:

First create the get all users route.
-
Define a class
UserRouteinuser.route.js -
Initialize a root path and new instance of express
Router -
Add a
getroute to theRouterwith the following handler:router.get("/", (req, res, next) => { res.send("Get all users"); });
-
Now create a handler (exported arrow function)
getList.jsin the handlers folder with the same middleware code as previously written in the route. -
Replace the inline middleware with a reference to the handler.
As a result you should have something like this:
// user.route.js
const { Router } = require("express");
const { getList } = require("./handlers/getList.handler.js");
class UserRoute {
constructor() {
this.router = Router();
this.path = "users";
this.router.get("/", getList);
}
}
module.exports = { UserRoute };// handlers/getList.handler.js
const getList = (req, res, next) => {
res.send("Get all users");
};
module.exports = { getList };The only thing left is to actually use the route in app.js. Currently, it's
only defined but not yet initialized when starting the application.
- Create a new instance of the class
UserRoutein the constructor ofApp - Use the
routerproperty to add the route to the host
const usersRoute = new UserRoute();
this.host.use(`/api/${usersRoute.path}`, usersRoute.router);When making a request to the endpoint you should be able to see the []
response
curl -X GET http://localhost:3000/api/usersWhen you want to import with .js extension automatically in VSCode you can add this setting to user settings
"javascript.preferences.importModuleSpecifierEnding": "js"
Instead of using a database, to keep things simple in the beginning, we will be using an in memory store. Since it's just a temporary solution we won't be going into detail about how to implement it.
You can just open the details pane below and copy paste the code
into src/controllers/users/handlers/user.store.js
Show Code
class UserStore {
static users = [];
static get(id) {
return this.users.find((user) => user.id === Number(id));
}
static getByEmail(email) {
return this.users.find((user) => user.email === email);
}
static find(search = undefined) {
return this.users.filter(
(user) =>
!search ||
Object.values(user).some((value) =>
value?.toString().includes(search)
)
);
}
static add(user) {
const u = { ...user, id: this.users.length };
this.users.push(u);
return u;
}
static update(id, input) {
const current = this.get(id);
const user = { ...current, ...input };
this.users.splice(
this.users.findIndex((x) => x === current),
1,
user
);
return user;
}
static delete(id) {
this.users.splice(
this.users.findIndex((x) => x.id === Number(id)),
1
);
}
}
module.exports = { UserStore };Use the UserStore in the getList handler and pass the result as a json
response.
const users = UserStore.find();
res.json(users);const users = UserStore.find("<name>");
res.json(users); // returns all users with name containing <name>Insomnia is a tool to test your API and manually call endpoints. Similar to Postman but a little bit easier and faster to use. It allows you to send requests by entering a URL, body, query parameters and headers.
- Install Insomnia
- Rerun app
- Add a request to test
GET /api/users
This should give you an empty json array
You can easily save a request for each endpoint or test you want to do, like
this:

From now on test each change you make with Insomnia.
For simplicity, We limit testing our functionality to manual testing through Insomnia for now. Relying solely on manual testing is insufficient, so in the next part (Module 2) we will dive deeper into automated testing using Mocha.
Lets add a basic user creation endpoint.
- Add a new handler
create.handler.jsto the handlers folder.- The body will be located on
req.body - Pass the body to
UserStore.addto create a new user. - Set the result as a json response.
- The body will be located on
- Add a new route
postto theUserRouteclass that uses this handler.
// create.handler.js
const { UserStore } = require("./user.store.js");
const create = (req, res, next) => {
const user = UserStore.add(req.body);
res.json(user);
};
module.exports = { create };// user.route.js
this.router.post("/", create);When you try out this endpoint with a JSON body now it will be ignored because express doesn't know how to parse the body yet.
Why does this happen?
The framework relies on the Content-Type header to determine how to parse the
body. Since we didn't specify a Content-Type header the framework doesn't know
how to parse the body.
To fix this you can add some built in middleware to parse the body. We'll talk about middleware later.
To enable it add the express.json middleware at the beginning of the
constructor of App.
this.host.use(express.json());When you try out the endpoint again you should get a success response.
There are 2 concepts we didn't talk about yet which you will need to complete the rest of the CRUD operations.
-
Query parameters To use queryparams you can use the request in the handler like you would do with the body
example-api.panenco.com?search=<somevalue>would translate toreq.query.search; // = <somevalue>
The Udemy course has a great explanation on this topic in Section 8 - 54. The Query String
-
Path parameters To use path parameters you can use the request in the handler like you would do with the body but you need to use the
:symbol to indicate the parameter in the route.this.router.get("/:id", get);
example-api.panenco.com/<someId>would translate toreq.params.id; // = <someId>
Add the following endpoints:
GET /api/users/:id- get a user by idPATCH /api/users/:id- update a user by idDELETE /api/users/:id- delete a user by id
Once that's working we can add a few small things:
GET /api/users/:idValidate if the user exists. If not return a 404 with a JSON error message.GET /api/usersUse the search query param to filter the users.POST /api/usersValidate if the user's name has been provided and if not return a 400 with a JSON error message.
Spoiler alert > View the result
// user.route.js
const { Router } = require("express");
const { create } = require("./handlers/create.handler.js");
const { deleteUser } = require("./handlers/delete.handler.js");
const { get } = require("./handlers/get.handler.js");
const { getList } = require("./handlers/getList.handler.js");
const { update } = require("./handlers/update.handler.js");
class UserRoute {
constructor() {
this.router = Router();
this.path = "users";
this.router.post("/", create);
this.router.get("/", getList);
this.router.get("/:id", get);
this.router.patch("/:id", update);
this.router.delete("/:id", deleteUser);
}
}
module.exports = { UserRoute };// create.handler.js
const { UserStore } = require("./user.store.js");
const create = (req, res, next) => {
if (!req.body.name) {
// Set the status of the response and send the error message
return res.status(400).json({
error: "name is required",
});
// We don't want to continue if the name is missing. So we return here.
}
const user = UserStore.add(req.body);
res.json(user);
};
module.exports = { create };// delete.handler.js
const { UserStore } = require("./user.store.js");
const deleteUser = (req, res, next) => {
const user = UserStore.get(req.params.id);
// Duplicated in multiple places for now. This will be refactored later.
if (!user) {
return res.status(404).json({ error: "User not found" });
}
UserStore.delete(req.params.id);
res.status(204);
res.send();
};
module.exports = { deleteUser };// get.handler.js
const { UserStore } = require("./user.store.js");
const get = (req, res, next) => {
const user = UserStore.get(req.params.id);
if (!user) {
res.status(404).json({ error: "User not found" });
return;
}
res.json(user);
};
module.exports = { get };// getList.handler.js
const { UserStore } = require("./user.store.js");
const getList = (req, res, next) => {
const users = UserStore.find(req.query.search);
res.json(users);
};
module.exports = { getList };// update.handler.js
const { UserStore } = require("./user.store.js");
const update = (req, res, next) => {
const user = UserStore.get(req.params.id);
if (!user) {
res.status(404).json({ error: "User not found" });
return;
}
const updated = UserStore.update(req.params.id, req.body);
res.json(updated);
};
module.exports = { update };Middleware is the absolute core of express. Endpoints consist mostly of a chain of middleware. For instance:
express.jsonwe added before is middleware that parses the json body for us.- Each handler we added is middleware.
Currently, we only have these, but we can add middleware for whatever we need, like authentication, logging, error handling or validation.
Middleware can be applied within a few different scopes:
- General: applied to all endpoints, configured in
Appon the express host - Router: applied on all endpoints within a router, configured in
the
*.route.jsfiles on the router instance. - Endpoint: applied only to the specified endpoint. Configured when defining the endpoint.
The order of middleware is very important.
The first middleware that's applied will be the first one that's called. If we
would move the express.json middleware below the initialization of
the UserRoute class the body would only be available after the handler would
have called the next function.
In our examples currently the handler is always the last middleware to be called
as no next function is called and a response is sent.
That's about the basics of express middleware. Checkout the express middleware documentation for a bit more detailed information.
The Udemy course also has a section dedicated to middleware: Section 12 - 108. **Express Middleware **
If this is all still a little bit unclear I really recommend you learn more about middleware.
Let's add some general middleware that runs before all endpoints.
A use case for this would be the express.json middleware we already have in
place. Or a logger that logs the url and the method for each request, so let's
add that.
To do so, call the use function on the express host in App before
the UserRoute is initialized.
When you call endpoints now you should see something like this:

Code:
this.host.use((req, res, next) => {
console.log(req.method, req.url);
next();
});Use cases for general after middleware are quite limited. Before we talked about
how the order of middleware is important and the next middleware will be called
after the next function is called. However in the handlers we don't call
the next function. Instead we return a response. So currently if we add a
general after middleware it will never be hit.
That leaves us only 1 option, a middleware for undefined routes: 404 Not Found error middleware.
Add the 404 middleware in App after the UserRoute is used. Send a 404
response with some error message.

Code:
this.host.use((req, res, next) => {
res.status(404).send("No Endpoint found");
});Whenever you want to execute some code before an endpoint is called, you can add
middleware for it.
For instance if you want to check if the user is authenticated before calling
the endpoint.
The easiest (not really secure) way to validate access is to check the request
for a fixed value. Requests can hold headers, like an auth header.
- Add an arrow function at the top of
user.route.jswith the same arguments as the other middleware, give it the nameadminMiddleware. - In the function body check if the
authheader matches a certain value. - If not, send a 401 response with a message.
- Otherwise call the
nextfunction - Now add the middleware to the post route as a third argument before the handler.
That's it, when calling the create user endpoint without the auth header set
to your value you should get a 401 response.

Code:
const adminMiddleware = (req, res, next) => {
if (req.header("auth") !== "api-key") {
return res.status(401).send("Unauthorized");
}
next();
};
...
this.router.post("/", adminMiddleware, create);When defined error middleware will be called when previously the next function
has been called with a parameter. Instead of 3 arguments error handling
middleware has 4. The first argument is the error object. The others are the
same as regular middleware.
More info in the express docs.
You need to build 2 things:
- The actual middleware
- Add middleware at the end of the constructor in
Appwith the 4th argument(error, req, res, next) - Set response status to 400 and send the error as json response.
- Add an error to handle
- Before we would have sent the error response directly from the handler. Now we can reuse the error handler
- In the create handler you should already have some validation in place. So
you can simply replace
res.status(400).jsonwithnextand the error will be handled by the error middleware. - Make sure to return so the remaining code of your handler is not executed
Code:
// app.js
this.host.use((error, req, res, next) => {
res.status(400).json(error);
});// create.handler.js
if (!req.body.name) {
return next({
error: "name is required",
});
}
