Criando um Controller com o Igniter.js no SaaS Boilerplate
Controllers são uma parte fundamental do framework Igniter. Eles lidam com requisições HTTP recebidas, processam dados e retornam respostas apropriadas. Os controllers no Igniter são projetados para ter segurança de tipos, serem modulares e fáceis de testar.
Criando um Controller
Para criar um controller no Igniter, você usa a função igniter.controller(). Aqui está um exemplo básico:
// src/features/user/controllers/user.controller.ts
import { igniter } from '@/igniter'
import { userService } from '../services/user.service'
export const userController = igniter.controller({
path: '/users',
actions: {
list: igniter.query({
path: '/',
handler: async (ctx) => {
const users = await userService.findAll()
return ctx.response.ok(users)
}
}),
get: igniter.query({
path: '/:id',
handler: async (ctx) => {
const { id } = ctx.params
const user = await userService.findById(id)
if (!user) {
return ctx.response.notFound('User not found')
}
return ctx.response.ok(user)
}
}),
create: igniter.mutation({
path: '/',
method: 'POST',
handler: async (ctx) => {
const data = ctx.body
const user = await userService.create(data)
return ctx.response.created(user)
}
}),
update: igniter.mutation({
path: '/:id',
method: 'PUT',
handler: async (ctx) => {
const { id } = ctx.params
const data = ctx.body
const user = await userService.update(id, data)
if (!user) {
return ctx.response.notFound('User not found')
}
return ctx.response.ok(user)
}
}),
delete: igniter.mutation({
path: '/:id',
method: 'DELETE',
handler: async (ctx) => {
const { id } = ctx.params
await userService.delete(id)
return ctx.response.noContent()
}
})
}
})
A configuração do controller inclui:
path: O caminho base para todas as ações no controller (por exemplo,/users)actions: Um objeto contendo todas as ações (endpoints) para o controller
Ações do controller
Cada ação em um controller representa um endpoint. O Igniter fornece dois tipos de ações:
Queries
Queries são usadas para operações de leitura (requisições GET). Elas não modificam dados e são seguras para chamar múltiplas vezes.
list: igniter.query({
path: '/',
handler: async (ctx) => {
// Handle GET request
}
})
Mutations
Mutations são usadas para operações de escrita (requisições POST, PUT, DELETE). Elas modificam dados e devem ser idempotentes quando possível.
create: igniter.mutation({
path: '/',
method: 'POST',
handler: async (ctx) => {
// Handle POST request
}
})
Contexto da Requisição
Cada ação do controller recebe um objeto de contexto (ctx) que fornece acesso aos dados da requisição e utilitários para gerar respostas:
handler: async (ctx) => {
// Dados da requisição
const params = ctx.params // Parâmetros de URL
const query = ctx.query // Parâmetros de query string
const body = ctx.body // Corpo da requisição
const headers = ctx.headers // Cabeçalhos da requisição
// Utilitários de resposta
return ctx.response.ok(data) // Resposta 200 OK
// Outros métodos de resposta: created, noContent, badRequest, unauthorized, forbidden, notFound, etc.
}
Usando Procedimentos com controllers
Procedimentos são funções de middleware que podem ser aplicadas às ações do controller. Eles permitem adicionar preocupações transversais como autenticação, validação e registro de logs.
import { igniter } from '@/igniter'
import { authProcedure } from '@/procedures/auth.procedure'
import { validateUserProcedure } from '@/procedures/validate-user.procedure'
export const userController = igniter.controller({
path: '/users',
actions: {
create: igniter.mutation({
path: '/',
method: 'POST',
use: [
authProcedure(), // Authenticate the request
validateUserProcedure() // Validate the user data
],
handler: async (ctx) => {
// Handle the request
}
})
}
})
Manipulando Respostas
O Igniter fornece um conjunto de utilitários de resposta para facilitar o retorno de respostas HTTP padronizadas:
// Respostas de sucesso ctx.response.ok(data) // 200 OK ctx.response.created(data) // 201 Created ctx.response.accepted() // 202 Accepted ctx.response.noContent() // 204 No Content // Respostas de erro do cliente ctx.response.badRequest(message) // 400 Bad Request ctx.response.unauthorized(message) // 401 Unauthorized ctx.response.forbidden(message) // 403 Forbidden ctx.response.notFound(message) // 404 Not Found ctx.response.methodNotAllowed(message) // 405 Method Not Allowed ctx.response.conflict(message) // 409 Conflict // Respostas de erro do servidor ctx.response.internalServerError(message) // 500 Internal Server Error ctx.response.notImplemented(message) // 501 Not Implemented ctx.response.badGateway(message) // 502 Bad Gateway ctx.response.serviceUnavailable(message) // 503 Service Unavailable
Segurança de Tipos
Um dos principais benefícios dos controllers Igniter é a segurança de tipos. O framework fornece inferência completa de tipos para suas ações de controller, garantindo que seu código cliente esteja sempre sincronizado com seu código de servidor.
// Define input and output types for your controller actions
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string(),
email: z.string().email(),
password: z.string().min(8)
})
type CreateUserInput = z.infer<typeof createUserSchema>
type User = { id: string; name: string; email: string }
export const userController = igniter.controller({
path: '/users',
actions: {
create: igniter.mutation<CreateUserInput, User>({
path: '/',
method: 'POST',
handler: async (ctx) => {
// ctx.body is typed as CreateUserInput
const user = await userService.create(ctx.body)
// Return value is typed as User
return ctx.response.created(user)
}
})
}
})
Fábricas de controllers
Você pode criar fábricas de controllers para gerar controllers com comportamento semelhante:
function createCrudController<T>(options: {
path: string,
service: CrudService<T>
}) {
return igniter.controller({
path: options.path,
actions: {
list: igniter.query({
path: '/',
handler: async (ctx) => {
const items = await options.service.findAll()
return ctx.response.ok(items)
}
}),
get: igniter.query({
path: '/:id',
handler: async (ctx) => {
const { id } = ctx.params
const item = await options.service.findById(id)
if (!item) {
return ctx.response.notFound('Item not found')
}
return ctx.response.ok(item)
}
}),
// Add other CRUD actions...
}
})
}
// Usage
export const userController = createCrudController({
path: '/users',
service: userService
})
Melhores Práticas
-
Mantenha os controllers Focados: Os controllers devem se concentrar em lidar com requisições e respostas HTTP. A lógica de negócios deve ser delegada aos serviços.
-
Use Procedimentos para Preocupações Transversais: Use procedimentos para autenticação, validação, registro de logs e outras preocupações transversais.
-
Formatos de Resposta Consistentes: Use os utilitários de resposta integrados para garantir formatos de resposta consistentes em toda a sua API.
-
Aproveite a Segurança de Tipos: Use o sistema de tipos do TypeScript para garantir que sua API seja segura em termos de tipos.
-
Organize por Recurso: Agrupe controllers por recurso ou domínio, em vez de por função técnica.
-
Valide a Entrada: Sempre valide os dados de entrada antes de processá-los.
-
Trate Erros com Elegância: Implemente tratamento adequado de erros para fornecer mensagens de erro significativas aos clientes.
-
Use Injeção de Dependência: Injete serviços e outras dependências em seus controllers para torná-los mais fáceis de testar.
-
Escreva Testes: Escreva testes unitários e de integração para seus controllers para garantir que funcionem conforme o esperado.
Quer desenvolver um SaaS em um final de semana?
Um único comando no terminal e pronto, você já tem seu projeto criado, com site, blog, central de ajuda, autenticação, onboarding, dashboard, emails... Para resumir, é assim que eu crio os meus SaaS em um Final de Semana.
Conheça minha estratégia