O sistema lida com duas entidades: usuários e endereços, que mantém um relacionamento do tipo 1:N
, ou seja, um usuário pode ter vários endereços, mas os endereços estão relacionados à apenas um usuário.
As operações requeridas são CRUDs para ambas as entidades:
- Criação de usuários
- Listagem de usuários e seus endereços
- Alteração de usuários
- Remoção de usuários
- Criação de endereços
- Listagem endereços do usuário
- Alteração de endereços
- Remoção de endereços
Como especificado, as operações devem ser fornecidas através de um servidor HTTP, portanto optou-se por expor endpoints seguindo os princípios REST (embora a API não seja Restful!). Isso envolve o uso semântico dos verbos HTTP e a definição de endpoints orientados a recursos.
Além disso, as rotas de listagem devem permitir a especificação de parâmetros de filtragem através de querystrings nas rotas.
Foram utilizadas as ferramentas descritas no enunciado, com destaque para as seguintes:
- Typescript
- Node.js
- Express
- Mysql
- Knex.js
♥️ Objection.js
Além dessas, foram utilizadas algumas outras ferramentas para auxiliar o desenvolvimento ou adicionar funcionalidades extras do sistema.
Buscou-se por implementar o sistema seguindo o padrão da clean architecture (porém não aderindo 100% a ele). Isso tem algumas implicações:
- Definição de camadas do sistema e suas responsabilidades.
- Desacoplamento entre as camadas.
- Desacoplamento entre regras de negócio e implementações externas.
Ao longo do desenvolvimento foram realizados testes unitários usando a biblioteca Jest. Os testes foram aplicados principalmente na camada de domínio e casos de uso, auxiliando tanto na especificação quanto na validação das funcionalidades mais importantes da aplicação.
Para cada entidade e caso de uso há ao menos um conjunto de testes que validam suas funcionalidades.
Para validar os dados enviados nas requisições à API, foram utilizadas duas bibliotecas:
- JOI para a definição e validação de schemas.
- Celebrate para a criação de middlewares de validação.
O tratamento de erros é feito de forma centralizada na aplicação, fazendo uso da função next()
do express em conjunto com um error handler customizado implementado como um middleware.
Os erros são definidos na camada de domínio, portanto não contém nenhuma informação externa às regras de negócio da aplicação.
export abstract class BaseError extends Error {
public readonly details?: string
constructor(message: string, details?: string) {
super(message)
this.details = details
}
}
Dessa classe base são derivadas as classes de erro específicas:
export class EntityNotFound extends BaseError {
constructor(public readonly entity: string, public readonly details?: string) {
super(`${entity} not found.`, details)
}
}
O papel do error handler é identificar os errors gerados em outras camadas da aplicação, atribuir a eles um status HTTP e então enviar uma resposta com o máximo de informação possível sobre o erro.
type StatusAssoc = {
[key: string]: number
}
const httpStatus: StatusAssoc = {
UserAlreadyExists: 400,
EntityNotFound: 404,
InvalidFieldError: 400,
}
export async function handler(error: BaseError, _req: Request, res: Response, _next: NextFunction): Promise<Response> {
const { message, details } = error
const name = error.constructor.name
const status = httpStatus[name] || 500
if (message) {
return res.status(status).json({ error: message, details: details })
}
return res.status(status).json({ 'Internal error': error.message })
}
Foi implementado também o envio de e-mails no momento do cadastro do usuário. A implementação atual faz uso da biblioteca nodemailer para o envio dos e-mails.
A documentação da API foi feita seguindo os padrões da OpenAPI Specification, utilizando a ferramenta Swagger como interface. A documentação é extraída das rotas através de JSDocs pela biblioteca swagger-jsdoc e servida em um endpoint /docs
através da biblioteca swagger-ui-express.
Muito obrigado!