Comportamiento por defecto
Cuando tu handler retorna un objeto plano, la plataforma responde con 200 OK y Content-Type: application/json:
handler: async (input) => {
return { nombre: "María", saldo: 150.00 };
}
// → 200 OK
// → { "nombre": "María", "saldo": 150.00 }
Esto es suficiente para la mayoría de los casos. Pero cuando necesitas un status code diferente o headers personalizados, usa el response builder.
Response builder
Importa response desde @jelou/functions:
import { define, response, z } from "@jelou/functions";
Status code personalizado
export default define({
description: "Crea un usuario",
input: z.object({ nombre: z.string(), email: z.string().email() }),
output: z.object({ id: z.string(), nombre: z.string() }),
handler: async (input) => {
const usuario = await crearUsuario(input);
return response
.status(201)
.json({
id: usuario.id,
nombre: usuario.nombre,
});
},
});
handler: async (input) => {
return response
.header("X-Request-Id", crypto.randomUUID())
.header("Cache-Control", "max-age=300")
.json({ datos: [] });
}
handler: async (input) => {
return response
.headers({
"X-Request-Id": crypto.randomUUID(),
"X-Powered-By": "Jelou Functions",
"Cache-Control": "no-store",
})
.json({ ok: true });
}
Encadenamiento completo
El builder es inmutable — cada método retorna una nueva instancia sin modificar la anterior:
handler: async (input) => {
return response
.status(201)
.header("Location", `/usuarios/${input.id}`)
.header("X-Created-By", "api")
.json({
id: input.id,
nombre: input.nombre,
});
}
// → 201 Created
// → Location: /usuarios/abc123
// → X-Created-By: api
// → { "id": "abc123", "nombre": "María" }
API
interface JsonResponseBuilder {
status(code: number): JsonResponseBuilder;
header(name: string, value: string): JsonResponseBuilder;
headers(init: HeadersInit | Record<string, string>): JsonResponseBuilder;
json<TBody extends Record<string, unknown>>(body: TBody): JsonResponse<TBody>;
}
| Método | Descripción |
|---|
response.status(code) | Establece el status HTTP (default: 200) |
response.header(name, value) | Agrega un header |
response.headers(init) | Agrega múltiples headers |
response.json(body) | Finaliza y retorna el JsonResponse |
.json() es el método terminal — después de llamarlo obtienes un JsonResponse, no un builder. Los métodos .status(), .header() y .headers() retornan un nuevo builder encadenable.
Ejemplos prácticos
201 Created
404 Not Found
Webhook acknowledgment
handler: async (input) => {
const ticket = await crearTicket(input);
return response
.status(201)
.header("Location", `/tickets/${ticket.id}`)
.json({ ticketId: ticket.id, estado: "abierto" });
}
handler: async (input) => {
const cliente = await buscarCliente(input.telefono);
if (!cliente) {
return response
.status(404)
.json({ error: "not_found", message: "Cliente no encontrado" });
}
return { nombre: cliente.nombre, saldo: cliente.saldo };
}
handler: async (input, ctx) => {
// Responder rápido al webhook
ctx.log("Webhook recibido", { event: input.event });
// Procesar en background (fire-and-forget)
procesarEvento(input).catch((err) =>
ctx.log("Error procesando evento", { error: err.message })
);
return response
.status(202)
.json({ acknowledged: true });
}
Validación de output
Cuando usas response.json(body) con un schema output definido, la validación se aplica al body del response — exactamente igual que con objetos planos:
export default define({
description: "Crear usuario",
input: z.object({ nombre: z.string() }),
output: z.object({ id: z.string(), nombre: z.string() }),
handler: async (input) => {
return response
.status(201)
.json({
id: "usr_123",
nombre: input.nombre,
// extra: "este campo no está en output pero no causa error"
});
},
});
La validación de output nunca bloquea la respuesta. Si el body no coincide con el schema, se registra un warning en los logs pero el cliente recibe la respuesta con el status que configuraste.
Funciona en app() también
import { app, define, response, z } from "@jelou/functions";
export default app({
tools: {
crearContacto: define({
description: "Crea un contacto en el CRM",
input: z.object({ nombre: z.string(), email: z.string().email() }),
handler: async (input) => {
return response
.status(201)
.header("X-Contact-Created", "true")
.json({ id: "ct_abc", nombre: input.nombre });
},
}),
buscarContacto: define({
description: "Busca un contacto por email",
input: z.object({ email: z.string() }),
handler: async (input) => {
const contacto = await buscar(input.email);
if (!contacto) {
return response.status(404).json({ error: "not_found" });
}
return contacto; // 200 por defecto
},
}),
},
});
Comportamiento en MCP
Cuando tu función es invocada vía MCP (por un agente IA en Brain Studio), el response builder funciona diferente:
- El body se emite como
structuredContent del tool result
- El status code y los headers se ignoran — MCP no tiene concepto de HTTP status
- También se envía el body como texto JSON para compatibilidad con clientes MCP que esperan
content[].text
// HTTP: → 201 Created + X-User-Created: true + { id: "usr_123", nombre: "María" }
// MCP: → structuredContent: { id: "usr_123", nombre: "María" } (status y headers ignorados)
handler: async (input) => {
return response
.status(201)
.header("X-User-Created", "true")
.json({ id: "usr_123", nombre: input.nombre });
}
No necesitas condicionar tu código para HTTP vs MCP — usa el response builder normalmente. La plataforma extrae el body y descarta los metadatos HTTP cuando la invocación es vía MCP.
Content-Type
El response builder siempre retorna application/json. Si intentas sobreescribir Content-Type, la plataforma lo fuerza de vuelta a application/json:
// El Content-Type siempre será application/json
response.header("Content-Type", "text/plain").json({ ok: true });
// → Content-Type: application/json
Para respuestas no-JSON (binarios, HTML, etc.), usa raw mode en lugar del response builder.
Limitaciones
| Limitación | Detalle |
|---|
| Solo JSON | .json() es el único método terminal — no hay .text(), .html(), .blob() |
| Sin Web Response nativo | Retornar un new Response() desde define() / app() no funciona (produce {}) |
| Content-Type fijo | Siempre application/json, no se puede sobreescribir |
| OpenAPI | La spec en /openapi.json solo documenta respuesta 200 por ahora |
Para necesidades más avanzadas (streaming, binarios, status dinámico basado en content-type), usa raw mode.
Mezclar con objetos planos
Puedes retornar objetos planos o response.json() desde el mismo handler — la plataforma detecta automáticamente cuál usas:
handler: async (input) => {
if (input.action === "crear") {
return response.status(201).json({ id: "new-123", creado: true });
}
// Objeto plano → 200 por defecto
return { id: "existing-456", creado: false };
}