Como construir uma API RESTful escalável com Node.js e Express

Publicado em 2026-02-01 • leitura estimada • ~5 min

Aprenda a criar APIs RESTful profissionais: arquitetura em camadas, validação, autenticação JWT, tratamento de erros e boas práticas de escalabilidade.

O que é uma API RESTful

REST (Representational State Transfer) é um estilo arquitetural para APIs baseado em HTTP. Uma API RESTful usa verbos HTTP (GET, POST, PUT, DELETE) para operações em recursos, retorna dados em JSON e segue princípios como statelessness e uso correto de status codes.

Princípios REST

  • Stateless: cada requisição contém todas as informações necessárias (JWT, por exemplo).
  • Interface uniforme: recursos identificados por URIs (/users/123).
  • Cache: respostas podem ser armazenadas em cache para otimizar performance.
  • Cliente-servidor: separação clara entre frontend e backend.

Por que Node.js e Express

Node.js usa arquitetura event-driven e non-blocking I/O, ideal para APIs de alta concorrência. Express é um framework minimalista que facilita roteamento, middlewares e tratamento de requisições HTTP.

Estrutura de pastas escalável

Uma boa arquitetura separa responsabilidades e facilita manutenção. Vamos usar o padrão de camadas (layers).

api-restful/
├── src/
│   ├── config/          # Configurações (DB, env vars)
│   ├── controllers/     # Lógica de controle de rotas
│   ├── middlewares/     # Validação, autenticação, logs
│   ├── models/          # Schemas e modelos de dados
│   ├── routes/          # Definição de endpoints
│   ├── services/        # Regras de negócio
│   ├── utils/           # Funções auxiliares
│   └── app.js           # Setup do Express
├── tests/               # Testes unitários e integração
├── .env                 # Variáveis de ambiente
├── package.json
└── server.js            # Entrada da aplicação

Passo 1: Setup inicial

Inicializar projeto

mkdir api-restful && cd api-restful
npm init -y
npm install express dotenv cors helmet morgan
npm install --save-dev nodemon

Estrutura básica (server.js)

// server.js
require('dotenv').config();
const app = require('./src/app');

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Configurar Express (src/app.js)

// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

const app = express();

	// Middlewares de segurança e logs
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Health check
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', timestamp: Date.now() });
});

// Rotas
const userRoutes = require('./routes/user.routes');
app.use('/api/users', userRoutes);

// Error handler global
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: {
      message: err.message || 'Internal Server Error',
      status: err.status || 500
    }
  });
});

module.exports = app;

Passo 2: Criar rotas RESTful

Definir endpoints (src/routes/user.routes.js)

// src/routes/user.routes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/user.controller');
const { validateUser } = require('../middlewares/validation.middleware');
const { authenticate } = require('../middlewares/auth.middleware');

	// Rotas públicas
router.post('/register', validateUser, userController.register);
router.post('/login', userController.login);

// Rotas protegidas
router.get('/', authenticate, userController.getAll);
router.get('/:id', authenticate, userController.getById);
router.put('/:id', authenticate, validateUser, userController.update);
router.delete('/:id', authenticate, userController.delete);

module.exports = router;

Passo 3: Controller (lógica de controle)

// src/controllers/user.controller.js
const userService = require('../services/user.service');

exports.register = async (req, res, next) => {
  try {
    const user = await userService.createUser(req.body);
    res.status(201).json({ data: user });
  } catch (error) {
    next(error);
  }
};

exports.login = async (req, res, next) => {
  try {
    const { email, password } = req.body;
    const result = await userService.authenticate(email, password);
    res.status(200).json({ data: result });
  } catch (error) {
    next(error);
  }
};

exports.getAll = async (req, res, next) => {
  try {
    const users = await userService.getAllUsers();
    res.status(200).json({ data: users });
  } catch (error) {
    next(error);
  }
};

exports.getById = async (req, res, next) => {
  try {
    const user = await userService.getUserById(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.status(200).json({ data: user });
  } catch (error) {
    next(error);
  }
};

exports.update = async (req, res, next) => {
  try {
    const user = await userService.updateUser(req.params.id, req.body);
    res.status(200).json({ data: user });
  } catch (error) {
    next(error);
  }
};

exports.delete = async (req, res, next) => {
  try {
    await userService.deleteUser(req.params.id);
    res.status(204).send();
  } catch (error) {
    next(error);
  }
};

Passo 4: Service (regras de negócio)

// src/services/user.service.js
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/user.model');

exports.createUser = async (userData) => {
  const existingUser = await User.findByEmail(userData.email);
  if (existingUser) {
    const error = new Error('Email already exists');
    error.status = 409;
    throw error;
  }

  const hashedPassword = await bcrypt.hash(userData.password, 10);
  const user = await User.create({
    ...userData,
    password: hashedPassword
  });

  // Remove password do retorno
  delete user.password;
  return user;
};

exports.authenticate = async (email, password) => {
  const user = await User.findByEmail(email);
  if (!user) {
    const error = new Error('Invalid credentials');
    error.status = 401;
    throw error;
  }

  const isPasswordValid = await bcrypt.compare(password, user.password);
  if (!isPasswordValid) {
    const error = new Error('Invalid credentials');
    error.status = 401;
    throw error;
  }

  const token = jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );

  return { token, user: { id: user.id, email: user.email, name: user.name } };
};

exports.getAllUsers = async () => {
  return await User.findAll();
};

exports.getUserById = async (id) => {
  return await User.findById(id);
};

exports.updateUser = async (id, userData) => {
  if (userData.password) {
    userData.password = await bcrypt.hash(userData.password, 10);
  }
  return await User.update(id, userData);
};

exports.deleteUser = async (id) => {
  return await User.delete(id);
};

Passo 5: Middleware de autenticação JWT

// src/middlewares/auth.middleware.js
const jwt = require('jsonwebtoken');

exports.authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

Passo 6: Middleware de validação

// src/middlewares/validation.middleware.js
const { body, validationResult } = require('express-validator');

exports.validateUser = [
  body('email').isEmail().withMessage('Invalid email'),
  body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
  body('name').notEmpty().withMessage('Name is required'),

  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    next();
  }
];

Passo 7: Model (exemplo com PostgreSQL)

// src/models/user.model.js
const pool = require('../config/database');

class User {
  static async create(userData) {
    const { name, email, password } = userData;
    const result = await pool.query(
      'INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING id, name, email',
      [name, email, password]
    );
    return result.rows[0];
  }

  static async findByEmail(email) {
    const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
    return result.rows[0];
  }

  static async findById(id) {
    const result = await pool.query('SELECT id, name, email, created_at FROM users WHERE id = $1', [id]);
    return result.rows[0];
  }

  static async findAll() {
    const result = await pool.query('SELECT id, name, email, created_at FROM users');
    return result.rows;
  }

  static async update(id, userData) {
    const { name, email, password } = userData;
    const result = await pool.query(
      'UPDATE users SET name = $1, email = $2, password = $3 WHERE id = $4 RETURNING id, name, email',
      [name, email, password, id]
    );
    return result.rows[0];
  }

  static async delete(id) {
    await pool.query('DELETE FROM users WHERE id = $1', [id]);
  }
}

module.exports = User;

Boas práticas de escalabilidade

1. Rate Limiting

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // max 100 requisições por IP
  message: 'Too many requests from this IP'
});

app.use('/api/', limiter);

2. Compressão de respostas

const compression = require('compression');
app.use(compression());

3. Logs estruturados

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

4. Paginação

exports.getAll = async (req, res, next) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const offset = (page - 1) * limit;

  const users = await User.findAll(limit, offset);
  const total = await User.count();

  res.json({
    data: users,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  });
};

5. Variável de ambiente (.env)

PORT=3000
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=your-secret-key-change-in-production
CORS_ORIGIN=https://myapp.com

Testando a API

Registro de usuário

curl -X POST http://localhost:3000/api/users/register \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com","password":"123456"}'

Login

curl -X POST http://localhost:3000/api/users/login \
  -H "Content-Type: application/json" \
  -d '{"email":"john@example.com","password":"123456"}'

Listar usuários (com token)

curl -X GET http://localhost:3000/api/users \
  -H "Authorization: Bearer SEU_TOKEN_AQUI"

Deploy em produção

Preparar para deploy

  • Use NODE_ENV=production
  • Configure HTTPS (Let's Encrypt ou load balancer)
  • Use PM2 ou Docker para gerenciar processos
  • Configure logs externos (Datadog, Papertrail)
  • Implemente health checks (/health)

Dockerfile basico

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Conclusão

Uma API RESTful bem arquitetada em Node.js e Express facilita manutenção, testes e escalabilidade. Separe responsabilidades em camadas, valide inputs, proteja rotas com JWT, implemente rate limiting e monitore logs. Com essas práticas, sua API estará pronta para produção.

FAQ

Qual banco de dados usar?

PostgreSQL para dados relacionais, MongoDB para NoSQL. Depende da estrutura dos seus dados.

Como lidar com uploads de arquivos?

Use multer para processar multipart/form-data e armazene em S3 ou storage local.

Como versionar a API?

Use prefixos nas rotas: /api/v1/users, /api/v2/users.

Como testar a API?

Use Jest + Supertest para testes de integração. Mocke o banco com bibliotecas como jest-mock-extended.

JWT é seguro?

Sim, se usar secret forte, HTTPS e expiração curta. Para refresh tokens, implemente rotação.