Sobre nós Guias Projetos Contactos
Админка
please wait

O Express.js é o padrão de facto para criar aplicações web e APIs em Node.js. O seu design minimalista e sem imposições permite-lhe estruturar as aplicações como precisar, ao mesmo tempo que fornece utilitários HTTP essenciais. Este guia aborda a criação de APIs REST prontas para produção com Express, na perspetiva de um programador sénior.

Porquê Express

O Express oferece vantagens convincentes:

  1. Sobrecarga mínima: Camada fina sobre o HTTP do Node.js
  2. Flexível: Sem estrutura ou padrões prescritos
  3. Ecossistema de middleware: Milhares de plugins disponíveis
  4. Desempenho: Rápido e testado em produção à escala
  5. Fácil de aprender: API simples, rápida para se tornar produtivo

Primeiros passos

Instalação

mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D nodemon # Reinício automático durante o desenvolvimento

Servidor básico

index.js:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json()); // Analisar corpos JSON
app.use(express.urlencoded({ extended: true })); // Analisar corpos URL-encoded
// Rotas
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the API' });
});
// Iniciar servidor
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

Atualize o package.json:

{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
}

Estrutura do projeto

Organize para facilitar a manutenção:

src/
├── config/
│ └── database.js
├── controllers/
│ └── userController.js
├── middleware/
│ ├── auth.js
│ ├── errorHandler.js
│ └── validate.js
├── models/
│ └── User.js
├── routes/
│ ├── index.js
│ └── users.js
├── services/
│ └── userService.js
├── utils/
│ └── helpers.js
└── app.js

Routing

Definição de rotas

// routes/users.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { authenticate } = require('../middleware/auth');
const { validateUser } = require('../middleware/validate');
// Rotas públicas
router.post('/register', validateUser, userController.register);
router.post('/login', userController.login);
// Rotas protegidas
router.use(authenticate); // Todas as rotas abaixo requerem autenticação
router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.put('/:id', validateUser, userController.update);
router.delete('/:id', userController.delete);
module.exports = router;

Montar rotas

// routes/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./users');
const postRoutes = require('./posts');
router.use('/users', userRoutes);
router.use('/posts', postRoutes);
// Verificação de saúde
router.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
module.exports = router;
// app.js
const routes = require('./routes');
app.use('/api/v1', routes);

Controllers

// controllers/userController.js
const userService = require('../services/userService');
exports.getAll = async (req, res, next) => {
try {
const { page = 1, limit = 10, search } = req.query;
const result = await userService.findAll({
page: parseInt(page),
limit: parseInt(limit),
search
});
res.json({
data: result.users,
meta: {
total: result.total,
page: result.page,
limit: result.limit,
pages: Math.ceil(result.total / result.limit)
}
});
} catch (error) {
next(error);
}
};
exports.getById = async (req, res, next) => {
try {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json({ data: user });
} catch (error) {
next(error);
}
};
exports.create = async (req, res, next) => {
try {
const user = await userService.create(req.body);
res.status(201).json({
data: user,
message: 'User created successfully'
});
} catch (error) {
next(error);
}
};
exports.update = async (req, res, next) => {
try {
const user = await userService.update(req.params.id, req.body);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json({
data: user,
message: 'User updated successfully'
});
} catch (error) {
next(error);
}
};
exports.delete = async (req, res, next) => {
try {
const deleted = await userService.delete(req.params.id);
if (!deleted) {
return res.status(404).json({
error: 'User not found'
});
}
res.status(204).send();
} catch (error) {
next(error);
}
};

Middleware

Autenticação

// middleware/auth.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'
});
}
};
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
};

Validação

// middleware/validate.js
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
role: Joi.string().valid('user', 'admin').default('user')
});
exports.validateUser = (req, res, next) => {
const { error, value } = userSchema.validate(req.body, {
abortEarly: false, // Devolver todos os erros
stripUnknown: true // Remover campos desconhecidos
});
if (error) {
const errors = error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}));
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
req.body = value; // Usar valores validados/sanitizados
next();
};

Handler de erros

// middleware/errorHandler.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
const errorHandler = (err, req, res, next) => {
console.error('Error:', err);
// Erros operacionais (esperados)
if (err.isOperational) {
return res.status(err.statusCode).json({
error: err.message
});
}
// Erro de validação do Mongoose
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
// Chave duplicada do Mongoose
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return res.status(409).json({
error: `${field} already exists`
});
}
// Erros JWT
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
error: 'Invalid token'
});
}
// Erros desconhecidos (não revelar detalhes em produção)
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
});
};
module.exports = { AppError, errorHandler };

Registo de pedidos

// middleware/logger.js
const morgan = require('morgan');
// Token personalizado para o tempo de resposta
morgan.token('response-time-ms', (req, res) => {
return `${res.responseTime}ms`;
});
// Desenvolvimento: colorido, verboso
const devFormat = ':method :url :status :response-time-ms - :res[content-length]';
// Produção: JSON para agregação de logs
const prodFormat = JSON.stringify({
method: ':method',
url: ':url',
status: ':status',
responseTime: ':response-time',
contentLength: ':res[content-length]',
userAgent: ':user-agent'
});
module.exports = process.env.NODE_ENV === 'production'
? morgan(prodFormat)
: morgan(devFormat);

Configuração completa da app

// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const compression = require('compression');
const routes = require('./routes');
const logger = require('./middleware/logger');
const { errorHandler } = require('./middleware/errorHandler');
const app = express();
// Middleware de segurança
app.use(helmet());
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Limitação de taxa
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // Limitar cada IP a 100 pedidos por janela
message: { error: 'Too many requests, please try again later' }
});
app.use('/api', limiter);
// Parsing e compressão
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));
app.use(compression());
// Logging
app.use(logger);
// Ficheiros estáticos
app.use('/static', express.static('public', {
maxAge: '1d',
etag: true
}));
// Cabeçalhos de controlo de cache
app.get('/static/*', (req, res, next) => {
res.set('Cache-Control', 'public, max-age=3600, must-revalidate');
next();
});
// Rotas
app.use('/api/v1', routes);
// Handler 404
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Handler de erros (tem de ser o último)
app.use(errorHandler);
module.exports = app;

Integração com base de dados

MongoDB com Mongoose

// config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
module.exports = connectDB;
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true, select: false },
role: { type: String, enum: ['user', 'admin'], default: 'user' }
}, { timestamps: true });
// Fazer hash da palavra-passe antes de guardar
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Método de comparação de palavra-passe
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);

Testes

// tests/users.test.js
const request = require('supertest');
const app = require('../src/app');
const mongoose = require('mongoose');
const User = require('../src/models/User');
describe('User API', () => {
beforeAll(async () => {
await mongoose.connect(process.env.TEST_MONGODB_URI);
});
afterAll(async () => {
await mongoose.connection.close();
});
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/v1/users/register', () => {
it('should create a new user', async () => {
const res = await request(app)
.post('/api/v1/users/register')
.send({
name: 'Test User',
email: '[email protected]',
password: 'password123'
});
expect(res.status).toBe(201);
expect(res.body.data).toHaveProperty('id');
expect(res.body.data.email).toBe('[email protected]');
});
it('should return 400 for invalid data', async () => {
const res = await request(app)
.post('/api/v1/users/register')
.send({ email: 'invalid' });
expect(res.status).toBe(400);
expect(res.body).toHaveProperty('error');
});
});
});

Principais conclusões

  1. Separar responsabilidades: Rotas, controllers, serviços, modelos
  2. O middleware é poderoso: Auth, validação, logging, erros
  3. Trate sempre os erros: Um handler global de erros apanha tudo
  4. Valide a entrada: Nunca confie nos dados do cliente
  5. Use async/await: Código assíncrono limpo com try/catch
  6. Segurança em primeiro lugar: Helmet, CORS, limitação de taxa desde o primeiro dia

O Express fornece a base — as suas decisões de arquitetura determinam a manutenibilidade. Mantenha-o simples, teste exaustivamente e adicione complexidade apenas quando necessário.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive