skills/03-backend-api/SKILL.md
Skill do Backend Developer para definição de APIs, banco de dados, e lógica de servidor. Use quando precisar definir schemas de banco, endpoints REST/GraphQL, validação server-side, autenticação, migrations, ou qualquer lógica de backend. Trigger em: "API", "endpoint", "banco de dados", "schema", "migration", "backend", "servidor", "autenticação", "JWT", "middleware", "ORM", "Prisma", "PostgreSQL", "Node.js", "Express", "NestJS", "validação server-side".
npx skillsauth add felvieira/claude-skills-fv backend-apiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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() } });
},
});
// src/db.js
const Database = require('better-sqlite3');
const path = require('path');
let _db = null;
function getDb() {
if (_db) return _db;
_db = new Database(path.join(__dirname, '..', 'data.db'));
_db.pragma('journal_mode = WAL');
_db.pragma('foreign_keys = ON');
_db.pragma('synchronous = NORMAL');
return _db;
}
module.exports = { getDb };
// src/migrate.js
const { getDb } = require('./db');
function migrate() {
const db = getDb();
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
`);
}
module.exports = { migrate };
// src/repositories/user.repository.js
const { getDb } = require('../db');
const UserRepo = {
findById(id) {
return getDb().prepare('SELECT * FROM users WHERE id = ? AND deleted_at IS NULL').get(id);
},
findByEmail(email) {
return getDb().prepare('SELECT * FROM users WHERE email = ? AND deleted_at IS NULL').get(email);
},
findAll({ page = 1, perPage = 20 } = {}) {
const offset = (page - 1) * perPage;
const rows = getDb()
.prepare('SELECT * FROM users WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?')
.all(perPage, offset);
const { total } = getDb()
.prepare('SELECT COUNT(*) as total FROM users WHERE deleted_at IS NULL')
.get();
return { data: rows, meta: { page, perPage, total, totalPages: Math.ceil(total / perPage) } };
},
create(data) {
const stmt = getDb().prepare(
'INSERT INTO users (email, name, password, role) VALUES (@email, @name, @password, @role)'
);
stmt.run(data);
return this.findByEmail(data.email);
},
softDelete(id) {
return getDb()
.prepare("UPDATE users SET deleted_at = datetime('now') WHERE id = ?")
.run(id);
},
};
module.exports = UserRepo;
// db.transaction() retorna função que roda tudo atomicamente
const { getDb } = require('./db');
function transferCredits(fromId, toId, amount) {
const db = getDb();
const transfer = db.transaction((from, to, amt) => {
db.prepare('UPDATE wallets SET credits = credits - ? WHERE id = ?').run(amt, from);
db.prepare('UPDATE wallets SET credits = credits + ? WHERE id = ?').run(amt, to);
});
transfer(fromId, toId, amount); // lança se qualquer stmt falhar
}
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 |
development
Skill compositora que pega texto/assunto e gera post de blog HTML completo no repo {blog_repo_path} ({github_user_repo_url}), com imagens (via skill 17 fal.ai ou skill 42 Playwright screenshot), commit+push automático, retorna URL pública via GitHub Pages. Trigger em: "post no blog", "publicar post", "escrever post", "blog post", "publish blog", "gera post", "criar post", "novo post no meu blog".
tools
Audita o peso de contexto carregado na sessão — CLAUDE.md, agents, MCP descriptions, rules ativas, skills invocadas e histórico acumulado. Estima tokens por componente, reporta headroom disponível e emite alertas de overflow. Distinto do cost-tracker (skill 30) que rastreia tokens gastos em completions runtime. Trigger em: "contexto inchado", "context overflow", "quanto contexto estou usando", "peso do contexto", "context budget", "tokens carregados", "sessao lenta", "respostas degradadas", "headroom de contexto", "custo fixo de contexto", "overhead de rules", "overhead dos agents", "impacto do MCP no contexto", "espaco no context window", "quanto cabe no context window"
development
Coleta e organiza informacao tecnica multi-fonte antes de escrever docs, PRDs, ADRs ou artigos. Busca em: docs oficiais, GitHub (repos + issues), Stack Overflow, papers e blogs de referencia. Ranqueia fontes por autoridade (oficial 40% + recencia 30% + profundidade 20% + comunidade 10%). Output: memory/research/<slug>.md pronto para alimentar skill 10 (documenter), skill 01 (po-feature-spec), skill 26 (prompt-engineer) ou skill 41 (blog-publisher). Trigger em: "pesquisa tecnica", "levanta informacao", "coleta docs", "busca referencias", "preciso de fontes", "research antes de escrever", "levanta o que existe sobre", "benchmark de solucoes", "o que existe sobre X", "quero entender o estado da arte", "compara abordagens", "levanta referencias", "faz um research de", "coleta fontes sobre", "pesquisa sobre", "quero saber o que existe de", "monta um dossie tecnico", "background tecnico", "due diligence tecnica", "levantamento de alternativas".
development
Extrai e codifica os padroes de coding do projeto existente (naming, estrutura de arquivos, error handling, testing style, import style, API design, async patterns) e usa esses padroes como restricao sobre novo codigo. Garante que o agente code "igual ao resto do projeto" em vez de inventar convencoes proprias. Produce um "code style map" salvo em memory/patterns.md que todas as skills de geracao de codigo devem consultar. Trigger em: "segue o padrao do projeto", "coda igual ao resto", "nao reinventa padrao", "detecta padroes do codebase", "code style do projeto", "padrao do projeto", "convencao do projeto", "coda consistente", "mesma convencao", "sem reinventar roda", "padrao de codigo", "patterns do codebase", "pattern enforcement", "conformidade de padrao", "convencoes de naming", "padrao de tratamento de erro", "mesma estrutura do projeto", "detecta as convencoes", "extrai padroes de coding", "como o projeto estrutura".