Przejdź do treści
Biblioteka skilli
Programowanie · Budowanie i kod

effect-http-api

/effect-http-api

Buduje typowane API HTTP w Effect: endpointy ze schematami, handlery, middleware bezpieczeństwa i dokumentację OpenAPI.

Licencja
MIT
Ostatnia zmiana repozytorium
02.11.2026

Ocena i bezpieczeństwo

91/ 100Klasa: dobry
Format25 / 25
Frontmatter z nazwą i opisem, poprawna nazwa, opis do 1024 znaków, nazwa zgodna z folderem.
Odkrywalność25 / 25
Opis mówi, kiedy użyć skilla, ma sensowną długość i jest w 3. osobie.
Treść26 / 35
Co najmniej 150 słów i do 500 linii, nagłówki, listy kroków, przykłady albo kod.
Higiena15 / 15
Bez zaślepek typu TODO, bez ścieżek Windows, z licencją.
Ryzykobrak sygnałów w skanie
Pliki w paczce
1
Skrypty
0
Komentarze HTML
3
Adresy URL w SKILL.md
11
Przestarzałość

Brak sygnałów. Repozytorium zmienione 02.11.2026, bez odwołań do wycofanych modeli i API.

Licencja

MIT, otwarta: można używać i zmieniać, z zachowaniem informacji o autorze.

Co sprawdzić przed instalacją
  • SKILL.md zawiera komentarze HTML (3). Nie widać ich w podglądzie Markdown, a model je czyta.

Co robi i kiedy

Buduje typowane API HTTP w Effect: endpointy ze schematami, handlery, middleware bezpieczeństwa i dokumentację OpenAPI.

Oryginalny opis (EN)

Build typed HTTP APIs with Effect's HttpApi — endpoints with schemas, handlers, security middleware, OpenAPI docs, derived clients, and handler unit tests. Use when building HTTP servers, REST APIs, or typed HTTP clients with Effect v4.

Jak działa

  1. Effect Source Reference
  2. Core Imports
  3. Architecture Overview
  4. Defining Endpoints
  5. Schema Annotations
  6. Groups
  7. API Definition
  8. Building Implementations
  9. Errors
  10. Security and Middleware
Sekcje z nagłówków SKILL.md, w oryginale · 1 962 linii, 8 018 słów
Podgląd SKILL.md
---
name: effect-http-api
description: Build typed HTTP APIs with Effect's HttpApi — endpoints with schemas, handlers, security middleware, OpenAPI docs, derived clients, and handler unit tests. Use when building HTTP servers, REST APIs, or typed HTTP clients with Effect v4.
---

You are an Effect TypeScript expert specializing in the HttpApi module for building schema-first HTTP APIs.

## Effect Source Reference

Use `HttpApi.ParseOptions` as the fallback for client and server codecs;
per-slot annotations override it (see below). `HttpApiBuilder.handler` defines a reusable
endpoint callback with inferred request, success, error, and service types.
Generated clients and AtomHttpApi calls accept per-call `sseOptions`.
SSE IDs may be absent: model them with `Schema.optional(Schema.String)`.
With excess-property errors, include the `event` field (default `message`) and
any `id`, including inherited IDs. `Sse.decodeSchema` / `ChannelSchema.decode`
accept parse options directly.

HTTP `QUERY` endpoints are supported. OpenAPI 3.1 represents them through
`x-oai-additionalOperations`, requiring consumer support for that extension;
the generator also accepts OpenAPI 3.2's native `query` operation.
Generated multipart binary fields use `File | Blob` and dedicated `*Multipart`
component exports. Keep generated imports aligned with the chosen transport.

The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Inspect the `effect@4.1.0` tag for this skill; main may be newer. These APIs remain `@stability unstable` and may break in minor releases despite their shorter import paths. Keep `effect` and companion packages on the same release.

Key reference files:

- `packages/effect/HTTPAPI.md` — canonical HttpApi documentation
- `packages/effect/src/http-api/*.ts` — module sources
- `packages/effect/typetest/http-api/*.tst.ts` — type-level contracts
- `packages/platform/node/test/HttpApi.test.ts` — comprehensive runtime tests
- `ai-docs/src/51_http-server/` — server walkthrough with fixtures
- `ai-docs/src/50_http-client/` — HttpClient walkthrough

## Core Imports

```ts
// HttpApi modules (definition + building + client + testing)
import {
	HttpApi,
	HttpApiBuilder,
	HttpApiClient,
	HttpApiEndpoint,
	HttpApiError,
	HttpApiGroup,
	HttpApiMiddleware,
	HttpApiScalar,
	HttpApiSchema,
	HttpApiSecurity,
	HttpApiSwagger,
	HttpApiTest,
	OpenApi
} from 'effect/http-api';

// HTTP primitives (router, server, client, multipart)
import {
	FetchHttpClient,
	HttpClient,
	HttpClientRequest,
	HttpClientResponse,
	HttpEffect,
	HttpRouter,
	HttpServer,
	HttpServerRequest,
	HttpServerResponse,
	HttpStatus,
	Multipart
} from 'effect/http';

// Platform server (Node.js — Bun has @effect/platform-bun/BunHttpServer)
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
```

## Architecture Overview

An API is built from three building blocks:

```
HttpApi
├── HttpApiGroup
│   ├── HttpApiEndpoint
│   └── HttpApiEndpoint
└── HttpApiGroup
    ├── HttpApiEndpoint
    └── HttpApiEndpoint
```

One definition powers the server, docs, and client — change it once and everything stays in sync.

**Critical design rule**: API definitions live in their own module/package, separate from server implementations, so clients can import them without pulling in server code or handler dependencies.

## Defining Endpoints

### HTTP Methods

```ts
HttpApiEndpoint.get('name', '/path', { ... });
HttpApiEndpoint.post('name', '/path', { ... });
HttpApiEndpoint.put('name', '/path', { ... });
HttpApiEndpoint.patch('name', '/path', { ... });
HttpApiEndpoint.delete('name', '/path', { ... });
HttpApiEndpoint.head('name', '/path', { ... });
HttpApiEndpoint.options('name', '/path', { ... });

HttpApiEndpoint.query('name', '/path', { ... });

// Generic factory for a supported HttpMethod
const query = HttpApiEndpoint.make('QUERY');
query('name', '/path', { ... });
```

The first argument is the endpoint name (used as the method name in the generated client). The second is the route path. The third is an options object with schemas. For methods with no body (`GET`, `HEAD`, `OPTIONS`, `TRACE`), the `payload` option is treated as a query-string-encoded record of fields. `DELETE` and `QUERY` can carry bodies; `make` accepts the closed `HttpMethod` union, not arbitrary method strings.

### Endpoint Options

```ts
HttpApiEndpoint.patch('updateUser', '/user/:id', {
	// Path parameters — parsed and validated from URL segments
	params: {
		id: Schema.FiniteFromString.check(Schema.isInt())
	},

	// Query string parameters (?key=value)
	query: {
		mode: Schema.Literals(['merge', 'replace']),
		page: Schema.optionalKey(Schema.FiniteFromString.check(Schema.isInt()))
	},

	// Request headers (always use lowercase keys — see warning below)
	headers: {
		'x-api-key': Schema.String,
		'x-request-id': Schema.String
	},

	// Request body — default encoding is JSON
	// Can be a single schema or array of schemas for content negotiation
	payload: Schema.Struct({
		name: Schema.String
	}),

	// Success response — default is 204 No Content if omitted
	// Can be a single schema or array for multiple response types
	success: User,

	// Error responses — each annotated with HTTP status code
	error: [UserNotFound, Unauthorized]
});
```

> **Important**: HTTP headers are normalized to lowercase. Always use lowercase keys in the `headers` option — `"x-api-key"`, never `"X-API-Key"`.

### Automatic Codec Wrapping & `disableCodecs`

By default, endpoint schemas are automatically wrapped with codec transformations:

- **Params, query, headers** are wrapped with `Schema.toCodecStringTree` (string ↔ typed value).
- **Payload, success, error** are wrapped with `Schema.toCodecJson` (JSON ↔ typed value) when the encoding is JSON.

This means you can pass plain `Schema.Struct.Fields` records and they "just work" — the framework handles serialization. To opt out and provide schemas that already handle their own encoding:

```ts
HttpApiEndpoint.get('raw', '/raw/:id', {
	disableCodecs: true,
	// With disableCodecs, schemas must already satisfy their transport constraints:
	// - params/query/headers must encode to string | string[] | undefined
	// - payload must encode to whatever the chosen encoding requires
	params: Schema.Struct({ id: Schema.String }),
	success: Schema.Struct({ data: Schema.String })
});
```

Use `disableCodecs: true` when your schemas already include their own transport transformations or when you need full control over decode/encode.

### Multiple Payload Schemas (Content Negotiation)

`payload` accepts an array of schemas, each declaring its own content-type via `HttpApiSchema.as*`. The framework picks the schema that matches the incoming `Content-Type`.

```ts
HttpApiEndpoint.post('create', '/items', {
	payload: [
		Schema.Struct({ a: Schema.String }), // application/json (default)
		Schema.String.pipe(HttpApiSchema.asText()), // text/plain
		Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()) // application/octet-stream
	],
	success: Item
});
```

> **Constraint**: buffered schemas sharing a content type and encoding form a
> union. Conflicting encodings for one content type raise `Multiple payload
> encodings for content-type: ...`. Only one multipart payload and one streaming
> payload are supported; a streaming schema cannot share its media type with a
> buffered schema.

### Multiple Success Schemas (Content Negotiation)

`success` works the same way:

```ts
HttpApiEndpoint.get('search', '/search', {
	payload: { search: Schema.String },
	success: [
		Schema.Array(User), // JSON (default)
		Schema.String.pipe(
			HttpApiSchema.asText({ contentType: 'text/csv' })
		)
	],
	error: [SearchQueryTooShort, HttpApiError.RequestTimeoutNoContent]
});
```

### No-Content Responses

```ts
// Default — omit success entirely → 204 No Content
HttpApiEndpoint.delete('deleteUser', '/user/:id', {
	params: { id: Schema.FiniteFromString.check(Schema.isInt()) }
});

// Explicit 204
HttpApiEndpoint.get('health', '/health', {
	success: HttpApiSchema.NoContent
});

// Other empty-body status codes
HttpApiEndpoint.post('create', '/items', {
	success: HttpApiSchema.Created // 201
});
HttpApiEndpoint.post('enqueue', '/jobs', {
	success: HttpApiSchema.Accepted // 202
});
HttpApiEndpoint.get('teapot', '/coffee', {
	success: HttpApiSchema.Empty(418) // any code
});

// asNoContent: empty body wire-side, but client decodes to a meaningful value
HttpApiEndpoint.get('me', '/me', {
	error: UserNotFound.pipe(
		HttpApiSchema.asNoContent({ decode: () => new UserNotFound() })
	)
});
```

### Catch-All Endpoint

Set the path to `"*"` for a fallback. Must be the last endpoint in the group. Not included in the OpenAPI spec.

```ts
HttpApiEndpoint.get('catchAll', '*', {
	success: Schema.String
});
```

### Prefixing

```ts
HttpApiEndpoint.get('endpointA', '/a', { success: Schema.String }).prefix(
	'/endpointPrefix'
); // → /endpointPrefix/a
```

Group- and API-level prefixing are described below.

## Schema Annotations

### Per-slot parse options

Annotate the API, group, or endpoint with `HttpApi.ParamsParseOptions`,
`QueryParseOptions`, `HeadersParseOptions`, `PayloadParseOptions`,
`SuccessParseOptions`, or `ErrorParseOptions`. Each controls both sides of that
codec: server decoding/client encoding for inputs, the reverse for outputs.
Success options cover streamed/SSE bodies too; header options cover request
headers and `WithHeaders` response headers independently of their bodies.

For the same annotation, endpoint overrides group, which overrides API. A slot
annotation at **any** level wins over fallback `ParseOptions` at **every** level.
Objects replace rather than merge. Annotate before constructing handler groups.
Real HTTP headers include transport fields: strict fallback parsing otherwise
rejects them, while `onExcessProperty: 'preserve'` retains them in decoded values.
Use an explicit empty header override to retain Schema defaults:

<!-- typecheck -->
```ts
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/http-api';
import * as Schema from 'effect/Schema';

class CreateUser extends Schema.Class<CreateUser>('CreateUser')({
	name: Schema.String
}) {}

const Api = HttpApi.make('StrictApi').add(
	HttpApiGroup.make('users').add(
		HttpApiEndpoint.post('create', '/users', {
			headers: { 'x-api-key': Schema.String },
			payload: CreateUser
		})
	)
).annotate(HttpApi.ParseOptions, { onExcessProperty: 'error' })
	.annotate(HttpApi.HeadersParseOptions, {});
```

### Status Codes

```ts
// Numeric form
Schema.Array(User).pipe(HttpApiSchema.status(206));
Schema.Struct({ message: Schema.String }).pipe(HttpApiSchema.status(404));

// Named-literal form (preferred for readability)
Schema.String.pipe(HttpApiSchema.status('PartialContent')); // 206
Schema.Void.pipe(HttpApiSchema.status('Forbidden')); // 403
SomeError.pipe(HttpApiSchema.status('UnprocessableEntity')); // 422
RateLimitError.pipe(HttpApiSchema.status('TooManyRequests')); // 429

// Or annotate an error class inline at definition
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
	'UserNotFound',
	{},
	{ httpApiStatus: 404 }
) {}
```

`HttpApiSchema.StatusLiteral` is the exported keyof type for the literal form. The full set covers the standard codes (`Continue`, `OK`, `Created`, `Accepted`, `NoContent`, `MovedPermanently`, `Found`, `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `MethodNotAllowed`, `NotAcceptable`, `RequestTimeout`, `Conflict`, `Gone`, `UnprocessableEntity`, `TooManyRequests`, `InternalServerError`, `NotImplemented`, `BadGateway`, `ServiceUnavailable`, `GatewayTimeout`, etc.). Unannotated success schemas default to 200, and unannotated error schemas default to 500. If you omit `success`, the endpoint defaults to `HttpApiSchema.NoContent` (204). `success: Schema.Void` is an empty 200 response unless you annotate it or use `HttpApiSchema.NoContent`.

The literal mapping is centralized in `HttpStatus` from `effect/http`. Use `HttpStatus.fromLiteral` when plain HTTP code needs the corresponding numeric literal type; `HttpApiSchema.status` uses the same mapping internally:

```ts
HttpStatus.fromLiteral('OK'); // 200
HttpStatus.fromLiteral('Conflict'); // 409
```

### Typed Response Headers

Wrap a success schema with `HttpApiSchema.WithHeaders` when response headers are part of the endpoint contract. Handlers return `HttpApiSchema.withHeaders(...)`; generated clients decode the same body-and-headers value, `HttpApiTest` preserves it, streaming bodies remain streams, and OpenAPI documents the headers.

```ts
const ListUsersSuccess = HttpApiSchema.WithHeaders(Schema.Array(User), {
	'x-total-count': Schema.FiniteFromString
});

const ListUsers = HttpApiEndpoint.get('list', '/users', {
	success: ListUsersSuccess
});

handlers.handle('list', () =>
	Effect.succeed(
		HttpApiSchema.withHeaders({
			body: users,
			headers: { 'x-total-count': users.length }
		})
	)
);

const result = yield* client.users.list();
result.body;
result.headers['x-total-count']; // number
```

The headers argument accepts either struct fields or a schema. Endpoint codec insertion applies `Schema.toCodecStringTree` to headers unless `disableCodecs` is enabled. Do not nest `WithHeaders`, and do not declare two responses with the same status and content type when one carries headers.

For error classes or other opaque domain values, use `HttpApiSchema.encodeToWithHeaders`: its encoded side is `{ body, headers }`, while handlers continue failing with the domain error value. Prefer structural `WithHeaders` for success values and streams.

### Empty Schemas

```ts
HttpApiSchema.NoContent; // Schema.Void with status 204
HttpApiSchema.Created; // Schema.Void with status 201
HttpApiSchema.Accepted; // Schema.Void with status 202
HttpApiSchema.Empty(418); // Schema.Void with arbitrary status

// Decode an empty wire response into a meaningful value (client side)
SomeSchema.pipe(HttpApiSchema.asNoContent({ decode: () => myValue }));
```

### Encodings

```ts
HttpApiSchema.asJson(); // application/json (default)
HttpApiSchema.asJson({ contentType: 'application/scim+json' });
HttpApiSchema.asText(); // text/plain (encoded type must be string)
HttpApiSchema.asText({ contentType: 'text/csv' });
HttpApiSchema.asFormUrlEncoded(); // application/x-www-form-urlencoded (encoded type must be string record)
HttpApiSchema.asUint8Array(); // application/octet-stream (encoded type must be Uint8Array)
HttpApiSchema.asUint8Array({ contentType: 'image/png' });
HttpApiSchema.asMultipart(); // multipart/form-data, buffered (request only)
HttpApiSchema.asMultipart({ maxParts: 100, maxFileSize: 10_000_000 });
HttpApiSchema.asMultipartStream(); // multipart/form-data, streaming (request only)
HttpApiSchema.asMultipartStream({ maxFileSize: 50_000_000 });
```

The multipart limits options are typed as `Multipart.withLimits.Options`. Multipart is **payload-only**; using it on a success/error schema throws.

### Multipart File Uploads

```ts
HttpApiEndpoint.post('upload', '/upload', {
	payload: Schema.Struct({
		files: Multipart.FilesSchema, // multiple files persisted to disk
		caption: Schema.String
	}).pipe(HttpApiSchema.asMultipart()),
	success: Schema.String
});

// For exactly one file
HttpApiEndpoint.post('avatar', '/avatar', {
	payload: Schema.Struct({
		file: Multipart.SingleFileSchema
	}).pipe(HttpApiSchema.asMultipart()),
	success: Schema.String
});

// Streaming variant — handler receives a Stream<Multipart.Part>
HttpApiEndpoint.post('uploadStream', '/upload/stream', {
	payload: Schema.Struct({ file: Multipart.SingleFileSchema }).pipe(
		HttpApiSchema.asMultipartStream()
	),
	success: Schema.String
});
```

### Streaming Bodies

`HttpApiSchema.StreamUint8Array({ contentType?, maxBytes? })` works in both
`payload` and `success`; unlike buffered `asUint8Array`, it passes a stream.
`StreamNdjson({ schema, contentType?, maxBytes?, maxElementBytes? })` does the same
for schema-decoded elements. Generated clients accept request streams without
collecting them, and infer their service requirements; `HttpApiTest`
preserves streams too. Handler body consumption is lazy, so consume the payload
inside the request lifetime and handle stream failures there.

Streaming payloads require a body-capable method. `StreamSse` remains response-only.

<!-- typecheck -->
```ts
import { HttpApiEndpoint, HttpApiSchema } from 'effect/http-api';
import * as Schema from 'effect/Schema';

class ImportEvent extends Schema.Class<ImportEvent>('ImportEvent')({
	id: Schema.String
}) {}

const importEvents = HttpApiEndpoint.post('importEvents', '/events', {
	payload: HttpApiSchema.StreamNdjson({
		schema: ImportEvent,
		maxBytes: '100 MiB',
		maxElementBytes: '1 MiB'
	}),
	success: Schema.Number
});

const download = HttpApiEndpoint.get('download', '/archive', {
	success: HttpApiSchema.StreamUint8Array()
});
```

NDJSON defaults to `application/x-ndjson` and a 1 MiB line limit. It accepts LF,
CRLF, bare CR, a leading BOM, and a final line without a newline; empty lines are
skipped, but malformed JSON, invalid UTF-8, or a failed element decode ends the
stream. `maxBytes` has **no default** and does not fall back to `MaxBodySize`.
`maxBytes` only applies to payloads; NDJSON line limits also govern
decoding streamed responses. OpenAPI emits `x-effect-stream` metadata.

Request parse failures become `HttpApiSchemaError` (`kind: 'Payload'`, 400).
Limits become `HttpApiPayloadTooLarge` (413), with `limit: 'maxBytes' |
'maxElementBytes'` and `size` holding the configured `ByteSize`, not bytes read.
An excessive `Content-Length` can be rejected before invoking the handler;
otherwise enforcement happens while reading. A response-stream failure aborts
the body rather than producing a clean end or a new HTTP error response. NDJSON
has no typed mid-stream error schema; use response-only `StreamSse` for that.

Streaming schemas can declare multiple content types. For response-only client
mode, `HttpApiClient.decodeResponse` decodes a returned response using the
endpoint contract when decoding is needed later.

### Raw HTTP/1.1 Upgrade Success

`success: HttpApiSchema.Upgrade({ protocol: 'tcp' })` declares a 101 response.
It can coexist with ordinary success schemas when a server declines the upgrade.
Buffered payloads are allowed; streaming payloads and `websocket` are rejected
(use `HttpServerRequest.upgrade` / `Socket.makeWebSocket` for WebSockets).
Declare typed 101 headers with `Upgrade({ protocol, headers })`. The handler
receives `upgrade()` / `upgrade({ headers })`, returning the socket. A request
not asking for the protocol gets 426 before payload decoding. Generated clients
send upgrade headers and return a branded `{ headers, socket }`; narrow mixed
results with `HttpApiClient.isUpgraded`. Client mocks must include
`[HttpApiClient.UpgradedTypeId]: HttpApiClient.UpgradedTypeId`. OpenAPI documents 101.
Custom `HttpServerRequest` services need `upgradeWith({ headers })`; WebSocket
upgrades reject supplied headers. Node 25 and older support Content-Length raw
upgrade payloads, while chunked ones receive 411. Use Node's raw HTTP transport:
Fetch/XHR reject these requests before sending, and Bun/Deno servers only
support WebSocket upgrades.

### RFC 9457 Problem Details

Problem Details is opt-in; existing error responses retain their defaults.
`ProblemDetails` from `effect/http` supplies the lenient codec, response
constructor, and `application/problem+json` media type. Use
`HttpApiSchema.asProblem` for built-in errors. Apply
`HttpApiMiddleware.ProblemDetails` and provide `layerProblemDetails()` to render
request decoding failures as 400 problems and response encoding failures as 500
problems; `includeDetails` adds per-field request errors.

<!-- typecheck -->
```ts
import { Effect, Schema } from 'effect';
import { HttpApiEndpoint, HttpApiError } from 'effect/http-api';

class OutOfCredit extends HttpApiError.Problem<OutOfCredit>()('OutOfCredit', {
  type: 'https://example.com/problems/out-of-credit',
  status: 403,
  title: 'Insufficient credit'
}, { balance: Schema.Number }) {}

const buy = HttpApiEndpoint.post('buy', '/buy', { error: OutOfCredit });
const failing = Effect.fail(new OutOfCredit({ balance: 30 }));
```

Problem classes discriminate on wire `type`, while `Effect.catchTag` still uses
the class tag. Extension fields are validated; unknown extensions are dropped.

## Groups

Groups organize related endpoints and apply shared middleware, prefixes, and annotations.

```ts
export class UsersApiGroup extends HttpApiGroup.make('users')
	.add(
		HttpApiEndpoint.get('list', '/', {
			success: Schema.Array(User)
		}),
		HttpApiEndpoint.get('getById', '/:id', {
			params: {
				id: Schema.FiniteFromString.pipe(Schema.decodeTo(UserId))
			},
			success: User,
			error: UserNotFound
		}),
		HttpApiEndpoint.post('create', '/', {
			payload: Schema.Struct({
				name: Schema.String,
				email: Schema.String
			}),
			success: User
		})
	)
	.middleware(Authorization)
	.prefix('/users')
	.annotateMerge(
		OpenApi.annotations({
			title: 'Users',
			description: 'User management endpoints'
		})
	) {}
```

### Top-Level Groups

```ts
export class SystemApi extends HttpApiGroup.make('system', {
	topLevel: true
}).add(
	HttpApiEndpoint.get('health', '/health', {
		success: HttpApiSchema.NoContent
	})
) {}
```

A top-level group exposes its endpoints at the **root** of the generated client (`client.health()` instead of `client.system.health()`) and at the root of the URL builder. The OpenAPI `operationId` also drops the group prefix.

### Group-Level Annotations

```ts
HttpApiGroup.make('users')
	.annotate(OpenApi.Description, 'User endpoints')
	.annotate(OpenApi.Title, 'Users') // renames the OpenAPI tag
	.annotate(OpenApi.ExternalDocs, { url: 'https://docs.example.com' })
	.annotate(OpenApi.Exclude, true); // hide entire group from OpenAPI

// Bulk-apply an annotation to every endpoint currently in the group:
HttpApiGroup.make('users')
	.add(/* endpoints */)
	.annotateEndpoints(OpenApi.Deprecated, true)
	.annotateEndpointsMerge(OpenApi.annotations({ deprecated: true }));
```

> **Caveat**: group middleware (`.middleware(M)`) and endpoint-level annotations (`.annotateEndpoints(...)`) only apply to endpoints **already added** at the time of the call. Endpoints added after `.middleware(M)` will not have `M` attached. Order your `.add(...).middleware(M)` calls accordingly.

## API Definition

```ts
export class Api extends HttpApi.make('my-api')
	.add(UsersApiGroup)
	.add(SystemApi)
	.annotateMerge(
		OpenApi.annotations({
			title: 'My API',
			description: 'My API description',
			version: '1.0.0'
		})
	) {}
```

### Composing APIs

```ts
// Add another HttpApi's groups into this one
class V0 extends HttpApi.make('v0').add(LegacyGroup) {}

class Api extends HttpApi.make('api').add(NewGroup).addHttpApi(V0) {}
```

`addHttpApi` merges the other API's groups (and propagates the donor API's annotations into them).

### API-Level Prefixing and Middleware

```ts
const Api = HttpApi.make('MyApi')
	.add(
		HttpApiGroup.make('group')
			.add(
				HttpApiEndpoint.get('endpointA', '/a', {
					success: Schema.String
				}).prefix('/endpointPrefix') // /apiPrefix/groupPrefix/endpointPrefix/a
			)
			.prefix('/groupPrefix')
	)
	.middleware(Authorization) // applies to all groups already added
	.prefix('/apiPrefix');
```

> Same caveat as for groups: API-level middleware only applies to groups already added at the time of `.middleware(...)`.

### Reading and Reflecting on an API

```ts
HttpApi.isHttpApi(value); // type guard
HttpApiGroup.isHttpApiGroup(value);
HttpApiEndpoint.isHttpApiEndpoint(value);

// Walk the API tree (used internally by OpenApi.fromApi and HttpApiClient)
HttpApi.reflect(api, {
	onGroup({ group, mergedAnnotations }) {
		/* ... */
	},
	onEndpoint({
		group,
		endpoint,
		middleware,
		successes,
		errors,
		mergedAnnotations
	}) {
		/* ... */
	},
	predicate: ({ endpoint, group }) => true
});
```

## Building Implementations

### Handler Groups

`HttpApiBuilder.group(api, groupName, build)` returns a `Layer` that implements every endpoint in the named group. The `build` callback can be either a synchronous `handlers => ...` function or an `Effect` returning the populated `Handlers` (use `Effect.fn` so you can `yield*` services).

```ts
const UsersApiHandlers = HttpApiBuilder.group(
	Api,
	'users',
	Effect.fn(function* (handlers) {
		const users = yield* Users;

		return handlers
			.handle('list', ({ query }) =>
				users.list(query.search).pipe(Effect.orDie)
			)
			.handle('getById', ({ params }) =>
				users.getById(params.id).pipe(
					Effect.catchReasons(
						'UsersError',
						{ UserNotFound: (e) => Effect.fail(e) },
						Effect.die
					)
				)
			)
			.handle('create', ({ payload }) =>
				users.create(payload).pipe(Effect.orDie)
			)
			.handle('me', () => CurrentUser);
	})
).pipe(Layer.provide([Users.layer, AuthorizationLayer]));
```

The framework checks at the type level that every endpoint in the group is handled — `ValidateReturn` produces an `"Endpoint not handled: <name>"` string-typed error otherwise.

### Handler Context

Each handler receives a typed context object:

```ts
handlers.handle('updateUser', (ctx) => {
	ctx.params; // typed path parameters
	ctx.query; // typed query parameters
	ctx.headers; // typed request headers
	ctx.payload; // typed request body
	ctx.request; // raw HttpServerRequest (method, url, cookies, raw headers)
	ctx.endpoint; // the HttpApiEndpoint definition (rare; useful in shared helpers)
	ctx.group; // the HttpApiGroup definition
	return Effect.succeed(/* ... */);
});
```

### Returning a Raw `HttpServerResponse`

A handler may return either the typed success value (which the framework encodes per the `success` schema) **or** an `HttpServerResponse` directly. The framework checks `HttpServerResponse.isHttpServerResponse(value)` and skips success-encoding when true. Use this for redirects, manual streaming, custom status codes outside the schema, etc.

```ts
handlers.handle('legacyRedirect', () =>
	Effect.succeed(HttpServerResponse.redirect('/new', { status: 302 }))
);
```

### `handleRaw` — Skipping Payload Decoding

`handlers.handleRaw(name, handler)` opts out of automatic payload decoding. The handler receives the same typed `params/query/headers/request/endpoint/group` but no decoded `payload` — read the body directly from `ctx.request`. Useful for endpoints that need streaming, custom parsing, or pass-through proxying.

```ts
handlers.handleRaw(
	'proxy',
	Effect.fn(function* ({ params, request }) {
		const body = (yield* Effect.orDie(request.json)) as { name: string };
		return HttpServerResponse.jsonUnsafe({
			id: params.id,
			name: body.name
		});
	})
);
```

### Uninterruptible Handlers

Both `handle` and `handleRaw` accept a third options object:

```ts
handlers.handle('charge', payHandler, { uninterruptible: true });
```

Use sparingly — only when a handler must not be cancelled mid-flight (e.g., once a transaction has been initiated downstream).

### Standalone Endpoint Handler

Sometimes you want to mount a single HttpApi endpoint inside an existing `HttpRouter` without going through `HttpApiBuilder.layer`. Use `HttpApiBuilder.endpoint`:

```ts
const helloHandler = yield* HttpApiBuilder.endpoint(
	Api,
	'greetings',
	'hello',
	() => Effect.succeed('Hi!')
);
// helloHandler: Effect<HttpServerResponse, ..., HttpServerRequest | RouteContext | ParsedSearchParams | ...>

yield* router.add('GET', '/api/hello', helloHandler);
```

### Building the Server Layer

`HttpApiBuilder.layer(api, options?)` produces the routes-into-router layer. `options.openapiPath` exposes the raw OpenAPI JSON at the given path.

```ts
const ApiRoutes = HttpApiBuilder.layer(Api, {
	openapiPath: '/openapi.json'
}).pipe(Layer.provide([UsersApiHandlers, SystemApiHandlers]));

const DocsRoute = HttpApiScalar.layer(Api, { path: '/docs' });

const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute);
```

If you forget to provide a group's handler layer you'll get a clear runtime defect:

```
HttpApiGroup "users" not found (key: "effect/http-api/HttpApiGroup/users").
Did you forget to provide HttpApiBuilder.group(api, "users", ...)?
Available groups: <list>
```

Missing middleware layers fail with `Service not found: <middleware key>`.

### Serving the API

```ts
// Option 1: Node.js HTTP server
export const HttpServerLayer = HttpRouter.serve(AllRoutes, {
	disableLogger: false, // default; set true to skip the request logger
	disableListenLog: false // default; set true to skip the "Listening on" log
}).pipe(Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })));

Layer.launch(HttpServerLayer).pipe(NodeRuntime.runMain);

// Option 2: Web handler for serverless / edge / custom HTTP frame
export const { handler, dispose } = HttpRouter.toWebHandler(
	Layer.mergeAll(AllRoutes.pipe(Layer.provide(HttpServer.layerServices)))
);
// handler: (request: Request, ctx?: Context.Context) => Promise<Response>
// dispose: () => Promise<void>  — call on shutdown
```

`HttpServer.layerServices` is a generic/test helper that includes a no-op `FileSystem`. Use it only when your routes do not need real filesystem access, file responses, persisted multipart files, or static serving. For Node/Bun HTTP servers with real platform behavior, prefer concrete layers such as `NodeHttpServer.layer(...)` / `BunHttpServer.layer(...)` or their `layerHttpServices` variants where applicable.

`HttpRouter.serve` and `HttpRouter.toWebHandler` both also accept `routerConfig` (passed to find-my-way) and `middleware` (a wrap function applied to the entire HTTP server pipeline).

`HttpRouter.serve` and `toHttpEffect` build with a fresh router in a forked layer
memo map. Pass registration layers directly to them; do not pre-provide
`HttpRouter.layer` to `ApiRoutes`. Services first built inside these entrypoints
are private; provide services shared with siblings outside them. `toWebHandler`
owns a separate build by default, but an explicit `memoMap` is used as supplied.

> There is no `HttpApiBuilder.toWebHandler` — always go through `HttpRouter.toWebHandler` (or `HttpRouter.serve` for a long-running server).

## Errors

### Custom Errors

Define errors with `Schema.TaggedError` and either pipe through `HttpApiSchema.status` or set `httpApiStatus` in the class options:

```ts
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
	'UserNotFound',
	{ message: Schema.String },
	{ httpApiStatus: 404 }
) {}

class Unauthorized extends Schema.TaggedError<Unauthorized>()(
	'Unauthorized',
	{ message: Schema.String },
	{ httpApiStatus: 401 }
) {}

// Or use status() on a struct schema (no class):
const NotFound = Schema.Struct({
	_tag: Schema.tag('NotFound'),
	message: Schema.String
}).pipe(HttpApiSchema.status('NotFound')); // 404
```

### Predefined Error Types

`HttpApiError` provides ready-made error classes for common HTTP status codes. They are full `Schema.Error` instances and also implement `HttpServerRespondable`, so they can be returned directly from plain `HttpRouter` handlers (outside HttpApi) and produce the right status response without further configuration.

| Class                 | Status | NoContent variant              |
| --------------------- | ------ | ------------------------------ |
| `BadRequest`          | 400    | `BadRequestNoContent`          |
| `Unauthorized`        | 401    | `UnauthorizedNoContent`        |
| `Forbidden`           | 403    | `ForbiddenNoContent`           |
| `NotFound`            | 404    | `NotFoundNoContent`            |
| `MethodNotAllowed`    | 405    | `MethodNotAllowedNoContent`    |
| `NotAcceptable`       | 406    | `NotAcceptableNoContent`       |
| `RequestTimeout`      | 408    | `RequestTimeoutNoContent`      |
| `Conflict`            | 409    | `ConflictNoContent`            |
| `Gone`                | 410    | `GoneNoContent`                |
| `InternalServerError` | 500    | `InternalServerErrorNoContent` |
| `NotImplemented`      | 501    | `NotImplementedNoContent`      |
| `ServiceUnavailable`  | 503    | `ServiceUnavailableNoContent`  |

Usage:

```ts
HttpApiEndpoint.get('getUser', '/user/:id', {
	params: { id: Schema.FiniteFromString.check(Schema.isInt()) },
	success: User,
	error: [HttpApiError.NotFound, HttpApiError.UnauthorizedNoContent]
});

handlers.handle('getUser', ({ params }) =>
	params.id === 1
		? Effect.fail(new HttpApiError.NotFound({}))
		: Effect.succeed(/* user */)
);
```

### Schema Validation Errors

When a request fails decoding or a response fails encoding, the framework wraps the underlying `Schema.SchemaError` in `HttpApiError.HttpApiSchemaError`:

```ts
{
  _tag: "HttpApiSchemaError",
  kind: "Params" | "Headers" | "Query" | "Payload" | "Body" | "ResponseHeaders",
  cause: Schema.SchemaError
}
```

By default these are defects interpreted at the HTTP boundary. Request kinds
`Params` / `Headers` / `Query` / `Payload` render empty **400** responses and are
ignored by error reporters. Response kinds `Body` / `ResponseHeaders` render
empty **500** responses and **are reported**: a server encoding bug is not bad
client input. Both remain available to schema-error transform middleware.
To surface details or choose a different status, install
`HttpApiMiddleware.layerSchemaErrorTransform`; avoid mapping response encoding
failures to user-validation errors:

```ts
class ValidationError extends Schema.TaggedError<ValidationError>()(
	'ValidationError',
	{ message: Schema.String, kind: Schema.String }
) {}

class SchemaErrorHandler extends HttpApiMiddleware.Service<SchemaErrorHandler>()(
	'api/SchemaErrorHandler',
	{
		error: ValidationError.pipe(HttpApiSchema.status('UnprocessableEntity'))
	}
) {}

const SchemaErrorHandlerLive = HttpApiMiddleware.layerSchemaErrorTransform(
	SchemaErrorHandler,
	(schemaError, { endpoint }) =>
		schemaError.kind === 'Body' || schemaError.kind === 'ResponseHeaders'
			? Effect.die(schemaError) // preserve reported server encoding defects
			: Effect.fail(
				new ValidationError({
					kind: schemaError.kind,
					message: `Invalid ${schemaError.kind} for ${endpoint.name}: ${String(schemaError.cause)}`
				})
			)
);

// Attach to an endpoint, group, or the entire API
const Api = HttpApi.make('api')
	.add(/* ... */)
	.middleware(SchemaErrorHandler);

const Live = HttpApiBuilder.layer(Api).pipe(
	Layer.provide(GroupHandlers),
	Layer.provide(SchemaErrorHandlerLive)
);
```

You can detect this error type explicitly with `HttpApiError.HttpApiSchemaError.is(value)` and wrap a `Schema.SchemaError`-failing effect with `HttpApiError.HttpApiSchemaError.wrap(kind, effect)`. `SchemaError` is no longer a standalone root module; use `Schema.SchemaError` and `Schema.isSchemaError`.

A declared error schema takes precedence over an error's `HttpServerRespondable`
implementation. If declared-error encoding fails, an otherwise respondable error
can still render itself instead of being replaced unconditionally by a 500.

Oversized multipart streaming uploads likewise render 413 rather than 500.

## Security and Middleware

### Security Schemes

```ts
HttpApiSecurity.http({ scheme: 'Digest' }); // Authorization: Digest ...
HttpApiSecurity.bearer; // predefined HTTP Bearer auth
HttpApiSecurity.basic; // HTTP Basic auth
HttpApiSecurity.apiKey({
	in: 'header', // "header" | "query" | "cookie"  (default: "header")
	key: 'x-api-key'
});
```

`HttpApiSecurity.bearer` is the predefined HTTP `Bearer` scheme; use `HttpApiSecurity.http({ scheme })` for custom `Authorization: <scheme> ...` schemes such as Digest. Middleware should validate the expected `Authorization` scheme/prefix itself when it matters: `HttpApiBuilder.securityDecode` currently slices by scheme length, and security schemes declare credential shape rather than authenticating.

You can attach metadata to a security scheme:

```ts
HttpApiSecurity.bearer.pipe(
	HttpApiSecurity.annotate(OpenApi.Description, 'Project-scoped token'),
	HttpApiSecurity.annotate(OpenApi.Format, 'JWT') // becomes bearerFormat in spec
);

const digestAuth = HttpApiSecurity.http({ scheme: 'Digest' }).pipe(
	HttpApiSecurity.annotate(OpenApi.Description, 'Digest token'),
	HttpApiSecurity.annotate(OpenApi.Format, 'DigestToken')
);
```

### Defining Middleware (Service)

```ts
class CurrentUser extends Context.Service<CurrentUser, User>()('CurrentUser') {}

class Unauthorized extends Schema.TaggedError<Unauthorized>()(
	'Unauthorized',
	{ message: Schema.String },
	{ httpApiStatus: 401 }
) {}

class Authorization extends HttpApiMiddleware.Service<
	Authorization,
	{
		// Services this middleware injects into the rest of the stack
		provides: CurrentUser;
		// Services this middleware itself depends on
		requires: never;
		// Optional: typed errors the *client* implementation may produce
		clientError: never;
	}
>()('Authorization', {
	// Force clients to provide a matching client middleware (see below)
	requiredForClient: true,
	// Security schemes — keys here become the keys of the security handler record
	security: {
		bearer: HttpApiSecurity.bearer
	},
	// Errors this middleware may raise — single schema or array of schemas
	error: Unauthorized
}) {}
```

`HttpApiMiddleware.Service<Self, Config>()` returns a class. The optional second argument controls type-level facets:

| Field               | Meaning                                                                              |
| ------------------- | ------------------------------------------------------------------------------------ |
| `provides`          | Services that the middleware adds to the handler context                             |
| `requires`          | Services that the middleware depends on                                              |
| `clientError`       | Typed error that the *client-side* counterpart may fail with (when `requiredForClient: true`) |

Class options (second positional arg):

| Field                | Meaning                                                                       |
| -------------------- | ----------------------------------------------------------------------------- |
| `error`              | One schema or an array of schemas the middleware may produce as failures      |
| `security`           | A record of named `HttpApiSecurity` schemes (defines security middleware)     |
| `requiredForClient`  | If `true`, generated clients require a matching `layerClient` to be provided  |

### Implementing Server-Side Security Middleware

Implement the middleware as a `Layer`. For each entry in `security: { ... }`, return a handler `(httpEffect, options) => Effect<HttpServerResponse, ...>` that decodes the credential and provides the resulting service.

```ts
const AuthorizationLayer = Layer.effect(
	Authorization,
	Effect.gen(function* () {
		yield* Effect.logInfo('Starting Authorization middleware');

		return Authorization.of({
			bearer: Effect.fn(function* (httpEffect, options) {
				// options.credential — the decoded credential (Redacted for bearer/apiKey, Credentials for basic)
				// options.endpoint  — the endpoint being invoked
				// options.group     — the group being invoked
				const token = Redacted.value(options.credential);
				if (token !== 'valid-token') {
					return yield* new Unauthorized({
						message: 'Invalid token'
					});
				}
				return yield* Effect.provideService(
					httpEffect,
					CurrentUser,
					new User({
						id: UserId.make(1),
						name: 'Dev User',
						email: 'dev@acme.com'
					})
				);
			})
		});
	})
);
```

When the middleware declares multiple `security` entries, the framework tries each in order; the first one whose handler succeeds wins.

For one-line handlers, `Layer.succeed` is convenient:

```ts
const AuthLive = Layer.succeed(Authorization)({
	bearer: (effect, opts) =>
		Effect.provideService(effect, CurrentUser, new User(/* ... */))
});
```

### Plain (Non-Security) Middleware

When the middleware has no `security`, the layer's value is a single function:

```ts
class Logger extends HttpApiMiddleware.Service<Logger>()('Http/Logger', {
	error: Schema.String.pipe(
		HttpApiSchema.status('MethodNotAllowed'),
		HttpApiSchema.asText()
	)
}) {}

const LoggerLive = Layer.effect(
	Logger,
	Effect.gen(function* () {
		yield* Effect.logInfo('creating Logger middleware');
		return (httpEffect, { endpoint, group }) =>
			Effect.gen(function* () {
				const request = yield* HttpServerRequest.HttpServerRequest;
				yield* Effect.logInfo(
					`Request: ${request.method} ${request.url} → ${group.identifier}.${endpoint.name}`
				);
				return yield* httpEffect;
			});
	})
);
```

### Schema-Error Transform Middleware

See the "Schema Validation Errors" section above. `HttpApiMiddleware.layerSchemaErrorTransform` is the canonical primitive for replacing the default empty-400 with a typed validation error.

### Applying Middleware

```ts
// To a single endpoint
HttpApiEndpoint.get('me', '/me', { success: User }).middleware(Authorization);

// To an entire group (only endpoints already added)
HttpApiGroup.make('users').add(/* ... */).middleware(Authorization);

// To the entire API (only groups already added)
HttpApi.make('api').add(/* ... */).middleware(Authorization);
```

### Middleware Ordering (LIFO)

Multiple middlewares chained on the same endpoint run in **last-in, first-out** order. Given `.middleware(M1).middleware(M2)`, the runtime order is:

```
M2-before → M1-before → handler → M1-after → M2-after
```

The same applies to client-side middleware. Be deliberate about the order if any middleware reads or mutates request/response state from another.

### Cookie-Based Security and `securitySetCookie`

```ts
const sessionCookie = HttpApiSecurity.apiKey({ in: 'cookie', key: 'session' });

class Auth extends HttpApiMiddleware.Service<Auth, { provides: CurrentUser }>()(
	'Auth',
	{
		error: Schema.String.annotate({ httpApiStatus: 401 }),
		security: { session: sessionCookie }
	}
) {}

// Setting a security cookie in a login handler:
handlers.handle('login', () =>
	HttpApiBuilder.securitySetCookie(
		sessionCookie,
		Redacted.make('secret-session-id')
	)
);
// Defaults: HttpOnly + Secure. Override via the third options argument.
```

For testing or custom decoding outside HttpApi, `HttpApiBuilder.securityDecode(security)` returns `Effect<credential, never, HttpServerRequest | ParsedSearchParams>`.

### Reading Cookies Directly (No Validation, No OpenAPI)

```ts
handlers.handle('me', (ctx) => {
	const lang = ctx.request.cookies['lang'] ?? 'en';
	return Effect.succeed(`Language: ${lang}`);
});
```

These cookies don't appear in the OpenAPI spec and aren't validated. For typed/spec-visible cookies, use a `HttpApiSecurity.apiKey({ in: "cookie", ... })` middleware.

## Clients

### `HttpApiClient.make` — Service-Based Client

```ts
const program = Effect.gen(function* () {
	const client = yield* HttpApiClient.make(Api, {
		baseUrl: 'http://localhost:3000'
	});

	// Methods are grouped: client.GroupName.endpointName(...)
	const users = yield* client.users.list();
	const user = yield* client.users.getById({ params: { id: 1 } });

	// Top-level groups: client.endpointName(...)
	yield* client.health();
});

program.pipe(Effect.provide(FetchHttpClient.layer), Effect.runFork);
```

`make` reads `HttpClient.HttpClient` from context. Provide a platform layer such as `FetchHttpClient.layer`, `BunHttpClient.layer`, Node's `NodeHttpClient.{layerFetch, layerUndici, layerNodeHttp}`, or Browser's `BrowserHttpClient.{layerFetch, layerXMLHttpRequest}`.

`make` accepts a `transformClient` option to wrap the underlying `HttpClient` (e.g., to set a base URL, attach default headers, enable retries). It also accepts `transformResponse` and `baseUrl`.

### `HttpApiClient.makeWith` — Bring Your Own `HttpClient`

```ts
const httpClient = (yield* HttpClient.HttpClient).pipe(
	HttpClient.tapRequest(/* tracing, logging, ... */)
);
const client = yield* HttpApiClient.makeWith(Api, {
	httpClient,
	baseUrl: 'http://localhost:3000'
});
```

### `HttpApiClient.group` and `HttpApiClient.endpoint` — Narrow Clients

```ts
// One group's endpoints, flat (no `client.users.` prefix):
const usersClient = yield* HttpApiClient.group(Api, {
	group: 'users',
	httpClient: yield* HttpClient.HttpClient
});
yield* usersClient.list();

// A single endpoint as a callable function:
const getUser = yield* HttpApiClient.endpoint(Api, {
	group: 'users',
	endpoint: 'getById',
	httpClient: yield* HttpClient.HttpClient
});
yield* getUser({ params: { id: 1 } });
```

### Wrapping the Client in a Service (Recommended Pattern)

```ts
class ApiClient extends Context.Service<
	ApiClient,
	HttpApiClient.ForApi<typeof Api>
>()('app/ApiClient') {
	static readonly layer = Layer.effect(
		ApiClient,
		HttpApiClient.make(Api, {
			transformClient: (client) =>
				client.pipe(
					HttpClient.mapRequest(
						flow(
							HttpClientRequest.prependUrl(
								'http://localhost:3000'
							)
						)
					),
					HttpClient.retryTransient({
						schedule: Schedule.exponential(Duration.millis(100)),
						times: 3
					})
				)
		})
	).pipe(
		Layer.provide(AuthorizationClient), // required client middleware (see below)
		Layer.provide(FetchHttpClient.layer)
	);
}
```

### Response Modes

Each generated client method accepts an optional `responseMode`:

| Mode                       | Return type                                  | Errors include `Schema.SchemaError` + endpoint errors? |
| -------------------------- | -------------------------------------------- | ------------------------------------------------ |
| `"decoded-only"` (default) | `Success`                                    | yes                                              |
| `"decoded-and-response"`   | `[Success, HttpClientResponse]` tuple        | yes                                              |
| `"response-only"`          | `HttpClientResponse` (no decoding performed) | no — only `HttpClientError` and middleware errors |

```ts
const client = yield* HttpApiClient.make(Api, { baseUrl });

// Default: just the decoded value
const user = yield* client.users.getById({ params: { id: 1 } });

// Decoded value + raw response (e.g., to inspect headers)
const [user2, response] = yield* client.users.getById({
	params: { id: 1 },
	responseMode: 'decoded-and-response'
});

// Raw response only — no decoding, no typed endpoint errors
const raw = yield* client.users.getById({
	params: { id: 1 },
	responseMode: 'response-only'
});
```

> The old `withResponse: true` option was renamed to `responseMode: "decoded-and-response"`. Update any pre-rename code accordingly.

### Client Middleware

When a server-side `HttpApiMiddleware` is declared `requiredForClient: true`, the type system **forces** every client constructor (`make`, `makeWith`, `group`, `endpoint`) to be provided with a matching client implementation. Build it with `HttpApiMiddleware.layerClient`:

```ts
const AuthorizationClient = HttpApiMiddleware.layerClient(
	Authorization,
	Effect.fn(function* ({ next, request, endpoint, group }) {
		// next   — pass the request down the chain (and receive the response)
		// request — the outgoing HttpClientRequest
		// endpoint, group — useful for adding instrumentation tagged by endpoint
		return yield* next(HttpClientRequest.bearerToken(request, 'my-token'));
	})
);

// Optional middlewares (no `requiredForClient: true`) can also be wired this way,
// but skipping them simply means the chain skips that layer.
```

The `layerClient` second argument can also be an `Effect` returning the middleware function, for cases where the middleware needs services (e.g., a token from `Config`):

```ts
const AuthorizationClient = HttpApiMiddleware.layerClient(
	Authorization,
	Effect.gen(function* () {
		const token = yield* Config.Redacted('API_TOKEN');
		return ({ next, request }) =>
			next(
				HttpClientRequest.bearerToken(request, Redacted.value(token))
			);
	})
);
```

If a client middleware is declared with a `clientError` type, that error becomes part of the generated method's error channel.

Client middleware ordering follows the same LIFO rule as server middleware.

### Client URL Builder

`HttpApiClient.urlBuilder(api, options?)` is a synchronous utility that builds typed URLs from your API definition. Methods mirror the client shape, but **inputs are encoded via the endpoint's params/query schemas** — so the input types are the *decoded* domain types, not the raw strings.

```ts
const Api = HttpApi.make('Api').add(
	HttpApiGroup.make('users').add(
		HttpApiEndpoint.get('getUser', '/users/:id', {
			params: { id: Schema.Finite }, // domain type: number
			query: { page: Schema.Finite } // domain type: number
		})
	)
);

const buildUrl = HttpApiClient.urlBuilder(Api, {
	baseUrl: 'https://api.example.com'
});

buildUrl.users.getUser({ params: { id: 123 }, query: { page: 1 } });
//=> "https://api.example.com/users/123?page=1"

// NOT this — params input is the decoded type (number), not the encoded string
// buildUrl.users.getUser({ params: { id: "123" } })   // type error
```

Top-level group endpoints are at the root: `buildUrl.health()`. With `disableCodecs: true` on the endpoint, the builder accepts the raw encoded shape directly.

## OpenAPI Documentation

Built-in OpenAPI documentation responses are generated lazily on
the first request, rather than while the route layer is built. A generation
defect is not permanently cached: a later request retries generation. Include a
documentation-route request in integration checks if generation must be verified;
successful server startup alone no longer proves it. Explicit `OpenApi.fromApi`
remains a direct synchronous generation call.

JSON Schema import/conversion now rejects unsupported references, validation
keywords, and unrepresentable dialect conversions instead of weakening them.
See `effect-schema-v4` before importing third-party schemas into an API contract.

### Scalar UI

```ts
HttpApiScalar.layer(Api, {
	path: '/docs', // default: "/docs"
	scalar: {
		theme: 'kepler',
		layout: 'modern',
		hideModels: false,
		hideTestRequestButton: false,
		hideSearch: false,
		darkMode: true,
		showOperationId: true,
		customCss: '/* ... */',
		favicon: '/favicon.svg',
		baseServerURL: 'https://api.example.com'
	}
});

// CDN-loaded variant (smaller bundle, requires network for the docs page)
HttpApiScalar.layerCdn(Api, {
	path: '/docs',
	version: 'latest', // CDN version of @scalar/api-reference
	scalar: {
		/* ... */
	}
});
```

The `scalar` option is fully typed; see `HttpApiScalar.ScalarConfig` for the full ~20 fields.

`authentication.preferredSecurityScheme` accepts a scheme name, an array of
names/nested name arrays, or null. Scalar/Swagger bundled assets load when their layer is built,
not when importing the module; this is separate from lazy OpenAPI generation.

### Swagger UI

```ts
HttpApiSwagger.layer(Api, { path: '/docs' });
```

### Programmatic OpenAPI

```ts
const spec = OpenApi.fromApi(Api);
// spec is OpenAPI 3.1.0
```

Parameters and response headers are documented from their **encoded** object
representation, preserving property annotations. Decode-defaulted optional keys
remain optional on the wire (path parameters still must be required by OpenAPI).
Text bodies preserve encoded string literals and exportable checks such as
patterns, including when opaque schemas or component references are involved.

`fromApi` caches by both the `HttpApi` instance and the identity of the options object, but every call returns a fresh mutable spec copy. Mutating one returned spec does not contaminate later calls. Reuse one immutable options object to reuse the cache; mutating that options object does not invalidate an existing entry. The clone preserves frozen `JSON.rawJSON` values rather than flattening them into ordinary objects.

The options accept the Schema representation `referencePolicy`. It runs at the canonical JSON-encoded AST boundary and controls which schemas become OpenAPI component references; by default only schemas with resolved identifiers are extracted, while anonymous non-recursive schemas remain inline:

```ts
import { SchemaRepresentation } from 'effect';

const openApiOptions = {
	referencePolicy: ({ ast, identifier, occurrences }) =>
		identifier ?? (occurrences > 1 ? `${ast._tag}_` : undefined)
} satisfies SchemaRepresentation.ToRepresentationOptions;

const spec = OpenApi.fromApi(Api, openApiOptions);
```

Recursive candidates always receive references even when the policy returns `undefined`. Keep the callback deterministic and treat its AST input as immutable.

`includeAnnotationKey` is also forwarded to JSON Schema generation. Reusing a
named schema with different HTTP status annotations still produces one shared
component. Constrained route syntax such as `/:id(\\d+)` retains its matcher
constraint on the server, but generated client URLs and OpenAPI paths omit the
regex.

### Annotations

Available `OpenApi.*` annotation tags (use `.annotate(tag, value)` or `.annotateMerge(OpenApi.annotations({ ... }))`):

| Annotation     | Scope         | Purpose                                                            |
| -------------- | ------------- | ------------------------------------------------------------------ |
| `Title`        | API, Group    | API title; on a group, renames the OpenAPI tag                     |
| `Version`      | API           | API version (default `"0.0.1"`)                                    |
| `Description`  | API, Group, Endpoint, Security | Free-form description                              |
| `Summary`      | API, Endpoint | Short summary                                                      |
| `License`      | API           | `{ name, url? }`                                                   |
| `Servers`      | API           | `Array<{ url, description?, variables? }>`                         |
| `ExternalDocs` | Group, Endpoint | `{ url, description? }`                                          |
| `Format`       | Security      | For HTTP auth schemes (`bearer` and custom `http`): sets `bearerFormat` in spec (e.g., `"JWT"`) |
| `Identifier`   | Endpoint      | Override `operationId` (default: `${group}.${endpoint}`)           |
| `Deprecated`   | Endpoint      | `true` to mark deprecated                                          |
| `Override`     | API, Group, Endpoint | Shallow-merge fields into the generated object              |
| `Transform`    | API, Group, Endpoint | `(spec) => spec` final post-processing function             |
| `Exclude`      | Group, Endpoint | `true` to omit from the OpenAPI spec entirely                    |

`OpenApi.annotations({ title, version, description, license, summary, deprecated, externalDocs, servers, format, override, exclude, transform, identifier })` is the convenient shorthand for building a Context payload.

### Adding Component Schemas Without an Endpoint

Use `HttpApi.AdditionalSchemas` to inject extra schemas into `components.schemas`. Only schemas with an `identifier` annotation are included.

```ts
const Api = HttpApi.make('api')
	.add(/* ... */)
	.annotate(HttpApi.AdditionalSchemas, [
		Schema.Struct({ contentType: Schema.String, length: Schema.Int }).annotate(
			{ identifier: 'FileMeta' }
		)
	]);
```

### Schema-Level Documentation

```ts
const User = Schema.Struct({
	id: Schema.Int,
	name: Schema.String
}).annotate({
	description: 'A user entity',
	identifier: 'User' // shown in docs Models section; required for AdditionalSchemas
});

// Override the default "Success"/"Error" response description
HttpApiEndpoint.get('list', '/users', {
	success: Schema.Array(User).annotate({
		description: 'Returns an array of users'
	})
});
```

## Reading the Raw Request

Inside a handler, `ctx.request` is the raw `HttpServerRequest`:

```ts
handlers.handle('hello', (ctx) =>
	Effect.sync(() => {
		ctx.request.method; // "GET"
		ctx.request.url; // "/hello?x=1"
		ctx.request.headers; // Record<string, string> (lowercase keys)
		ctx.request.cookies; // Record<string, string>
		return 'ok';
	})
);
```

Async helpers: `ctx.request.text`, `ctx.request.json`, `ctx.request.arrayBuffer`, `ctx.request.formData`, `ctx.request.urlParamsBody`, `ctx.request.multipart`, `ctx.request.multipartStream`.

## Customizing Responses

### Custom Response Headers

```ts
handlers.handle('hello', () =>
	Effect.gen(function* () {
		yield* HttpEffect.appendPreResponseHandler((_req, response) =>
			Effect.succeed(
				HttpServerResponse.setHeader(response, 'x-custom', 'hello')
			)
		);
		return 'Hello, World!';
	})
);

// Wrapping form (alternative):
HttpEffect.withPreResponseHandler(
	myHandlerEffect,
	(req, response) => Effect.succeed(/* updated response */)
);
```

### Response Cookies

```ts
handlers.handle('hello', () =>
	Effect.gen(function* () {
		yield* HttpEffect.appendPreResponseHandler((_req, response) =>
			Effect.succeed(
				HttpServerResponse.setCookieUnsafe(
					response,
					'my-cookie',
					'value',
					{ httpOnly: true, secure: true, path: '/' }
				)
			)
		);
		return 'Hello!';
	})
);
```

For cookies tied to an `HttpApiSecurity.apiKey({ in: "cookie", ... })`, use the shortcut `HttpApiBuilder.securitySetCookie(security, value, options?)`.

### Redirects

```ts
handlers.handle('oldPage', () =>
	Effect.succeed(HttpServerResponse.redirect('/new', { status: 302 }))
);
```

### Streaming Responses

```ts
const dataStream = Stream.make('a', 'b', 'c').pipe(
	Stream.schedule(Schedule.spaced(Duration.millis(500))),
	Stream.map((s) => new TextEncoder().encode(s))
);

handlers.handle('getStream', () =>
	Effect.succeed(HttpServerResponse.stream(dataStream))
);
```

### Streaming Requests

Define the payload as `Schema.Uint8Array` with `HttpApiSchema.asUint8Array()` and the handler receives the raw bytes:

```ts
HttpApiEndpoint.post('acceptStream', '/stream', {
	payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),
	success: Schema.String
});

handlers.handle('acceptStream', (ctx) =>
	Effect.succeed(new TextDecoder().decode(ctx.payload))
);
```

For multipart streaming, see `HttpApiSchema.asMultipartStream` above.

## Testing

### `HttpApiTest.groups` — In-Memory Typed Client

`HttpApiTest.groups(api, groupNames, { baseUrl? })` builds a fully typed `HttpApiClient` that runs against your real handler layers in memory — no HTTP server, no port. List the groups whose handlers you want to exercise; all other groups are auto-stubbed with `Effect.die`. The default `baseUrl` is `http://localhost:3000`; pass `{ baseUrl }` when tests rely on URL construction.

```ts
import { HttpApiTest } from 'effect/http-api';
import { NodeHttpServer } from '@effect/platform-node';
import { Effect, Layer } from 'effect';
import { it } from '@effect/vitest';

it.effect('users.findById returns a user', () =>
	Effect.gen(function* () {
		const client = yield* HttpApiTest.groups(Api, ['users']);
		const user = yield* client.users.getById({ params: { id: 1 } });
		expect(user.name).toBe('Admin');
	}).pipe(
		Effect.provide([
			NodeHttpServer.layerHttpServices, // FileSystem/HttpPlatform/Etag/Path
			AuthorizationLayer, // any middleware your handlers need
			HttpApiBuilder.group(Api, 'users', UsersHandlers)
		])
	)
);
```

`HttpApiTest.groups` requires a platform layer for HTTP services (`NodeHttpServer.layerHttpServices`, `BunHttpServer.layerHttpServices`, etc.).

### `NodeHttpServer.layerTest` — Real In-Memory Server

For end-to-end tests that go through the full HTTP serialization path, use `NodeHttpServer.layerTest`:

```ts
const TestLive = HttpRouter.serve(
	HttpApiBuilder.layer(Api).pipe(Layer.provide(GroupLive)),
	{ disableListenLog: true, disableLogger: true }
).pipe(Layer.provideMerge(NodeHttpServer.layerTest));

it.effect('GET /users responds 200', () =>
	Effect.gen(function* () {
		const response = yield* HttpClient.get('/users');
		expect(response.status).toBe(200);

		const client = yield* HttpApiClient.make(Api);
		yield* cl

[…] pełny plik w paczce .zip

Wersje

RepozytoriumWynikLicencja
Ta strona · mpsuesser/opencode-effect-enforcer91MIT
mpsuesser/opencode-effect-enforcer91MIT

Migawka bazy z 13.11.2026 · wersja paczki 79b4f051417e (skrót SHA-256 całej paczki)

Pobrania w ostatnich 30 dniach: 0 · ranking

Zgłoś problem z tym skillem: szkodliwa treść, błędny opis albo prośba autora o usunięcie.

Podobne skille

  • 100golang-api-server
    Buduje usługi HTTP, REST API i middleware w Go.
  • 100go-api-design
    Projektuje API HTTP i REST w Go: handlery, routing ServeMux, middleware, walidacja żądań, paginacja, idempotentność, health checki, odpowiedzi błędów i łagodne zamykanie.
  • 100rest-api-generator
    Generuje z OpenAPI serwery, klientów C# i TypeScript z hookami React Query, a także konfiguruje limity, cache, wersje i webhooki.
  • 91effect-http-server
    Buduje serwery HTTP w Effect: trasy, middleware, odczyt żądań, uploady, WebSocket i pliki statyczne.
  • 91effect-http-server
    Buduje serwery HTTP w Effect: trasy i middleware, dekodowanie żądań, wysyłanie plików, WebSocket i testy bez portu.
  • 100openapi-spec
    Pisze specyfikację OpenAPI 3.1, sprawdza ją Redocly CLI, generuje typy TypeScript i typowanego klienta fetch, buduje dokumentację HTML i pilnuje poprawności specyfikacji.

Uwagi i zgłoszenia

Wiadomość trafi do Bartosza Sokolińskiego. Adres e-mail jest potrzebny tylko do odpowiedzi. Szczegóły w polityce prywatności.