API REST em Node.js e TypeScript com Fastify e PostgreSQL, desenvolvida como parte do Tech Challenge da Pós Tech (FIAP).
Antes de começar, certifique-se de ter instalado:
- Node.js 22 ou superior
- npm (incluso na instalação do Node.js)
- Docker e Docker Compose
git clone <url-do-repositorio>
cd techchallengeCopie o arquivo de exemplo e ajuste se necessário:
cp .env.example .envOs valores devem corresponder às credenciais definidas no docker-compose.yml:
PORT=3000
POSTGRES_USER=root
POSTGRES_PASSWORD=techchallenge
POSTGRES_DB=techchallenge
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
PROFESSOR_ACCESS_TOKEN=professor-dev-token-change-me
ALUNO_ACCESS_TOKEN=aluno-dev-token-change-meO PostgreSQL roda em container via Docker Compose. Na primeira execução, o script bd/schema_bd.sql é aplicado automaticamente:
docker compose up -dPara verificar se o container está saudável:
docker compose psPara parar o banco:
docker compose downPara gerar primeiros registros no banco para testes (tabelas, tipos de usuário com tokens, 2 usuários de mock e 2 posts):
docker exec -i techchallenge-postgres psql -U root -d techchallenge < ./bd/schema_completo.sqlAplicação também auto-inicializa o schema: ao subir o servidor (
npm run start:devounpm run start), o módulosrc/lib/pg/init-schema.tsgarante que as 3 tabelas (techchallenge_posts,techchallenge_usertype,techchallenge_user) existam e insere os dois tipos de usuário padrão (PROFESSOR e ALUNO) com os tokens do.env. Ou seja: mesmo em um banco vazio/volume novo, basta subir a app e o schema + roles são criados automaticamente.
npm installDesenvolvimento (com hot reload):
npm run start:devProdução local (build + execução):
npm run build
npm run startA API ficará disponível em http://localhost:3000 (ou na porta definida em PORT).
A aplicação segue uma arquitetura em camadas: controllers HTTP delegam para use cases, que dependem de interfaces de repositório implementadas com o driver pg.
| Camada | Responsabilidade |
|---|---|
http/controllers/ |
Rotas e handlers HTTP |
http/middleware/ |
Autenticação e autorização por access_token |
use-cases/ |
Regras de aplicação e orquestração |
repositories/ |
Contratos e implementações de acesso a dados |
entities/ |
Modelos de domínio (Post) |
lib/pg/ |
Pool de conexão PostgreSQL |
env/ |
Validação de variáveis de ambiente com Zod |
utils/ |
Tratamento global de erros |
Todas as rotas de conteúdo (/posts/*) exigem um access_token enviado no header:
Authorization: Bearer <access_token>
O front-end não precisa saber os tokens hardcoded do .env. Em vez disso:
- O usuário informa suas credenciais (email + senha) em uma tela de login no front
- O front chama
POST /loginpassando{ email, senha } - O backend:
- Valida email+senha na tabela
techchallenge_user - Resolve o papel (role) via
techchallenge_user.tipo_usuario_id→techchallenge_usertype - Retorna
{ token, role, usuario }
- Valida email+senha na tabela
- O front usa o
tokenrecebido no headerAuthorization: Bearer <token>nas próximas chamadas a/posts/*
⚠️ Nota: nesta versão, o token não é JWT. É uma string estática, idêntica em todos os logins do mesmo papel, e salva emtechchallenge_usertype.token. O middlewareauthenticatecontinua comparando os tokens com os valores do.env(que estão sincronizados com o banco pela inicialização automática). Em versões futuras pode ser substituído por JWT sem alterar a interface do/login.
Os tokens são configurados estaticamente no .env e automaticamente sincronizados na tabela techchallenge_usertype quando a aplicação sobe:
| Variável | Papel (role) |
Permissões |
|---|---|---|
PROFESSOR_ACCESS_TOKEN |
PROFESSOR |
Leitura e escrita (GET, POST, PUT, DELETE) |
ALUNO_ACCESS_TOKEN |
ALUNO |
Apenas leitura (GET /posts, GET /posts/search, GET /posts/:id) |
| Status | Situação |
|---|---|
400 |
Body inválido no /login (campos ausentes / vazios) — via Zod |
401 |
Credenciais inválidas no /login (email ou senha errados; ou inconsistência no role) ou token ausente/malformado/inválido nos endpoints de posts |
403 |
Token válido, mas sem permissão para a operação (ex.: aluno tentando criar/editar/deletar post) |
O schema completo (incluindo tabelas de login) está em bd/schema_completo.sql.
Tabela techchallenge_usertype — papéis / roles + token de acesso:
| Coluna | Tipo | Descrição |
|---|---|---|
id |
BIGSERIAL PK | |
descricao |
VARCHAR(50) | PROFESSOR ou ALUNO |
token |
VARCHAR(255) | access_token usado no header Authorization |
Registros padrão (criados automaticamente):
| id | descricao | token |
|---|---|---|
| 1 | PROFESSOR | professor-dev-token-change-me |
| 2 | ALUNO | aluno-dev-token-change-me |
Tabela techchallenge_user — usuários cadastrados:
| Coluna | Tipo | Descrição |
|---|---|---|
id |
BIGSERIAL PK | |
nome |
VARCHAR(255) | Nome completo do usuário |
email |
VARCHAR(255) UNIQUE | Login do usuário (identificador único) |
senha |
VARCHAR(255) | Senha em texto plano nesta versão |
tipo_usuario_id |
BIGINT FK → techchallenge_usertype.id |
PAPEL do usuário (PROFESSOR / ALUNO) |
data_criacao |
TIMESTAMPTZ | default CURRENT_TIMESTAMP |
Usuários de mock do schema_completo.sql:
| nome | senha | papel | |
|---|---|---|---|
| Dr. Carlos Mendes | carlos.mendes@professor.fiap.br |
senha-professor-123 |
PROFESSOR |
| Ana Beatriz Silva | ana.beatriz@aluno.fiap.br |
senha-aluno-456 |
ALUNO |
| Método | Rota | Descrição | Autenticação | Body / Query params |
|---|---|---|---|---|
POST |
/login |
Autentica usuário e retorna token (Bearer) para chamar os outros endpoints | Pública (sem header) | { "email": "string", "senha": "string" } — retorna 200 com { token, role, usuario }, 401 se credenciais inválidas, 400 se body inválido |
| Método | Rota | Descrição | Autenticação | Body / Query params |
|---|---|---|---|---|
GET |
/posts |
Lista posts com paginação | Professor ou Aluno | page (padrão: 1), limit (padrão: 10, máx: 100) |
GET |
/posts/search |
Busca posts por palavra-chave no título/conteúdo | Professor ou Aluno | search (obrigatório) — retorna 200 com lista de posts |
GET |
/posts/:id |
Busca um post pelo id | Professor ou Aluno | id na URL — retorna 404 se não encontrado |
POST |
/posts |
Cria um novo post | Professor | { "titulo": "string", "conteudo": "string" } |
PUT |
/posts/:id |
Atualiza um post pelo id | Professor | id na URL; body { "titulo": "string", "conteudo": "string" } — retorna 404 se não encontrado |
DELETE |
/posts/:id |
Remove um post pelo id | Professor | id na URL — retorna 404 se não encontrado |
Tela de login do usuário ──► POST /login {email,senha}
│
├── 400 → mostra erros de validação dos campos (Zod)
├── 401 → mostra "email ou senha inválidos" (mensagem genérica)
└── 200 → salva {token, role, usuario} no estado do front (ex.: localStorage)
│
▼
Todas as próximas requisições a /posts enviam:
Authorization: Bearer <token>
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email": "carlos.mendes@professor.fiap.br", "senha": "senha-professor-123"}'Resposta (200):
{
"token": "professor-dev-token-change-me",
"role": "PROFESSOR",
"usuario": {
"id": 1,
"nome": "Dr. Carlos Mendes",
"email": "carlos.mendes@professor.fiap.br"
}
}curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email": "ana.beatriz@aluno.fiap.br", "senha": "senha-aluno-456"}'Resposta (200):
{
"token": "aluno-dev-token-change-me",
"role": "ALUNO",
"usuario": {
"id": 2,
"nome": "Ana Beatriz Silva",
"email": "ana.beatriz@aluno.fiap.br"
}
}# 1) Faz login (salva o TOKEN retornado)
TOKEN=$(curl -s -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"carlos.mendes@professor.fiap.br","senha":"senha-professor-123"}' \
| node -e "process.stdin.on('data', d => console.log(JSON.parse(d).token))")
# 2) Usa o token para listar posts (qualquer endpoint de /posts)
curl "http://localhost:3000/posts?page=1&limit=10" \
-H "Authorization: Bearer $TOKEN"curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"ana.beatriz@aluno.fiap.br","senha":"senha-errada"}'Resposta (401 Unauthorized, mensagem genérica — não revela se erro foi email ou senha):
{ "message": "Unauthorized" }curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{}'Resposta (400 Validation error com detalhes de campo via Zod):
{
"message": "Validation error",
"errors": {
"email": ["Invalid input: expected string, received undefined"],
"senha": ["Invalid input: expected string, received undefined"]
}
}curl -X POST http://localhost:3000/posts \
-H "Authorization: Bearer professor-dev-token-change-me" \
-H "Content-Type: application/json" \
-d '{"titulo": "Novo post", "conteudo": "Texto do post"}'Resposta (201):
{
"id": 3,
"titulo": "Novo post",
"conteudo": "Texto do post",
"data_publicacao": "2026-06-30T01:15:20.103Z",
"data_atualizacao": "2026-06-30T01:15:20.103Z"
}curl -X DELETE http://localhost:3000/posts/1 \
-H "Authorization: Bearer professor-dev-token-change-me"Resposta (204): sem corpo.
curl -X PUT http://localhost:3000/posts/1 \
-H "Authorization: Bearer professor-dev-token-change-me" \
-H "Content-Type: application/json" \
-d '{"titulo": "Post atualizado", "conteudo": "Novo conteúdo do post"}'Resposta (200):
{
"id": 1,
"titulo": "Post atualizado",
"conteudo": "Novo conteúdo do post",
"data_publicacao": "2026-06-30T01:15:20.103Z",
"data_atualizacao": "2026-07-05T17:30:00.000Z"
}curl "http://localhost:3000/posts?page=1&limit=10" \
-H "Authorization: Bearer aluno-dev-token-change-me"Resposta (200):
{
"posts": [
{
"id": 1,
"titulo": "Primeiro post",
"conteudo": "Conteúdo do primeiro post",
"data_publicacao": "2026-06-30T01:15:20.103Z",
"data_atualizacao": "2026-06-30T01:15:20.103Z"
}
],
"page": 1,
"limit": 10,
"total": 1
}curl http://localhost:3000/posts/1 \
-H "Authorization: Bearer aluno-dev-token-change-me"Resposta (200):
{
"id": 1,
"titulo": "Primeiro post",
"conteudo": "Conteúdo do primeiro post",
"data_publicacao": "2026-06-30T01:15:20.103Z",
"data_atualizacao": "2026-06-30T01:15:20.103Z"
}curl "http://localhost:3000/posts/search?search=primeiro" \
-H "Authorization: Bearer aluno-dev-token-change-me"Resposta (200):
{
"posts": [
{
"id": 1,
"titulo": "Primeiro post",
"conteudo": "Conteúdo do primeiro post",
"data_publicacao": "2026-06-30T01:15:20.103Z",
"data_atualizacao": "2026-06-30T01:15:20.103Z"
}
]
}| Script | Comando | Descrição |
|---|---|---|
start:dev |
tsx watch src/server.ts |
Desenvolvimento com recarga automática |
start |
tsx src/server.js |
Execução após build |
build |
tsup src --out-dir build |
Compila TypeScript para build/ |
lint |
eslint src |
Verifica problemas de lint |
format |
prettier --write . |
Formata os arquivos do projeto |
test |
jest |
Executa testes unitários |
test:coverage |
jest --coverage |
Executa testes e gera relatório de cobertura |
Os testes utilizam Jest com mocks de repositório e banco de dados, portanto não dependem do PostgreSQL estar em execução.
npm test
npm run test:coveragenpm run lintO comando analisa os arquivos em src/ usando ESLint com as regras recomendadas para JavaScript/TypeScript. Se houver problemas, o ESLint lista o arquivo, a linha e a regra violada para que possam ser corrigidos os apontamentos. Executar novamente até o comando terminar sem erros.
Para formatar o código automaticamente com Prettier:
npm run formatA configuração do ESLint fica em eslint.config.js na raiz do projeto.
O projeto possui um workflow de CI no GitHub Actions em .github/workflows/main.yaml.
O pipeline é executado automaticamente em push e pull request para a branch master, com as seguintes etapas:
- Instalação de dependências (
npm install) - Lint (
npm run lint) - Testes unitários (
npm test) - Build (
npm run build)
Os testes no CI utilizam mocks e não exigem PostgreSQL. Para validar localmente antes do push:
npm install
npm run lint
npm test
npm run buildO status das execuções pode ser acompanhado na aba Actions do repositório no GitHub.
techchallenge/
├── .github/
│ └── workflows/
│ └── main.yaml # Pipeline de CI (lint, test, build)
├── bd/
│ ├── schema_bd.sql # Script mínimo de inicialização (tabela de posts)
│ └── schema_completo.sql # Schema completo + dados de mock (posts, users, usertypes)
├── src/
│ ├── app.ts # Bootstrap Fastify e registro de rotas (auth + posts)
│ ├── server.ts # Entrada do servidor HTTP + inicialização do schema
│ ├── entities/
│ │ ├── models/ # Contratos TypeScript:
│ │ │ ├── post.model.ts # IPost, ICreatePost, IUpdatePost
│ │ │ ├── auth.model.ts # Role ("PROFESSOR"|"ALUNO"), AuthContext
│ │ │ └── user.model.ts # IUser, IUserType, ILoginRequest, ILoginResponse
│ │ ├── post.ts # Classe de domínio Post
│ │ ├── user.ts # Classe de domínio User
│ │ └── usertype.ts # Classe de domínio UserType
│ ├── env/ # Validação de variáveis de ambiente com Zod
│ ├── http/
│ │ ├── controllers/
│ │ │ ├── auth/
│ │ │ │ └── routes.ts # POST /login (pública, sem autenticação)
│ │ │ └── post/
│ │ │ └── routes.ts # GET/POST/PUT/DELETE /posts (protegidas)
│ │ └── middleware/
│ │ ├── authenticate.ts # Valida Bearer token (comparando com .env)
│ │ └── authorize.ts # Valida papel (role) permitido na rota
│ ├── lib/pg/
│ │ ├── index.ts # Pool de conexão PostgreSQL (driver pg)
│ │ └── init-schema.ts # Auto-cria 3 tabelas + roles/tokens na inicialização
│ ├── repositories/
│ │ ├── post.repository.interface.ts
│ │ ├── user.repository.interface.ts
│ │ └── pg/ # Implementações PostRepository, UserRepository (placeholders $1,$2)
│ ├── use-cases/
│ │ ├── find-posts.use-case.ts
│ │ ├── find-post-by-id.use-case.ts
│ │ ├── search-posts.use-case.ts
│ │ ├── create-post.use-case.ts
│ │ ├── update-post.use-case.ts
│ │ ├── delete-post.use-case.ts
│ │ ├── login.use-case.ts # Valida credenciais e resolve token/role
│ │ ├── errors/ # UnauthorizedError, ForbiddenError, ResourceNotFoundError
│ │ └── factory/ # Factories: make-create-post-use-case, make-login-use-case, ...
│ ├── __tests__/ # Testes unitários (Jest + mocks) — 38 testes
│ └── utils/global-error-handler.ts # Tratamento global (Zod 400, 401, 403, 404, 500)
├── jest.config.ts # Configuração do Jest
├── coverage/ # Relatório de cobertura (gerado por npm run test:coverage)
├── docker-compose.yml # Configuração do PostgreSQL
├── .env.example # Modelo de variáveis de ambiente
├── package.json
└── .env # Variáveis de ambiente (criar localmente)
Porta 5432 já em uso
Altere a porta no docker-compose.yml (ex.: "5433:5432") e atualize POSTGRES_PORT no .env.
Container do PostgreSQL não inicia
Verifique os logs:
docker compose logs postgresErro de conexão com o banco
Confirme que o container está rodando (docker compose ps) e que as variáveis do .env coincidem com as do docker-compose.yml.
Foi criada uma release no GitHub, de modo a taguear o "fim" dessa fase. Não sei como será a continuidade, mas se seguirmos este projeto, acredito que fará sentido para acompanhar os marcos de entrega.
Tempo para o volume de aulas
Encontrei dificuldade em conseguir acompanhar todas as aulas em vídeo, por conta de tempo disponível X volume de aulas. Com isso, tentei realizar a entrega mais próxima possível do que vi em aula, e utilizei-me do recurso de IA para principalmente apoiar na documentação do projeto.