Node.js: Projeto API RESTful
Última atualização: 2026-08-26
1. Contexto do projeto
Alice e Bob receberam uma tarefa: desenvolver uma API de gerenciamento de livros para uma livraria online. Alice ficou responsável por projetar a arquitetura de roteamento e de middleware, enquanto Bob ficou encarregado de escrever a lógica de negócios. Os dois chegaram a um acordo sobre um formato padronizado de resposta e convenções de tratamento de erros e, em duas horas, concluíram todo o projeto — desde a configuração do projeto até a implementação completa da funcionalidade CRUD.
(1) Você aprenderá
- Criar uma API completa combinando o Express, o roteamento e o middleware
- Definir a estrutura de diretórios do projeto (rotas / controladores / middleware / modelos)
- Armazenamento de dados em memória e implementação de operações CRUD completas
- Validação de solicitações e tratamento unificado de erros
- Métodos de teste de API (curl / Postman)
- Elaboração de um formato unificado de resposta
2. Definição da estrutura de diretórios do projeto
Uma hierarquia de diretórios bem estruturada é a base da facilidade de manutenção de um projeto. Seguindo as melhores práticas da comunidade, Alice organiza o projeto em camadas com base nas responsabilidades:
book-api/
├── app.js
├── routes/
│ └── books.js
├── controllers/
│ └── bookController.js
├── middleware/
│ ├── validate.js
│ └── errorHandler.js
└── models/
└── bookModel.js
| Diretório/Arquivo | Responsabilidades | Descrição |
|---|---|---|
app.js |
Arquivo de entrada | Criar uma instância do Express, associar rotas e middleware |
routes/ |
Definição de rota | Define os métodos HTTP e os caminhos que apontam para o controlador correspondente |
controllers/ |
Lógica de negócios | Processar a solicitação, chamar o modelo e retornar uma resposta |
middleware/ |
Middleware | Questões transversais, como validação de solicitações e tratamento de erros |
models/ |
Modelos de dados | Armazenamento de dados e encapsulamento de operações com dados |
3. Projeto de pontos de extremidade da API
Com base nos requisitos de negócios, Bob elaborou uma lista de todos os endpoints da API:
| Método HTTP | Caminho | Função | Código de status de sucesso | Código de status de falha |
|---|---|---|---|---|
| OBTER | /api/books |
Obter todos os livros | 200 | — |
| GET | /api/books/:id |
Recuperar um único livro | 200 | 404 |
| POST | /api/books |
Novos livros | 201 | 400 |
| PUT | /api/books/:id |
Atualizar livro | 200 | 404 / 400 |
| EXCLUIR | /api/books/:id |
Excluir livro | 200 | 404 |
4. Formato padronizado de resposta
Alice insiste em um formato de resposta padronizado para que a equipe de front-end não precise adivinhar os nomes dos campos:
▶ Exemplo: Resposta bem-sucedida
{
"success": true,
"data": { "id": 1, "title": "Node.js Guide", "author": "Alice" }
}
▶ Exemplo: Resposta de erro
{
"success": false,
"error": { "code": 404, "message": "Book not found" }
}
| Campo | Tipo | Descrição |
|---|---|---|
success |
booleano | Se a solicitação foi bem-sucedida |
data |
qualquer | Dados retornados em caso de sucesso |
error.code |
número | Código de status de erro |
error.message |
string | Descrição do erro |
5. Camada do modelo de dados
Bob usa matrizes no diretório models para simular um banco de dados e encapsular todas as operações com dados:
▶ Exemplo: bookModel.js
const books = [
{ id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }
];
let nextId = 2;
function findAll() {
return books;
}
function findById(id) {
return books.find(b => b.id === id);
}
function create(data) {
const book = { id: nextId++, ...data };
books.push(book);
return book;
}
function update(id, data) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return null;
books[index] = { ...books[index], ...data };
return books[index];
}
function remove(id) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return false;
books.splice(index, 1);
return true;
}
module.exports = { findAll, findById, create, update, remove };
6. Middleware de validação de solicitações
Alice implementa a validação de dados na camada de middleware para garantir que solicitações inválidas não cheguem ao controlador:
▶ Exemplo: validate.js
function validateBook(req, res, next) {
const { title, author, year } = req.body;
const errors = [];
if (!title || typeof title !== "string") {
errors.push("title is required and must be a string");
}
if (!author || typeof author !== "string") {
errors.push("author is required and must be a string");
}
if (year !== undefined && (typeof year !== "number" || year < 0)) {
errors.push("year must be a non-negative number");
}
if (errors.length > 0) {
return res.status(400).json({
success: false,
error: { code: 400, message: errors.join("; ") }
});
}
next();
}
module.exports = { validateBook };
7. Middleware para tratamento de erros
▶ Exemplo: errorHandler.js
function errorHandler(err, req, res, next) {
console.error(err.stack);
const status = err.status || 500;
res.status(status).json({
success: false,
error: { code: status, message: err.message || "Internal Server Error" }
});
}
function createError(status, message) {
const err = new Error(message);
err.status = status;
return err;
}
module.exports = { errorHandler, createError };
8. Camada do controlador
Bob lida com a lógica de negócios no controlador, chama o modelo e retorna uma resposta em um formato padronizado:
▶ Exemplo: bookController.js
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");
function getAllBooks(req, res) {
res.json({ success: true, data: Book.findAll() });
}
function getBookById(req, res, next) {
const book = Book.findById(Number(req.params.id));
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function createBook(req, res) {
const book = Book.create(req.body);
res.status(201).json({ success: true, data: book });
}
function updateBook(req, res, next) {
const book = Book.update(Number(req.params.id), req.body);
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function deleteBook(req, res, next) {
const removed = Book.remove(Number(req.params.id));
if (!removed) return next(createError(404, "Book not found"));
res.json({ success: true, data: { message: "Book deleted" } });
}
module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };
9. Definições de rotas
O Alice separa o roteamento do controlador; o roteamento é responsável apenas pelo mapeamento:
▶ Exemplo: routes/books.js
const express = require("express");
const router = express.Router();
const controller = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");
router.get("/", controller.getAllBooks);
router.get("/:id", controller.getBookById);
router.post("/", validateBook, controller.createBook);
router.put("/:id", validateBook, controller.updateBook);
router.delete("/:id", controller.deleteBook);
module.exports = router;
10. Arquivo de entrada app.js
▶ Exemplo: app.js
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/api/books", booksRoute);
app.use(errorHandler);
const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
11. Visão geral da arquitetura do projeto
Alice desenhou um diagrama de arquitetura que ilustra claramente o fluxo de solicitações:
graph TD
A[app.js Entrance] --> B[routes/books.js]
B --> C{Needs verification?}
C -->|POST/PUT| D[middleware/validate.js]
C -->|GET/DELETE| E[controllers/bookController.js]
D --> E
E --> F[models/bookModel.js]
F --> E
E --> G[Standardized Response Format]
G --> H[Client]
E -->|Exception| I[middleware/errorHandler.js]
I --> H
12. Testes de API
Bob usa curl para verificar cada ponto final, um por um:
| comando curl | Descrição |
|---|---|
curl localhost:3000/api/books |
Ver todos os livros |
curl localhost:3000/api/books/1 |
Encontrar um livro específico |
curl -X POST -H "Content-Type: application/json" -d '{"title":"New Book","author":"Bob","year":2025}' localhost:3000/api/books |
Novos livros |
curl -X PUT -H "Content-Type: application/json" -d '{"year":2026}' localhost:3000/api/books/1 |
Atualizar livros |
curl -X DELETE localhost:3000/api/books/1 |
Excluir livro |
▶ Exemplo: Testando operações de inserção e consulta
# Add a book
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"Express in Action","author":"Evan","year":2024}' \
localhost:3000/api/books
{"success":true,"data":{"id":2,"タイトル":"Express in Action","author":"Evan","year":2024}}
# Search All Books
curl localhost:3000/api/books
{"success":true,"data":[{"id":1,"title":"Node.js Guide","author":"Alice","year":2024},{"id":2,"title":"Express in Action","author":"Evan","year":2024}]}
13. Exemplo abrangente: API completa de gerenciamento de livros
Combinando todos os módulos anteriores, eis o fluxo principal do código para o projeto completo:
// models/bookModel.js
const books = [{ id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }];
let nextId = 2;
function findAll() { return books; }
function findById(id) { return books.find(b => b.id === id); }
function create(data) {
const book = { id: nextId++, ...data };
books.push(book);
return book;
}
function update(id, data) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return null;
books[index] = { ...books[index], ...data };
return books[index];
}
function remove(id) {
const index = books.findIndex(b => b.id === id);
if (index === -1) return false;
books.splice(index, 1);
return true;
}
module.exports = { findAll, findById, create, update, remove };
// middleware/validate.js
function validateBook(req, res, next) {
const { title, author } = req.body;
const errors = [];
if (!title || typeof title !== "string") errors.push("title is required");
if (!author || typeof author !== "string") errors.push("author is required");
if (errors.length > 0) {
return res.status(400).json({
success: false,
error: { code: 400, message: errors.join("; ") }
});
}
next();
}
module.exports = { validateBook };
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
const status = err.status || 500;
res.status(status).json({
success: false,
error: { code: status, message: err.message || "Internal Server Error" }
});
}
function createError(status, message) {
const err = new Error(message);
err.status = status;
return err;
}
module.exports = { errorHandler, createError };
// controllers/bookController.js
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");
function getAllBooks(req, res) {
res.json({ success: true, data: Book.findAll() });
}
function getBookById(req, res, next) {
const book = Book.findById(Number(req.params.id));
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function createBook(req, res) {
const book = Book.create(req.body);
res.status(201).json({ success: true, data: book });
}
function updateBook(req, res, next) {
const book = Book.update(Number(req.params.id), req.body);
if (!book) return next(createError(404, "Book not found"));
res.json({ success: true, data: book });
}
function deleteBook(req, res, next) {
const removed = Book.remove(Number(req.params.id));
if (!removed) return next(createError(404, "Book not found"));
res.json({ success: true, data: { message: "Book deleted" } });
}
module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };
// routes/books.js
const express = require("express");
const router = express.Router();
const ctrl = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");
router.get("/", ctrl.getAllBooks);
router.get("/:id", ctrl.getBookById);
router.post("/", validateBook, ctrl.createBook);
router.put("/:id", validateBook, ctrl.updateBook);
router.delete("/:id", ctrl.deleteBook);
module.exports = router;
// app.js
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/api/books", booksRoute);
app.use(errorHandler);
app.listen(3000, () => console.log("Server running on port 3000"));
# Start and Test
node app.js
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"Clean Code","author":"Robert","year":2008}' \
localhost:3000/api/books
curl localhost:3000/api/books/1
curl -X DELETE localhost:3000/api/books/1
❓ Perguntas Frequentes
P: Por que os controladores e as rotas são separados? R: Separação de interesses — as rotas definem os mapeamentos de caminho, enquanto os controladores se concentram na lógica de negócios. Ao dissociar os dois, eles podem ser modificados e testados de forma independente.
P: Como faço para testar uma API? R: Use o
curlna linha de comando, o Postman ou o plug-in Thunder Client para o VS Code, se preferir uma interface gráfica, e a bibliotecasupertestpara testes automatizados.
P: Em qual camada a validação de dados deve ocorrer? R: Ela deve ser realizada na camada de middleware para interceptar dados inválidos antes que a solicitação chegue ao controlador, garantindo que a lógica do controlador permaneça limpa.
P: Como devem ser tratados os recursos que não podem ser encontrados? R: Retorne um código de status 404 juntamente com o formato de erro padronizado
{ success: false, error: { code: 404, message: "Book not found" } }.
P: Devo usar TypeScript no meu projeto? R: O JavaScript é suficiente para pequenos projetos de prática; para grandes projetos de produção, recomenda-se o uso do TypeScript, pois ele oferece segurança de tipos e melhor suporte em IDEs.
P: Os dados armazenados na memória serão perdidos após uma reinicialização? R: Sim, os arrays são armazenados na memória do processo; portanto, os dados são apagados quando o serviço é reiniciado. Em um ambiente de produção, você deve usar um banco de dados (MongoDB, PostgreSQL etc.).
P: Qual é a diferença entre PUT e PATCH? R: O PUT exige o envio do recurso inteiro para uma substituição completa, enquanto o PATCH envia apenas os campos que precisam ser modificados para uma atualização parcial. Nesta lição, usaremos o PUT por uma questão de simplicidade.
📖 Resumo
- Conceitos fundamentais e aplicação do contexto do projeto
- Conceitos fundamentais e utilização do projeto de estrutura de diretórios
- Conceitos fundamentais e uso do projeto de pontos de extremidade de API
- Conceitos básicos e uso do Formato Unificado de Resposta
- Conceitos fundamentais e uso da camada do modelo de dados
- Conceitos básicos e uso do middleware de validação de solicitações
- Conceitos básicos e uso de middleware para tratamento de erros
- Conceitos fundamentais e uso da camada de controlador
📝 Exercícios
- Conclua todos os exemplos de código desta lição e certifique-se de que cada um deles seja executado corretamente.
- Modifique o exemplo completo e adicione suas próprias extensões
- Analise a documentação oficial, identifique 1 ou 2 APIs que não foram abordadas nesta aula e escreva um código de teste para elas.
- Reflexão: Como você aplicaria o que aprendeu nesta aula a um projeto do mundo real?
- Tente combinar o que você aprendeu nesta aula com o conteúdo das aulas anteriores para criar um pequeno projeto.