Some time ago I made a post about creating a Fullstack project with Node.js, React.js, and MongoDB. This is a very cool starter project that could help us get up and running with the basics.
But implementing a better architecture is very important, especially if you have a big project and you are working with a large team. This will help you develop and maintain easily your project.
So the objective of this post is to share my current API architecture and the way I found to create a better structure, applying design patterns and clean code.
Let's dive into code.
First of all, let's create our work folder and the initial files.
$ mkdir node-starter
$ cd node-starter
$ touch index.js
$ npm init -y
Creating the structure
Now, let's create the base folders for the project
$ mkdir config src src/controllers src/models src/services src/helpers
Add dependencies
For this project, we are going to use Express and MongoDB, so let's add our initial dependencies.
$ npm install --save body-parser express mongoose mongoose-unique-validator slugify
Add DEV dependencies
As we want to be able to use the latest ES6 syntax in this project, let's add babel and configure it.
npm i -D @babel/node @babel/core @babel/preset-env babel-loader nodemon
Here we also added nodemon as a dev dependency to run and test the project easily.
Setting up babel
In the main folder, create a file called .babelrc with the following code:
{
"presets": [
"@babel/preset-env"
]
}
Now go over to your package.json and add the following scripts
"scripts": {
"start": "babel-node index.js",
"dev:start": "clear; nodemon --exec babel-node index.js"
}
Create the server
Under config folder, create a file called server.js with the following code
import express from "express";
import bodyParser from "body-parser";
const server = express();
server.use(bodyParser.json());
export default server;
Now let's import our server config into our index.js file:
import server from './config/server';
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
console.log(`app running on port ${PORT}`);
});
At this point, you should be able to run your server with the following script:
$ npm run dev:start
And you should get a response like this:
[nodemon] 1.19.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `babel-node index.js`
app running on port 5000
Setting up the Database
Now let's set up our database.
For this, you must have MongoDB up and running in your local machine.
Under config, add the file database.js
//database.js
import mongoose from "mongoose";
class Connection {
constructor() {
const url =
process.env.MONGODB_URI || `mongodb://localhost:27017/node-starter`;
console.log("Establish new connection with url", url);
mongoose.Promise = global.Promise;
mongoose.set("useNewUrlParser", true);
mongoose.set("useFindAndModify", false);
mongoose.set("useCreateIndex", true);
mongoose.set("useUnifiedTopology", true);
mongoose.connect(url);
}
}
export default new Connection();
Here we are creating a singleton instance of our database by exporting a new Connection. This is automatically handled by node when you export it like this and it makes sure that you are only going to have one single instance of this class in your application.
And now, import it right in the beginning of your index.js file.
//index.js
import './config/database';
//...
Create a model
Now let's create our first model.
Under src/models, create a file called Post.js with the following content.
//src/models/Post.js
import mongoose, { Schema } from "mongoose";
import uniqueValidator from "mongoose-unique-validator";
import slugify from 'slugify';
class Post {
initSchema() {
const schema = new Schema({
title: {
type: String,
required: true,
},
slug: String,
subtitle: {
type: String,
required: false,
},
description: {
type: String,
required: false,
},
content: {
type: String,
required: true,
}
}, { timestamps: true });
schema.pre(
"save",
function(next) {
let post = this;
if (!post.isModified("title")) {
return next();
}
post.slug = slugify(post.title, "_");
console.log('set slug', post.slug);
return next();
},
function(err) {
next(err);
}
);
schema.plugin(uniqueValidator);
mongoose.model("posts", schema);
}
getInstance() {
this.initSchema();
return mongoose.model("posts");
}
}
export default Post;
Create our services
Let's create a Service class that is going to have all the common functionalities for our API, making it available for other services to inherit them.
Create a file Service.js under src/services folder;
//src/services/Service.js
import mongoose from "mongoose";
class Service {
constructor(model) {
this.model = model;
this.getAll = this.getAll.bind(this);
this.insert = this.insert.bind(this);
this.update = this.update.bind(this);
this.delete = this.delete.bind(this);
}
async getAll(query) {
let { skip, limit } = query;
skip = skip ? Number(skip) : 0;
limit = limit ? Number(limit) : 10;
delete query.skip;
delete query.limit;
if (query._id) {
try {
query._id = new mongoose.mongo.ObjectId(query._id);
} catch (error) {
console.log("not able to generate mongoose id with content", query._id);
}
}
try {
let items = await this.model
.find(query)
.skip(skip)
.limit(limit);
let total = await this.model.count();
return {
error: false,
statusCode: 200,
data: items,
total
};
} catch (errors) {
return {
error: true,
statusCode: 500,
errors
};
}
}
async insert(data) {
try {
let item = await this.model.create(data);
if (item)
return {
error: false,
item
};
} catch (error) {
console.log("error", error);
return {
error: true,
statusCode: 500,
message: error.errmsg || "Not able to create item",
errors: error.errors
};
}
}
async update(id, data) {
try {
let item = await this.model.findByIdAndUpdate(id, data, { new: true });
return {
error: false,
statusCode: 202,
item
};
} catch (error) {
return {
error: true,
statusCode: 500,
error
};
}
}
async delete(id) {
try {
let item = await this.model.findByIdAndDelete(id);
if (!item)
return {
error: true,
statusCode: 404,
message: "item not found"
};
return {
error: false,
deleted: true,
statusCode: 202,
item
};
} catch (error) {
return {
error: true,
statusCode: 500,
error
};
}
}
}
export default Service;
Ok, this seems a lot of code.
In this service, we created the main functionality (a basic CRUD) for our application, adding functions to get, insert, update and delete items.
Now, let's create our Post service and inherit all this functionality we just created.
Under src/services, create a file PostService.js with the following content:
//src/services/PostService
import Service from './Service';
class PostService extends Service {
constructor(model) {
super(model);
}
};
export default PostService;
It is as simple as that, it inherits all the functionality we created in our main Service.js file and it can be repeated across your API for all the other endpoints.
Create the controllers
We are going to follow the same principle we had while creating our services, here we are going to create a main Controller.js file that will have all the common functionalities and make the other controllers inherit it.
Create a file Controller.js under src/controllers and add the following code:
//src/controllers/Controller.js
class Controller {
constructor(service) {
this.service = service;
this.getAll = this.getAll.bind(this);
this.insert = this.insert.bind(this);
this.update = this.update.bind(this);
this.delete = this.delete.bind(this);
}
async getAll(req, res) {
return res.status(200).send(await this.service.getAll(req.query));
}
async insert(req, res) {
let response = await this.service.insert(req.body);
if (response.error) return res.status(response.statusCode).send(response);
return res.status(201).send(response);
}
async update(req, res) {
const { id } = req.params;
let response = await this.service.update(id, req.body);
return res.status(response.statusCode).send(response);
}
async delete(req, res) {
const { id } = req.params;
let response = await this.service.delete(id);
return res.status(response.statusCode).send(response);
}
}
export default Controller;
Now, let's create a PostController file under src/controllers
//src/controllers/PostController.js
import Controller from './Controller';
import PostService from "./../services/PostService";
import Post from "./../models/Post";
const postService = new PostService(
new Post().getInstance()
);
class PostController extends Controller {
constructor(service) {
super(service);
}
}
export default new PostController(postService);
Here, we are importing the desired service and model and we are also creating an instance of our Post service passing a Post model instance to its constructor.
Create the routes
Now it's time to create the routes for our API.
Under the config folder, create a file routes.js
//config/routes.js
import PostController from './../src/controllers/PostController';
export default (server) => {
// POST ROUTES
server.get(`/api/post`, PostController.getAll);
server.post(`/api/post`, PostController.insert)
server.put(`/api/post/:id`, PostController.update);
server.delete(`/api/post/:id`, PostController.delete);
}
This file imports the Post controller and map the functions to the desired routes.
Now we have to import our routes into our server.js file right after our body parser setup, like so:
//config/server.js
//...
import setRoutes from "./routes";
setRoutes(server);
//...
At this point, you should be able to make requests to all the routes created, so let's test it.
Make a POST request for the route /api/post with the following json body:
Here you can use an API client like Postman or Insomnia to this task
{
"title": "post 1",
"subtitle": "subtitle post 1",
"content": "content post 1"
}
You should get something like this:
{
"error": false,
"item": {
"_id": "5dbdea2e188d860cf3bd07d1",
"title": "post 1",
"subtitle": "subtitle post 1",
"content": "content post 1",
"createdAt": "2019-11-02T20:42:22.339Z",
"updatedAt": "2019-11-02T20:42:22.339Z",
"slug": "post_1",
"__v": 0
}
}
Conclusion
There are lots of ways to design the architecture of an API, and the goal is always to have a cleaner and reusable code, don't repeating yourself and helping others to work along easily, plus it will also help yourself with maintenance and adding new functionalities.
You can find the source code here
Hope you may find it useful.
Bye!
Top comments (99)
I tried adding another model(Comment) that is related to Post, but when I try to add .populate('comments') to getAll method in Service class I get
MissingSchemaError: Schema hasn't been registered for model "Comment".
This is how I've added it to Post model
comments: [
{
type: Schema.Types.ObjectId,
ref: 'Comment'
}
],
Hi Aleksandar,
The property
ref
must receive the name of the schema you added inmongoose.model
, not the name of the class you created.For example, if your Comment schema is exporting the following:
mongoose.model("comments")
, then your ref should be:'ref': 'comments'
.Let me know if that makes sense and if it solves the problem :)
Great article about how to start off a project and how to modularise your code. But it's not about architecting an API. This isn't architecture. Please don't call it architecture, you'll confuse people. If you want to talk about the architecture of an API, at the very least that would require a huge amount more background, a much more complex real-world based scenario, a lot of discussion, and a series of articles with diagrams.
Nonsense...so what is your definition of architecture then? Giving background and a real world example over and above what's written here would all of a sudden qualify this post of being worthy of being labelled a architecture discussion? Utter nonsense.
Good article on how to architect your node apps. Thanks Thiago.
Architecture isn't how to organise stuff. You don't tell your kid to "go upstairs and architect your room". Architecture is an altogether higher level discipline. As I said, it's a good article. But it's not about architecture.
There is no professional body (especially with regards to JS dev) that will define exactly what "architecture" is and isn't. For me, architecture is a very loose term that can mean many things, code structure being one of them. Personally, when I think "application architecture" then the data model is fundamental and most important - but the code structure is also a part of it (as is choice of technology etc).
I think we might actually agree (that architecture is a loose and general term), I just felt it unnecessarily pedantic to moan about the title of this post when it's perfectly "fine".
"Structuring your Node.js code in a better way" just doesn't have the same ring to it!
😁
developer.android.com/jetpack/guide
Android team seems to agree that this is app architecture.
If this is like "sending your kids to architect their bedrooms", doing it at a higher level would be just telling someone to choose who is going to organize a room.
This is just an architecture, planning and adacting an architecture if no just "where to put the code" is also about breaking dependencies, how each the different parts of the code interact with each other in a correct manner so each of them can be easily be manteined and tested and also be able to change technologies, and apis without having to touch a large number of files in the project. Doing app architecture at a higher level is more or less the same, just at a higher level... (highly abstracted, It could be harder or it could be easier it depends on many variables and kinds of project)
No. Architecture is not organisation. It is about deciding how a process is systemised. It's the difference between a bottlenecked system and one that's almost infinitely scalable. It's how flexible and adaptable the system is to fit the underlying process it's running.
Everything you describe is not architecture. It's being tidy and organised. It's just low-grade devops. Sure the Android team thinks that's architecture, because let's face it, Android doesn't run at the Enterprise level, it runs at single device level. But I'd rather they didn't pollute the namespace by usurping irreplaceable words which already have established meaning.
A construction site should be kept tidy and organised for health & safety reasons. But that's not architecture. Folder structures for Node.js apps are just a way of keeping your construction site tidy & organised.
Misusing terms creates confusion. There's already enough of that. Let's not create even more.
I disagree. That is one interpretation of an application architecture. However is not the only one. I wrote my Master Thesis on exploring the concept of software achitechture and among the conclusions is that, by necessity, the scope of "the architecture" is not always tangible or strict. It can (but more often is not) be defined for one single functionality, for the database, for the logging, or for every and any part of the application.
The tangible documentation of the architecture is usually referenced on older literature, but modern software solutions have extrapolated multiple layers of complexity and tend to be higher level, thus defining the architecture on a separate set of documents, ideas or mandates is less and less practical (where do we stop if we have an N number of references, do we specify those as well? do we use partial documentation of "obvious" dependencies? what if we need to take over one of those dependencies? where do we stop the abstractions? when is it more pragmatic to make an actual implementation that is self documenting?).
Often architecture is interpreted as "how to organize the project" and for many authors that means the way the project is literally organized on a file system is an implicit declaration of it's architecture. Under that construct, it is completely valid to say that this article defines the application's architecture in it's own realization.
The purpose of my thesis was to explicitly demystify and make the concept of architecture less esoteric, since is supposed to be a tool for a more concrete plan of realization of an application. Another conclusion is that things like a simple diagram could define an architecture and that any author can express it as detailed and using as many tools as the intention (which is express the organization for a purpose) require or was practically desired. Under those circumstances, declaring that this cannot be called architecture is up actually up to the author and whoever receives this information, if it helps clarify how the application works, then definitely serves as the architecture. The recipient of the information cancan request a more concrete definition of any of the parts (but I can hardly find a more concrete way than the code explained). Architecture in software serves the purpose of communication. The tools could be the discussion, articles or diagrams, but that on their own do not exclusively define the architecture.
Architecture is a way of solving the problem so for solving the problem one has to organize the structure so that one can find and fix the right solutions to the right issue. So there is a architecture involved ... it could be a traditional MVC all i can see is model & controller
Removed.
I don't have much experience writing anything not even in my native language. I am just trying to share something that could be helpful for somebody.
If you could tell me what is wrong with the article I appreciate, that would help me and others that want to share something.
Thanks so much for this article Bro. and please keep up the good work.
Thank you very much Samson, I appreciate it! :)
You've done a big favor writting this post. I was digging for 2 days inorder to find this way of implementation.
Thanks Buddy, Cheers!
Thank you very much Tajesh! This is really motivating.
Let me know if you have any doubts about it.
Oh the irony! "The rest if that"???
Don't be such a douche bud, especially when you can't do any better. That post is about programming, it's not a spelling bee or a grammar contest. Worse case, you can mention and correct the mistakes, so that he will learn.
You don't have to say that it's a waste of your precious time because nobody gives a damn, and it makes you look like a jerk.
It's the very reason why I use Grammarly 🚀. Grammarly is free if you only want to correct grammatical errors.
Thank you for the suggestion, Grammarly is really amazing and I should have used that in the beginning.
So you're saying Grammarly doesn't work?
Is there a grammatical error in my sentence? 😂 The plugin says it is fine 😎
Yes. Two.
I'm listening.
I'm pretty sure it's fine. To me, it's slightly strange when you say "it is" instead of "it's" and when you say "if you want only to" rather than "if you only want to". They're not actually wrong though.
Thanks for the feedback 👍
Two Grammatical errors does not necessitate this comment, to be honest. Nor does it equal a lack of respect. I'm sure not everything you've ever written has been the height of the English language?
I didn't mean to be unkind. Just please get these things checked! :)
“I don’t want to read the rest if that, thanks.” Oh the irony.
Typically for architecture related posts, there's a high level diagram of what's happening. I don't see any here. Consider adding one 🙂.
Thank you for your suggestion, I am going to add that.
Hi can you help me test the API using insomia.
What would be my URL for POST?
Thanks in advance!
Hi Norbin,
You can test the app sending a post request to localhost:5000/api/post
Yes done i've already did that, I also tried localhost:5000/api/getAll.
Im getting error below.
Cannot POST /api/post
Cannot GET /api/getAll
Thanks for this post Thiago! Its really helpful. By the way, I also have 2 questions to ask you -
Question 1 -> Do we need to write the business logic only inside the service module?
Question 2 -> Should we separate the database queries into a new module (a repository) or is it better to structure the application without a repository like you did?
Hi Mridul, thanks for the feedback!
For the question one, yes the main idea was to have the business logic in the service layer.
For the second question I totally agree that it would be great to separate the database queries into a repository. I didn’t separate it like that in this tutorial for simplicity purposes.
I have created recently another structure that might accommodate these changes and some other features. For this new one I am using typescript and I also have the tests setup.
I am thinking about creating a series of articles about it, do you think it would be a helpful?
Thanks again!
Yes, It would definitely help me and other beginners like me. I was also looking for some good articles on unit testing and integration testing. Will it be possible for you to incorporate that in your articles?
Yes please make an article and/or repository about this architecture with TypeScript. I'm struggling to create a strictly typed backend with Express + Mongoose.
I'll try to work on a tutorial about it this weekend. I can send you a repo earlier if you'd like to play around with the idea.
Awesome, I'd love to get a link to the repo!
@thiago Pacheco: I like the implementation of the architecture,
I am having problem runnning the project can you help me out.
in Base Controller, I get this error when I run the project.
this.get = this.get.bind(this);
can not read property bind of undefined.
NOTE:I have not changed anything and trying to run the code
Can you please me in running code so I can continue working on it?
Hi Muhammad,
Thank you for sharing that.
Actually, this is an extra line that was not supposed to be in this file.
I was trying to reduce the size of the project to make it simpler, but I forgot to remove this call.
You can just remove the following line from the Controller constructor:
this.get = this.get.bind(this);
I already fixed it in the post, sorry for the error.
Let me know if that worked for you.
It worked for me, Thanks a lot .
I also removed
server.get(
/api/post/:params
, PostController.get);from routes.js
Thanks a lot.
Perfect Muhammad, thank you very much!
Nice post
However your
get()
method in theController
class refers to a method on the service instance also calledget()
which doesn't exist.Service
class hasgetAll
,insert
,update
anddelete
methods but notget
Thank you for seeing that Ben, I was trying to reduce the functions because the post does was very long but I've forgotten to remove this one too.
I've updated the post now.
In the official mongoose documentation they refer to using the save() instead of create().
I'm getting an error while trying to use the save() function, I've tried to fix this (maybe I'm being a fancy), but I couldn't.
// this.model.save();
It would be great if we could solve this.
Hey Reynaldo,
Mongoose also offers the create method that does basically the same thing, and it also offers some support to save multiple documents, check it out here: mongoosejs.com/docs/api.html#model...
Could you share the error you are getting in this part and the source code, maybe we can help you.:)
I didn't know that calling create was triggering the same effects as calling the save() function, it was guiding me from this post (twm.me/correct-way-to-use-mongoose/).
Maybe this is no longer necessary, but I was getting an output of (this.model.save () is not a function).
Very good job by the way.
Great article but The first image of "Create the server" heading Line no 5 is "app.use(bodyParser.json())" while it should be "server.use(bodyParser.json())"
because there is no variable with name "app".
Thanks
That is totally right Aashir, thank you very much for reporting that. I just fixed the post :)
hey thanks in advance for this tutorial, but I download your repo from github and make this post with postman and dont works, stay Sending request.. and no work, dont return nothing, some advice could you give me ?
Hi, are you sending a JSON body in your POST request? Do any errors show up in your terminal?
If you could, show me how you are making the request, please.
Dont worry master, my bad because Im begginer and I have no installed mongoDB for that no show nothing,any error or any response, after I installed mongo all works like a charm;otherwise can you help me with other thing? please?, im trying to do this...
import Controller from './Controller';
import PostService from "./../services/PostService";
import RecordService from "./../services/RecordService";
import Post from "./../models/Post";
import Record from "./../models/Record";
const postService = new PostService(
new Post().getInstance()
);
const recordService = new RecordService(
new Record().getInstance()
);
class PostController extends Controller {
constructor(service) {
super(service);
}
async insert(req, res)
{
console.log("usando overwrite");
let response = await this.postService.insert(req.body);
console.log("creando record");
let response2 = await this.recordService.insert({"title":response.slung,"status":"created","post": response });
console.log("creado "+response2);
if (response.error) return res.status(response.statusCode).send(response);
return res.status(201).send(response);
}
}
export default new PostController(postService);