/perspective-work-example

A simple implementation of the CLEAN architecture

Primary LanguageTypeScript

Backend Engineer Work Sample

This project skeleton contains a basic Express setup one endpoint to create a user and one endpoint to fetch all users, as well as a basic empty unit test.

Scripts

npm start starts the server

npm test executes the tests

Goal

  1. Adjust POST /users that it accepts a user and stores it in a database.
    • The user should have a unique id, a name, a unique email address and a creation date
  2. Adjust GET /users that it returns (all) users from the database.
    • This endpoint should be able to receive a query parameter created which sorts users by creation date ascending or descending.

Feel free to add or change this project as you like.

My changes

i have refactored the app to use Hexagonal/Clean Architecture a with the idea being to demonstrate an understanding of software design paradigms.

clean architecture attempts to break down code into different units that do one very specific thing, keep them Modular and maintainable

Directory Strucuture

  • Application

    • services
    • use-cases
  • Domain

    • entities
    • validation
    • repositories
  • Infrastructure

    • inbound
      • http
    • outbound
      • database

Application layer

files in Application layer deal with data manipulation and business logic these are services and use-cases.

in this example use-cases are the meat of the application they gather information from different services, do transforms and pass the data to either a user or to a service.

services are more like data transport, moving data from use-cases to data stores or from data stores to use cases, in the form of entities. services can also perform some business logic but rather offload this information to the use-cases or to entities themselves. services will most likely instantiate an entity and cary the information in that form.

Domain

This layer mostly deals with types in our system here we can find our user entity or user type, this class is responsible for defining what a user is and what a user can do.

we also have other types here such as ports (repository) which must be implanted by any outside source that would like to receive data from our system.

Infrastructure

these are outside exchangeable parts of our system, they contain no business logic things like (databases, http servers) these can be added or swapped out, in this example we use MongoDB and Express but we could just as easily swap these out for postgreSQL and a hapijs server and the rest of our business logic will not change or even need to be refactored.

The database reading and writing is handled by mongoose via our repository, the repository here does gets an interface from the domain layer this interface will tell it which methods to implement(which methods the userService will call and which data will be passed.

The http server

The http server in this case an expressjs server will interact with our system via use-cases.

The server is setup in a pretty standard manner

  • controllers
  • routes
  • middleware
  • utils
  • validation
  • app.ts
  • server.ts
  • container.ts

since we are using dependency injection we have a container class which will setup all the instances, we also have a route factory which will take an instance of a controller and return the routes appropriate for this controller by looking at the instance type.

this way we can automatically inject all decencies right into the system when we start up the server.

API Response

the api responses are formated as follows

If the result is a single object

{
    "data": {
        "firstName": "asdasd",
        "lastName": "dkfsdf",
        "email": "as53@s.com",
        "created_at": "2024-03-14T10:34:29.913Z",
        "uId": "bb14d64d-e750-4dc8-9a7f-6d4d67d95441"
    },
    "message": "Success"
}

If the result is an Array

{
    "data": [
        {
            "firstName": "asdasd",
            "lastName": "dkfsdf",
            "email": "as53@s.com",
            "created_at": "2024-03-14T10:04:47.082Z",
            "uId": "1c6eea12-fef5-4c06-ac61-95071e8a0b12"
        },
        {
            "firstName": "asdasd",
            "lastName": "dkfsdf",
            "email": "as53@s.com",
            "created_at": "2024-03-14T10:05:34.320Z",
            "uId": "32002771-1fd3-4821-b931-a0f746166cfa"
        }
    ],
    "count": 2,
    "message": "Success"
}

I did not add pagination yet to this example but I plan to

Example: inbound data

you create a user via REST POST /user. The controller gathers the data describing your user and invokes a "create-user" use case in the User context. The use case, in turn, calls the UserService which will take the plain data entered create a user entity this entity (an instance of the user calls, the entity is than passed onto the repository the repository is responsible for saving the entity to a data store.

Exanple: outbound data

you make a request for all the users via REST GET /user in this case the data sent via query params will mostly follow the same path as above but no new user is created the user service requests the repository to make a query the repository will return an array of MongoDB documents these documents are than parsed by the services and converted into actual user instances before being returned to the use case the use-case (get-all-users) will than return to the Controller a new array of objects by calling the User.toJSON method.

The idea is that internally in the application layer we only work with entities, these entities encapsulate everything they need validation, entity specific logic etc when the application needs to give data to the outside it can simply request a safe json representation of the entity to share via toJSON

Setup and Docker

The app is dockerized and orchastrated with docker-compose, a simple docker-compose up is all you need to get started. Docker will spin up

  • a mongodb container
  • the application nodejs container, running alpine node 21

Testing

There are unit tests covering most of the application since its built with clean architecture its very easy to isolate and test specific components, I've used jest for the testing.

Error handling

All parts of the application are allowed to throw errors these errors will be caught in our error handling middleware and formatted into an appropriate http response.

There are custom error types as well in our domain layer

Data Validation

data is validated in multiple layers

  1. The http layer validates all data coming in through its endpoints using yup, the helps catch and reject requests early on before they even cause load onto our application.

  2. donain entities are also responsible for validating all data passed to them especially during instantiation, the entity knows which data it needs and can reject bad data

  3. database layer also validates data via its schema and can also reject malformed data

Getting Started

  1. setup your environment, copy the .env.example to a new file .env
  2. install the dependencies npm i
  3. run the tests npm run test

If you are using docker

  1. build and run the containers docker-compose up -d or docker-compose up if you like logs
  • mongo container on port 27017
  • application container on port 4111

If you are running the app on your local machine

  1. make sure your have a mongoDB running on localhost:27017

Using Docker

you might need to add the project folder to shared paths from Docker -> Preferences... -> Resources -> File Sharing

If you encounter errors with bcypt you probably need to remove your local node_modules rm -rf node_modules/

than run a clean build docker-compose up --build be