Refactoring : Simple API with NodeJS & Express
Summary
This is extension to previous article on how to create “simple API with NodeJS & Express”. In this article we will understand how to refactor code at different levels.
What is Refactoring
Code refactoring is a technique to restructure and clean up an existing code without altering the functionality of the code (or behavior) at all.
Common purpose of refactoring is — Enhance code readability, Reduce complexity, Improve maintainability of source code, Improve extensibility, Enhance performance, Facilitate fast program execution.
Let’s begin
Basic things to consider for refactoring.
- Folder structure — Application should have organized folder structure. Folder name should gives us idea on what it contains.
- Files structure —Code should be organized & should follow similar naming conventions.
- Readable code — Code should be easily understood by anyone.
- Naming conventions — Correct & easy naming conventions for Folders, Files, Functions, Variables, etc. helps to understand code easily.
- Design Principals — Apply design principals such as DRY ( Do Not Repeat).
Refactoring of Folder Structure
We have organized our folder structure as shown below.
src
folder is to keep all the source code.
tests
folder is to keep your tests.
deployments
folder is to keep you deployment related code.
src/config
folder is to keep the configuration files.
src/api
folder is to keep the API related code here.
src/services
folder is to keep the various services you are using such as Auth Service, Caching Service, Event Publisher Service etc. These services can be used across the application. This helps to avoid duplication of code and keeps common services at a single place.
src/api/books
folder to keep controller, route, models for books API.
Old Folder Structure
node-basic-api/
┣ .vscode/
┃ ┗ launch.json
┣ src/
┃ ┣ routes/
┃ ┃ ┗ books.js
┃ ┗ server.js
┣ package-lock.json
┗ package.json
Refactored Folder Structure
node-basic-api-refactored-1/
┣ .vscode/
┃ ┗ launch.json
┣ deployments/
┣ src/
┃ ┣ api/
┃ ┃ ┣ authors/
┃ ┃ ┣ books/
┃ ┃ ┃ ┣ controller.js
┃ ┃ ┃ ┗ routes.js
┃ ┃ ┗ routes.js
┃ ┣ config/
┃ ┣ domain/
┃ ┣ services/
┃ ┃ ┗ auth/
┃ ┗ server.js
┣ tests/
┣ package-lock.json
┗ package.json
Refactoring of Code
Server.js : Old Code
server.js
is the entry point for our code. There are lot of chances to get lot of code in this file.- The code for API is intended to increase over the period of time. We should not have route registration logic & common middleware in this file, instead have a separate file.
- Lets look at what to refactor from
server.js
, here is existing code.
// src/server.js
// import the express
const express = require("express");
const registerRoutes = require("routes/books"); // problem 1. registering routes for each api here.
// define the Express app
const app = express();
app.use(express.json()); // problem 2. Middleware added here
// registerRoutes(app);
registerRoutes(app);
// starting the server
app.listen(3000, () => {
console.log("listening on localhost:3000");
});
Server.js : Refactored Code
// src/server.js
// import the express
const express = require("express");
const registerRoutes = require("./api/routes");
// define the Express app
const app = express();
// register routes & global middlewares
registerRoutes(app);
// starting the server
app.listen(3000, () => {
console.log("listening on localhost:3000");
});
src/routes/books.js : Old Code
- This file manages both routes & the code to handle the request.
- We should not write routes, request handling code & business logic at same place.
- Instead separate the concerns.
let books = [
{
id: 1,
title: "Learning JavaScript Design Patterns",
pages: 200,
},
{
id: 2,
title: "You Don't Know JS Yet",
pages: 230,
},
{
id: 3,
title: "Pro Git",
pages: 450,
},
];
export default function registerRoutes(app) {
app.get("/books", (req, res) => {
res.send(books); // Problem 1. API route & business logic is mixed.
});
app.post("/books", (req, res) => {
const newBook = { id: books.length + 1, ...req.body };
books.push(newBook);
res.status(201).send(books);
});
app.put("/books/:id", (req, res) => {
const book = books.find((item) => item.id === parseInt(req.params.id));
if (!book)
return res
.status(404)
.send(`The book with the id (${req.params.id}) was not found.`);
book.title = req.body.title;
book.pages = req.body.pages;
res.send(book);
});
app.delete("/books/:id", (req, res) => {
const book = books.find((item) => item.id === parseInt(req.params.id));
if (!book)
return res
.status(404)
.send(`The book with the id (${req.params.id}) was not found.`);
const index = books.indexOf(book);
books.splice(index, 1);
res.send(book);
});
}
src/routes/books.js : Refactored Code
- We have introduced
api
folder name under thesrc
folder.api
folder will keep all API related stuff for each category. - We have introduced
routes.js
to keep common logic for mapping API routes, common middleware, & global error handlers
const bookRoutes = require("../api/books/routes");
function registerMiddlewares(app) {
app.use(express.json());
}
function registerApiRoutes(app) {
app.use(bookRoutes);
}
function registerErrorHandlers(app) {
// get global error handlers here
//app.use(errorHandlers);
}
function registerRoutes(app) {
registerMiddlewares(app);
registerApiRoutes(app);
registerErrorHandlers(app);
}
module.exports = registerRoutes;
src/api/books: Refactored Code
api
folder is to keep all API related stuff. We may have API for various resources such as Books, Authors, Users and so on.- Each resource may have CRUD operation. Hence we should create a folder for each resource and manage its
controller
&route
in it. - For each resource we have we can have same naming conventions for file such as
controller.js
&route.js
.
src/api/books/route.js
This file is to manage routes for books resource, here is the new code.
const router = require("express").Router();
const {
listBooks,
addBook,
updateBook,
deleteBook,
} = require("./controller");
router.get("/books", listBooks);
router.post("/books", addBook);
router.put("/books/:id", updateBook);
router.delete("/books/:id", deleteBook);
module.exports = router;
src/api/books/controller.js
This file is for handling incoming request & returning response, here is the code.
let books = [
{
id: 1,
title: "Learning JavaScript Design Patterns",
pages: 200,
},
{
id: 2,
title: "You Don't Know JS Yet",
pages: 230,
},
{
id: 3,
title: "Pro Git",
pages: 450,
},
];
function listBooks(_req, res) {
res.send(books);
}
function addBook(req, res) {
const newBook = { id: books.length + 1, ...req.body };
books.push(newBook);
res.status(201).send(books);
}
function updateBook(req, res) {
const book = books.find((item) => item.id === parseInt(req.params.id));
if (!book)
return res
.status(404)
.send(`The book with the id (${req.params.id}) was not found.`);
book.title = req.body.title;
book.pages = req.body.pages;
res.send(book);
}
function deleteBook(req, res) {
const book = books.find((item) => item.id === parseInt(req.params.id));
if (!book)
return res
.status(404)
.send(`The book with the id (${req.params.id}) was not found.`);
const index = books.indexOf(book);
books.splice(index, 1);
res.send(book);
}
module.exports = { listBooks, addBook, updateBook, deleteBook };
Conclusion
We did refactoring of the Simple API application, however this is not the end and there is a scope to refactor more. Here are few pointers you can explore & try.
- Code commenting is also important & can be added in proper format.
- In
server.js
express application is created as well as server listening logic is at same file. We can divide appcreation logic & server starting logic. - There is more scope for refactoring in
controller.js
file. The purpose of Controllers is to receive request , parse the request to model & pass to domain layers & send response. - Introducing Business Application layer, which will take care how the request-model to be processed & business logic implementation.
- One can also use Typescript to have more modular & reusable code as well as get benefits of Object Oriented Programming (OOP) concept.
- Implement code with SOLID Principals.