- Sebastián Ituarte @sebajax
POST /api/v1/user
Parameter | Type | Description |
---|---|---|
name |
string |
Required. User name to be created |
email |
string |
Required. Email to be created |
Request
{
"name": "Jonh Doo",
"email": "[email protected]"
}
Response
{
"error": false,
"message": "USER_CREATED",
"data": {
"userId": 1,
"name": "John Doo",
"email": "[email protected]",
"updatedAt": "2023-05-25T22:13:07.013Z",
"createdAt": "2023-05-25T22:13:07.013Z"
}
}
Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, PUT, DELETE).
user.route.ts
// module import
import express, {Request, Response} from 'express';
// middleware import
import checkBodyMiddleware from '../middleware/checkBody.middleware';
// interface import
import {addUserController} from '../../controller/user/addUser/addUser.controller.interface';
const router = express.Router();
// add a new user
router.post(
'/',
checkBodyMiddleware,
(req: Request, res: Response): Promise<Response> => {
// execute controller
return addUserController.addUser(req, res);
}
);
export default router;
Observations
- We are adding a middleware to check that there is a body present in the POST execution.
- We also include addUserController to inject the dependency in the user route factory and not a direct injection so we can add a unit test mocking this model later.
Service is the layer that defines the use case definition resolution. In service main folder we add a folder with this specific service name in this example we use user so the structure will be /service/user. In this folder we have 3 files that are needed: user.response.ts (This holds all the use case resposes positive or negative) user.service.interface.ts (This holds the service factory for dependency injection and all it's definitions) user.service.ts (This holds the service coding for resolution)
- Create the needed structure in service folder create a new folder user if needed, then create a folder [addUser]
- Create a file named addUser.reponse.ts to addUser folder
- Create a file named addUser.service.interface.ts to addUser folder
- Create a file named addUser.service.ts to addUser folder
addUser.response.ts
// module import
import {StatusCodes} from 'http-status-codes';
// domain import
import IResponseDomain from '../../../domain/response.domain';
// interface import
import {IAddUserResponse} from './addUser.service.interface';
/*
* BAD_REQUEST
*/
const USER_EXISTS: IResponseDomain = {
error: true,
message: 'USER_EXISTS',
code: StatusCodes.BAD_REQUEST,
};
/*
* INTERNAL_SERVER_ERROR
*/
const INSERT_USER_ERROR: IResponseDomain = {
error: true,
message: 'INSERT_USER_ERROR',
code: StatusCodes.INTERNAL_SERVER_ERROR,
};
/*
* CREATED
*/
const CREATED: IResponseDomain = {
error: false,
message: 'USER_CREATED',
code: StatusCodes.CREATED,
};
// response to export with definition
const addUserResponse: IAddUserResponse = {
USER_EXISTS,
INSERT_USER_ERROR,
CREATED,
};
export default addUserResponse;
export {USER_EXISTS, INSERT_USER_ERROR, CREATED};
Observations
- We use IResponseDomain this interface comes with the structure to define how the controller receives the response from the service.
- We also include IAddUserResponse this is only an interface in the service.interface file to define all the responses that the service will have positive or negative.
- We will need a basic service.interface file with the needed interface structure like in the example below.
// for addMessageService response domain
interface IAddUserResponse {
USER_EXISTS: IResponseDomain;
INSERT_USER_ERROR: IResponseDomain;
CREATED: IResponseDomain;
}
addUser.service.interface.ts
// domain import
import {IUserData} from '../../../controller/user/addUser/addUser.controller.interface';
import IResponseDomain from '../../../domain/response.domain';
// model import
import {userModel} from '../../../model/user/user.model.interface';
// service import
import AddUserService from './addUser.service';
// for addMessageService response domain
interface IAddUserResponse {
USER_EXISTS: IResponseDomain;
INSERT_USER_ERROR: IResponseDomain;
CREATED: IResponseDomain;
}
// interface to implement the service
interface IAddUserService {
addUser(userData: IUserData): Promise<IResponseDomain>;
}
/*
* service factory init
*/
const addUserService: IAddUserService = new AddUserService(userModel);
export {IAddUserResponse, IAddUserService, addUserService};
Observations
- We use IResponseDomain this interface comes with the structure to define how the controller receives the response from the service.
- We also include userModel to inject the dependency in the addUserService factory and not a direct injection so we can add a unit test mocking this model later.
- We will need a basic service implementation file with the class and that implements this interface.
addUser.service.ts
// domain import
import IResponseDomain from '../../../domain/response.domain';
// interface import
import {IUserModel} from '../../../model/user/user.model.interface';
import {IUserData} from '../../../controller/user/addUser/addUser.controller.interface';
import {IAddUserResponse, IAddUserService} from './addUser.service.interface';
// service main class import
import Service from '../../service';
// response import
import addUserResponse from './addUser.response';
class AddUserService extends Service implements IAddUserService {
private response: IAddUserResponse;
private userModel: IUserModel;
public constructor(userModel: IUserModel) {
super();
this.response = addUserResponse;
this.userModel = userModel;
}
public async addUser(userData: IUserData): Promise<IResponseDomain> {
try {
// map user data to user model data
const user = {
name: userData.name,
email: userData.email,
};
const createdUser = await this.userModel.createUser(user);
// if all the process was succuessfully we return an OK status
return {
...this.response.CREATED,
data: createdUser,
};
} catch (error) {
this.logger.error(`${AddUserService.name} error ${error}`);
return this.response.INSERT_USER_ERROR;
}
}
}
export default AddUserService;
Observations
- We use IResponseDomain this interface comes with the structure to define how the controller receives the response from the service.
- This class needs to extend Service class that is part of the archetype.
- It has to implement IAddUserService because it has all the interface contract of this class.
Model is the layer that defines the interactaion with a datasource from the infraestructure layer, this layer colud be a relational database or a no relational database like mongodb. For this layer the structure needs a folder for the model and 2 files: user.model.interface.ts (This holds the model factory for dependency injection and all it's definitions) user.model.ts (This holds the model definition depending on the datasource)
- Create the needed structure in model folder create a new folder user.
- Create a file named user.model.interface.ts to user folder
- Create a file named user.model.ts to user folder
user.model.interface.ts
// module import
import {Optional} from 'sequelize';
// model import
import {UserModel} from '../index';
interface IUserModel {
userId: number;
name: string;
email: string;
createdAt: Date;
updatedAt: Date;
createUser(data: object): Promise<object>;
}
type UserCreationAttributes = Optional<IUserModel, 'userId'>;
/*
* model factory init
*/
const userModel: IUserModel = new UserModel();
export {IUserModel, UserCreationAttributes, userModel};
Observations
- We will need a basic user.model file all the models file will be called from model/index.ts and not directly because it will add the models to the database session.
model/index.ts
// interface import
import db from '../infraestructure/database/db';
// model import
import MessageModel from './message/message.model';
import UserModel from './user/user.model';
// map models to db session
db.addModels([UserModel, MessageModel]);
export {UserModel, MessageModel};
// module import
import {Model, Table, Column, DataType, HasMany} from 'sequelize-typescript';
// interface import
import {IUserModel, UserCreationAttributes} from './user.model.interface';
// model import
import MessageModel from '../message/message.model';
@Table({
tableName: 'user',
timestamps: true,
})
class UserModel
extends Model<IUserModel, UserCreationAttributes>
implements IUserModel
{
@Column({
type: DataType.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'user_id',
})
userId: number;
@Column({
type: DataType.STRING,
allowNull: false,
})
name: string;
@Column({
type: DataType.STRING,
allowNull: false,
})
email: string;
@Column({
type: DataType.DATE,
allowNull: false,
field: 'created_at',
})
createdAt: Date;
@Column({
type: DataType.DATE,
allowNull: false,
field: 'updated_at',
})
updatedAt: Date;
@HasMany(() => MessageModel)
messages: MessageModel[];
// method for creating a new user into the databse
public async createUser(user: UserCreationAttributes): Promise<object> {
const userCreate = new UserModel(user);
return userCreate.save();
}
}
export default UserModel;
Observations
- It will implement the interface defined in user.model.interface and will extend Model from sequelize adding the generics needed to operate.
Contoller is the layer that will be in between the route and the service it will serve has a validation layer previously to calling the service, and will have all the response definition needed fot the api. In controller main folder we add a folder with this specific controller name in this example we use user so the structure will be /controller/user. In this folder we have 2 files that are needed: user.controller.interface.ts (This holds the controller factory for dependency injection and all it's definitions) user.controller.ts (This holds the controller coding for resolution)
- Create the needed structure in controller folder create a new folder user if needed, then create a folder [addUser]
- Create a file named addUser.controller.interface.ts to addUser folder
- Create a file named addUser.controller.ts to addUser folder
addUser.controller.interface.ts
// controller import
import AddUserController from '../addUser/addUser.controller';
// schema import
import {addUserSchema} from '../../../schema/user.schema';
// interface import
import {addUserService} from '../../../service/user/addUser/addUser.service.interface';
/*
* body request data interface
*/
interface IUserData {
name: string;
email: string;
}
/*
* controller factory init
*/
const addUserController: AddUserController = new AddUserController(
addUserSchema,
addUserService
);
export {IUserData, addUserController};
Observations
- We also include addUserService to inject the dependency in the addUserController factory and not a direct injection so we can add a unit test mocking this model later.
- We will need a basic controller implementation file with the class and that implements this interface.
addUser.contoller.ts
// module import
import {Request, Response} from 'express';
import {ObjectSchema} from 'joi';
import {StatusCodes} from 'http-status-codes';
// domain import
import IResponseDomain from '../../../domain/response.domain';
// controller main class import
import Controller from '../../controller';
// interface import
import {IAddMessageService} from '../../../service/message/addMessage/addMessage.service.interface';
import {IMessageData} from './addMessage.controller.interface';
class AddMessageController extends Controller {
private addMessageService: IAddMessageService;
public constructor(
addMessageSchema: ObjectSchema,
addMessageService: IAddMessageService
) {
super(addMessageSchema);
this.addMessageService = addMessageService;
}
public async addMessage(req: Request, res: Response): Promise<Response> {
// get controller name for logging
const controller = AddMessageController.name;
const method = 'post';
// casting body to interface type
const body = req.body as IMessageData;
// log end point execution
this.logger.info(
this.logMessage.init(req.url, method, req.method, controller, body)
);
try {
// validate request with schema
const errorRequest = this.schemaValidation(body, controller, method);
if (errorRequest !== null) {
return res
.status(StatusCodes.BAD_REQUEST)
.send({error: true, message: errorRequest});
}
// call service
const {error, message, code, data}: IResponseDomain =
await this.addMessageService.addMessage(body);
return res.status(code).send({error, message, data});
} catch (error) {
this.logMessage.error(
req.method,
controller,
StatusCodes.INTERNAL_SERVER_ERROR,
error
);
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.send({error: true, message: 'SERVER_ERROR'});
}
}
}
export default AddMessageController;
Observations
- We use IResponseDomain this interface comes with the structure to define how the controller receives the response from the service.
- This class needs to extend Controller class that is part of the archetype.
Schema is the layer that has all the request validations it will be used by the controller and only the controller. In this layer we use joi that is a validation library from javascript. In the schema folder we will need to add a single for each main controller. For this example in all the user controller we will have a single user.schema.ts that will hold all the user validations.
user.schema.ts
// module import
import Joi from 'joi';
const addUserSchema = Joi.object({
name: Joi.string().min(1).required(),
email: Joi.string().email({minDomainSegments: 2}).max(100).required(),
}).required();
export {addUserSchema};
Util is the layer that will have logic that will be re use in multiple services in the application, it will also hold the providers if we need to call a external source.
Infraestructure is the layer that will handle the connections to the infraestructure layer for example a database. This is the most outter layer of the application and is needed to be only injected between layers, so we don't add a infraestructure dependecy to inner layers.
// module import
import {Sequelize} from 'sequelize-typescript';
// logger import
import {logger} from '../log/logger';
/**
* create a connection to the database
*/
const db = new Sequelize(
`${process.env.DB_NAME}`,
`${process.env.DB_USER}`,
`${process.env.DB_PASSWORD}`,
{
logging: msg => logger.debug(msg),
host: `${process.env.DB_HOST}`,
dialect: 'postgres',
dialectOptions: {
socketPath: process.env.DB_SOCKET || null,
},
pool: {
max: parseInt(`${process.env.DB_POOL_MAX}`, 10) || 5,
min: parseInt(`${process.env.DB_POOL_MIN}`, 10) || 0,
acquire: parseInt(`${process.env.DB_POOL_ACQUIRE}`, 10) || 30000,
idle: parseInt(`${process.env.DB_POOL_IDLE}`, 10) || 10000,
},
}
);
(async () => {
try {
await db.authenticate();
logger.info('connection to database has been established successfully.');
} catch (error) {
logger.error(`unable to connect to the database: ${error}`);
}
})();
export default db;
Build server
docker-compose -p nodejs-ts-clean-architecture build
Start server
docker-compose up -d
Stop server
docker-compose down
npm run dev
To run unit testing
npm run test
To run unit testing coverage
npm run test_cov
To run this project, you will need to add the following environment variables to your .env file
NODE_ENV
PORT
API_NAME
API_VERSION
LOG_LEVEL
DB_NAME
DB_USER
DB_PASSWORD
DB_PORT
DB_HOST
MIGRATION_DIR