Ticker

6/recent/ticker-posts

Hexagonal Architecture API with Node + Express + Sequelize + Typescript + Mysql (Update 2022)


 Hexagonal Architecture

  • Clean Architecture by Uncle Bob.
  • Onion Architecture.
  • Separate infrastructure from business logic.
  • Inputs and outputs in border the our design.

Layers

  • Domain -> Entities: Represent business objects (Interfaces).
  • Domain -> Interactors or Services: Functions that interact with entities, business logic .
  • Domain -> Repositories: Interfaces that have operations with entities, for example, getbyId, getAll, save.
  • Aplication -> Transport Layer: Communication methods, for example, HTTP or SQS (Input andOutput).
  • Infratructure -> Data Sources: Class that implements the functions of the repositories.


Install dependencies

 npm install -D @types/express @types/node @types/validator ts-node typescript

npm install express mysql2 sequelize

 

Structure Project

├─ src
│ ├─ controllers
│ │ └─ http
│ │ └─ PokemonController.ts
│ ├─ core
│ │ ├─ const
│ │ │ └─ http.ts
│ │ ├─ entities
│ │ │ └─ Pokemon.ts
│ │ ├─ interactors
│ │ │ ├─ IResponse.ts
│ │ │ ├─ index.ts
│ │ │ └─ pokemonInteractor.ts
│ │ └─ repositories
│ │ └─ pokemonRepository.ts
│ ├─ dataSources
│ │ ├─ PokemonDataSource.ts
│ │ └─ sequelize
│ │ ├─ PokemonModel.ts
│ │ └─ index.ts
│ └─ server
│ └─ index.ts

 

 

Endpoint app src/server/index.ts

import express from 'express';
import { Request, Response } from 'express';

import PokemonController from '../controllers/http/PokemonController';

const app = express();

app.get('/', (req: Request, res: Response) => {
res.send('Application works!');
});
app.use('/api/v1/pokemon', PokemonController);

app.listen(3000, () => {
console.log('Application started on port 3000!');
});

  

src/controllers/http/PokemonController.ts

import PokemonInteractor from '../../core/interactors';
import express from 'express';
import { Response, Request } from 'express';

const router = express.Router();

router.get('/:id', async (req: Request, res: Response) => {
const { id } = req.params;
const { status, data, error } = await PokemonInteractor.getById(parseInt(id));
res.status(status).json({ data, error });
});

router.get('/', async (req: Request, res: Response) => {
const { status, data, error } = await PokemonInteractor.getAll();
res.status(status).json({ data, error });
});

export default router;

 

src/core/interactors/index.ts

import PokemonInteractor from './pokemonInteractor';
import PokemonDataSource from '../../dataSources/PokemonDataSource';

const pokemonDataSource = new PokemonDataSource();
const pokemonInteractor = new PokemonInteractor(pokemonDataSource);

export default pokemonInteractor;


src/core/interactors/pokemonInteractor.ts

import PokemonRepository from '../repositories/pokemonRepository';
import PokemonEntity from '../entities/Pokemon';
import { HTTP_STATUS } from '../const/http';
import { IResponse } from './IResponse';

class PokemonInteractor {
pokemonRepository: PokemonRepository;

constructor(pokemonRepository: PokemonRepository) {
this.pokemonRepository = pokemonRepository;
}

async getById(id: number): Promise<IResponse<PokemonEntity | undefined>> {
const pokemonModel: PokemonEntity = await this.pokemonRepository.getById(
id,
);
if (!pokemonModel) {
return { status: HTTP_STATUS.NOT_FOUND, error: 'not found' };
}

const pokemon: PokemonEntity = {
id: pokemonModel.id,
name: pokemonModel.name,
type: pokemonModel.type,
image: pokemonModel.image,
};

return { status: HTTP_STATUS.OK, data: pokemon };
}

async getAll(): Promise<IResponse<PokemonEntity[]>> {
try {
const pokemonModel = await this.pokemonRepository.getAll();
return { status: HTTP_STATUS.OK, data: pokemonModel };
} catch (e) {
return { status: HTTP_STATUS.INTERNAL_ERROR, error: e.message };
}
}
}
export default PokemonInteractor;

 src/core/reporsitories/pokemonRepository.ts

import Pokemon from '../entities/Pokemon';

interface PokemonRepository {
getById(id: number): Promise<Pokemon>;
getAll(): Promise<Pokemon[]>;
}
export default PokemonRepository;

 

 src/core/entities/pokemon.ts 

export default interface Pokemon {
id: number;
name: string;
type: string;
image: string;
}

 

src/dataSources/PokemonDataSource.ts

import pokemonRepository from '../core/repositories/pokemonRepository';
import Pokemon from '../core/entities/Pokemon';
import PokemonModel from './sequelize/PokemonModel';

class PokemonDataSource implements pokemonRepository {
public async getById(id: number): Promise<Pokemon> {
const pokemonModel: Pokemon = await PokemonModel.findOne({ where: { id } });
return pokemonModel;
}

public async getAll(): Promise<Pokemon[]> {
const pokemonModel: Pokemon[] = await PokemonModel.findAll();
return pokemonModel;
}
}

export default PokemonDataSource;

 

src/dataSources/sequelize/PokemonModel.ts

import { Model, DataTypes, Optional } from 'sequelize';

import PokemonAttributes from '../../core/entities/Pokemon';
import sequelizeInstance from './index';

interface PokemonCreationAttributes extends Optional<PokemonAttributes, 'id'> {}

class Pokemon
extends Model<PokemonAttributes, PokemonCreationAttributes>
implements PokemonAttributes
{
public id!: number;
public name!: string;
public image!: string;
public type!: string;

// timestamps!
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
}

Pokemon.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
name: {
type: new DataTypes.STRING(128),
allowNull: false,
},
image: {
type: new DataTypes.STRING(128),
allowNull: false,
},
type: {
type: new DataTypes.STRING(128),
allowNull: false,
},
},
{
tableName: 'pokemon',
sequelize: sequelizeInstance,
},
);

export default Pokemon;


 

src/dataSources/sequelize/index.ts 

import { Sequelize } from 'sequelize';

const sequelize = new Sequelize(
'mysql://unsername:password@localhost:3306/pokemonDb',
);

export default sequelize;

 

Git Repository: https://github.com/sistemasnegros/api-hex-arch-express-types 

Reacciones:

Post a Comment

1 Comments

  1. Excelente artículo! bien explicado, bien detallado además de bien estructurado el proyecto. Muchas felicidades!!

    ReplyDelete