Este guia explica o código da pasta server/ na ordem em que vale a pena estudar. Cada seção diz o que o arquivo faz, por que foi feito assim e o que você pode experimentar.
# 1) Front (na raiz do projeto)
npm install
# 2) Back
npm run server:install # = npm --prefix server install
cp server/.env.example server/.env # abra e preencha MONGODB_URI e JWT_SECRET
# 3) Em DOIS terminais:
npm run dev # front -> http://localhost:5173
npm run server # back -> http://localhost:3001O front chama /api/.... O Vite (configurado no vite.config.ts) repassa essas chamadas para localhost:3001. Por isso não há erro de CORS durante o desenvolvimento.
Exemplo: alguém preenche o cadastro de artista e clica em enviar.
SingUp.tsx --fetch POST /api/auth/registrar--> Vite (proxy) --> Express (server.ts)
-> routes/auth.ts (valida os dados)
-> models/User.ts (regras do banco + hash da senha)
-> MongoDB (grava o documento na coleção "users")
<- JSON { token, usuario } volta até o front
Cada camada tem uma responsabilidade:
| Camada | Pasta/arquivo | Responsabilidade |
|---|---|---|
| Entrada | server.ts |
Liga o Express, conecta o banco, registra as rotas |
| Rotas | routes/*.ts |
Recebem a requisição, validam, chamam o model, respondem |
| Middleware | middleware/auth.ts |
Código que roda antes da rota (ex.: checar login) |
| Models | models/*.ts |
Definem a forma dos dados e as regras do banco |
| Conexão | db.ts |
Abre a conexão com o MongoDB |
| Banco relacional (suas tabelas) | MongoDB |
|---|---|
| Banco | Banco (database) |
| Tabela | Coleção (collection) |
| Linha | Documento (parecido com um objeto JSON) |
| Coluna | Campo |
PK (id_usuario) |
_id (criado automaticamente, tipo ObjectId) |
| FK | Campo ObjectId com ref (referência) |
| JOIN | populate() (no Mongoose) ou embutir os dados |
Um documento de usuário artista fica assim no banco:
{
"_id": "66fb1c...",
"nome": "Katz",
"email": "katz@teste.com",
"senha": "$2a$10$...(hash)...",
"tipoUsuario": "artista",
"status": "ativo",
"artista": {
"nomeArtistico": "Katz",
"categoriaArtistica": "Pintura",
"cidade": "Santos",
"disponibilidade": "disponivel",
"statusPerfil": "publicado"
},
"enderecos": [],
"dataCadastro": "2026-10-01T..."
}- Embutir (dentro do mesmo documento): quando os dados são lidos juntos, são 1:1 ou 1:poucos e não crescem sem parar. Exemplos: artista dentro do usuário, trabalhos dentro do portfólio, itens dentro do pedido.
- Referenciar (guardar o
_iddo outro documento): quando é N:N, quando o outro documento é usado por muita gente ou quando cresce sem limite. Exemplos:mensagens(milhares por conversa),pedidos(um usuário tem muitos),produtos(de um artista).
Regra prática: "eu sempre leio isso junto?" Se sim, embuta. "Isso cresce sem parar?" Se sim, coleção própria.
Além disso, o pedido guarda o nome e preço do produto no momento da compra (cópia, não só referência). Se o artista mudar o preço depois, o pedido antigo continua certo. Em banco relacional isso também é boa prática.
Dependências:
express: o servidor web (define rotas comoGET /api/artistas).mongoose: conversa com o MongoDB e define os schemas (formato dos dados).bcryptjs: gera o hash da senha.jsonwebtoken: cria e confere os tokens de login (JWT).cors: libera o front a chamar a API.dotenv: lê o arquivo.env.tsx(dev): roda TypeScript direto, sem compilar. É por isso que o script étsx watch src/server.ts(owatchreinicia o servidor quando você salva um arquivo).
"type": "module" faz o Node usar import ... from (a mesma sintaxe do seu React).
O .env guarda segredos (senha do banco, chave do JWT) fora do código. O .env.example é só um modelo, sem segredos, que vai para o GitHub. O .env real nunca pode ser enviado ao GitHub (já está no .gitignore).
MONGODB_URI: o endereço do banco. Formato Atlas:mongodb+srv://usuario:senha@cluster.xxx.mongodb.net/awaw?.... O/awawé o nome do banco. Se a senha tiver caracteres especiais (@,#,/), troque por código URL (ex.:@vira%40).JWT_SECRET: texto longo e aleatório. Quem souber esse texto consegue forjar logins, então é secreto.
await mongoose.connect(uri);Abre a conexão uma vez. O Mongoose a reaproveita em todas as consultas. async/await é usado porque conectar leva tempo e o código espera terminar antes de seguir.
Ponto por ponto:
import "dotenv/config"precisa ser o primeiro import, para o.envjá estar carregado quando os outros arquivos forem lidos.app.use(cors(...)): permite chamadas vindas dehttp://localhost:5173.app.use(express.json()): sem isso,req.bodychega vazio. Ele transforma o JSON recebido em objeto JavaScript.app.use("/api/auth", authRoutes): tudo que começa com/api/authé tratado emroutes/auth.ts. Dentro de lá, a rota"/login"vira/api/auth/login.conectarBanco().then(() => app.listen(...)): só sobe o servidor depois de conectar no banco. Se a conexão falhar, mostra o erro e encerra.
- Schema = a "planta" do documento (campos, tipos, regras).
- Model = ferramenta criada a partir do schema para consultar e gravar (
User.find(),User.create()...).
model("User", userSchema) cria o model User. O Mongoose coloca o nome no plural e minúsculo para a coleção: users.
email: { type: String, required: true, unique: true, lowercase: true, trim: true }required: não grava sem esse campo.unique: cria um índice único no banco, então dois usuários não têm o mesmo e-mail.lowercaseetrim: normalizam o texto antes de gravar (" Ana@X.com "vira"ana@x.com").enum: [...]: só aceita esses valores (tipoUsuario,status...).default: valor usado quando nada é enviado.
Faz a senha não vir em nenhuma consulta. Por isso, no login, é preciso pedir explicitamente: .select("+senha"). Isso evita vazar o hash por engano numa rota.
São schemas menores dentro do userSchema. O { _id: false } evita criar um _id desnecessário para cada um (só o usuário precisa de _id). Como um usuário é de um tipo só, os outros dois ficam ausentes naquele documento.
{ timestamps: { createdAt: "dataCadastro", updatedAt: "atualizadoEm" } }O Mongoose preenche essas datas sozinho (na criação e em toda alteração). Trocamos os nomes padrão (createdAt/updatedAt) para combinar com as suas tabelas (data_cadastro).
userSchema.index({ "cliente.cpf": 1 }, { unique: true, sparse: true });Índice é um "sumário" que deixa a busca rápida. unique impede CPF repetido. sparse ignora documentos que não têm o campo (artistas não têm CPF e, sem o sparse, o Mongo trataria "sem CPF" como um valor repetido). O índice de listagem (tipoUsuario + categoria + cidade) acelera a busca de artistas.
userSchema.pre("save", async function () {
if (!this.isModified("senha")) return;
this.senha = await bcrypt.hash(this.senha, 10);
});- É um hook: roda automaticamente antes de gravar.
isModified("senha"): só refaz o hash se a senha mudou. Sem isso, cadasave()faria hash de um hash e ninguém conseguiria logar.bcrypt.hash(senha, 10): o10é o custo (quanto mais alto, mais lento e mais seguro). O hash não tem volta: para conferir o login, o bcrypt refaz o cálculo e compara.
Atenção: esse hook roda em
create()esave(). Se um dia você usarupdateOnepara trocar a senha, ele não roda.
JWT é um "crachá" assinado. Fluxo:
- O usuário faz login. O servidor confere a senha e entrega um token.
- O front guarda o token (aqui, em
localStorage). - Nas rotas protegidas, o front manda
Authorization: Bearer <token>. - O servidor verifica a assinatura com o
JWT_SECRET. Se for válida, sabe quem é o usuário sem consultar o banco.
jwt.sign({ tipoUsuario }, segredo(), { subject: id, expiresIn: "7d" })- O
subjecté o id do usuário (no token, vira o camposub). expiresIn: "7d": o token vale 7 dias.
exigirLogin é um middleware: função (req, res, next). Se o token for válido, ela guarda {id, tipoUsuario} em res.locals.usuario e chama next() (segue para a rota). Se não for, responde 401 e a rota nem executa. Uso:
router.put("/me", exigirLogin, async (req, res) => { ... })O token não é criptografado, só assinado: qualquer um consegue ler o conteúdo (tente colar um token em jwt.io). Por isso nunca coloque senha ou dados sensíveis dentro dele.
- Valida o básico (nome, e-mail, senha de 6+ caracteres).
- Confere
tipoUsuariocontra a listaTIPOS_PERMITIDOS.adminnão está nela de propósito, senão qualquer pessoa se cadastraria como administradora. - A função
pegar(origem, campos)copia só os campos permitidos. Sem isso, alguém poderia mandar{"artista": {"statusPerfil": "suspenso", ...}}ou qualquer campo extra e gravar o que quisesse (isso se chama mass assignment). User.create(...)grava (e o hook faz o hash).- Erros:
e.code === 11000: erro de índice único do Mongo (e-mail/CPF/CNPJ repetido), devolvemos409.e.name === "ValidationError": algumrequired/enumfalhou, devolvemos400.- Qualquer outro:
500, e o erro real aparece só no terminal do servidor.
- Busca o usuário pelo e-mail com
.select("+senha"). bcrypt.compare(senhaDigitada, hashDoBanco).- A mensagem é a mesma para "e-mail não existe" e "senha errada". Assim ninguém descobre quais e-mails estão cadastrados.
200 ok · 201 criado · 400 dados inválidos · 401 não autenticado · 403 sem permissão · 404 não achado · 409 conflito (duplicado) · 500 erro do servidor.
User.find(filtro).select("artista").sort(...).skip(...).limit(...).lean()find(filtro): o filtro é um objeto. A chave"artista.cidade"(com ponto) filtra dentro do sub-documento..select("artista"): devolve só o perfil artístico (nada de e-mail ou telefone)..skip()e.limit(): paginação. Página 2 com 12 por página:skip(12).limit(12)..lean(): devolve objetos JavaScript simples, mais rápidos que documentos Mongoose completos. Use quando só vai ler e mandar como JSON.Promise.all([...]): roda a busca e a contagem ao mesmo tempo.- Os limites (
Math.min(50, ...)) impedem alguém de pedir 1 milhão de itens.
mongoose.isValidObjectId(id): se alguém mandar/api/artistas/abc, evitamos um erro estranho do Mongo e respondemos404.- Busca o artista, depois os portfólios dele. Filtramos os trabalhos
publicadoem JavaScript. - É isso que alimentaria a página da Katz com dados reais.
- Passa por
exigirLogin, e o id vem do token, nunca do corpo da requisição. Assim ninguém edita o perfil de outra pessoa. - Monta
{ "artista.biografia": "...", ... }e usa$setpara alterar só esses campos, sem apagar o resto. runValidators: true: sem isso, o Mongoose não valida osenumemfindByIdAndUpdate.
| Arquivo | O que observar |
|---|---|
Portfolio.ts |
trabalhos: [trabalhoSchema] é um array de sub-documentos. Cada trabalho ganha seu próprio _id. |
Produto.ts |
Duas coleções no mesmo arquivo. O campo categoria é uma referência (ref). dimensoes agrupa peso/altura/largura/profundidade. |
Pedido.ts |
Itens embutidos com cópia de preço. validate garante pelo menos 1 item. |
Contratacao.ts |
Evento e contrato embutidos (1:1). numeroContrato único (sparse). Status com fluxo: solicitada → aceita → em andamento → concluída. |
Pagamento.ts |
Hook pre("validate") garante pedido XOR contratação (um dos dois, nunca ambos nem nenhum). |
Avaliacao.ts |
Índice único {contratacao, avaliador}: ninguém avalia duas vezes a mesma contratação. nota entre 1 e 5. |
Conversa.ts |
Dois models. Mensagens ficam separadas (crescem sem limite), com índice {conversa, dataEnvio} para listar o histórico em ordem rápido. |
Favorito.ts |
Índices únicos parciais (partialFilterExpression): impedem favoritar o mesmo artista/produto duas vezes, mas sem conflitar quando o outro campo está vazio. |
Esses models já existem, mas ainda não têm rotas. Só users e portfolios são usados pelas rotas atuais.
// Criar
await User.create({ ... });
// Buscar
await User.find({ tipoUsuario: "artista" }); // vários
await User.findOne({ email: "a@b.com" }); // um
await User.findById(id); // pelo _id
await User.countDocuments({ status: "ativo" }); // contar
// Atualizar
await User.findByIdAndUpdate(id, { $set: { telefone: "1199..." } }, { new: true, runValidators: true });
// Apagar
await User.findByIdAndDelete(id);
// "JOIN": troca o _id pelo documento referenciado
await Produto.find().populate("artista", "artista.nomeArtistico");
// Operadores comuns de filtro
{ valor: { $gte: 100, $lte: 500 } } // entre 100 e 500
{ categoria: { $in: ["Pintura", "Foto"] } }
{ nome: /ana/i } // contém "ana" (sem diferenciar maiúscula)# Cadastrar
curl -X POST localhost:3001/api/auth/registrar -H "Content-Type: application/json" \
-d '{"nome":"Katz","email":"katz@teste.com","senha":"123456","tipoUsuario":"artista","artista":{"nomeArtistico":"Katz","categoriaArtistica":"Pintura","cidade":"Santos"}}'
# Login (copie o "token" da resposta)
curl -X POST localhost:3001/api/auth/login -H "Content-Type: application/json" \
-d '{"email":"katz@teste.com","senha":"123456"}'
# Rota protegida
curl -X PUT localhost:3001/api/artistas/me -H "Content-Type: application/json" \
-H "Authorization: Bearer COLE_O_TOKEN_AQUI" \
-d '{"biografia":"Artista visual de Santos."}'
# Listar
curl "localhost:3001/api/artistas?cidade=Santos"Abra o MongoDB Compass, conecte com a mesma MONGODB_URI e olhe a coleção users: confira o hash da senha e o formato do documento.
| Sintoma | Causa provável |
|---|---|
MONGODB_URI não definido |
Faltou criar o server/.env (ou o nome está errado) |
MongoServerSelectionError |
Seu IP não está liberado no Atlas (Network Access), ou a string de conexão está errada |
bad auth |
Usuário/senha do banco errados (ou caractere especial na senha sem codificar) |
| Front mostra "Não foi possível conectar ao servidor" | O npm run server não está rodando |
E-mail ... já cadastrado (409) |
Você já cadastrou esse e-mail; use outro ou apague o documento no Compass |
req.body vem undefined |
Faltou express.json() ou o cabeçalho Content-Type: application/json |
- Fácil: adicione o campo
instagramao sub-documentoartista, reinicie o servidor, cadastre um artista e confira no Compass. - Fácil: em
GET /api/artistas, adicione um filtro?nome=usando regex emartista.nomeArtistico. - Médio: crie
routes/portfolios.tscomPOST /api/portfolios(protegido, só artista,artista= id do token) e adicione a rota noserver.ts. - Médio: crie
POST /api/portfolios/:id/trabalhosusando$pushpara adicionar um trabalho ao array. - Médio: crie a rota
PUT /api/auth/senha(protegida) que confere a senha atual e salva a nova. Dica: busque com.select("+senha"), altere e use.save()para o hook rodar. - Difícil: crie
POST /api/pedidos: receba[{produtoId, quantidade}], busque os produtos no banco, calcule o total no servidor (nunca confie no preço vindo do front) e grave o pedido com os itens. - Difícil: na rota de avaliações, calcule a média de notas de um artista com
Avaliacao.aggregate([...])($match+$groupcom$avg).
Validação com zod, rate limit no login (express-rate-limit), helmet (cabeçalhos de segurança), upload de imagens (Cloudinary ou S3, guardando só a URL no banco), refresh token, e deploy (Render/Railway para o back, Vercel/Netlify para o front, Atlas para o banco).