Neste projeto foi desenvolvido uma API e um banco de dados de conteúdo para um blog!
Uma aplicação em Node.js
usando o ORM Sequelize
para fazer um CRUD
de posts.
-
Foi utilizado o Docker, para o desenvolvimento.
-
Foi desenvolvido endpoints que estão conectados ao banco de dados seguindo os princípios do REST;
-
Foi desenvolvido uma camada de autenticação de pessoas usuárias.
-
E também foi criado o relacionamento de categorias para os posts, trabalhando, assim, a relação de
posts
paracategories
e decategories
paraposts
.
- Rodando as Migrations e Seeds.
- Criação de usuário, através de um endpoint.
- Login na aplicação.
- Listando usuários, somente com token JWT.
Obs: Já foi implementada a cryptografia da senha. :p
-
Criar uma aplicação usando o Docker para o desenvolvimento.
-
Montar uma API e um banco de dados, seguindo boas práticas de desenvolvimento de software!
-
Criar e associar tabelas usando um ORM.
-
Criar endpoints para manipular as informações de um banco de dados MySQL.
-
Autenticar pessoas usuárias através do JWT.
-
Colocar sob nova ótica o jeito programar. Foi necessário, a todo momento, pensar “será que essa é a melhor forma de fazer isso?” ;)
-
Aderência do código à especificação. O programa deve se comportar como especificado.
-
Respeitar os princípios de uma API REST
-
Com a ajuda do Sequelize, criar as relaçẽos entre as entidades, seguindo um modelo DER.
-
Ter uma autenticação em JWT.
-
Organização do código.
ℹ️ Rode os serviços do
node
e odb
com o comandodocker-compose up -d
.
-
Lembre-se o
mysql
tem que estar rodadno localmente na porta padrão (3306
). -
Esses serviços irão inicializar um container chamado
blogs_api
e outro chamadoblogs_api_db
; -
A partir daqui você pode rodar o container node
blogs_api
via CLI:
ℹ️ Use o comando
docker exec -it blogs_api bash
.
- Ele te dará acesso ao terminal interativo do container criado pelo compose, que está rodando em segundo plano.
ℹ️ Instale as dependências com
npm install
. (Instale dentro do container)
⚠️ Atenção: Caso opte por utilizar o Docker, TODOS os comandos disponíveis nopackage.json
(npm run dev, npm run db:reset, npm run db:seed:all, ...) devem ser executados DENTRO do container.
ℹ️ Crie o DB e rode as migrations com
npm run db:reset
ℹ️ Popule o DB com
npm run db:seed:all
ℹ️ Rode a aplicação em modo dev(nodemon) com
npm run dev
⚠️ Atenção: Você vai precisar ter o MySQL instalado e rodando no seu PC.
ℹ️ Instale as dependências com
npm install
ℹ️ Crie o DB e rode as migrations com
npm run db:reset
ℹ️ Popule o DB com
npm run db:seed:all
ℹ️ Rode a aplicação em modo dev(nodemon) com
npm run dev
- ✨ Dica: Para rodar o projeto desta forma, obrigatoriamente você deve ter o
node
instalado em seu computador.
Obs: Se você optar em usar a extenção do VSCode
Thunder Client
, use o arquivothunder-collection_Blogs_api.json
para importar a collection Blog Api
A construção das tabelas foi orientada pelo DER a seguir usando o Sequelize ORM:
Saiba mais:
O esquema apresentado na imagem descreve as tabelas de um banco de dados e suas relações em um sistema de blog.
id INT
: Chave primária que identifica cada categoria de forma única.name VARCHAR(255)
: Nome da categoria.
post_id INT
: Chave estrangeira que referencia oid
dosblog_posts
.category_id INT
: Chave estrangeira que referencia oid
dascategories
.
id INT
: Chave primária que identifica cada post do blog.title VARCHAR(255)
: Título do post.content VARCHAR(255)
: Conteúdo do post.user_id INT
: Chave estrangeira que referencia oid
do autor na tabelausers
.published DATETIME
: Data e hora de publicação do post.updated DATETIME
: Data e hora da última atualização do post.
id INT
: Chave primária que identifica cada usuário.display_name VARCHAR(255)
: Nome que é exibido no blog.email VARCHAR(255)
: E-mail do usuário.password VARCHAR(255)
: Senha do usuário.image VARCHAR(255)
: Caminho ou link para a imagem de perfil do usuário.
- Um-para-Muitos: Entre
users
eblog_posts
, indicando que um usuário pode escrever vários posts. - Muitos-para-Muitos: Entre
blog_posts
ecategories
, facilitado pela tabelaposts_categories
, significando que um post pode ter várias categorias e uma categoria pode incluir vários posts.
GET /status
-
Objetivo: Uma rota /status é comumente usada para verificar a saúde de uma API. Quando os clientes acessam essa rota, o servidor pode responder com informações sobre o status do serviço, se ele está funcionando corretamente ou se há algum problema. É uma maneira útil de monitorar a integridade da sua API.
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
{ "message": "[Healthy] - API on!!!" }
GET /login
Cenários de Exceção:
400 - BAD_REQUEST
-> se o campo 'email' ou 'password' forem inválidos ou não cadastrados401 - UNAUTHORIZED
-> se o campo 'email' não estiver cadastrado404 - NOT_FOUND
-> se a rota na API não existir500 - INTERNAL_SERVER_ERROR
-> se der outro erro
-
Objetivo: Autenticar um usuário, já criado anteriormente, na aplicação
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjp7InVzZXJJZCI6MywibmFtZSI6Ik1hcmNlbG8gQXRhw61kZSIsImF2YXRhciI6Imh0dHA6Ly90ZXN0ZSJ9LCJpYXQiOjE3MDI5ODgyOTgsImV4cCI6MTcwMjk4OTE5OH0.CrIXONvWKLD98NndrLSDmEQNmuoO1zM_2ur6msF17hY" }
GET /user
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.404 - NOT_FOUND
-> se a rota na API não existir500 - INTERNAL_SERVER_ERROR
-> se der erro ao tentar listar os usuários
-
Objetivo: Listar todos os usuários da aplicação
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
[ { "id": 1, "displayName": "Lewis Hamilton", "email": "lewishamilton@gmail.com", "image": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lewis_Hamilton_2016_Malaysia_2.jpg" }, { "id": 2, "displayName": "Michael Schumacher", "email": "MichaelSchumacher@gmail.com", "image": "https://sportbuzz.uol.com.br/media/_versions/gettyimages-52491565_widelg.jpg" } ]
GET /user/{id}
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.404 - NOT_FOUND
-> se o usuário com o ID especificado não for encontrado500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar recuperar o usuário.
-
Objetivo: Recuperar informações de um usuário específico através do seu ID.
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
{ "id": 1, "displayName": "Lewis Hamilton", "email": "lewishamilton@gmail.com", "image": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lewis_Hamilton_2016_Malaysia_2.jpg" }
POST /user
Cenários de Exceção:
400 - BAD_REQUEST
-> se os campos forem inválidos401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.404 - NOT_FOUND
-> se a rota na API não existir409 - CONFLICT
-> se o usuário já existir500 - INTERNAL_SERVER_ERROR
-> se der erro ao tentar listar os usuários
-
Objetivo: Listar todos os usuários da aplicação
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
[ { "id": 1, "displayName": "Lewis Hamilton", "email": "lewishamilton@gmail.com", "image": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lewis_Hamilton_2016_Malaysia_2.jpg" }, { "id": 2, "displayName": "Michael Schumacher", "email": "MichaelSchumacher@gmail.com", "image": "https://sportbuzz.uol.com.br/media/_versions/gettyimages-52491565_widelg.jpg" } ]
DELETE /user
Cenários de Exceção:
500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar deletar o post
-
Objetivo: Permitir que o usuário logado delete sua própria conta.
-
Código HTTP (sucesso):
204 - No Content
-
Nota: Não há output para este método, pois a resposta bem-sucedida é um status HTTP
204 - NO_CONTENT
.
GET /categories
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar listar as categorias
-
Objetivo: Listar todas as categorias de posts disponíveis na aplicação.
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
[ { "id": 1, "name": "Inovação" }, { "id": 2, "name": "Escola" } ]
POST /categories
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.400 - BAD_REQUEST
-> se o campo 'name' não for fornecido ou for menor que 5 caracteres500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar criar a categoria
-
Objetivo: Criar uma nova categoria de posts.
-
Código HTTP (sucesso):
201 - CREATED
-
Input (exemplo):
{ "name": "Nova categoria" }
GET /post
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar listar os posts.
-
Objetivo: Listar todos os posts criados, incluindo informações sobre suas categorias e o usuário que os criou.
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
[ { "id": 1, "title": "Post do Ano", "content": "Melhor post do ano", "userId": 1, "published": "2022-08-01T19:58:00.000Z", "updated": "2022-08-01T19:58:51.000Z", "user": { "id": 1, "displayName": "Lewis Hamilton", "email": "lewishamilton@gmail.com", "image": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lewis_Hamilton_2016_Malaysia_2.jpg" }, "categories": [ { "id": 1, "name": "Inovação", "PostCategory": { "postId": 1, "categoryId": 1 } } ] }, { "id": 2, "title": "Vamos que vamos", "content": "Foguete não tem ré", "userId": 1, "published": "2022-08-01T19:58:00.000Z", "updated": "2022-08-01T19:58:51.000Z", "user": { "id": 1, "displayName": "Lewis Hamilton", "email": "lewishamilton@gmail.com", "image": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lewis_Hamilton_2016_Malaysia_2.jpg" }, "categories": [ { "id": 2, "name": "Escola", "PostCategory": { "postId": 2, "categoryId": 2 } } ] } ]
GET /post/search
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.404 - NOT_FOUND
-> se nenhum post corresponder ao termo de pesquisa500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro durante a busca.
-
Objetivo: Listar todos os posts que contenham em seu título ou conteúdo a palavra pesquisada.
-
Código HTTP (sucesso):
200 - OK
-
Exemplo de uso da query:
- URL:
http://localhost:3000/post/search?q=foguete
- URL:
-
Output (exemplo):
[ { "id": 2, "title": "Vamos que vamos", "content": "Foguete não tem ré", "userId": 1, "published": "2022-08-01T19:58:00.000Z", "updated": "2022-08-01T19:58:51.000Z", "user": { "id": 1, "displayName": "Lewis Hamilton", "email": "lewishamilton@gmail.com", "image": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lewis_Hamilton_2016_Malaysia_2.jpg" }, "categories": [ { "id": 2, "name": "Escola", "PostCategory": { "postId": 2, "categoryId": 2 } } ] } ]
GET /post/{id}
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.404 - NOT_FOUND
-> se o post com o ID especificado não for encontrado500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar recuperar o post
-
Objetivo: Recuperar informações de um post específico através do seu ID.
-
Código HTTP (sucesso):
200 - OK
-
Output (exemplo):
{ "id": 1, "title": "Post do Ano", "content": "Melhor post do ano", "userId": 1, "published": "2022-08-01T19:58:00.000Z", "updated": "2022-08-01T19:58:51.000Z", "user": { "id": 1, "displayName": "Lewis Hamilton", "email": "lewishamilton@gmail.com", "image": "https://upload.wikimedia.org/wikipedia/commons/1/18/Lewis_Hamilton_2016_Malaysia_2.jpg" }, "categories": [ { "id": 1, "name": "Inovação", "PostCategory": { "postId": 1, "categoryId": 1 } } ] }
POST /post
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.400 - BAD_REQUEST
-> se algum dos campos obrigatórios não for fornecido ou não atender aos requisitos mínimos500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar criar o post
-
Objetivo: Criar um novo post.
-
Código HTTP (sucesso):
201 - CREATED
-
Input (exemplo):
{ "title": "Título um", "content": "conteúdo um", "categoryIds": [2, 3] }
-
Output (exemplo):
{ "id": 3, "title": "Título um", "content": "conteúdo um", "userId": 3, "updated": "2023-12-20T12:07:57.044Z", "published": "2023-12-20T12:07:57.044Z" }
PUT /post/{id}
Cenários de Exceção:
401 - UNAUTHORIZED
-> se não encontrar o token ou for inválido ou expirado.401 - UNAUTHORIZED
-> se o post não for do usuário que esta logado.404 - NOT_FOUND
-> se o post com o ID especificado não for encontrado400 - BAD_REQUEST
-> se algum dos campos obrigatórios não for fornecido ou não atender aos requisitos mínimos500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar atualizar o post
-
Objetivo: Atualizar um post específico através do seu ID, somente se o usuário for o dono do post.
-
Código HTTP (sucesso):
200 - OK
-
Input (exemplo):
{ "title": "Título Novo 2", "content": "Conteúdo novo 2" }
-
Output (exemplo):
{ "id": 3, "title": "Título Novo 2", "content": "Conteúdo novo 2", "userId": 3, "published": "2023-12-20T12:07:57.000Z", "updated": "2023-12-20T12:07:57.000Z", "user": { "id": 3, "displayName": "Marcelo Ataíde", "email": "marcelo@email.com", "image": "http://teste" }, "categories": [ { "id": 2, "name": "Escola", "PostCategory": { "postId": 3, "categoryId": 2 } }, { "id": 3, "name": "Nova categoria", "PostCategory": { "postId": 3, "categoryId": 3 } } ] }
DELETE /post/{id}
Cenários de Exceção:
401 - UNAUTHORIZED
-> se o usuário tentando deletar o post não for o dono dele404 - NOT_FOUND
-> se o post com o ID especificado não for encontrado500 - INTERNAL_SERVER_ERROR
-> se ocorrer um erro ao tentar deletar o post
-
Objetivo: Deletar um post específico, sendo permitido apenas se o usuário for o dono do post.
-
Código HTTP (sucesso):
204 - NO_CONTENT
-
Nota: Não há output para este método, pois a resposta bem-sucedida é um status HTTP
204 - NO_CONTENT
.
- Criptografar a senha antes de salvar o usuário no banco. ✅
- Fazer os teste de unidade e de integração.