Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

🏃 Runner Circle — Backend

License: MIT NestJS TypeORM PostgreSQL

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.


✨ O que ele faz

  • 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.

Feed do Runner Circle


🧱 Stack e como foi feito

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) e heartRateBpm (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, commentsCount e workoutsCount saem de COUNT nas tabelas — o banco é a fonte da verdade.
  • likedByMe por requisição — o feed marca o que o usuário atual curtiu; anônimo recebe false. É o que destrava o toggle e a invalidação no React Query.
  • username sem @ — 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 em UPLOAD_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 de API_BASE_URL (robusto a troca de host).

🚀 Como rodar

Opção 1 — Docker (um comando)

Pré-requisito: Docker com Compose.

docker compose up --build

Isso sobe o Postgres, roda as migrations, aplica o seed e inicia a API. Pronto:

Para derrubar (mantendo os dados): docker compose down. Para zerar o banco também: docker compose down -v.

Opção 2 — Local (desenvolvimento)

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:dev

Variáveis de ambiente

Veja .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.1 já é 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).


🌱 Dados semeados (seed)

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.


📚 Endpoints

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.

Cadastro e login — campo único "Email ou usuário"

As telas usam um só campo para email/usuário. O contrato acompanha:

// POST /users  (cadastro)
{ "name": "Júlio Oliveira", "login": "julio@example.com", "password": "secret123" }
// Se "login" é email  → guarda o email e gera um @username único a partir dele.
// Se "login" é handle → vira o @username; email fica null.
// Conflito → 409 { "message": "Esse usuário já existe" }

// POST /auth/login
{ "login": "julio@example.com", "password": "secret123" }
// "login" casa com o email OU o username. Resposta: { token, user }.

Toda coleção paginada usa o mesmo envelope:

{ "data": [ /* ... */ ], "page": 1, "limit": 10, "total": 42 }

Modelos

  • 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).

🖼️ Telas do front que a API alimenta

Feed Perfil
Feed Perfil
Nova postagem Editar perfil Login
Nova postagem Editar perfil Login

🛠️ Scripts úteis

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 banco

🤝 Contribuindo

Issues e PRs são bem-vindos — é um projeto educacional e aberto. Rode npm run lint e npm run build antes de abrir um PR.

📄 Licença

MIT — sem fins lucrativos, feito para aprender.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages