> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jelou.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Respuestas HTTP

> Controla el status code y headers de tus respuestas con el response builder: status personalizados, headers custom y comportamiento en MCP.

## Comportamiento por defecto

Cuando tu handler retorna un objeto plano, la plataforma responde con `200 OK` y `Content-Type: application/json`:

```typescript theme={null}
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`:

```typescript theme={null}
import { define, response, z } from "@jelou/functions";
```

### Status code personalizado

```typescript theme={null}
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,
      });
  },
});
```

### Headers personalizados

```typescript theme={null}
handler: async (input) => {
  return response
    .header("X-Request-Id", crypto.randomUUID())
    .header("Cache-Control", "max-age=300")
    .json({ datos: [] });
}
```

### Múltiples headers de una vez

```typescript theme={null}
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:

```typescript theme={null}
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

```typescript theme={null}
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 con un body JSON               |
| `response.noContent()`         | Finaliza con `204 No Content` sin body  |

<Note>
  `.json()` y `.noContent()` son los métodos terminales — después de llamarlos obtienes un response final, no un builder. Los métodos `.status()`, `.header()` y `.headers()` retornan un nuevo builder encadenable.
</Note>

## Ejemplos prácticos

<Tabs>
  <Tab title="201 Created">
    ```typescript theme={null}
    handler: async (input) => {
      const ticket = await crearTicket(input);

      return response
        .status(201)
        .header("Location", `/tickets/${ticket.id}`)
        .json({ ticketId: ticket.id, estado: "abierto" });
    }
    ```
  </Tab>

  <Tab title="404 Not Found">
    ```typescript theme={null}
    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 };
    }
    ```
  </Tab>

  <Tab title="Cache headers">
    ```typescript theme={null}
    handler: async (input) => {
      const productos = await listarProductos(input.categoria);

      return response
        .header("Cache-Control", "public, max-age=300")
        .header("ETag", `"${hashProductos(productos)}"`)
        .json({ productos, total: productos.length });
    }
    ```
  </Tab>

  <Tab title="202 Accepted">
    ```typescript theme={null}
    handler: async (input, ctx) => {
      ctx.log("Webhook recibido", { event: input.event });

      procesarEvento(input).catch((err) =>
        ctx.log("Error procesando evento", { error: err.message })
      );

      return response
        .status(202)
        .json({ acknowledged: true });
    }
    ```
  </Tab>

  <Tab title="204 No Content">
    ```typescript theme={null}
    handler: async (input, ctx) => {
      await eliminarRecurso(input.id);

      return response.noContent();
    }
    // → 204 No Content (sin body)
    ```
  </Tab>
</Tabs>

## 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:

```typescript theme={null}
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"
      });
  },
});
```

<Note>
  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.
</Note>

## Funciona en `app()` también

```typescript theme={null}
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`

```typescript theme={null}
// 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 });
}
```

<Tip>
  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.
</Tip>

## Content-Type

El response builder **siempre** retorna `application/json`. Si intentas sobreescribir `Content-Type`, la plataforma lo fuerza de vuelta a `application/json`:

```typescript theme={null}
// 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                                                                            |
| ----------------------- | ---------------------------------------------------------------------------------- |
| JSON o vacío            | Solo `.json()` y `.noContent()` — 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` en `.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:

```typescript theme={null}
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 };
}
```
