D E V Β Β Β G U I D E L I N E S
The purpose of the guidelines are:
- to bring consistency & uniformity in Node.js backend code
- to enable developers to focus on the business value, instead of the app itself
- to enable developers to write code faster, that are:
- clean
- readable & maintainable
- scalable
Some files are added as examples in the project structure. Noteworthy that, this project is intended for a monolith.
- Simple & scalable project structure
- Consistent dev environment
- precommit hooks to format code, for uniformity in code style
- support for multiple platform i.e. Windows, Mac, Linux
- TypeScript-integrated ORM, Sequelize
- TypeScript-integrated validator, Joi
- TypeScript-integrated tests for API response compatibility, to check and fail tests, if there are:
- changes in properties' data-type
- new properties
- removed properties
- null-able properties
- Built-in DevMail, to alert developers when backend crashed in production
TODO
Built-in support for file uploadTODO
Built-in security features
.
βββ server.ts (the main file)
βββ app.ts (the application)
βββ bin/ (various scripts)
βββ test/
βββ src/
βββ configs/
βββ constants/
βββ db/ (database models)
βββ dto/ (request data validations)
βββ migrations/ (database migrations)
βββ utils/ (helper utils)
βββ services/ (business domains)
βββ index.ts (exports all apis & service methods)
βββ order/
βββ shop/
βββ user/
Let's take from examples.
Firstly, let's say we want to add an Item service to our application. The admin will manage the items. So, it's going to be simple CRUD APIs, storing data into DB. To implement, we'll create a simple file with 5 standard methods in src/services/item/index.ts
:
export default class Item {
static create(req, rest) {}
static update(req, res) {}
static list(req, res) {}
static get(req, res) {}
static remove(req, res) {}
}
Secondly, let's say we want to add a Cart service to our application. Now, this is relatively complicated than Item service. Lots of methods & business logic, security, and performance to consider. In this case, we'd create files like below:
services/
βββ cart/
βββ index.ts
βββ add.ts
βββ update.ts
βββ checkout.ts
βββ free-campaign.ts
βββ calculate.ts
βββ list.ts
One of the most important guiding principles of REST is stateless. Meaning, the requests do not reuse any previous context. Each request contains enough info to understand the individual request.
To learn in details about API design, every backend developer should read the Google Cloud API Design guide.
In REST, the primary data representation is called a "resource".
- A collection resource (plural naming), identified by URI
/users
. - A single document resource (singular naming), identified by URI
/users/:user_id
. - Sub-collection resources are nested. In the URI
/users/:user_id/repositories
, "repositories" is a sub-collection resource. Similarly, a singleton in that sub-collection will be/users/:user_id/repositories/:repo_id
.
Some constraints in REST API naming ensures a design of scalable API endpoints:
- Use plural nouns to name, and represent resources. Example:
users
,orders
,categories
. - Use hypen, not underscores. Use lowercase letters in URI, never camel-case.
GET /food-categories
, notGET /foodCategories
. - No trailing forward slash. Example:
GET /users/
is wrong, andGET /users
is correct. - Never use CRUD function names in URIs, like
/users/list
or/users/create
. Rather use:GET /users
- get all users.GET /users/:id
- get a single user.POST /users/:id
- create a single user.PUT /users/:id
- update a single user.DELETE /users/:id
- delete a single user.
- Use query components to filter collection, never use for anything else. Example:
/users?region=Malaysia&sort=createdAt
.
Joi is used to validate/sanitize the client data. JSON is primarily supported. The purpose of request fields are:
req.body
:- To send data in POST and PUT
- To create/update.
- In many platforms, in a GET request, the request body is ignored/removed. Example: Swagger.
req.query
:- To control what data is returned in endpoint responses.
- To sort.
- To filter.
- To add search condition.
- Example:
/users?region=Malaysia&sort=createdAt
.
req.params
:- Path variables are used to get a singleton from a collection resource.
- Example:
GET /users/{user_id}
,GET /categories/{id}
.
To get type-annotated & type-safe DTO:
//// file: src/dto/user.ts
const signup = {
body : joi.object({
userType : joi.string().required().valid(userTypeList),
password : joi.string().password.required(),
mobile : joi.string().optional(),
firstName : joi.string().required(),
email : joi.string().email().required(),
}).required(),
}
//// file: src/modules/user/index.ts
import {Request} from 'express'
import dto from '@src/dto'
function signup (req:Request, res) {
const data = req.data(dto.user.signup, 'body')
data.email = 123 // TS ERROR, since TS knows that `email` is `string`.
}
Sequelize is the most popular Node.js ORM in the entire NPM registry, Mongoose being an ODM. Sequelize and Prisma compete closely, so they are almost the same popular. However, much of the features of Sequlize requires dynamic configuration internally. So static analysis doesn't become possible, and Sequelize doesn't provide much advantage of type-anotation with TypeScript. That's why efforts were undertaken in this regard.
To get type-annotated & type-safe DAO:
//// file: src/db/user.ts
@Table
export default class user extends Model<attr, crAttr> {
@PrimaryKey
@Column(DataType.UUIDV4)
id!: CreationOptional<string>
@Column
name!: string
@Column
email!: string
}
//// file: src/services/user/index.ts
import db from '@src/db'
async function login (req, res) {
const user = await db.user.findById(10)
user.email = 123 // TS ERROR, since TS knows that `email` is `string`.
user.save()
}