Default behavior
When your handler returns a plain object, the platform responds with 200 OK and Content-Type: application/json:
handler: async (input) => {
return { name: "Maria", balance: 150.00 };
}
// → 200 OK
// → { "name": "Maria", "balance": 150.00 }
When you need a different status code or custom headers, use the response builder.
Response builder
Import response from @jelou/functions:
import { define, response, z } from "@jelou/functions";
Custom status code
export default define({
description: "Create a user",
input: z.object({ name: z.string(), email: z.string().email() }),
output: z.object({ id: z.string(), name: z.string() }),
handler: async (input) => {
const user = await createUser(input);
return response
.status(201)
.json({ id: user.id, name: user.name });
},
});
handler: async (input) => {
return response
.header("X-Request-Id", crypto.randomUUID())
.header("Cache-Control", "max-age=300")
.json({ data: [] });
}
handler: async (input) => {
return response
.headers({
"X-Request-Id": crypto.randomUUID(),
"Cache-Control": "no-store",
})
.json({ ok: true });
}
Full chaining
The builder is immutable — each method returns a new instance:
handler: async (input) => {
return response
.status(201)
.header("Location", `/users/${input.id}`)
.header("X-Created-By", "api")
.json({ id: input.id, name: input.name });
}
// → 201 Created
// → Location: /users/abc123
// → { "id": "abc123", "name": "Maria" }
API
| Method | Description |
|---|
response.status(code) | Set HTTP status (default: 200) |
response.header(name, value) | Add a header |
response.headers(init) | Add multiple headers |
response.json(body) | Finalize and return JsonResponse |
.json() is the terminal method — after calling it you get a JsonResponse, not a builder.
Practical examples
201 Created
404 Not Found
202 Accepted
handler: async (input) => {
const ticket = await createTicket(input);
return response
.status(201)
.header("Location", `/tickets/${ticket.id}`)
.json({ ticketId: ticket.id, status: "open" });
}
handler: async (input) => {
const customer = await findCustomer(input.phone);
if (!customer) {
return response
.status(404)
.json({ error: "not_found", message: "Customer not found" });
}
return { name: customer.name, balance: customer.balance };
}
handler: async (input, ctx) => {
ctx.log("Webhook received", { event: input.event });
procesarEvento(input).catch((err) =>
ctx.log("Error", { error: err.message })
);
return response.status(202).json({ acknowledged: true });
}
Output validation
When using response.json(body) with an output schema, validation applies to the body — same as with plain objects. Validation never blocks the response.
MCP behavior
When invoked via MCP (Brain Studio AI agents):
- The body is emitted as
structuredContent
- Status code and headers are ignored — MCP has no HTTP status concept
- The body is also sent as JSON text for compatibility
No need to condition your code for HTTP vs MCP — use the response builder normally. The platform extracts the body and discards HTTP metadata for MCP invocations.
Limitations
| Limitation | Detail |
|---|
| JSON only | .json() is the only terminal method — no .text(), .html() |
| No native Web Response | Returning new Response() from define()/app() doesn’t work |
| Fixed Content-Type | Always application/json, cannot be overridden |
| OpenAPI | Spec at /openapi.json only documents 200 responses for now |
For advanced needs (streaming, binary, dynamic content-type), use raw mode.