After having built an Application Programming Interface (in short a REST API ) using core NodeJS so without using any framework, it is now time to explore Express, one of the most popular and widely used nodeJS frameworks. If you have missed my previous article, and have not yet built the first version of this project, you can do so by following this Link.
If you want to see a demo of this project, I have it running up at this Link . Also, at the end of the article, you can find the github repo with the code for this project.
We will build this project from scratch, just like the previous one. Start by initializing an npm script:
npm init --y
Next, let's install express do so by running:
npm i express
Express is a minimalistic NodeJS framework that can be used for building scalable and robust web applications. For starters, let's get our server up and running. Create a server.js file and paste the below code in it:
const express =require("express");const app =express();const port =3000|| process.env.PORT;app.get("/",(req, res)=>{return res.end("Welcome to the homepage!");});console.log(`server up and running on port ${port}`);app.listen(port);
Now, if you run node server in a terminal, you will start the server and if you go to http://localhost:3000, you will see our homepage response. Note that the server is an instance of the express function being executed (that's the 'app' object).
*Note also how it has methods for all the http methods (we have used app.get() but it also supports post, put, delete, etc...).
Another important detail to keep in mind, is the fact that the first argument of the get method (and this is true for all the methods) is a relative path, to which the user has to go, in order for the method to be executed. In our case that relative path is / that's why we go to http://localhost:3000 in order to get our homepage response. If the argument had been '/test', we would have to go to http://localhost:3000/test.
The second argument to the method is a callback function that receives a request and a response object. This is similar to the request hadler that we previously used with the createServer() method and you can't help not noticing that the 'express' ones are simply 'wrappers' on top of the native nodejs request and response objects.
Now let's install nodemon and add the start and dev scripts to our package.json file. Run:
npm i nodemon
Next, make the scripts key of your package.json file to look like this:
Now we can run the dev script and keep developing without having to re-start the server upon each change. Another cool thing express does for us, is some generic error handling. Try going to: http://localhost:3000/test.
*Note how you get a 'default' 404 response which comes directly from express. Let's now write our own, custom 404 response. Add the below snippet in your server.js file right above the console.log(..) line:
//generic middlewareapp.use((req, res)=>{if(req.method!=="GET"){ res.statusCode=405;return res.end("Method not allowed");} res.statusCode=404;return res.end("Unfortunately this page could not be found X_x");});
app.use() is a generic method, that responds to all http methods. So it will answer to POST, PUT, DELETE etc... just as if we had written: app.get(), app.post(), app.put(), etc... each with the same logic of checking the request type and aswering appropriately. Instead of that, express gives us the handy use() method and we can create this 'generic middleware'.
When working with express you will often encounter this concept of middleware. A middleware, is a function that has access to the request-response lifecycle, and it can be used to further configure the server response or the behaviour of our application. So we can execute a middleware before sending the client response (for instance we can use such a function to check if the user is authenticated to our application or not, or even to authenticate him). Furthermore, we can configure the middleware so that it ends the server response by itself, if needed.
In order to better understand middleware, let's add the below 2 middlewares in our code, paste them above the app.get() line:
Now, if you hit one of the endpoints again (so / or /test) you will notice 2 things. First, the middleware2 is executed and the second log is shown. Secondly, you might have noticed that the server response is not quite finished. It may keep on going (so the browser looks like it keeps loading without finishing) or the whole server response might not be sent at all. This is what happens when we start executing a middleware without actually calling next() or without ending the server response somehow (by using res.end() for instance).
So what we learn from this? We can use middleware functions to further configure our server response or to perform additional logic before sending the client response. Also, middlewares are called and they can either pass the control flow to the subsequent middleware (so the next one if there is any) or it can end the client response by itself. A middleware function usually receives 3 arguments (req res and next). You already know what req and res are. The 3rd one, next is a function, which once called, executes the subsequent middleware (so the next one).
A middleware function can also receive a 4th argument err which is an error object. If an error object is caught, we can handle it accordingly so that our application does not crash. We will use this 4th argument with a middleware by the end of the article. For now, remove the 2 middlewares and let's start creating routes.
Routes are endpoints which we configure so our API responds accordingly if a certain request hits a certain route. We will re-create the 'resources' API from the previous article.
We will first create some data for our API to read and serve. Create a 'data' directory in your project, and inside of it create a db.json file. Inside this file paste the below:
The router variable is another instance of the express() app. It has the same methods available as our app, and we can use it in this way to better structure our routes based on business logic and in keep each set of routes in dedicated files. (imagine how useful this is if you have many types of business entities like users, products, orders, etc..) in your application.
Now that we have exported the resources routes, we can use them in our server.js script. Paste the below 2 lines in the server.js file, right above the app.get() call:
*Note how we are mounting the resources routes at the /resources path. In the resources.js file, we have called app.get("/") but we are actually overwriting that path with /resources when mounting the routes in the app entry point (the server.js) script.
Now, if we go to http://localhost:3000/resources in our browser, we can see our resources being read form the file and served from the server.
You might or might have noticed that in our resources.js file we are using the res.send() method instead of res.end(). The send() method is something express gives us and it is very handy as it serializes the data for us (so if we send an object it will perform JSON.stringify() onto it before sending it, and it will also set the Content-Type header to application/json for us by itself). This is just one of the many cool things a framework will do for you to speed up your development process.
Now that we can read our resources, let's add a POST endpoint so we can create them too. Before doing that, however, we need to create a Resource controller. It will be 'responsible' for handling our resources 'resources' (so create, delete, read, etc... them). Create a controller directory and inside of it create a Resource.js file. Inside this file paste the below:
Now, go back to the routes/resources.js and change the app.get("/") call to the below:
app.get("/",(req, res)=>{const data =read();return res.send(data);});
Also, don't forget to import the read() method up top in the /routes/resources.js:
const{ read }=require("../controller/Resource");
You can also remove the const fs = require("fs"); line from routes/resources.js as the controller is now the one reading the file and needing to use the fileSystem module.
Let's next add the POST endpoint. Paste it in the /routes/resources.js file under the app.get("/") route:
/*
* POST
* payload color: string, name: string
* returns JSON payload with resource id
*/app.post("/",(req, res)=>{const{ color, name }= req.body;const id =getId();const data ={ name, color, id,};returnadd(data, res);});
Finally, we need to import the add() method from the controller in the /routes/resources.js file. Update the const { read } = require("../controller/Resource"); line to:
With this, we confirmed that our /post endpoint works. However, we need some improvements in our code as we have only tested and implemented for the ideal case that we get a good, complete and properly formated payload. We are also not handling duplicate cases so if we run the above command again, we can create a duplicate entry which will only have a different id.
Let's deal with the duplicates and with handling the payload. We will create 3 new helper modules. Start by creating a 'helpers' directory, and inside of it create a securityHelper.js file. Inside of it, paste the below:
const{ getId }=require("../controller/Resource");constvalidatePayload=(payload)=>{if(Object.prototype.hasOwnProperty.call(payload,"name")&&Object.prototype.hasOwnProperty.call(payload,"color")&&typeof payload.name==="string"&&typeof payload.color==="string"&& payload.color.length<=15&& payload.name.length<=15&&Object.keys(payload).includes("color")&&Object.keys(payload).includes("name")&&Object.keys(payload).length===2){returntrue;}returnfalse;};constgrabPayload=(payload)=>{const{ color, name }= payload;const id =getId();const data ={ name, color, id,};return data;};module.exports={ validatePayload, grabPayload,};
This little helper module has 2 functions in it, one of them validates that the payload is good (has all the required properties and that the properties are of the good data type and respect some length restriction) and the second one destructures the payload by taking what we need from it.
Next, create another file inside the 'helpers' directory and name it resourceHelper.js. Inside of it, paste the below:
We will get a 400 badRequest response for a 'duplicate entry'. Note how we also validate that the payload is good, it has a certain type and maximum length, or if it's missing some field or is bad in any other way. This is a way more robust approach. Feel free to test some more using a tool such as Postman by making POST requests to http://localhost:3000/resources with various payloads.
Now that our create works, let's add an update endpoint. And actually we will add 2 of them. We want users to be able to update a resource with both a POST and a PUT request.
Add the POST endpoint in the routes/resources.js right under the first POST call:
/*
* POST
* payload color: string, name: string
* returns JSON payload with success message with resource id
*/app.post("/:id",(req, res)=>{const isValid =validatePayload(req.body);const{ id }= req.params;if(isValid){const found =findById(id);if(found){const data =grabPayload(req.body); data.id= id;update(id, data);sendOk(res,`item with id ${id} updated successfully`);}else{badRequest(res,`item with id ${id} could not be found`);}}else{badRequest(res,"Some field is missing or is bad X_X");}});
Next in the same file, update the const { badRequest } = require("../helpers/errorHandlerHelper"); to:
*Note the command worked for me but you should replace 2698a34172627119768788a22bb02a with your own id.
So our POST call onto the resources/:id endpoint works, let's add a similar PUT method in the routes file. Paste it under the app.post('/:id') call:
/*
* PUT
* payload color: string, name: string
* returns JSON payload with success message with resource id
*/app.put("/:id",(req, res)=>{const payload = req.body;console.log(payload);const isValid =validatePayload(payload);const{ id }= req.params;if(isValid){const crtData =read();const found = crtData.find((i)=> i.id=== id);if(found){const data = payload; data.id= id;update(id, data);return res.send(`item with id ${id} updated successfully`);}else{ res.statusCode=400;return res.end(`item with id ${id} could not be found`);}} res.statusCode=400;return res.end("Some field is missing or is bad X_X");});
Now, we can simply run the below command to test the PUT endpoint too:
Next, let's add a DELETE endpoint too. Paste it in the /routes/resources.js file:
/*
* DELETE
* payload null
* returns JSON payload with deleted item id
*/app.delete("/:id",(req, res)=>{const{ id }= req.params;const crtData =read();const found = crtData.find((i)=> i.id=== id);if(found){deleteItem(id);sendOk(res,`resource with id ${id} deleted successfully`);}else{ res.statusCode=400;return res.end(`item with id ${id} could not be found`);}});
In the same file, update the const { read, add, update } = require("../controller/Resource"); to:
Now, we need to create 2 more endpoints: 1 to get a resource individually (with a get request to the /resources/:id endpoint ) and a simple delete all endpoint, made to the /resources url.
Let's start by adding the endpoint for an individual resource. Paste it in the resources.js routes file:
/*
* GET
* payload null
* returns JSON payload with respective item
*/app.get("/:id",(req, res)=>{const{ id }= req.params;const item =findById(id);if(item){return res.send(item);}else{ res.statusCode=404;return res.end(`resource with id ${id} not found`);}});
The last thing we are going to do before wrapping up, is create a new middleware function, in order to improve a bit the error handling of our application. With the current setup, if we get a badly formatted payload (like: {name: "test", color: "red" //note ending curly brace missing ) the app doesn't crash but it just 'spits' out an error and we do not control this at all. Let's create a middleware to catch this error and any other error for this matter. Create a /middleware folder and inside of it create a middleware.js file and inside of it paste the below:
constcheckIfJson=(err, req, res, next)=>{if(err){return res
.status(500).send("payload is badly formatted or some other error occurred =)!");}next();};module.exports={ checkIfJson,};
*Note how this middleware has 4 arguments (the last one being an error which we can catch and react to). Once we catch that error, we send a custom error response to the client. If no error is caught, we call next() and continue the code execution (so if we match any route we respond accordingly).
Next, in the server.js import the middleware up top:
Finally, add this line in the server.js file to actually use the middleware (put it right under the app.use(express.json()); line):
app.use(checkIfJson);
With this change, if you try to hit any of the endpoint with a bad payload that is missing a curly brace (e.g. {name: "test" color: "blue" you will get our custom configured response).
We have now re-implemented our full CRUD functionality but this time using the Express framework. As you can see, it is much easier than using core NodeJS code. And if we are speaking of code, I have a github repo with all the code at this Link if you want to have a look at it.
I think it is very important to know how the node runtime works under the hood in itself before diving into a framework. Thanks a lot for sticking with me so far and I hope you enjoyed rebuilding this little project.