From dev-team-kit-fv
Defines backend APIs, database schemas, REST/GraphQL endpoints, server-side validation, JWT auth, and migrations using Node.js, Prisma, PostgreSQL, and Zod.
npx claudepluginhub felvieira/claude-skills-fvThis skill uses the workspace's default tool permissions.
O Backend define a fundação de dados e lógica de negócio que sustenta toda a aplicação.
Implements and reviews backend APIs, databases, authentication, and server logic using clean Repository/Service/Router architecture. For REST/GraphQL endpoints, migrations, and auth.
Enforces workflow for full-stack apps: requirements, architecture decisions, scaffolding checklists, patterns for API integration, auth, error handling, real-time (SSE/WebSocket) across Node/React/Next.js, Python, Go.
Provides patterns for RESTful API design, TypeScript response formats, HTTP status codes, and JWT authentication flows. Use for backend development tasks.
Share bugs, ideas, or general feedback.
O Backend define a fundação de dados e lógica de negócio que sustenta toda a aplicação.
Esta skill segue GLOBAL.md, policies/execution.md, policies/handoffs.md, policies/quality-gates.md, policies/token-efficiency.md, policies/stack-flexibility.md, policies/tool-safety.md e policies/evals.md.
Para exemplos extensos de schema, auth e migracoes, consultar docs/skill-guides/backend-api.md apenas quando necessario.
Runtime: Node.js (LTS)
Framework: Express / NestJS (dependendo da complexidade)
ORM: Prisma
Banco: PostgreSQL
Validação: Zod
Auth: JWT (access em memoria + refresh em HttpOnly cookie)
Cache: Redis (quando necessário)
Documentação: OpenAPI/Swagger auto-gerado
prisma/schema.prisma
model User {
id String @id @default(uuid())
email String @unique
name String
password String
role Role @default(USER)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
posts Post[]
sessions Session[]
@@map("users")
@@index([email])
@@index([deletedAt])
}
enum Role {
USER
ADMIN
MODERATOR
}
model Session {
id String @id @default(uuid())
userId String
refreshToken String @unique
userAgent String?
ip String?
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@map("sessions")
@@index([userId])
@@index([expiresAt])
}
Convenções:
snake_case plural (via @@map)camelCase no PrismacreatedAt + updatedAtdeletedAt nullableToda resposta da API segue este formato:
Sucesso:
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"perPage": 20,
"total": 100,
"totalPages": 5
}
}
Erro:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Email inválido",
"details": [
{
"field": "email",
"message": "Formato de email inválido"
}
]
}
}
GET /api/v1/resources → Lista (com paginação, filtro, sort)
GET /api/v1/resources/:id → Detalhe
POST /api/v1/resources → Criar
PATCH /api/v1/resources/:id → Atualizar parcial
DELETE /api/v1/resources/:id → Soft delete
POST /api/v1/auth/register → Registro
POST /api/v1/auth/login → Login (retorna access token, seta refresh cookie)
POST /api/v1/auth/refresh → Refresh token
POST /api/v1/auth/logout → Logout (invalida session)
GET /api/v1/auth/me → Usuário logado
Query params para listagem:
?page=1&perPage=20 → Paginação
?sort=createdAt&order=desc → Ordenação
?search=termo → Busca fulltext
?filter[status]=active → Filtros
?include=author,comments → Relations
Login:
1. POST /auth/login { email, password }
2. Valida credenciais
3. Gera access token (JWT, 15min, retornado no response body para uso em memoria)
4. Gera refresh token (UUID, 7d, HttpOnly cookie)
5. Salva session no banco
6. Retorna { accessToken, user }
Refresh:
1. POST /auth/refresh (cookie com refresh token)
2. Valida refresh token no banco
3. Verifica se session não expirou
4. Gera novo access token
5. Opcionalmente rotaciona refresh token
6. Retorna { accessToken, user }
Logout:
1. POST /auth/logout (com access token)
2. Remove session do banco
3. Limpa cookie do refresh token
src/validators/user.validator.ts
import { z } from 'zod';
const emailSchema = z.string().email('Email inválido').toLowerCase().trim();
const passwordSchema = z.string()
.min(8, 'Mínimo 8 caracteres')
.regex(/[A-Z]/, 'Precisa de letra maiúscula')
.regex(/[0-9]/, 'Precisa de número')
.regex(/[^A-Za-z0-9]/, 'Precisa de caractere especial');
export const createUserSchema = z.object({
email: emailSchema,
password: passwordSchema,
name: z.string().min(2).max(100).trim(),
});
export const updateUserSchema = createUserSchema.partial().omit({ password: true });
export const loginSchema = z.object({
email: emailSchema,
password: z.string().min(1, 'Senha obrigatória'),
});
export const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
perPage: z.coerce.number().int().min(1).max(100).default(20),
sort: z.string().optional(),
order: z.enum(['asc', 'desc']).default('desc'),
search: z.string().optional(),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type PaginationInput = z.infer<typeof paginationSchema>;
src/middleware/validate.ts
import { ZodSchema } from 'zod';
export const validate = (schema: ZodSchema, source: 'body' | 'query' | 'params' = 'body') => {
return (req, res, next) => {
const result = schema.safeParse(req[source]);
if (!result.success) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Dados inválidos',
details: result.error.issues.map(i => ({
field: i.path.join('.'),
message: i.message,
})),
},
});
}
req.validated = result.data;
next();
};
};
src/middleware/auth.ts
export const authenticate = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED' } });
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch {
return res.status(401).json({ success: false, error: { code: 'TOKEN_EXPIRED' } });
}
};
export const authorize = (...roles: Role[]) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ success: false, error: { code: 'FORBIDDEN' } });
}
next();
};
};
src/middleware/errorHandler.ts
export const errorHandler = (err, req, res, next) => {
console.error(err);
if (err.code === 'P2002') {
return res.status(409).json({
success: false,
error: { code: 'DUPLICATE', message: 'Registro já existe' },
});
}
res.status(err.status || 500).json({
success: false,
error: {
code: err.code || 'INTERNAL_ERROR',
message: process.env.NODE_ENV === 'production' ? 'Erro interno' : err.message,
},
});
};
src/services/base.service.ts
export const createBaseService = <T>(model: any) => ({
async findMany(params: PaginationInput & { where?: any }) {
const { page, perPage, sort, order, search, ...filters } = params;
const skip = (page - 1) * perPage;
const where = { deletedAt: null, ...filters.where };
const [data, total] = await Promise.all([
model.findMany({
where,
skip,
take: perPage,
orderBy: sort ? { [sort]: order } : { createdAt: 'desc' },
}),
model.count({ where }),
]);
return {
data,
meta: { page, perPage, total, totalPages: Math.ceil(total / perPage) },
};
},
async findById(id: string) {
return model.findFirst({ where: { id, deletedAt: null } });
},
async create(data: Partial<T>) {
return model.create({ data });
},
async update(id: string, data: Partial<T>) {
return model.update({ where: { id }, data });
},
async softDelete(id: string) {
return model.update({ where: { id }, data: { deletedAt: new Date() } });
},
});
Entregar:
Codigo deve priorizar clareza. Comentarios so fazem sentido quando explicam contexto nao obvio, restricoes externas ou workarounds temporarios.
Se você reconhece um desses pensamentos, PARE e siga o processo. Ver policies/anti-rationalization.md.
| Racionalização | Realidade |
|---|---|
| "Validação no frontend já cobre" | Frontend é bypassável. Backend é a última linha de defesa |
| "Trato erros depois" | Erros não tratados viram 500s em produção e logs inúteis |
| "É só um endpoint simples" | Endpoints simples sem rate limit, validação e auth são vetores de ataque |
| "ORM protege contra SQL injection" | ORM protege queries normais. Raw queries e query builders não |
| "Logs são overhead desnecessário" | Logs são a única forma de debugar produção. Sem logs = voo cego |