Translations 🇧🇷
- This back-end technical testing project involves the development of a RESTful API. It enables external users to sign up and, upon logging in, allows for the registration of clients and products, as well as the recording of product sales to clients. The project utilizes the AdonisJs framework to structure the API with a layered architecture (MVC), omitting views. Lucid is employed as an ORM, with Bcrypt for password encryption and JWT for token authentication, adhering to the principles of RESTful APIs and the MVC pattern.
-
Copy the repository to a local folder using the terminal with the following command:
git clone git@github.com:juliocmatias/adonis-be.git
If git is not installed, it can be installed using this command in the Debian/Ubuntu bash:
Debian/Ubuntu bash:
sudo apt-get install git
other kernel follow the instructions on the website Git.
windows/powershell:
winget install --id Git.Git -e --source winget
Or you can follow the website git documentation for more installation means.
-
install the dependencies:
npm install
This method of installing pending issues only works if the node installation package is npm, if you use another one, just switch to npm for the package used
you need to have node installed to be able to install the dependency packages If you don't have it, you can run the command if your operating system is Linux:
sudo apt update sudo apt install nodejs sudo apt install npm
If not, follow the installation instructions on the Node.js website.
⚠️ For the application to run correctly, the node version must be >= 18.
⚠️ It is also important to remember that for you to run the API using Docker, you will need to have it installed and configured on your machine. See the documentation to learn more about Docker. In addition to having docker-compose installed and configured on your computer. To install your operating system version, click here Docker Compose.
⚠️ Feel free to open the project in your preferred IDE, but you can run it just fine in the terminal.
- configure the .env file:
cp .env.example .env
This command will copy the .env.example file to .env, which is the file that contains the project's environment variables. You can open it in your preferred text editor and configure it however you want. If you don't want it, it already comes with the necessary settings for the application to work.
⚠️ You can use Docker to launch the API and MySQL database containers. This project comes with theDockerfile
anddocker-compose.yml
files configured. If you choose to use the API via a terminal with a database installed locally or in the cloud, you must correctly configure the.env
file for the application to work correctly.
🐋 Docker
⚠️ Before you begin, your docker-compose needs to be at version 1.29 or higher. See here or in the documentation how to install it. In the first article, you can replace where it is with1.26.0
with1.29.2
.
⚠️ It's important to note that the containers will run on port 3333 for API and 3306 for MySQL database, so if you are using them, simply swap the ports in the .env file.
-
For the API to start working, you first have to run docker-compose:
docker-compose up -d
-
When executing this command, wait for the API to become functional. checking through the adonis_app container log with the command:
docker logs -f adonis_app
When typing this command, a log should appear in the terminal with the same information as in the image below. Showing that the API is operational.
-
Once the application is operational, migrations and seeders will be processed. And the API will be operational to receive requests.
⚠️ Due to the mysql-dev-init.sql file located in the dockerConfig folder, docker-compose is configured to execute the queries from this file, creating a user and a database. If you wish to change the user in the environment variable and the database name, this file must also be edited, and the containers must be taken down and restarted, being rebuilt. -
If you need to reset the API database, run the command:
npm run db:reset
This command will delete all data from the database and run the migrations and seeders again.
💻 Terminal
To run the API via terminal, as already mentioned, you need to put the necessary information in the .env file for the API to connect to the MySQL database. By choosing this option, you must provide the necessary information for the API to be functional.
⚠️ Before running any command, it is necessary to ensure that the connection to the database is working and that a database has already been created. Pay attention to the name provided in the environment variable in .env:DB_DATABASE
.
-
Run in the terminal:
npm run dev
This command will start the API in development mode, and you will be able to access it via
http://localhost:3333
if that is the port passed. If you want to change the port, you can do so in the .env file. This way the API will be functional and ready to receive requests. -
To reset the API, run the command:
npm run db:reset
-
To stop the API in terminal, press
Ctrl + C
or case macOSCmd + C
.
If you use VSCode as your IDE, you can use the Thunder Client extension to make requests to the API. In the root of the project, there is a file named
thunder-collection_adonis_be.json
with a collection of pre-configured routes; just import it into the extension to use it.
🫂 Entity and Relationship Diagram
- The diagram shows the relationship between the entities in the database, where the user can have many clients, and the client can have many sales. The product can also have many sales, and the sale can have only one client and one product. I used the diagram to guide me in building the migrations, based on the information provided about each entity.
-
The API uses JWT tokens for route authentication, which must be sent via the authentication bearer token.
-
The API has the following routes:
⚠️ For the address and phone entities, routes, controllers, and services were not created as I focused on the main requirements of the challenge to deliver it on time. However, the migration with relationships and the models were implemented.
📖 Routers
Method | Endpoint | Description | Authentication |
---|---|---|---|
POST | /user | create a new user | NOT |
PUT | /user/:id | update a user by id | YES |
PATCH | /user/:id | update specific fields of a user by id | YES |
Method | Endpoint | Description | Authentication |
---|---|---|---|
POST | /login | login user | NOT |
Method | Endpoint | Description | Authentication |
---|---|---|---|
GET | /client | list all clients | YES |
GET | /client/:clientId/sales | list all sales of a client by id and filter by date by month and year | YES |
POST | /client | create a new client | YES |
PUT | /client/:id | update a client by id | YES |
PATCH | /client/:id | update specific fields of a client by id | YES |
DELETE | /client/:id | delete a client by id and automatically delete all sales of this client | YES |
Method | Endpoint | Description | Authentication |
---|---|---|---|
GET | /product | list all products | YES |
POST | /product | create a new product | YES |
PUT | /product/:id | update a product by id | YES |
PATCH | /product/:id | update specific fields of a product by id | YES |
DELETE | /product/:id | delete a product by id (soft delete) | YES |
Method | Endpoint | Description | Authentication |
---|---|---|---|
GET | /sale | list all sales with client and product | YES |
POST | /sale | create a new sale | YES |
DELETE | /sale/:id | delete a sale by id | YES |
🌎 Request and Response
Users
Create a new user
- Method: POST
- Endpoint: /user
- Description: Create a new user
- Authentication: NO
- body:
{
"email": "johndoe@doe.com",
"password": "123456"
}
-
validation:
- email: required, unique, format email valid
- password: required, min:6
-
exemple of request:
-
Method: POST
-
URL:
http://localhost:3333/user
-
exemple of response:
-
Status: 201
{
"id": 1,
"email": "johndoe@doe.com"
}
- status: 400
{
"message": "Invalid format for email"
}
- status: 409
{
"message": "Email already exists"
}
- status: 500
{
"message": "Internal server error"
}
Update a user by id
⚠️ Since Adonis uses the same method in the controller for both PUT and PATCH, which is update, this route will accept both the PUT method to update all properties and the PATCH method to update only one.
- Method: PUT
- Endpoint: /user/:id
- Description: Update a user by id
- Authentication: YES
- body:
{
"email": "exemple@exemple.com",
"password": "123456"
}
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
validation:
- email: required if not provided password, unique, format email valid
- password: required if not provided email, min:6
-
exemple of request:
-
Method: PUT
-
URL:
http://localhost:3333/user/1
-
exemple of response:
-
Status: 200
{
"id": 1,
"email": "exemple@exemple.com"
}
- status: 400
{
"message": "Invalid format for email"
}
- status: 401
{
"message": "Token not provided"
}
- status: 409
{
"message": "Email already exists"
}
- status: 500
{
"message": "Internal server error"
}
Login
- Method: POST
- Endpoint: /login
- Description: Login user
- Authentication: NO
- body:
{
"email": "user@user.com",
"password": "123456"
}
-
validation:
- email: required, format email valid
- password: required, min:6
-
exemple of request:
-
Method: POST
-
URL:
http://localhost:3333/login
-
exemple of response:
-
Status: 200
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJ1c2VyQHVzZXIuY29tIiwiaWF0IjoxNzIwMTU5MDAxLCJleHAiOjE3MjEwMjMwMDF9.3UQBzWrJXSSaoipfKWBU1f0-hMt_-JDbE8EhIMCfKSE"
}
- status: 400
{
"message": "Invalid format for email"
}
- status: 401
{
"message": "Invalid email or password"
}
- status: 500
{
"message": "Internal server error"
}
Clients
List all clients
⚠️ Clients will be ordered by id!
- Method: GET
- Endpoint: /client
- Description: List all clients ordered by id asc
- Authentication: YES
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
exemple of request:
-
Method: GET
-
URL:
http://localhost:3333/client
-
exemple of response:
-
Status: 200
[
{
"id": 1,
"name": "John Snow",
"taxId": "12345678912"
},
{
"id": 2,
"name": "Daenerys Targaryen",
"taxId": "98765432112"
}
]
- status: 401
{
"message": "Token not provided"
}
- status: 500
{
"message": "Internal server error"
}
List all sales of a client by id
- Method: GET
- Endpoint: /client/:clientId/sales
- Description: List all sales of a client by id and filter by date by month and year
- Authentication: YES
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
query params:
- month: not required, number, min:1, max:12
- year: not required, number, min:1900, max: current year
example of request:
- Method: GET
- URL:
http://localhost:3333/client/1/sales?month=1&year=2021
⚠️ Sales will be ordered by date, always showing the most recent one first!
- exemple of response:
200 - Query Params Not Informed
- Status: 200
- Query Params: Not Informed
{
"id": 1,
"name": "John Snow",
"taxId": "12345678912",
"sales": [
{
"id": 3,
"clientId": 1,
"productId": 1,
"quantity": 10,
"price": "10.00",
"totalPrice": "100.00",
"date": "2021-02-02T00:00:00.000Z"
},
{
"id": 4,
"clientId": 1,
"productId": 2,
"quantity": 10,
"price": "20.00",
"totalPrice": "200.00",
"date": "2021-02-01T00:00:00.000Z"
},
{
"id": 2,
"clientId": 1,
"productId": 2,
"quantity": 10,
"price": "20.00",
"totalPrice": "200.00",
"date": "2021-01-02T00:00:00.000Z"
},
{
"id": 1,
"clientId": 1,
"productId": 1,
"quantity": 10,
"price": "10.00",
"totalPrice": "100.00",
"date": "2021-01-01T00:00:00.000Z"
}
]
}
200 - Query Params Informed
⚠️ The response will be the same as the previous one, but with the sales filtered by the month and year informed in the query params.
⚠️ To filter by month and year, it is mandatory to send two queries; sending only one will not make the filter work correctly.
- Status: 200
- Query Params: Informed
{
"id": 1,
"name": "John Snow",
"taxId": "12345678912",
"sales": [
{
"id": 2,
"clientId": 1,
"productId": 2,
"quantity": 10,
"price": "20.00",
"totalPrice": "200.00",
"date": "2021-01-02T00:00:00.000Z"
},
{
"id": 1,
"clientId": 1,
"productId": 1,
"quantity": 10,
"price": "10.00",
"totalPrice": "100.00",
"date": "2021-01-01T00:00:00.000Z"
}
]
}
- status: 400
{
"message": "Invalid month or year"
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Client not found"
}
- status: 500
{
"message": "Internal server error"
}
create a new client
- Method: POST
- Endpoint: /client
- Description: Create a new client
- Authentication: YES
- body example:
{
"name": "John Snow",
"taxId": "12345678912"
}
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
validation:
- name: required, min:3
- taxId: required, unique, exact:11
example of request:
- Method: POST
- URL:
http://localhost:3333/client
-
exemple of response:
-
Status: 201
{
"id": 1
}
- status: 400
{
"message": "Name and taxId are required"
}
- status: 409
{
"message": "Client already exists"
}
- status: 500
{
"message": "Internal server error"
}
Update a client by id
⚠️ Since Adonis uses the same method in the controller for both PUT and PATCH, which is update, this route will accept both the PUT method to update all properties and the PATCH method to update only one.
- Method: PUT
- Endpoint: /client/:id
- Description: Update a client by id
- Authentication: YES
- body example:
{
"name": "John Snow",
"taxId": "12345678912"
}
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
validation:
- name: not required, min:3
- taxId: not required, unique, exact:11
example of request:
- Method: PUT
- URL:
http://localhost:3333/client/1
-
exemple of response:
-
Status: 200
{
"message": "Client updated"
}
- status: 400
{
"message": "Name must be at least 3 characters"
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Client not found"
}
- status: 409
{
"message": "Client already exists"
}
- status: 500
{
"message": "Internal server error"
}
Delete a client by id
⚠️ When a client is deleted, all sales associated with this client will also be deleted due to the foreign key settings in the sales migration.
- Method: DELETE
- Endpoint: /client/:id
- Description: Delete a client by id and automatically delete all sales of this client
- Authentication: YES
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
example of request:
- Method: DELETE
- URL:
http://localhost:3333/client/1
-
exemple of response:
-
Status: 200
{
"message": "Client deleted"
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Client not found"
}
- status: 500
{
"message": "Internal server error"
}
Products
List all products
⚠️ Products ordered alphabetically!
- Method: GET
- Endpoint: /product
- Description: List all products ordered by id asc
- Authentication: YES
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
query params:
- all: not required, boolean, example: all=true
- deleted: not required, boolean, example: deleted=true
⚠️ When passing the query all=true, all products will be shown, whether deleted (soft delete) or not. If you want only the deleted ones, pass the query deleted=true. If you want only the available ones, do not pass any query. If both are passed, all products, deleted or not, will be shown. If a different value than true is passed in the all query, only available products will be shown. If the value of the deleted query is not equal to true, deleted products will not be shown.
- exemple of request:
- Method: GET
- URL:
http://localhost:3333/product?all=true
- exemple of response:
200 - Query Params Not Informed - Products no deleted
- Status: 200
- Query Params: Not Informed
[
{
"id": 1,
"name": "Product 1",
"quantity": 100,
"price": "10.00",
"deleted": 0
},
{
"id": 2,
"name": "Product 2",
"quantity": 200,
"price": "20.00",
"deleted": 0
}
]
200 - Query Params Informed - Products no deleted
- Status: 200
- Query Params: something deleted=true
[
{
"id": 3,
"name": "Iron Throne",
"quantity": 100,
"price": "3000.00",
"deleted": 1
},
{
"id": 4,
"name": "Longclaw",
"quantity": 100,
"price": "3000.00",
"deleted": 1
}
]
200 - Query Params Informed - All Products
- Status: 200
- Query Params: all=true
[
{
"id": 3,
"name": "Iron Throne",
"quantity": 100,
"price": "3000.00",
"deleted": 1
},
{
"id": 4,
"name": "Longclaw",
"quantity": 100,
"price": "3000.00",
"deleted": 1
},
{
"id": 1,
"name": "Product 1",
"quantity": 100,
"price": "10.00",
"deleted": 0
},
{
"id": 2,
"name": "Product 2",
"quantity": 200,
"price": "20.00",
"deleted": 0
}
]
- status: 401
{
"message": "Token not provided"
}
- status: 500
{
"message": "Internal server error"
}
Get product by id
- Method: GET
- Endpoint: /product/:id
- Description: Get product by id
- Authentication: YES
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
example of request:
- Method: GET
- URL:
http://localhost:3333/product/1
-
exemple of response:
-
Status: 200
{
"id": 1,
"name": "Product 1",
"quantity": 100,
"price": "10.00",
"deleted": 0
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Product not found"
}
- status: 500
{
"message": "Internal server error"
}
Create a new product
- Method: POST
- Endpoint: /product
- Description: Create a new product
- Authentication: YES
- body example:
{
"name": "Product 1",
"quantity": 100,
"price": "10.00"
}
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
validation:
- name: required, min:3
- quantity: required, number
- price: required, number
example of request:
- Method: POST
- URL:
http://localhost:3333/product
-
exemple of response:
-
Status: 201
{
"id": 1
}
- status: 400
{
"message": "Missing required fields"
}
- status: 401
{
"message": "Token not provided"
}
- status: 500
{
"message": "Internal server error"
}
Update a product by id
⚠️ Since Adonis uses the same method in the controller for both PUT and PATCH, which is update, this route will accept both the PUT method to update all properties and the PATCH method to update only one.
- Method: PUT
- Endpoint: /product/:id
- Description: Update a product by id
- Authentication: YES
- body example:
{
"name": "Product 1",
"quantity": 100,
"price": "10.00"
}
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
validation:
- name: not required, min:3
- quantity: not required, number
- price: not required, number
example of request:
- Method: PUT
- URL:
http://localhost:3333/product/1
-
exemple of response:
-
Status: 200
{
"message": "Product updated"
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Product not found"
}
- status: 500
{
"message": "Internal server error"
}
Delete a product by id
⚠️ Soft delete has been implemented so that when a product is deleted, it is not actually removed from the database. Its record is retained, with the deleted column set to 1 when deleted, and to 0 (the default) when available.
- Method: DELETE
- Endpoint: /product/:id
- Description: Delete a product by id (soft delete)
- Authentication: YES
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
example of request:
- Method: DELETE
- URL:
http://localhost:3333/product/1
-
exemple of response:
-
Status: 200
{
"message": "Product deleted"
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Product not found"
}
- status: 500
{
"message": "Internal server error"
}
Sales
List all sales
⚠️ Sales will be ordered by id asc!
- Method: GET
- Endpoint: /sale
- Description: List all sales with client and product
- Authentication: YES
- authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
example of request:
- Method: GET
- URL:
http://localhost:3333/sale
- exemple of response:
200 - Sales
- Status: 200
[
{
"id": 1,
"clientId": 1,
"productId": 1,
"quantity": 10,
"price": "10.00",
"totalPrice": "100.00",
"date": "2021-01-01T00:00:00.000Z",
"client": {
"id": 1,
"name": "John Snow",
"taxId": "12345678912"
},
"product": {
"id": 1,
"name": "Product 1",
"quantity": 100,
"price": "10.00",
"deleted": 0
}
},
{
"id": 8,
"clientId": 2,
"productId": 1,
"quantity": 20,
"price": "10.00",
"totalPrice": "200.00",
"date": "2021-02-01T00:00:00.000Z",
"client": {
"id": 2,
"name": "Daenerys Targaryen",
"taxId": "98765432112"
},
"product": {
"id": 1,
"name": "Product 1",
"quantity": 100,
"price": "10.00",
"deleted": 0
}
}
]
- status: 401
{
"message": "Token not provided"
}
- status: 500
{
"message": "Internal server error"
}
Create a new sale
- Method: POST
- Endpoint: /sale
- Description: Create a new sale
- Authentication: YES
- body example:
{
"clientId": 1,
"productId": 1,
"quantity": 10,
"date": "2024-03-01"
}
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
-
validation:
- clientId: required, number
- productId: required, number
- quantity: required, number
- date: not required, date valid
⚠️ The date field is not mandatory, and if not provided, the current date will be used. If provided, it must be in dateTime format (2024-03-01T00:00:00) or a valid date format (2024-03-01).
⚠️ When creating a sale, it will be checked if the product is available (not deleted) and if there is sufficient quantity to complete the sale. If so, the quantity will be decremented in the database.
⚠️ The price will be obtained directly from the Product model, without the need to pass it as a parameter. Thus, the total price will also be calculated automatically, making it unnecessary to pass it as a parameter..
example of request:
- Method: POST
- URL:
http://localhost:3333/sale
-
exemple of response:
-
Status: 201
{
"clientId": 1,
"productId": 1,
"quantity": 2,
"price": "10.00",
"totalPrice": 20,
"date": "2024-03-01T00:00:00.000+00:00",
"id": 10
}
- status: 400
{
"message": "Client, product, and quantity are required"
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Client or product not found"
}
- status: 500
{
"message": "Internal server error"
}
Delete a sale by id
- Method: DELETE
- Endpoint: /sale/:id
- Description: Delete a sale by id
- Authentication: YES
-
params:
- id: required, number
-
authentication: Bearer Token
- token: required
- format: Bearer token
- Get the token in the login route
⚠️ When a sale is deleted, the product quantity will be incremented in the database, restoring the amount that was decremented when the sale was made. If the product was deleted due to being out of stock, it will become available again.
example of request:
- Method: DELETE
- URL:
http://localhost:3333/sale/1
-
exemple of response:
-
Status: 200
{
"message": "Sale deleted"
}
- status: 401
{
"message": "Token not provided"
}
- status: 404
{
"message": "Sale not found"
}
- status: 500
{
"message": "Internal server error"
}