Take-home assignments: estruturar, testar e apresentar seu projeto para entrevistas
Por John Pires · 10 de março de 2026

Cenário prático do dia a dia
Você recebeu um take-home assignment com um enunciado curto: "Crie uma API que gerencie tarefas com endpoints para criar, listar e marcar concluído". Prazo: 48 horas. Resultado esperado: repo no GitHub com instruções. O que normalmente acontece na pressa: um endpoint grande, validação frágil, nenhum teste automatizado e um README genérico. Isso é o que vou demonstrar — primeiro o jeito errado, depois o jeito certo.
No fluxo real de entrevistas, avaliadores não só olham se a feature funciona; avaliam design, testes, documentação e a capacidade de comunicar decisões. Um repositório organizado e com testes claros vale mais que uma feature com uma UI bonita feita às pressas.
Este cenário assume que você tem 1–3 anos de experiência: já fez APIs básicas, conhece JavaScript/Node e tem contato inicial com testes. Não vou reexplicar o que é Express ou Jest — vou mostrar padrões que turnam um take-home aceitável em um take-home que impressiona.
As decisões práticas que você tomará: como limitar o escopo, como estruturar pastas, quais testes escrever, como documentar trade-offs e como preparar a apresentação em entrevista. Cada item tem exemplos concretos e blocos de código prontos para copiar e adaptar.
Antes de qualquer linha de código, responda em texto no README: qual problema você resolveu, quais trade-offs escolheu, o que ficou fora do escopo e como testar localmente. Isso reduz perguntas triviais na entrevista. A seguir mostramos o erro comum e a correção.
Erro comum: copy-paste e endpoint monolítico
Um erro recorrente é implementar tudo em um único arquivo sem separação de responsabilidades. Isso cria dificuldades para testes e para explicar decisões. Abaixo um exemplo real de como developers apressados costumam implementar o POST /tasks.
// arquivo: server-bad.js
const express = require('express');
const app = express();
app.use(express.json());
let tasks = [];
app.post('/tasks', (req, res) => {
const t = req.body;
// sem validação, sem id único, sem tratamento de erro
t.createdAt = new Date().toISOString();
t.done = false;
tasks.push(t);
res.status(201).json(t);
});
app.get('/tasks', (req, res) => res.json(tasks));
app.listen(3000);
O código acima funciona para um demo rápido, mas falha em testes, não valida entrada, não garante id único e mistura armazenamento em memória com lógica de roteamento. Na revisão de código, apontamos: ausência de contrato, acoplamento, falta de testes e falta de scripts de execução.
Correção: separação por camadas e contrato explícito
Substituímos o monolito por uma estrutura mínima: rota, controller, service e modelo simples. Isso melhora testabilidade e comunicação de responsabilidades. No próximo bloco mostramos a estrutura final do projeto e o código principal do endpoint.
Arquitetura da solução
Decisão de arquitetura para um take-home deve priorizar clareza e testabilidade, não desempenho absoluto. Uma arquitetura pragmática para este tipo de tarefa:
- API REST simples com Express (rotas mínimas)
- Camada de service para lógica de negócio
- Armazenamento in-memory com interface substituível (facilita mocks)
- Testes automatizados (unit + integração leve com supertest)
- Scripts e CI para rodar testes e linters
Explique essas escolhas no README: usar in-memory não é um pecado em take-homes quando você documenta que a camada é trocável e fornece um mock ou instruções para rodar com um DB real.
Estrutura de pastas recomendada
Prefira uma estrutura enxuta e óbvia:
- src/ - código da aplicação
- src/routes - definição de rotas
- src/controllers - adaptação entre http e services
- src/services - regras de negócio
- src/repositories - interface de armazenamento
- tests/ - testes unit e integração
- README.md, package.json, .github/workflows - scripts e CI
Essa divisão facilita um code review: o revisor abre cada camada e entende rapidamente onde mexer para testar. Também facilita narrar a solução na entrevista: "a rota delega, o controller valida, o service aplica regras".
Contrato API e validação
Defina claramente o contrato JSON esperado. Em vez de validar manualmente no handler, use um esquema mínimo (por exemplo, Joi ou uma validação simples). Abaixo mostramos validação leve dentro do controller.
Código completo com explicações
Aqui está uma implementação enxuta e funcional em Node.js/Express. Primeiro o arquivo principal do servidor com roteamento desacoplado. Contexto: este arquivo inicializa o app e conecta as rotas; não contém lógica de negócio.
// arquivo: src/server.js
const express = require('express');
const tasksRouter = require('./routes/tasks');
function createApp() {
const app = express();
app.use(express.json());
app.use('/tasks', tasksRouter);
// health
app.get('/healthz', (req, res) => res.sendStatus(200));
return app;
}
if (require.main === module) {
const app = createApp();
app.listen(3000, () => console.log('listening on 3000'));
}
module.exports = createApp;
Depois de startup, a rota delega para um controller. Contexto: controller valida entrada, chama service e retorna respostas HTTP corretas.
// arquivo: src/routes/tasks.js
const express = require('express');
const router = express.Router();
const controller = require('../controllers/tasksController');
router.post('/', controller.createTask);
router.get('/', controller.listTasks);
router.post('/:id/complete', controller.completeTask);
module.exports = router;
O controller abaixo demonstra validação manual simples (suficiente para um take-home), tratamento de status HTTP e uso da camada service.
// arquivo: src/controllers/tasksController.js
const service = require('../services/tasksService');
function validateTaskPayload(body) {
if (!body || typeof body.title !== 'string' || body.title.trim() === '') {
return { valid: false, error: 'title is required and must be a non-empty string' };
}
return { valid: true };
}
module.exports.createTask = (req, res) => {
const v = validateTaskPayload(req.body);
if (!v.valid) return res.status(400).json({ error: v.error });
const task = service.create({ title: req.body.title });
res.status(201).json(task);
};
module.exports.listTasks = (req, res) => {
const list = service.list();
res.json(list);
};
module.exports.completeTask = (req, res) => {
const task = service.complete(req.params.id);
if (!task) return res.status(404).json({ error: 'not found' });
res.json(task);
};
Contexto do service: contém regras de negócio e abstrai o repositório. Aqui usamos um repositório em memória com interface simples.
// arquivo: src/services/tasksService.js
const repo = require('../repositories/inMemoryTasks');
const { v4: uuidv4 } = require('uuid');
module.exports.create = ({ title }) => {
const task = { id: uuidv4(), title, done: false, createdAt: new Date().toISOString() };
repo.save(task);
return task;
};
module.exports.list = () => repo.getAll();
module.exports.complete = (id) => {
const t = repo.getById(id);
if (!t) return null;
t.done = true;
repo.save(t);
return t;
};
Repositório em memória (troque por um DB em produção). Mantém contratos simples: save, getAll, getById.
// arquivo: src/repositories/inMemoryTasks.js
const store = new Map();
module.exports.save = (task) => store.set(task.id, JSON.parse(JSON.stringify(task)));
module.exports.getAll = () => Array.from(store.values());
module.exports.getById = (id) => store.get(id) || null;
module.exports.clear = () => store.clear();
Depois do código, adicione scripts para rodar testes. Um package.json mínimo:
// fragments/package.json (scripts relevantes)
{
"scripts": {
"start": "node src/server.js",
"test": "jest --runInBand --detectOpenHandles",
"lint": "eslint . --ext .js",
"ci": "npm run lint && npm test"
},
"devDependencies": {
"jest": "^29.0.0",
"supertest": "^6.0.0",
"eslint": "^8.0.0"
}
}
Contexto: ter um script ci facilita integrar com GitHub Actions. No bloco de testes a seguir, mostramos testes de integração usando supertest para cobrir o fluxo feliz e alguns edge-cases.
// arquivo: tests/tasks.integration.test.js
const request = require('supertest');
const createApp = require('../src/server');
const repo = require('../src/repositories/inMemoryTasks');
let app;
beforeEach(() => { repo.clear(); app = createApp(); });
test('cria e lista tarefas', async () => {
const res = await request(app).post('/tasks').send({ title: 'comprar leite' }).expect(201);
expect(res.body).toHaveProperty('id');
const list = await request(app).get('/tasks').expect(200);
expect(list.body).toHaveLength(1);
expect(list.body[0].title).toBe('comprar leite');
});
test('validação de título ausente', async () => {
await request(app).post('/tasks').send({}).expect(400);
});
Esses testes provam comportamento básico e documentam o contrato: criar sem title retorna 400. Em uma entrevista verbal, mostre esse arquivo e explique por que escolheu integração leve em vez de apenas unit tests.
Tratamento de erros e edge cases
Take-homes frequentemente falham nos detalhes: ID duplicado, payload inválido, concorrência, persistência volátil. Cobrir os principais edge cases e documentá-los é crítico.
Liste de checks que sempre faço num take-home:
- Validação de entrada (camada controller)
- Respostas HTTP corretas (201, 400, 404, 500)
- Mensagens de erro consistentes (formato JSON)
- Testes para fluxo feliz e falhas previsíveis
- Documentação para setup local e para alteração do storage
Erros comuns e armadilhas
Erro 1: respostas inconsistentes. Ex.: algumas rotas retornam string, outras retornam objeto. Isso confunde avaliadores e impede integração front-end simples. Padronize: sempre JSON com chave error para falhas.
Erro 2: usar timezone/localDate sem especificar formato. Use ISO strings no backend e documente. Erro 3: testes que dependem de estado global entre execuções. Sempre limpar repositórios em testes (veja exemplos acima com repo.clear()).
Ao revisar take-homes, eu rodo os testes e leio o README. Se o README não explica como rodar localmente em menos de 2 minutos, faz falta na entrevista.
Exceções e logs
Não deixe stacktraces vazarem para o cliente. Em um take-home, implemente um middleware de erro simples que loga e retorna um 500 genérico:
// arquivo: src/middlewares/errorHandler.js
module.exports = (err, req, res, next) => {
// em produção, salvar em um logger central
console.error(err);
res.status(500).json({ error: 'internal server error' });
};
Inclua esse middleware no final da cadeia de middlewares em server.js. Isso mostra maturidade: você pensa em observabilidade mesmo em um exercício pequeno.
Performance e otimização
Performance raramente é o foco inicial em um take-home simples, mas alguns cuidados fazem diferença na avaliação: evitar algoritmos O(n^2) desnecessários, não ler arquivos sincronamente no request path, e manter endpoints idempotentes quando faz sentido.
Para nosso exemplo de tasks, a maior melhoria possível é trocar o repositório em memória por um DB quando solicitado. Mas antes, garanta que a interface do repositório suporte operações atômicas e consultas eficientes.
Melhorias de complexidade
Se a listagem de tarefas precisar de paginação/filtros, não busque tudo e filtre em memória para datasets grandes. Implemente parâmetros limit/offset e documente-os no README. Para take-homes, um par de testes cobrindo paginação é suficiente para provar que você considerou escala.
Benchmarks e trade-offs
Em entrevistas, explique trade-offs: "optei por in-memory para acelerar entrega e foco em testes; trocaria por PostgreSQL usando uma camada repository e adicionaria migrations se fosse uma feature real". Deixe um comentário no README com estimativas: possíveis gargalos e como resolver (cache, índices, batch updates).
Exemplo rápido: se marcar task como completa for uma operação frequente, garanta que o esquema do banco tenha índice por id e que a operação seja uma UPDATE por PK. Para soluções NoSQL, documente como garantir atomicidade na sua abordagem.
Apresentando o projeto na entrevista
Não importe apenas o que você fez; importe como você comunica. Estruture a apresentação com 3 blocos: Contexto, Decisões e Demo/Próximos passos. Use o README para suportar cada bloco e não leia o README — use como guia.
- Contexto: objetivo do take-home e o que ficou fora do escopo.
- Decisões: arquitetura, trade-offs e por quê (ex.: in-memory para velocidade).
- Demo: rodar testes e executar endpoint localmente. Mostre um teste falhando e como corrigi-lo.
Durante a entrevista, espere perguntas como "por que não usar GraphQL?" ou "como escalar isso?" — responda com alternativas claras e o que mudaria no repositório para suportar cada alternativa.
Dica prática: no PR ou no README, inclua um arquivo NOTES.md com pontos que você não teve tempo de implementar, mas que você considera importantes. Isso demonstra julgamento e priorização.
Conclusão prática
Um take-home bem-sucedido é o equilíbrio entre escopo resolvido, qualidade do código e capacidade de comunicação. Evite o monolito, escreva ao menos testes de integração leves, padronize erros e documente trade-offs.
Checklist final que eu uso em code reviews de take-homes:
- README com como rodar e decisões técnicas
- Código organizado em camadas (routes/controllers/services/repo)
- Testes cobrindo fluxo feliz e falhas previsíveis
- Scripts de lint/test/ci configurados
- Erro handler e mensagens de erro consistentes
- Comentários sobre como trocar storage por um DB real
Ao seguir esse padrão você transforma um exercício simples em uma demonstração de disciplina técnica — e isso pesa muito em entrevistas para profissionais com 1–3 anos de experiência. Apresente com clareza, mostre testes falhando e recuperando, e você terá o diferencial que os recrutadores procuram.
Exporte este repositório como um template, inclua instruções para rodar em Docker e um workflow CI simples, e você terá um take-home robusto, testável e fácil de discutir em entrevistas.
Assuntos: takehome, entrevistas, tests, Node.js, ci, boas-praticas
Leia também
Teste take‑home: checklist prático — README, testes, Docker, CI e entrega
Checklist prático para testes take‑home: README claro, testes, Docker, CI e como estruturar a entrega. Guia passo a passo com templates, scripts e armadilhas…
Transforme Pull Requests em estudos de caso práticos para seu portfólio
Use pull requests reais como estudos de caso: extraia problema, decisão técnica e métricas. Automação com scripts e GitHub Actions transforma PRs em cases…
Usar IA para melhorar seu portfólio técnico sem inventar resultados
Aprenda a usar IA para melhorar descrições e organização do seu portfólio técnico sem inventar resultados: padrões, scripts, validações automáticas e…
Publicar um pacote NPM que impressiona recrutadores: semver, CI e docs
Mostre maturidade com um pacote NPM: semver correto, CI com semantic-release, testes e README executável. Transforme um repositório hobby em um ativo de…
Respostas STAR para entrevistas técnicas: 10 exemplos práticos para júnior
Aprenda a estruturar respostas STAR para entrevistas técnicas com 10 exemplos práticos, scripts para gerar respostas em Python e Node.js, e um mini-projeto…
Respostas STAR para entrevistas de programação: modelos e exercícios práticos
Aprenda a usar a técnica STAR para estruturar respostas em entrevistas de programação: modelos, exercícios práticos, scripts para treinar e exemplos…