Esse é um projeto básico criado com o objetivo de aprender mais sobre APIs/backend, assim como TypeScript e NodeJS. É um projeto com fins unicamente de estudos.
A API tem como principal função publicar e obter frases, sendo necessário se registrar para tal. Possui um CRUD completo de frases para usuários logados, com opções de adicionar, obter, atualizar e excluir frases, além de contar com o recurso de paginação ao obter as frases. Os dados são armazenados em um banco de dados PostegreSQL.
- NodeJS e Express;
- Knex: Query builder;
- PostgreSQL: Banco de dados;
- Jest e SuperTest: Testes de integração;
- Bcrypt.js: Criptografia de senhas;
- Zod: Validação de requisições;
- Http-Status-Codes: Status codes mais legíveis.
Estou utilizando o yarn, mas você pode utilizar o gerenciador de pacotes de sua preferência. Antes de executar localmente, é necessário definir as variáveis de ambiente no arquivo .env. Você precisa criar o arquivo .env e copiar as variáveis do .env.example, preenchendo com os valores de configuração adequados.
IMPORTANTE: ACCESS_TOKEN_SECRET_KEY e REFRESH_TOKEN_SECRET_KEY devem ter valores diferentes, caso contrário, um refresh token poderá ser usado como access token.
- yarn dev: Executar localmente.
- yarn test ou yarn jest: Rodar todos os testes.
- yarn knex:migrate: Executar migrations.
- yarn knex:rollback: Executar rollback.
- yarn knex:seed: Executar seeds.
Rotas /auth:
Rotas /quotes:
Manipulação de erros:
POST /auth/register
- Request - Body:
{
"email": "Email do usuário",
"username": "Nome do usuário. Deve conter no mínimo 3 caracteres e no máximo 50",
"password": "Senha do usuário. Deve conter no mínimo 6 caracteres"
}
- Response - Status 201: ID do usuário registrado (int).
POST /auth/login
- Request - Body:
{
"email": "Email do usuário",
"password": "Senha do usuário. Deve conter no mínimo 6 caracteres"
}
- Response - Status 200:
{
"accessToken": "Access token válido por 1h",
"refreshToken": "Refresh token válido por 7 dias (ou até ser utilizado para gerar um novo access token)"
}
POST /auth/refresh-token
- Request - Body:
{
"refreshToken": "Refresh token válido no formato JWT gerado durante o login"
}
- Response - Status 200:
{
"accessToken": "Novo access token válido por 1h",
"refreshToken": "Novo refresh token válido por 7 dias (ou até ser utilizado para gerar um novo access token)"
}
NOTA IMPORTANTE: Todas as rotas de frases são protegidas e precisam de um Authorization Header.
Passe Authorization: Bearer access token no header de requisição.
- Você pode pesquisar por frases ao fornecer um filter e obter todas as frases de um determinado usuário ao fornecer o userId.
GET /quotes
GET /quotes?page=1
GET /quotes?filter=conhecimento
GET /quotes?userId=1
GET /quotes?page=1&filter=conhecimento
GET /quotes?userId=1&filter=conhecimento
- Response - Status 200:
{
"info": {
"count": 20,
"pages": 2,
"next": "/quotes?page=2",
"previous": null
},
"results": [
{
"id": 1,
"quote": "A imaginação é mais importante que o conhecimento.",
"author": "Albert Einstein",
"postedByUsername": "John",
"postedByUserId": 1,
"publicationDate": "2023-06-21T15:20:28.936Z"
},
[...]
]
}
- count: Número total de frases.
- pages: Número total de páginas disponíveis.
- next: Próxima página. null se não houver nenhuma.
- previous: Página anterior. null se não houver nenhuma.
- results: Frases encontradas.
- O limite de frases por página é 15.
GET /quotes/:id
- Response - Status 200:
{
"id": 1,
"quote": "A imaginação é mais importante que o conhecimento.",
"author": "Albert Einstein",
"postedByUsername": "John",
"postedByUserId": 1,
"publicationDate": "2023-06-21T15:20:28.936Z"
}
POST /quotes
- Request - Body:
{
"quote": "Frase. Deve conter no mínimo 7 caracteres e no máximo 1000",
"author": "Nome do autor. Deve conter no mínimo 1 caractere e no máximo 80"
}
- Response - Status 201:
{
"id": 1,
"quote": "A imaginação é mais importante que o conhecimento.",
"author": "Albert Einstein",
"postedByUsername": "John",
"postedByUserId": 1,
"publicationDate": "2023-06-21T15:20:28.936Z"
}
PUT /quotes/:id
- Request - Body:
{
"quote": "Frase atualizada. Deve conter no mínimo 7 caracteres e no máximo 1000",
"author": "Nome do autor atualizado. Deve conter no mínimo 1 caractere e no máximo 80"
}
- Response - Status 204: Não há body de resposta.
DELETE /quotes/:id
- Response - Status 204: Não há body de resposta.
Todos os endpoints que precisam de um body/params/query podem retornar uma BAD_REQUEST (400) se a requisição contiver valores inválidos. Exemplos de erros:
- Login:
{
"errors": {
"body": {
"email": "Invalid email",
"password": "String must contain at least 6 character(s)"
}
}
}
- Tentar obter todas as frases com ?page=abc:
{
"errors": {
"query": {
"page": "Expected number, received nan"
}
}
}
- Atualizar frase com campos no body e id inválidos:
{
"errors": {
"body": {
"quote": "String must contain at least 7 character(s)",
"author": "String must contain at least 1 character(s)"
},
"params": {
"id": "Expected number, received nan"
}
}
}
Todos os endpoints podem retornar um body contendo um error e error_code padrão. error_code sempre será retornado com um valor "unespecifed" caso não seja importante distinguir o tipo de erro. Códigos de erro disponíveis:
- email_already_exists: Quando tentar se registrar com e-mail que já existe.
- username_already_exists: Quando tentar se registrar com nome de usuário que já existe.
- search_without_results: Quando nenhuma frase com o "filter" aplicado for encontrada.
- user_without_posts: Quando o usuário não possuir nenhuma frase publicada.
- invalid_page: Quando uma página inválida for informada na paginação.
{
"error": "Email not available",
"error_code": "email_not_available"
}
Alguns endpoints podem retornar NOT_FOUND (404) se o recurso não for encontrado ou a frase não pertencer ao usuário.
- Tentar atualizar ou excluir com o id de uma frase que não existe ou não pertence ao usuário:
{
"error": "There is no quote with the given id",
"error_code": "unspecified"
}
- Tentar obter frases com uma page maior do que a quantidade de páginas disponíveis:
{
"error": "Invalid page, no more items",
"error_code": "invalid_page"
}