@ferjssilva/fast-crud-api
Version:
A complete and fast crud API generator
736 lines (606 loc) • 21.7 kB
Markdown
# Exemplos de Lógica de Negócios com Fast CRUD API
Este documento demonstra como adicionar lógica de negócios personalizada aos endpoints gerados pelo `fast-crud-api` sem modificar o código da biblioteca. Utilizamos os recursos nativos do Fastify para estender a funcionalidade.
## Índice
1. [Hooks Globais e Específicos](#hooks-globais-e-específicos)
2. [Autenticação e Autorização](#autenticação-e-autorização)
3. [Recursos User-Scoped](#recursos-user-scoped)
4. [Validação Personalizada](#validação-personalizada)
5. [Processamento de Dados](#processamento-de-dados)
6. [Filtros Avançados](#filtros-avançados)
7. [Exemplo Completo](#exemplo-completo)
## Hooks Globais e Específicos
O Fastify permite adicionar hooks que executam lógica antes ou depois de uma requisição ser processada.
```javascript
const fastify = require('fastify')();
const fastCrudApi = require('@ferjssilva/fast-crud-api');
const User = require('./models/User');
// Hook global para todas as rotas
fastify.addHook('preHandler', (request, reply, done) => {
console.log(`${request.method} ${request.url} - ${new Date().toISOString()}`);
done();
});
// Hook específico para rotas de usuário
fastify.register(async (userRoutes) => {
userRoutes.addHook('preHandler', (request, reply, done) => {
console.log('Acessando rota de usuário');
done();
});
}, { prefix: '/api/users' });
// Registrar o plugin após configurar os hooks
fastify.register(fastCrudApi, {
prefix: '/api',
models: [User]
});
```
## Autenticação e Autorização
Controle de acesso para rotas específicas usando hooks:
```javascript
const fastify = require('fastify')();
const fastCrudApi = require('@ferjssilva/fast-crud-api');
const User = require('./models/User');
const Post = require('./models/Post');
// Função para verificar token JWT
const verifyToken = async (request) => {
const token = request.headers.authorization?.split(' ')[1];
// Implemente sua lógica de verificação de token
return token ? { id: 'user123', role: 'admin' } : null;
};
// Hook de autenticação para todas as rotas
fastify.addHook('preHandler', async (request, reply) => {
if (request.url.startsWith('/api/admin')) {
const user = await verifyToken(request);
if (!user) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'Token de autenticação inválido ou ausente'
});
}
if (user.role !== 'admin') {
return reply.code(403).send({
error: 'Forbidden',
message: 'Apenas administradores podem acessar este recurso'
});
}
// Armazenar usuário autenticado para uso posterior
request.user = user;
}
});
// API pública
fastify.register(fastCrudApi, {
prefix: '/api',
models: [Post],
methods: {
posts: ['GET'] // Apenas leitura pública
}
});
// API administrativa protegida
fastify.register(async (adminApi) => {
adminApi.register(fastCrudApi, {
prefix: '', // Já estamos em /api/admin
models: [User, Post],
methods: {
users: ['GET', 'POST', 'PUT', 'DELETE'],
posts: ['POST', 'PUT', 'DELETE']
}
});
}, { prefix: '/api/admin' });
```
## Recursos User-Scoped
O `fast-crud-api` oferece suporte nativo para recursos isolados por usuário, onde cada usuário acessa apenas seus próprios dados. Esta funcionalidade é ideal para aplicações multi-tenant.
### Configuração Básica
```javascript
const fastify = require('fastify')();
const fastCrudApi = require('@ferjssilva/fast-crud-api');
const UserHabit = require('./models/UserHabit');
const UserProfile = require('./models/UserProfile');
// Middleware de autenticação - Define request.userId
fastify.addHook('preHandler', async (request, reply) => {
const token = request.headers.authorization?.split(' ')[1];
if (!token) {
return; // Permitir rotas públicas continuarem
}
try {
// Verificar e decodificar o token (use sua lógica de autenticação)
const user = await verifyJWT(token);
request.userId = user.id; // Definir userId no request
} catch (error) {
// Token inválido - não definir userId
console.error('Token inválido:', error);
}
});
// Registrar recursos user-scoped
fastify.register(fastCrudApi, {
prefix: '/api',
models: [UserHabit, UserProfile],
userScoped: ['user-habits', 'user-profiles'] // Recursos isolados por usuário
});
```
### Como Funciona
Quando um recurso é marcado como `userScoped`:
1. **Autenticação Obrigatória**: Todas as operações requerem `request.userId`
2. **Filtragem Automática**:
- `GET` - Filtra automaticamente por `userId`
- `POST` - Injeta `userId` nos novos documentos
- `PUT/DELETE` - Verifica propriedade atomicamente
3. **Segurança**:
- Usuários não podem acessar dados de outros usuários
- Usuários não podem modificar o `userId` dos seus recursos
- Verificações atômicas previnem vulnerabilidades TOCTOU
### Exemplo de Modelo User-Scoped
```javascript
// models/UserHabit.js
const mongoose = require('mongoose');
const userHabitSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
index: true // Importante para performance
},
habitId: {
type: String,
required: true
},
frequency: {
type: String,
enum: ['daily', 'weekly', 'monthly'],
default: 'daily'
},
completedDates: [Date],
createdAt: {
type: Date,
default: Date.now
}
});
// Índice composto para queries eficientes
userHabitSchema.index({ userId: 1, habitId: 1 }, { unique: true });
module.exports = mongoose.model('UserHabit', userHabitSchema);
```
### Uso da API
```javascript
// Cliente fazendo requisições
// 1. Listar hábitos do usuário autenticado
// GET /api/user-habits
// Authorization: Bearer <token>
// Resposta: Apenas hábitos do usuário logado
// 2. Criar novo hábito
// POST /api/user-habits
// Authorization: Bearer <token>
// Body: { "habitId": "exercise", "frequency": "daily" }
// O userId é injetado automaticamente
// 3. Tentar acessar dados de outro usuário (bloqueado)
// GET /api/user-habits?userId=outro-usuario-id
// Resposta: 403 Forbidden - Cannot access other users' data
// 4. Atualizar hábito (apenas se pertencer ao usuário)
// PUT /api/user-habits/:id
// Authorization: Bearer <token>
// Body: { "frequency": "weekly" }
// Retorna 404 se o hábito não pertencer ao usuário
// 5. Deletar hábito (apenas se pertencer ao usuário)
// DELETE /api/user-habits/:id
// Authorization: Bearer <token>
// Retorna 404 se o hábito não pertencer ao usuário
```
### Combinando User-Scoped com Outros Recursos
```javascript
const fastify = require('fastify')();
const fastCrudApi = require('@ferjssilva/fast-crud-api');
// Modelos
const User = require('./models/User');
const Post = require('./models/Post'); // Público
const UserHabit = require('./models/UserHabit'); // User-scoped
const UserProfile = require('./models/UserProfile'); // User-scoped
// Middleware de autenticação
fastify.addHook('preHandler', async (request, reply) => {
const token = request.headers.authorization?.split(' ')[1];
if (token) {
try {
const user = await verifyJWT(token);
request.userId = user.id;
request.user = user;
} catch (error) {
// Token inválido
}
}
});
// Recursos públicos (sem user-scoped)
fastify.register(fastCrudApi, {
prefix: '/api/public',
models: [Post],
methods: {
posts: ['GET'] // Apenas leitura pública
}
});
// Recursos user-scoped (requerem autenticação)
fastify.register(fastCrudApi, {
prefix: '/api/user',
models: [UserHabit, UserProfile],
userScoped: ['user-habits', 'user-profiles'],
methods: {
'user-habits': ['GET', 'POST', 'PUT', 'DELETE'],
'user-profiles': ['GET', 'PUT'] // Sem POST/DELETE
}
});
// Recursos administrativos (requer role de admin)
fastify.register(async (adminApi) => {
// Hook adicional para verificar role de admin
adminApi.addHook('preHandler', async (request, reply) => {
if (!request.user || request.user.role !== 'admin') {
return reply.code(403).send({
error: 'Forbidden',
message: 'Acesso restrito a administradores'
});
}
});
adminApi.register(fastCrudApi, {
prefix: '',
models: [User, Post],
methods: {
users: ['GET', 'POST', 'PUT', 'DELETE'],
posts: ['GET', 'POST', 'PUT', 'DELETE']
}
});
}, { prefix: '/api/admin' });
```
### Boas Práticas
1. **Índices no Banco de Dados**:
```javascript
// Sempre adicione índice no campo userId
userHabitSchema.index({ userId: 1 });
// Considere índices compostos para queries comuns
userHabitSchema.index({ userId: 1, createdAt: -1 });
```
2. **Middleware de Autenticação Robusto**:
```javascript
fastify.addHook('preHandler', async (request, reply) => {
const token = request.headers.authorization?.split(' ')[1];
if (!token) {
return; // Permitir rotas públicas
}
try {
const decoded = await verifyJWT(token);
// Validar se o usuário ainda existe
const user = await User.findById(decoded.id);
if (!user) {
throw new Error('Usuário não encontrado');
}
// Verificar se o usuário está ativo
if (!user.isActive) {
throw new Error('Usuário inativo');
}
request.userId = user._id.toString();
request.user = user;
} catch (error) {
// Log do erro mas não bloquear rotas públicas
console.error('Erro de autenticação:', error.message);
}
});
```
3. **Modelo de Dados Consistente**:
```javascript
// Sempre use o mesmo tipo para userId (String ou ObjectId)
const schema = new mongoose.Schema({
userId: {
type: String, // ou mongoose.Schema.Types.ObjectId
required: true,
index: true
},
// outros campos...
});
```
4. **Tratamento de Erros**:
```javascript
// Os recursos user-scoped retornam:
// - 401 Unauthorized: quando request.userId não está definido
// - 403 Forbidden: quando tenta acessar dados de outro usuário
// - 404 Not Found: quando tenta modificar/deletar recurso de outro usuário
// (para não vazar informação sobre existência do recurso)
```
### Cenários Avançados
**Compartilhamento de Recursos entre Usuários**:
```javascript
// Para recursos que podem ser compartilhados,
// não use user-scoped. Em vez disso, use hooks personalizados:
fastify.addHook('preHandler', async (request, reply) => {
if (request.method === 'GET' && request.url.includes('/api/shared-docs')) {
const docId = request.params.id;
// Verificar se o usuário tem acesso ao documento
const doc = await SharedDoc.findOne({
_id: docId,
$or: [
{ ownerId: request.userId },
{ sharedWith: request.userId }
]
});
if (!doc) {
return reply.code(404).send({
error: 'NotFound',
message: 'Documento não encontrado'
});
}
}
});
```
## Validação Personalizada
Adicione validação avançada além dos validadores do Mongoose:
```javascript
const fastify = require('fastify')();
const fastCrudApi = require('@ferjssilva/fast-crud-api');
const User = require('./models/User');
// Funções de validação personalizada
const validateUser = (userData) => {
const errors = [];
// Validar formato de email
if (userData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) {
errors.push('Formato de email inválido');
}
// Validar complexidade de senha
if (userData.password &&
(userData.password.length < 8 ||
!/[A-Z]/.test(userData.password) ||
!/[0-9]/.test(userData.password))) {
errors.push('A senha deve ter pelo menos 8 caracteres, uma letra maiúscula e um número');
}
return { valid: errors.length === 0, errors };
};
// Hook para validar dados de usuário antes de salvar
fastify.addHook('preHandler', (request, reply, done) => {
if ((request.method === 'POST' || request.method === 'PUT') &&
request.url.includes('/api/users')) {
const validation = validateUser(request.body);
if (!validation.valid) {
return reply.code(400).send({
error: 'ValidationError',
message: 'Erro de validação',
details: validation.errors
});
}
}
done();
});
// Registrar o plugin após configurar validação
fastify.register(fastCrudApi, {
prefix: '/api',
models: [User]
});
```
## Processamento de Dados
Manipulação de dados antes ou depois de operações CRUD:
```javascript
const fastify = require('fastify')();
const fastCrudApi = require('@ferjssilva/fast-crud-api');
const User = require('./models/User');
const Post = require('./models/Post');
const bcrypt = require('bcrypt');
// Pre-processamento - Hash de senha antes de salvar
fastify.addHook('preHandler', async (request, reply) => {
if ((request.method === 'POST' || request.method === 'PUT') &&
request.url.includes('/api/users') &&
request.body.password) {
// Hash de senha
request.body.password = await bcrypt.hash(request.body.password, 10);
}
});
// Pós-processamento - Limpeza de dados sensíveis
fastify.addHook('onSend', (request, reply, payload, done) => {
if (request.url.includes('/api/users')) {
try {
const data = JSON.parse(payload);
// Remover campos sensíveis da resposta
if (data.password) delete data.password;
if (data.secretKey) delete data.secretKey;
done(null, JSON.stringify(data));
} catch (err) {
done(null, payload);
}
} else {
done();
}
});
// Registrar o plugin
fastify.register(fastCrudApi, {
prefix: '/api',
models: [User, Post]
});
```
## Filtros Avançados
Adicione lógica de filtragem personalizada além dos filtros básicos:
```javascript
const fastify = require('fastify')();
const fastCrudApi = require('@ferjssilva/fast-crud-api');
const User = require('./models/User');
const Post = require('./models/Post');
// Hooks para manipulação de parâmetros de consulta
fastify.addHook('preHandler', (request, reply, done) => {
if (request.method === 'GET' && request.url.startsWith('/api/posts')) {
// Filtro por intervalo de datas
if (request.query.dateFrom || request.query.dateTo) {
request.query.createdAt = {};
if (request.query.dateFrom) {
request.query.createdAt.$gte = new Date(request.query.dateFrom);
delete request.query.dateFrom;
}
if (request.query.dateTo) {
request.query.createdAt.$lte = new Date(request.query.dateTo);
delete request.query.dateTo;
}
}
// Filtro por status com valores personalizados
if (request.query.status === 'active') {
request.query.isPublished = true;
request.query.isArchived = false;
delete request.query.status;
} else if (request.query.status === 'archived') {
request.query.isArchived = true;
delete request.query.status;
}
}
done();
});
// Registrar o plugin
fastify.register(fastCrudApi, {
prefix: '/api',
models: [User, Post]
});
```
## Exemplo Completo
Um exemplo completo integrando várias técnicas:
```javascript
const fastify = require('fastify')();
const mongoose = require('mongoose');
const fastCrudApi = require('@ferjssilva/fast-crud-api');
// Modelos
const User = require('./models/User');
const Post = require('./models/Post');
const Order = require('./models/Order');
const UserHabit = require('./models/UserHabit');
const UserProfile = require('./models/UserProfile');
// Utilitários
const { verifyToken, isAdmin } = require('./utils/auth');
const { validateUser, validatePost } = require('./utils/validators');
const { sanitizeData, enrichData } = require('./utils/dataProcessors');
const { applyBusinessRules } = require('./utils/businessRules');
// Registrar decoradores para lógica de negócios
fastify.decorateRequest('applyBusinessRules', applyBusinessRules);
// 1. Hook global para logging e rastreamento
fastify.addHook('preHandler', (request, reply, done) => {
request.requestId = Date.now().toString();
console.log(`[${request.requestId}] ${request.method} ${request.url}`);
// Adicionar headers de rastreamento
reply.header('X-Request-ID', request.requestId);
done();
});
// 2. Hook de autenticação
fastify.addHook('preHandler', async (request, reply) => {
// Ignorar rotas públicas
if (request.url.match(/^\/api\/(login|register|public)/)) {
return;
}
try {
// Verificar token de autenticação
const user = await verifyToken(request);
if (!user) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'Autenticação necessária'
});
}
// Verificar permissões para rotas administrativas
if (request.url.includes('/api/admin') && !isAdmin(user)) {
return reply.code(403).send({
error: 'Forbidden',
message: 'Acesso negado'
});
}
// Armazenar usuário no contexto
request.user = user;
} catch (error) {
return reply.code(401).send({
error: 'AuthError',
message: error.message
});
}
});
// 3. Validação personalizada para cada modelo
fastify.addHook('preHandler', (request, reply, done) => {
if (request.method !== 'POST' && request.method !== 'PUT') {
return done();
}
let validation = { valid: true };
// Selecionar validador com base na URL
if (request.url.includes('/api/users')) {
validation = validateUser(request.body);
} else if (request.url.includes('/api/posts')) {
validation = validatePost(request.body);
}
if (!validation.valid) {
return reply.code(400).send({
error: 'ValidationError',
message: 'Dados inválidos',
details: validation.errors
});
}
done();
});
// 4. Pré-processamento de dados
fastify.addHook('preHandler', (request, reply, done) => {
if (request.method === 'POST' || request.method === 'PUT') {
// Enriquecer dados com metadados
enrichData(request.body, request.user);
// Aplicar regras de negócios específicas
request.applyBusinessRules(request.body, request.url);
}
done();
});
// 5. Pós-processamento de resposta
fastify.addHook('onSend', (request, reply, payload, done) => {
if (payload) {
try {
const data = JSON.parse(payload);
// Sanitizar dados sensíveis
const sanitized = sanitizeData(data, request.url);
done(null, JSON.stringify(sanitized));
} catch (err) {
done(null, payload);
}
} else {
done(null, payload);
}
});
// Rotas públicas - sem autenticação
fastify.register(async (publicApi) => {
publicApi.register(fastCrudApi, {
prefix: '', // Já estamos em /api/public
models: [User, Post],
methods: {
users: ['POST'], // Apenas para registro
posts: ['GET'] // Leitura pública
}
});
}, { prefix: '/api/public' });
// API principal - requer autenticação
fastify.register(fastCrudApi, {
prefix: '/api',
models: [User, Post, Order],
methods: {
users: ['GET', 'PUT'], // Usuários podem ler e atualizar seus dados
posts: ['GET', 'POST', 'PUT', 'DELETE'],
orders: ['GET', 'POST', 'PUT']
}
});
// Recursos user-scoped - isolamento automático por usuário
fastify.register(fastCrudApi, {
prefix: '/api/user',
models: [UserHabit, UserProfile],
userScoped: ['user-habits', 'user-profiles'], // Isolamento automático
methods: {
'user-habits': ['GET', 'POST', 'PUT', 'DELETE'],
'user-profiles': ['GET', 'PUT']
}
});
// API administrativa - requer permissões de admin
fastify.register(async (adminApi) => {
adminApi.register(fastCrudApi, {
prefix: '', // Já estamos em /api/admin
models: [User, Post, Order],
methods: {
users: ['GET', 'POST', 'PUT', 'DELETE'],
posts: ['GET', 'POST', 'PUT', 'DELETE'],
orders: ['GET', 'POST', 'PUT', 'DELETE']
}
});
}, { prefix: '/api/admin' });
// Iniciar servidor
fastify.listen(3000, (err) => {
if (err) throw err;
console.log('Servidor rodando na porta 3000');
});
```
Este exemplo demonstra:
1. **Logging e rastreamento** para todas as requisições
2. **Autenticação e autorização** com rotas protegidas
3. **Isolamento automático por usuário** com recursos user-scoped
4. **Validação personalizada** para diferentes modelos
5. **Processamento de dados** antes e depois das operações CRUD
6. **Regras de negócios** aplicadas via decoradores
7. **Múltiplos contextos de API** (público, autenticado, user-scoped, administrativo)
Ao usar estas técnicas, você pode estender significativamente as funcionalidades do `fast-crud-api` sem modificar seu código-fonte, mantendo a separação de responsabilidades e facilitando a manutenção da sua aplicação.