The best express architecture and routing for Node.js (2023)

I've been working with NodeJS for some time as a hobby and as a professional on the backend. So I stumbled upon open source enterprise grade Javascript code for NodeJS. Last year I led the redesign of one of Intuit's products from .NET to NodeJS, which you can read abouton here. For those of you who have worked in the NodeJS environment, you may also have some experience with some of their web frameworks; Hapi, Koa and Express are some examples. No matter what web framework you're familiar with, I think I've seen some common anti-patternsmust be stopped. So, in this post, I'll talk about how you can write a more modular, extensible, testable, and scalable implementation of your routing. Whether you're writing an API or the backend for your frontend, this can be useful.

Given theexpressis one of the most popular web frameworks for NodeJS, I'll use it as the main base for this post. While this post uses Express for its examples, these layout methods can be applied to any other web framework.

So what are the problems?

I wouldn't necessarily call this a problem, but these web structures are inherently minimalist. This is done to give developers the flexibility to implement their own design and architecture around these frameworks. That's great (in theory...)! However, since there is no "stubborn" way to organize or configure your structure or paths, one of the unintended results is that developers simply design their environment using simple examples they see online. Therefore, you will often see express routes configured like this:

...express aus 'express' importieren;const app = express();...//// Node.js-Middleware registrieren// ------------------ - ------------------------------------------------- - - --------app.use(express.static(path.join(__dirname, 'public')));app.use(cookieParser());app.use(bodyParser.urlencoded({erweitert : wahr }));app.use(bodyParser.json());//// autenticación// ---------------------- - --- ----------------------------------------------- aplicación .uso (expressJwt({ secret: auth.jwt.secret, CredentialsRequired: false, getToken: req => req.cookies.id_token,}));app.use(passport.initialize());if (process.env NODE_ENV != = 'producción') { app.enable('trust proxy');}app.get('/login/facebook', password.authenticate('facebook', { scope: ['email', 'user_location' ], sesión : false }),);app.get('/login/facebook/return', password.authenticate('facebook', { failRedirect: '/login', sesión: false }), (req, res ) => { const expiresIn = 60 * 60 * 24 * 180 // 180 Tag const token = jwt.sign(req.user, aut h.jwt.sec ret, { ex piresIn }); res.cookie('id_token', token, { maxAge: 1000 * expiresIn, httpOnly: true }); res.redirect('/'); },);//// Registro de API-Middleware// ------------------------------------ - -----------------------------------app.use('/graphql', expressGraphQL (req => ({ esquema, graphiql: process.env.NODE_ENV !== 'producción', rootValue: { solicitud: req }, pretty: process.env.NODE_ENV !== 'producción',})));/ /// serverseitige Registro de representación de middleware// --------------------------------------- ----- ---------------------------------app.get('*', asíncrono (req, res, próximo) = > { probar { ... const html = ReactDOM.renderToStaticMarkup(<Html {...data} />); res.status(ruta.status || 200); res.send(`<! doctype html>${ html}`); } catch (err) { next(err); }});//// Fehlerbehandlung// ----------------- ------ -------------------------------------------- ------ ----const pe = new PrettyError();pe.skipNodeFiles();pe.skipPackage('express');app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars console.log(pe.render(err)); // eslint-disable-line no-console const html = ReactDOM.renderToSt aticMarku p(...); res.s estado(fehlerstatus || 500); res.send(`<!doctype html>${html}`);});//// Iniciar servidor// --------------------- - -------------------------------------------------- - -----/* eslint-disable no-console */models.sync().catch(err => console.error(err.stack)).then(() => { app.listen(port, ( ) => { console.log(`Der Server läuft unter http://localhost:${port}/`); });});/* eslint-enable no-console */

This code is actually much longer, but I've condensed it for you. As you can see in this code, this is a server file that does everything from setting up your server, initializing all routes, setting up API routes and display routes, error handling routes, etc.load of shit

Why is this an anti-pattern?Well, for starters, this file is... a big file. It's not necessarily easy to follow, read, modify, or test without breaking a bunch of other stuff. Suppose you want to write tests for your Facebook authentication path. That implementation logic is contained in this file and module. If you made another change to any part of this module that breaks everything, thenKaboom, Your test for a completely different function just failed.What if you wanted to change the Facebook path to just/Recordinstead of that/Enter not Facebook. Well now you need to find all instances of/Enter not Facebookand replace it with/Record.

Heck, why am I so critical of this random block of code?You're probably thinking that I took it from a random project. But no, brother. This comes straight fromReact-Starter-Kit, a project with more than 12,500 stars on Github.

(Video) ExpressJS Routes Tutorial - Separating Routes into Different Files

So how can we do better?

We improve on this by taking inspiration from some mature web technologies and frameworks. For example, let's take a look at the .NET Framework and the .NET MVC/Web API.

I won't go into detail about how the .NET Web API solves this, but theTL;RDis that in the .NET Web API they have a concept of controllers, route configurations and views.There is a clear separation of responsibilities.Route settings are responsible for initializing and associating routes with controllers. Controllers are responsible for receiving route input and invoking the appropriate actions to take. These actions may include rendering a view or invoking a service if that particular controller is responsible for an API endpoint.

Using the same design principles that .NET segregates responsibilities throughout the framework, we intend to do the same in our NodeJS web frameworks to enable extensible and modular code that can be tested.

Let's see a better code

To demonstrate these design principles, I wrote a project inGithub, this is a sample environment with Express. The rest of this blog post will use this project in the future, so I recommend downloading it if you want to get involved.

Download the sample code here:https://github.com/kelyvin/express-env-example

clear folder structure

The first thing you notice is the clear folder structure. There is an app folder specifically for all things UI and UI, while there is a server folder specifically for all things server. This is already a good first step since we are not mixing display code with our server code.

For anyone familiar with working in NodeJS, the first place to start when getting new code is usually to look at thepackage.jsonIindex.js. The first thing you will see is the upper level.index.jshas only one task: starting the server.

(Video) Node JS Express Project Structure : Best Practices

What is the difference between "controllers" and "services"

"Controllers" are used to define how the user interacts with their routes. Whether the purpose of your route is to render a display page or to interact with an API, it should handle both. For example, if your route is intended to serve an API (such as RESTful APIs), you must answer the following questions: Is it a POST request? GET request? What is expected in the request body? Does the user need to be authenticated? Are you authorized to access this API? etc. If your route is intended to serve a display page, you should also answer similar questions.

Now, if your controller provides a view, most of the time you don't have to worry about interacting with "services". But when you're writing an API, the "services" contain your actual business logic or CRUD operations. There are many different opinions/methods, but essentially a service can serve two purposes:

  1. Works as a microservice: performs CRUD (Create, Read, Update, Delete) on some data or database
  2. Orchestrate multiple microservices – Mix and interact with different microservices to perform some kind of business logic or complex situation

Why have both when I can put all the logic in the controller?

There are many reasons for this separation, but the main one is that you don't want to confuse the APIs the user interacts with (ie controllers) with how your backend actually works (ie services). Ignoring principles like "version control" or the validity of RESTful APIs, there will undoubtedly be use cases where your business logic changes completely, but the user never realizes that they made those changes because of the APIs, without ever changing who you interacted with. Separation is important to reduce coupling and helps to follow the single responsibility principle. Of course this will also help with testing and test writing, but I won't go there.

Server

In the server folder you will see that there is a higher levelindex.jsand three subfolders: Controllers, Routes, and Services.All this is a clear separation of concerns.Again, following NodeJS conventions, we know that our application will start the server through thisindex.js.

create = funcion (config) { let rutas = require ('./routes'); // server settings server.set('env', config.env); server.set('porta', config.porta); servidor.set('nombre de host', config.nombre de host); servidor.set('viewDir', config.viewDir); // Returns middleware parsing json server.use(bodyParser.json()); // set up view engine server.engine('.hbs', expressHandlebars({ defaultLayout: 'default', layoutsDir: config.viewDir + '/layouts', extname: '.hbs' })); server.set('vistas', server.get('viewDir')); server.set('Visualizar Mechanismo', '.hbs'); // Set up routes routes.init(servidor);};

You will see that this is his responsibility.index.jsit's just configuring the server. It initializes all the middleware, configures the display engine, etc. Finally, routes are configured by transferring this responsibility to theindex.jsin the routes folder.

routes

The routes directory is only responsible for defining our routes. Insideindex.jsIn this folder you will see that it is responsible for configuring our first level routes and delegating its responsibilities to each of their respective route files. Each respective route file defines additional subroutes and controller actions for each.

(Video) How to implement Clean Architecture in Node.js (and why it's important)

// ruta/index.jsserver.get('/', function (req, res) { res.redirect('/home');});server.use('/api', apiRoute);server.use( '/home', homeRoute);server.use('/error', errorRoute);

Controller

Controllers are responsible for calling the appropriate action. If a controller's responsibility is to present a hearing, he shall provide the appropriate hearing of theapplication/viewsDirectory.

// controllers/home.jsfunction index (req, res) { res.render('home/index', { title: 'Home' });}

When a controller is associated with an API endpoint, it invokes a corresponding service within theservicesDirectory.

// controladores/apis/dogs/index.jsconst express = require('express'), dogService = require('../../../services/dogs');let router = express.Router();router .get('/', dogService.getDogs);router.get('/:id', dogService.getDogWithId);module.exports = rotador;

Why is all this valuable?

This improved architecture is extremely valuable because you can change everything on the fly.

Example 1: scalability

Now you will see that there/I'm getting marriedDefined end point. What if tomorrow your product manager comes to your engineering team and says that he doesn't want to use it anymore?/I'm getting marriedand all pages inside that path should now be/Brand. With the current architecture, you can open the routeindex.jsArchive and change top level/I'm getting marriedfor/Brandto change all child pages.

// ruta/index.js// server.use('/home', homeRoute);server.use('/brand', homeRoute);

And if he says he wants it too/mark/envelopemi/mark/morerefer to the same pageview? Then open the relevant tag path file and add a new one/the morepath to the same controller function as shown/death information.

// enrutador/home.jslet enrutador = express.Router();router.get('/', homeController.index);router.get('/info', homeController.info);router.get('/more' , homeController.info);

You no longer need to search for all instances and declarations of a specific path to make changes.

(Video) Folder Structure for API's - Beginner, Intermediate, and Advanced

Example 2: modularity

Each directory and file has concise responsibilities. This means that each file is its own module that can be used where needed. For example, in the project there is a sample API I wrote that links to/APIRoute. If you're following a RESTful API pattern, you might have different versions of the API that you want to support. So we expose v1 and v2 defined in the API directoriesindex.jsFile, Archive.

// enrutador/api/index.jslet enrutador = express.Router();router.use('/v1', v1ApiController);router.use('/v2', v2ApiController);

Each API can expose different URL endpoints. In this case, one of the top-level resources in v1 could be "dogs", while we want the top-level resource in v2 to be "animals".

//routes/api/v1/index.jsconst express = require('express'), dogsController = require('../../../controllers/apis/dogs');let router = express.Router() ;router.use('/caes', caesController);
//routes/api/v2/index.jsconst express = require('express'), animalsController = require('../../../controllers/apis/animals');let router = express.Router() ;router.use('/animais',animaisController);

For endpoints, the API v2 (/animals/dogs) can still do the same as the v1 APIs (/dogs) final point. So instead of trying to rewrite the implementation, let's refer to the same module that was used for/dogsno/animals/dogs.

// controladores/apis/dogs/index.jsconst express = require('express'), dogService = require('../../../services/dogs');let router = express.Router();router .get('/', dogService.getDogs);router.get('/:id', dogService.getDogWithId);
// controladores/apis/animales/index.jsconst express = require('express'), dogController = require('../dogs');let router = express.Router();router.use('/dogs', cãoController);

As you can see, our v2 API can still take advantage of the same modules as v1 while maintaining backwards compatibility. All without duplicate code and with a clear separation of logic.

The central theses

if you download theExample of the Express environment that I set up, you can clearly see that the improved folder and module structure allows for more modular routing that scales easily. By simply making a few tweaks and changes, your code is much more digestible and you can help other developers contribute without worrying about breaking or changing another feature. Really,This is an excellent example of how to write loosely coupled code.. Each individual module is independent and it is not known who may be using it, but each one is responsible for a specific task.

There are many opinions and ideas on how to properly configure your web framework for NodeJS; this may not be the best solution for you. However, this is based on the same inspiration and ideas currently present in the "tried and true" back-end technologies that have been around for years. Express and other web frameworks for NodeJS are minimal out of the box, so you can decide how the architecture should be structured. So if you have better thoughts, ideas, or something that worked well for you, please share!

(Video) Learn Express JS In 35 Minutes

keywords

  • Code
  • JavaScript
  • Internal process
  • Nodejs

Videos

1. How To Structure & Setup Node.js + Express.js Apps | Best Way To Set Up Express.js Projects | 2020
(THE SHOW)
2. Learn MVC Pattern with ExpressJS and NodeJS - Tutorial Beginner
(PedroTech)
3. ONCE WE'VE MADE A BEST NODEJS FRAMEWORKS LIST....
(TECH IN 5 MINUTES)
4. Nodejs with the Express framework with necessary Routings, Controller, Middleware
(Tech Tarzan)
5. #67 MVC Architecture in NODE JS | Using MongoDB with Express| A Complete NODE JS Course
(procademy)
6. #43 Creating & using Routes module | Working with Express JS | A Complete NODE JS Course
(procademy)

References

Top Articles
Latest Posts
Article information

Author: Terence Hammes MD

Last Updated: 09/20/2023

Views: 6533

Rating: 4.9 / 5 (69 voted)

Reviews: 92% of readers found this page helpful

Author information

Name: Terence Hammes MD

Birthday: 1992-04-11

Address: Suite 408 9446 Mercy Mews, West Roxie, CT 04904

Phone: +50312511349175

Job: Product Consulting Liaison

Hobby: Jogging, Motor sports, Nordic skating, Jigsaw puzzles, Bird watching, Nordic skating, Sculpting

Introduction: My name is Terence Hammes MD, I am a inexpensive, energetic, jolly, faithful, cheerful, proud, rich person who loves writing and wants to share my knowledge and understanding with you.