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

# Response

> Build every controller's reply with the fluent context.response object — JSON, exceptions, redirects, headers, and cookies — resolved to a native Web API Response.

Every [controller](/basics/controller) returns its reply through `context.response`, an `IResponse` object the framework hands you on the request context. You never construct a `Response` by hand: you call fluent builder methods (`json`, `exception`, `notFound`, `redirect`) that set the status, body, and headers, then the framework calls `get()` for you and sends a standard Web API `Response` to the client. Each builder returns the response instance, so calls chain, and the last write wins.

## Why this object

* **Fluent and chainable.** `json()`, `exception()`, `notFound()`, and `redirect()` each return `IResponse`, so you compose status, body, and headers in one expression.
* **Structured JSON envelope.** JSON responses are wrapped in a consistent shape — `success`, `status`, `message`, `data`, error flags, and environment info — so every client parses replies the same way.
* **Web standards out.** Internally `get()` produces a native `Response`; the body, status, and headers are exactly what a browser or `fetch` client expects.
* **Type-safe data.** `HttpResponse<Data>` is generic over your payload type, so `json(data)` is checked against the shape you declared.
* **Headers and cookies built in.** `context.response.header` exposes the full [header](/basics/request) API — set headers, attach cookies, manage caching — and they ride along with whatever body you set.
* **Socket-aware.** A public `done` flag tracks completion for WebSocket responses without changing the HTTP body.

## How it works

You mutate the response object during the request; the framework reads it afterward. Each builder method resets the others — calling `redirect()` clears any JSON body and sets the `Location` header, calling `json()` clears the redirect URL — so the response always reflects the last builder you called.

| Stage              | What happens                                                                                                                                             |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| In the controller  | You call a builder on `context.response` (e.g. `context.response.json(data, 201)`), optionally setting headers and cookies on `context.response.header`. |
| Last write wins    | Each builder clears the state set by the others, so the final call determines the body, status, and `Content-Type`.                                      |
| Framework resolves | The framework calls `get(env)` on your response, which serializes the JSON envelope (or builds the redirect/stream) into a native `Response`.            |
| Sent to client     | The `Response` — status, headers, body — is returned over the wire.                                                                                      |

## The response builder

These are the methods on `IResponse` (implemented by `HttpResponse`). The builder methods return the response instance for chaining; the inspector methods read its current state.

| Method                        | Purpose                                                                                                                          |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `json(data, status?)`         | Set a JSON body. `status` defaults to `200`. Wraps `data` in the standard envelope.                                              |
| `exception(message, config?)` | Build an error response. `status` defaults to `500`. `config` accepts `key`, `data`, and `status`.                               |
| `notFound(message, config?)`  | Build a 404 response. `status` defaults to `404`, `key` defaults to `"NOT_FOUND"`. `config` accepts `key`, `data`, and `status`. |
| `redirect(url, status?)`      | Set a `Location` header and an empty body. `status` defaults to `302` (Found). Accepts a `string` or `URL`.                      |
| `stream(body, config?)`       | Stream a `ReadableStream`, async iterable, or producer function. `config` accepts `status` and `contentType`.                    |
| `sse(producer, config?)`      | Stream Server-Sent Events from a producer function. Sets the `text/event-stream` headers.                                        |
| `header`                      | The `IHeader` instance — set response headers, cookies, and cache directives.                                                    |
| `done`                        | A public `boolean` flag, default `false`, for tracking WebSocket response completion.                                            |
| `getData()`                   | Read the currently set data payload, or `null`.                                                                                  |
| `getStatus()`                 | Read the current status code.                                                                                                    |
| `isStream()`                  | `true` if a streaming/SSE body is set.                                                                                           |
| `get(env?)`                   | Build and return the native Web API `Response`. The framework calls this for you.                                                |

## The JSON envelope

`json()` does not send your data verbatim — it wraps it in a consistent envelope so every client reads success and error the same way. `get(env)` produces this body:

```typescript theme={null}
{
  key: string | null,        // optional machine-readable code (set via exception/notFound)
  data: Data,                // your payload, or {} when none
  message: string | null,    // human-readable message (set via exception/notFound)
  success: boolean,          // true when the status is a 2xx
  done: boolean,             // the socket completion flag
  status: number,            // the HTTP status code
  isClientError: boolean,    // true for 4xx
  isServerError: boolean,    // true for 5xx
  isNotFound: boolean,
  isUnauthorized: boolean,
  isForbidden: boolean,
  app: {
    env: "production" | "development" | ...  // the app environment
  }
}
```

The `success`, `isClientError`, and `isServerError` flags are derived from the status code automatically. A `redirect()` or `stream()`/`sse()` response skips this envelope: a redirect sends an empty body with the `Location` header, and a stream sends its raw bytes.

## Returning JSON

Return data with the default `200`, or pass a status code as the second argument — `201` when you create a resource. The controller receives `context` and returns the response object.

```typescript theme={null}
import type { ContextType } from "@ooneex/controller";
import { decorator, type IController } from "@ooneex/controller";

@decorator.controller({ path: "/users", method: "POST" })
export class CreateUserController implements IController {
  public async action(context: ContextType): Promise<IResponse> {
    const user = await createUser(/* ... */);

    // 201 Created, with the new resource in `data`
    return context.response.json({ user }, 201);
  }
}
```

A plain read returns the default `200`:

```typescript theme={null}
return context.response.json({ users });
```

## Returning errors

Use `exception()` for failures. The `message` is surfaced to the client; `config` lets you set the status, a machine-readable `key`, and extra `data`. The status defaults to `500`.

```typescript theme={null}
// 500 Internal Server Error
return context.response.exception("Something went wrong");

// 400 with a key and structured detail
return context.response.exception("Validation failed", {
  status: 400,
  key: "INVALID_EMAIL",
  data: { field: "email", reason: "invalid format" },
});
```

For richer, typed error handling, throw a domain exception and let the framework's exception layer format the response. See [exceptions](/basics/exception).

## Returning a 404

`notFound()` is a dedicated helper for missing resources. The status defaults to `404` and the `key` defaults to `"NOT_FOUND"`.

```typescript theme={null}
const user = await findUser(context.request.params.get("id"));

if (!user) {
  return context.response.notFound("User not found", {
    data: { id: context.request.params.get("id") },
  });
}

return context.response.json({ user });
```

## Redirecting

`redirect()` sets the `Location` header and an empty body. The status defaults to `302` (Found); pass another redirect code for permanent moves or other semantics.

```typescript theme={null}
// 302 Found
return context.response.redirect("/login");

// 301 Moved Permanently
return context.response.redirect("/new-path", 301);

// A URL object works too
return context.response.redirect(new URL("https://example.com/v2"));
```

## Streaming a response

`stream()` sends a body incrementally instead of buffering it. It accepts a `ReadableStream`, any async iterable of `Uint8Array` or `string` chunks, or a **producer function** that receives a writer and pushes chunks over time. `config` sets the `status` (default `200`) and `contentType` (default `application/octet-stream`). A streamed response skips the JSON envelope and sends raw bytes.

Pass an async iterable to stream values as they are produced:

```typescript theme={null}
async function* lines() {
  for await (const row of rows) {
    yield `${JSON.stringify(row)}\n`;
  }
}

return context.response.stream(lines(), {
  contentType: "application/x-ndjson",
});
```

Or pass a producer function. The writer exposes `write(chunk)`, `close()`, and a `signal` (`AbortSignal`) that aborts when the client disconnects — check it to stop work early:

```typescript theme={null}
return context.response.stream(async (writer) => {
  for (const chunk of chunks) {
    if (writer.signal.aborted) break;
    await writer.write(chunk);
  }
  writer.close();
});
```

The stream closes automatically when the producer resolves, so `writer.close()` is optional — call it to end the stream before the producer returns.

## Server-Sent Events

`sse()` streams [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) to the client. It takes a producer function and automatically sets the `text/event-stream`, `Cache-Control: no-cache`, and `Connection: keep-alive` headers. `config` accepts a `status` (default `200`).

The writer's `send()` accepts a string or an event object — `{ data, event?, id?, retry? }`. Object or array `data` is JSON-encoded; `comment()` sends a keep-alive comment line, and `signal` aborts when the client disconnects.

```typescript theme={null}
return context.response.sse(async (writer) => {
  for await (const update of updates) {
    if (writer.signal.aborted) break;

    await writer.send({
      event: "update",
      id: update.id,
      data: { value: update.value },
    });
  }

  writer.close();
});
```

A plain `send("message")` emits a bare `data:` frame, while `send({ data, event, id, retry })` sets the optional `event`, `id`, and `retry` fields. As with `stream()`, the connection closes when the producer resolves.

## Headers and cookies

`context.response.header` is the full [header](/basics/request) API. Set headers before returning — they travel with whatever body you set.

```typescript theme={null}
context.response.header.set("X-Request-Id", requestId);
context.response.header.setCacheControl("public, max-age=3600");

return context.response.json({ users });
```

Attach cookies with `setCookie(name, value, options?)`. Options cover `path`, `domain`, `expires`, `maxAge`, `secure`, `httpOnly`, and `sameSite` (`"Strict" | "Lax" | "None"`).

```typescript theme={null}
context.response.header.setCookie("session", token, {
  path: "/",
  httpOnly: true,
  secure: true,
  sameSite: "Strict",
  maxAge: 60 * 60 * 24,
});

return context.response.json({ ok: true });
```

Use `setCookies([...])` to set several at once, and `removeCookie(name, options?)` to expire one (it sends the cookie with an expired date and `Max-Age=0`).

## Best practices

* **Return the response object.** A controller's `action` returns `context.response`; the framework calls `get()` and sends the native `Response` — don't build a `Response` yourself.
* **Pick the right builder.** `json` for data, `exception` for errors, `notFound` for missing resources, `redirect` for location changes. Each clears the others, so the last call wins.
* **Set the status that fits.** `201` on create, `400`/`422` for bad input, `404` for missing — clients and the JSON envelope's `success`/error flags depend on it.
* **Add a `key` to errors.** A stable machine-readable `key` lets clients branch on error type without parsing the message.
* **Set headers before returning.** Headers and cookies on `context.response.header` are read when the response is resolved, so configure them before the final builder call.
* **Prefer typed exceptions for control flow.** For anything beyond a one-off error, throw a domain exception and let the [exception](/basics/exception) layer shape the response consistently.

## Related

* [Controllers](/basics/controller) — where you build and return the response.
* [Exceptions](/basics/exception) — typed errors that the framework turns into error responses.
* [Routing](/basics/routing) — how a request reaches the controller that produces the response.
