Backend de uma rede social de treinos de corrida/caminhada — o Runner Circle. Feito para o curso de React da Alura: os alunos já recebem o front-end pronto e, ao longo das aulas, conectam-no a esta API real (fetch, JWT, upload, React Query…). É a mesma ideia do backend do Fokus: entregue pronto, sobe com um comando, tem Swagger navegável e já vem com dados semeados para o feed nunca aparecer vazio.
Projeto open-source, MIT, sem fins lucrativos. Existe para fins educacionais. Fique à vontade para usar, estudar, adaptar e contribuir.
- Autenticação JWT — cadastro, login e rotas protegidas.
- Feed de posts paginado, ordenado do mais novo pro mais antigo, com autor embutido e contagens de curtidas/comentários.
- Publicação de treino com upload de foto (
multipart/form-data). - Perfil do usuário logado, com abas Posts e Curtidas, e edição (incluindo troca de avatar).
- Curtidas idempotentes com contagem autoritativa (sincroniza o React Query).
- Comentários por post (lista + criar).
- Swagger/OpenAPI navegável e testável em
/docs. - Seed de usuários, posts, curtidas e comentários já populados.
| Camada | Tecnologia |
|---|---|
| Runtime | Node.js 22 |
| Framework | NestJS 11 (Express) |
| Banco | PostgreSQL 16 |
| ORM | TypeORM — entities, migrations e seed |
| Auth | JWT (@nestjs/jwt + Passport) + bcryptjs |
| Validação | class-validator / class-transformer |
| Uploads | Multer + arquivos estáticos servidos por URL absoluta |
| Docs | @nestjs/swagger (OpenAPI) |
| Infra | Docker + Docker Compose |
Arquitetura modular (um módulo por domínio): auth, users, posts,
comments, além de config, common (filtro de erro + paginação) e uploads.
Algumas decisões de design que valem a leitura:
- Números crus, unidade no nome do campo. A API guarda e devolve
distanceMeters(metros),durationSeconds,calories(kcal) eheartRateBpm(bpm) como inteiros — sem string de unidade. Quem formata para exibição (2000→"2 Km") é o front. Na entrada vale a simetria: o formulário coleta km e o front converte para metros antes de enviar. - Contagens são derivadas, não colunas.
likesCount,commentsCounteworkoutsCountsaem deCOUNTnas tabelas — o banco é a fonte da verdade. likedByMepor requisição — o feed marca o que o usuário atual curtiu; anônimo recebefalse. É o que destrava o toggle e a invalidação no React Query.usernamesem@— o handle é guardado limpo (julio); o@é decoração de exibição que o front prepõe. Um@inicial enviado no cadastro é removido.- Erros sempre em
{ "message": "..." }— um filtro global normaliza tudo, então o front exibe a mensagem direto. - Uploads são convertidos para WebP no backend (via
sharp): toda imagem enviada (png/jpg/webp, até 15 MB — configurável emUPLOAD_MAX_MB) é redimensionada (máx. 1600px) e comprimida em WebP antes de gravar. Guarda-se só o nome do arquivo; a URL absoluta é montada na resposta a partir deAPI_BASE_URL(robusto a troca de host).
Pré-requisito: Docker com Compose.
docker compose up --buildIsso sobe o Postgres, roda as migrations, aplica o seed e inicia a API. Pronto:
- API: http://localhost:3000
- Swagger: http://localhost:3000/docs
Para derrubar (mantendo os dados): docker compose down.
Para zerar o banco também: docker compose down -v.
Pré-requisitos: Node 22+ e um Postgres acessível.
# 1. Instale as dependências
npm install
# 2. Configure o ambiente
cp .env.example .env # ajuste DATABASE_URL etc. se precisar
# 3. Rode migrations e seed
npm run migration:run
npm run seed
# 4. Suba em modo watch
npm run start:devVeja .env.example. As principais:
| Variável | Descrição | Exemplo |
|---|---|---|
PORT |
Porta HTTP | 3000 |
DATABASE_URL |
Conexão Postgres | postgres://runner:runner@localhost:5432/runner_circle |
JWT_SECRET |
Segredo do JWT | troque-em-producao |
JWT_EXPIRES_IN |
Validade do token | 7d |
API_BASE_URL |
URL pública (monta URLs de upload) | http://localhost:3000 |
CORS_ORIGINS |
Origens de produção liberadas (vírgula) | https://meuapp.com |
CORS: qualquer porta de
localhost/127.0.0.1já é liberada automaticamente em dev — não precisa configurar nada.CORS_ORIGINSé só para adicionar domínios de produção.
No front: a base URL da API entra por variável de ambiente (ex.:
VITE_API_URL=http://localhost:3000).
O seed cria 5 usuários (todos com a senha secret123), posts variados,
curtidas e comentários. O usuário de teste principal já vem com as duas abas do
perfil populadas (é autor de posts e curtiu posts de outros):
| Login | Senha |
|---|---|
julio@example.com |
secret123 |
laura@example.com |
secret123 |
julia@example.com |
secret123 |
pedro@example.com |
secret123 |
marina@example.com |
secret123 |
O seed é idempotente: se já houver usuários, ele não faz nada.
Documentação completa e testável no Swagger (/docs). Resumo:
| Método | Rota | Auth | Descrição |
|---|---|---|---|
POST |
/auth/login |
público | Login → { token, user } |
POST |
/users |
público | Cadastro |
GET |
/users/me |
🔒 | Usuário logado |
PUT |
/users/me |
🔒 | Edita perfil (multipart, avatar) |
GET |
/users/:username |
público | Perfil público |
GET |
/posts |
opcional | Feed paginado — ?page&limit&authorId&likedBy=me&createdBy=me |
POST |
/posts |
🔒 | Cria post (multipart, foto) |
GET |
/posts/:id |
opcional | Post único |
POST |
/posts/:id/likes |
🔒 | Curte (idempotente) |
DELETE |
/posts/:id/likes |
🔒 | Descurte |
GET |
/posts/:id/comments |
público | Lista comentários (paginado) |
POST |
/posts/:id/comments |
🔒 | Comenta |
Feed:
GET /postsé público (é consumido antes da auth existir, na aula 2). O filtro?likedBy=me(aba Curtidas) é a exceção: exige token (401 se anônimo). Já?createdBy=me(meus posts) é tolerante: com token filtra pelo autor logado, sem token o filtro é simplesmente ignorado.
As telas usam um só campo para email/usuário. O contrato acompanha:
Toda coleção paginada usa o mesmo envelope:
{ "data": [ /* ... */ ], "page": 1, "limit": 10, "total": 42 }- User / PublicUser —
id, name, username (sem @, sempre presente), email (nullable — só se cadastrou com email), bio, avatarUrl, workoutsCount, createdAt(nunca expõe o hash da senha). - Post —
id, author, imageUrl, durationSeconds, type (walking|running), distanceMeters, calories, heartRateBpm, description, likesCount, likedByMe, commentsCount, createdAt. - Comment —
id, postId, author, text, createdAt. - Like — relação usuário↔post (par único).
| Feed | Perfil |
|---|---|
![]() |
![]() |
| Nova postagem | Editar perfil | Login |
|---|---|---|
![]() |
![]() |
![]() |
npm run start:dev # API em modo watch
npm run build # compila para dist/
npm run lint # eslint --fix
npm run migration:run # aplica migrations (dev, ts-node)
npm run migration:generate -- src/migrations/NomeDaMigration # gera migration a partir das entities
npm run seed # popula o bancoIssues e PRs são bem-vindos — é um projeto educacional e aberto. Rode
npm run lint e npm run build antes de abrir um PR.
MIT — sem fins lucrativos, feito para aprender.




