How you can structure your Express app by using Grogu.js, a lightweight Express boilerplate
Introduction
If you have read some of our previous articles, you might have noticed that we mostly use NodeJS and Express for our backend developments.
Born in approximately 41 BBY, during the era of the Galactic Republic, this is a work in progress lightweight express framework. A youngling framework.
You can find the Grogu.js repo here.
Importance of structuring your code
Code management is very important, whether you are employing someone to write code for you or you write it yourself. Following a set protocol for writing, code is much recommended as many coders tend to neglect code management before starting writing their application and lose track of their development and eventually the code readability takes a big hit.
Advantages of code management:
- Better code readability.
- It gets easier for the next person to write code in your app to understand and proceed forward.
- You know where your files are/have to be.
Structure your code with Grogu.js
Let’s talk about the structure used in Grogu.js. The structure can be seen in the image below:
The beauty of Grogu.js is that the controllers, services and middlewares are all independent and defined in different directories.
Let’s start by explaining the different folders in Grogu.js with code examples.
Controllers
The quickest way to get started writing controller files is to just create .js
files inside /controllers
.
A controller file is a PascalCased file, which exports routes and globalMiddlewares (explanation given below the image).
For example, a User
Controller could be created at controllers/User.js
file containing:
globalMiddlewares
is a list of middleware names defined in /middlewares
to be applied to all the routes defined inside User.js
routes
is a function that returns a key/value pair of routes and their definition and receives Services and config as dependency arguments for usage inside express request handler functions.
The above example code leads to the creation of route as GET
on localhost:<PORT>/User/v1.0/hello
and POST
on localhost:<PORT>/User/v2.2/hello
The handler
key holds the response to the request.
Just like globalMiddlewares
, you can specify a localMiddleware
as well for a specific service.
The dependency injections, Services
and config
are global and can be used in any Service, Middleware or Controller.
Note that you can change the versions of the API by changing the version: “”
to a string of your choice.
Services
Services are functions that help divide and selectively use the core logic of the app. These service functions can be created by defining .js
file inside /services
. A service file is a PascalCased file, which exports a function that returns an object with all the functions related to that service. For example, a UserService
could be created at services/UserService.js
file containing:
module.exports = async function ({ config, Services }) {
return {
iDoSomething: async function () {
return "hello world";
},
};
};
Similar to routes in the controller, the function exported from inside the service file receives config and Services as the dependency so as to enable cross usage and sharing of service functions between files.
These functions defined inside each service file then can be called directly inside as seen in Controllers and Middlewares using the syntax as Services.UserService.iDoSomething
Middlewares
Middlewares are express middlewares to be applied on routes definitions. These middlewares are defined as .js
files in /middlewares
. For example a middleware named checkSomething
could be created as middlewares/checkSomething.js
module.exports = async function (req, res, next, { Services, config }) {
next();
};
Here the express middleware function also has a dependency injection of config and Services for usage inside the middleware function.
Config
This is used for using config variables, constants directly inside Services, Middlewares and Controllers. The root config file can be found at config/conf.js
containing:
module.exports = {
aws: {
key: process.env.AWS_KEY,
secret: process.env.AWS_SECRET,
ses: {
from: {
default: "noreply@abc.in>",
},
region: "us-west-2",
},
},
};
and similarly, constants are defined inside config/constants
containing:
module.exports = {
DEFAULT_NULL_VALUE: "_NULL_",
};
this config and constants are coupled together and can be used directly using syntax config.aws.key
and the constants as config.CONSTANTS.DEFAULT_NULL_VALUE
wherever the config dependency is available to us.
Note that the constants
in config.CONSTANTS.DEFAULT_NULL_VALUE
is in all caps.
Models
This will be a folder with a collection of javascript/JSON files that will describe the schema of tables, indexes. The files reside inside models/
and these files are only for informative purposes only. If for example there is a table called users
inside a database called example
then there will be a folder called models/example.js
containing:
module.exports = {
collections: ["users"],
schema: {
users: { username: "string", password: "string" },
},
indexes: {
users: [
[{ username: 1, _id: 1 }, { unique: true }],
[{ username: 1 }, { unique: true }],
],
},
};
Alternatively, this configuration can be used to initialize any indexes or tables.
HTTP server middlewares
These middlewares are HTTP server level middlewares, these are defined inside config/http.js
containing:
let bodyParser = require("body-parser").json({ limit: "50mb" });
let compression = require("compression");
let cors = require("cors");
/* This whitelist can only filter requests from the browser clients */
var whitelist = ["http://localhost:3000", "http://localhost:3000/"];var corsOptions = {
origin: function (origin, callback) {
// console.log("HTTP Origin given = ", origin)
if (!origin) {
// console.log("origin undefined")
callback(null, true);
} else if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else if (origin == null) {
// console.log("origin null")
callback(null, true);
} else if (origin.indexOf("chrome-extension") >= 0) {
callback(null, true);
} else {
console.log("[Not allowed by CORS] but allowed temporarily", origin);
callback(null, true);
// callback("Not allowed by CORS", false)
// callback(new Error("Not allowed by CORS"), false)
}
return;
},
};
let corsMiddle = cors(corsOptions);module.exports = [bodyParser, compression(), corsMiddle];
Here mainly body-parser
, cors
and other such HTTP server level middlewares are defined.
We hope that this article would help you to structure your Express app in a better, cleaner way.
If you have any suggestions/additions to this repo, do let us know through our social media handles:
Facebook: OrderStack
Instagram: orderstack
Twitter: @orderstack