# @rconjoe/effect-http

> High-level declarative HTTP API for effect-ts (forked from sukovanej/effect-http)

Latest version **0.35.1-a** (published 2023-10-26) · MIT license · 0 weekly downloads

## Install

```sh
npm install @rconjoe/effect-http
pnpm add @rconjoe/effect-http
yarn add @rconjoe/effect-http
bun add @rconjoe/effect-http
```

## Health

**Score 30/100 (F)** — status: abandoned.

Positive: has types; esm support; no vulnerabilities; high quality score.

Warnings: low downloads; pre 1.0.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 0.35.1-a |
| Published | 2023-10-26 |
| First published | 2023-09-21 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 5 |
| Unpacked size | 280.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Milan Suk |
| Maintainers | rconjoe |

## Links

- npm: https://www.npmjs.com/package/@rconjoe/effect-http
- Homepage: https://github.com/rconjoe/effect-http
- npm.io page: https://npm.io/package/@rconjoe/effect-http

## Dependencies (5)

- [express](https://npm.io/package/express.md) ^4.18.2
- [schema-openapi](https://npm.io/package/schema-openapi.md) ^0.19.1
- [@effect/platform](https://npm.io/package/@effect/platform.md) ^0.25.1
- [swagger-ui-express](https://npm.io/package/swagger-ui-express.md) ^5.0.0
- [@effect/platform-node](https://npm.io/package/@effect/platform-node.md) ^0.26.1

## Recent versions

- 0.35.1-a (latest) — 2023-10-26
- 0.30.2-b-rc1 (canary) — 2023-09-24
- 0.33.1-a — 2023-10-17
- 0.30.2-c — 2023-10-17
- 0.30.2-b — 2023-09-24
- 0.30.2-a — 2023-09-24
- 0.30.2 — 2023-09-21

## README

# effect-http

![download badge](https://img.shields.io/npm/dm/effect-http.svg)

High-level declarative HTTP API for [effect-ts](https://github.com/Effect-TS).

- :star: **Client derivation**. Write the api specification once, get the type-safe client with runtime validation for free.
- :rainbow: **OpenAPI derivation**. `/docs` endpoint with OpenAPI UI out of box.
- :battery: **Batteries included server implementation**. Automatic runtime request and response validation.
- :crystal_ball: **Example server derivation**. Automatic derivation of example server implementation.
- :bug: **Mock client derivation**. Test safely against a specified API.

**Under development.** Please note that currently any release might introduce
breaking changes and the internals and the public API are still evolving and changing.

## Quickstart

- [Quickstart](#quickstart)
- [Request validation](#request-validation)
  - [Example](#example)
  - [Optional path parameters](#optional-path-parameters)
- [Headers](#headers)
- [Responses](#responses)
- [Testing the server](#testing-the-server)
- [Error handling](#error-handling)
  - [Reporting errors in handlers](#reporting-errors-in-handlers)
  - [Example API with conflict API error](#example-api-with-conflict-api-error)
- [Grouping endpoints](#grouping-endpoints)
- [Incremental adoption into existing express app](#incremental-adoption-into-existing-express-app)
- [Cookbook](#cookbook)
  - [Handler input type derivation](#handler-input-type-derivation)
  - [Descriptions in OpenApi](#descriptions-in-openapi)
- [API on the client side](#api-on-the-client-side)
  - [Example server](#example-server)
  - [Mock client](#mock-client)
  - [Common headers](#common-headers)
- [Extensions](#extensions)
- [Compatibility](#compatibility)

Install together with `effect` using

```bash
pnpm add effect effect-http
```

Bootstrap a simple API specification.

```typescript
import * as Schema from "@effect/schema/Schema";
import { Effect, pipe } from "effect";

import * as Http from "effect-http";

const responseSchema = Schema.struct({ name: Schema.string });
const query = Schema.struct({ id: Schema.number });

const api = pipe(
  Http.api({ title: "Users API" }),
  Http.get("getUser", "/user", {
    response: responseSchema,
    request: { query: query },
  }),
);
```

Create the server implementation.

```typescript
const server = pipe(
  api,
  Http.server,
  Http.handle("getUser", ({ query }) => Effect.succeed({ name: "milan" })),
  Http.exhaustive,
);
```

Now, we can generate an object providing the HTTP client interface using `Http.client`.

```typescript
const client = Http.client(api, new URL("http://localhost:3000"));
```

And spawn the server on port 3000 and call it using the `client`.

```typescript
const callServer = () =>
  pipe(
    client.getUser({ query: { id: 12 } }),
    Effect.flatMap((user) => Effect.logInfo(`Got ${user.name}, nice!`)),
  );

pipe(
  server,
  Http.listen({ port: 3000, onStart: callServer }),
  Effect.runPromise,
);
```

Also, check the auto-generated OpenAPI UI running on
[localhost:3000/docs](http://localhost:3000/docs/). How awesome is that!

![open api ui](https://raw.githubusercontent.com/sukovanej/effect-http/master/assets/example-openapi-ui.png)

### Request validation

Each endpoint can declare expectations on the request format. Specifically,

- `body` - request body
- `query` - query parameters
- `params` - path parameters
- `headers` - request headers

They are specified in the input schemas object (3rd argument of `Http.get`, `Http.post`, ...).

#### Example

```typescript
import * as Schema from "@effect/schema/Schema";
import { pipe } from "effect";

import * as Http from "effect-http";

export const api = pipe(
  Http.api({ title: "My api" }),
  Http.get("stuff", "/stuff/:param", {
    response: Schema.struct({ value: Schema.number }),
    request: {
      body: Schema.struct({ bodyField: Schema.array(Schema.string) }),
      query: Schema.struct({ query: Schema.string }),
      params: Schema.struct({ param: Schema.string }),
    },
  }),
);
```

_(This is a complete standalone code example)_

#### Optional path parameters

Optional parameter is denoted using a question mark in the path
match pattern. In the request param schema, use `Schema.optional(<schema>)`.

In the following example the last `:another` path parameter can be
ommited on the client side.

```typescript
import * as Schema from "@effect/schema/Schema";
import { pipe } from "effect";

import * as Http from "effect-http";

export const api = pipe(
  Http.api({ title: "My api" }),
  Http.get("stuff", "/stuff/:param/:another?", {
    response: Schema.struct({ value: Schema.number }),
    request: {
      params: Schema.struct({
        param: Schema.string,
        another: Schema.optional(Schema.string),
      }),
    },
  }),
);
```

### Headers

Request headers are part of input schemas along with the request body or query parameters.
Their schema is specified similarly to query parameters and path parameters, i.e. using
a mapping from header names onto their schemas. The example below shows an API with
a single endpoint `/hello` which expects a header `X-Client-Id` to be present.

```typescript
import * as Schema from "@effect/schema/Schema";

import * as Http from "effect-http";

const api = pipe(
  Http.api(),
  Http.get("hello", "/hello", {
    response: Schema.string,
    request: {
      headers: Schema.struct({ "X-Client-Id": Schema.string }),
    },
  }),
);
```

_(This is a complete standalone code example)_

Server implementation deals with the validation the usual way. For example, if we try
to call the endpoint without the header we will get the following error response.

```json
{ "error": "InvalidHeadersError", "details": "x-client-id is missing" }
```

And as usual, the information about headers will be reflected in the generated
OpenAPI UI.

![example-headers-openapi-ui](https://raw.githubusercontent.com/sukovanej/effect-http/master/assets/example-headers-openapi-ui.png)

**Important note**. You might have noticed the `details` field of the error response
describes the missing header using lower-case. This is not an error but rather a
consequence of the fact that HTTP headers are case-insensitive and internally `effect-http`
converts all header names to lower-case to simplify integration with the underlying
http library - [express](https://github.com/expressjs/express).

Don't worry, this is also encoded into the type information and if you were to
implement the handler, both autocompletion and type-checking would hint the
lower-cased form of the header name.

```typescript
type Api = typeof api;

const handleHello = ({
  headers: { "x-client-id": clientId },
}: Http.Input<Api, "hello">) => Effect.succeed("all good");
```

Take a look at [examples/headers.ts](examples/headers.ts) to see a complete example
API implementation with in-memory rate-limiting and client identification using headers.

### Responses

Response can be specified using a `Schema.Schema<I, O>` which automatically
returns status code 200 and includes only default headers.

If you want a response with custom headers and status code, use the full response
schema instead. The following example will enforce (both for types and runtime)
that returned status, content and headers conform the specified response.

```ts
const api = pipe(
  Http.api(),
  Http.post("hello", "/hello", {
    response: {
      status: 200,
      content: Schema.number,
      headers: { "My-Header": Schema.string },
    },
  }),
);
```

It is also possible to specify multiple full response schemas.

```ts
const api = pipe(
  Http.api(),
  Http.post("hello", "/hello", {
    response: [
      {
        status: 201,
        content: Schema.number,
      },
      {
        status: 200,
        content: Schema.number,
        headers: { "My-Header": Schema.string },
      },
      {
        status: 204,
        headers: { "X-Another": Schema.NumberFromString },
      },
    ],
  }),
);
```

The server implemention is type-checked against the api responses
and one of the specified response objects must be returned.

The response object can be generated using a `ResponseUtil`. It is
a derived object based on the `Api` and operation id and it provides
methods named `response<status>` that create the response.

```ts
const server = pipe(
  Http.server(api),
  Http.handle("hello", ({ ResponseUtil }) =>
    Effect.succeed(
      ResponseUtil.response200({ headers: { "my-header": 12 }, content: 12 }),
    ),
  ),
);
```

Note that one can create the response util object using `Http.responseUtil`
if needed independently of the server implementation.

```ts
const HelloResponseUtil = Http.responseUtil(api, "hello");
const response200 = HelloResponseUtil.response200({
  headers: { "my-header": 12 },
  content: 12,
});
```

The derived client for this `Api` exposes a `hello` method that
returns the following type.

```ts
type DerivedTypeOfHelloMethod =
  | {
      content: number;
      status: 201;
    }
  | {
      headers: {
        readonly "my-header": number;
      };
      content: number;
      status: 200;
    }
  | {
      headers: {
        readonly "x-another": number;
      };
      status: 204;
    };
```

### Testing the server

While most of your tests should focus on the functionality independent
of HTTP exposure, it can be beneficial to perform integration or
contract tests for your endpoints. The `Testing` module offers a
`Http.testingClient` combinator that generates a testing client from
the Server. This derived testing client has a similar interface
to the one derived by `Http.client`, but with the distinction that
it returns a `Response` object and bypasses the network by
directly triggering the handlers defined in the Server.

Now, let's write an example test for the following server.

```ts
const api = pipe(
  Http.api(),
  Http.get("hello", "/hello", {
    response: Schema.string,
  }),
);

const server = pipe(
  api,
  Http.server,
  Http.handle("hello", ({ query }) => Effect.succeed(`${query.input + 1}`)),
);
```

The test might look as follows.

```ts
test("test /hello endpoint", async () => {
  const testingClient = Http.testingClient(server);

  const response = await Effect.runPromise(
    client.hello({ query: { input: 12 } }),
  );

  expect(response).toEqual("13");
});
```

In comparison to the `Client` we need to run our endpoint handlers
in place. Therefore, in case your server uses DI services, you need to
provide them in the test code. This contract is type safe and you'll be
notified by the type-checker if the `Effect` isn't invoked with all
the required services.

### Error handling

Validation of query parameters, path parameters, body and even responses is
handled for you out of box. By default, failed validation will be reported
to clients in the response body. On the server side, you get warn logs with
the same information.

#### Reporting errors in handlers

On top of the automatic input and output validation, handlers can fail for variety
of different reasons.

Suppose we're creating user management API. When persisting a new user, we want
to guarantee we don't attempt to persist a user with an already taken name.
If the user name check fails, the API should return `409 CONFLICT` error because the client
is attempting to trigger an operatin conflicting with the current state of the server.
For these cases, `effect-http` provides error types and corresponding creational
functions we can use in the error rail of the handler effect.

##### 4xx

- 400 `Http.invalidQueryError` - _query parameters validation failed_
- 400 `Http.invalidParamsError` - _path parameters validation failed_
- 400 `Http.invalidBodyError` - _request body validation failed_
- 400 `Http.invalidHeadersError` - _request headers validation failed_
- 401 `Http.unauthorizedError` - _invalid authentication credentials_
- 403 `Http.forbiddenError` - _authorization failure_
- 404 `Http.notFoundError` - _cannot find the requested resource_
- 409 `Http.conflictError` - _request conflicts with the current state of the server_
- 415 `Http.unsupportedMediaTypeError` - _unsupported payload format_
- 429 `Http.tooManyRequestsError` - _the user has sent too many requests in a given amount of time_

##### 5xx

- 500 `Http.invalidResponseError` - _internal server error because of response validation failure_
- 500 `Http.internalServerError` - _internal server error_
- 501 `Http.notImplementedError` - _functionality to fulfill the request is not supported_
- 502 `Http.badGatewayError` - _invalid response from the upstream server_
- 503 `Http.serviceunavailableError` - _server is not ready to handle the request_
- 504 `Http.gatewayTimeoutError` - _request timeout from the upstream server_

Using these errors, `Server` runtime can generate human-readable details
in HTTP responses and logs. Also, it can correctly decide what status code
should be returned to the client.

The formatting of `details` field of the error JSON object is abstracted using
[Http.ValidationErrorFormatter](src/Server/ValidationErrorFormatter.ts). The
`ValidationErrorFormatter` is a function taking a `Schema.ParseError` and returning
its string representation. It can be overridden using layers. The following example
will perform a direct JSON serialization of the `Schema.ParserError` to construct
the error details.

```typescript
const myValidationErrorFormatter: Http.ValidationErrorFormatter = (error) =>
  JSON.stringify(error);

pipe(
  server,
  Http.listen({ port: 3000 }),
  Effect.provide(
    Http.setValidationErrorFormatter(myValidationErrorFormatter),
  ),
  Effect.runPromise,
);
```

#### Example API with conflict API error

Let's see it in action and implement the mentioned user management API. The
API will look as follows.

```typescript
import * as Schema from "@effect/schema/Schema";
import { Context, Effect, pipe } from "effect";

import * as Http from "effect-http";

const api = pipe(
  Http.api({ title: "Users API" }),
  Http.post("storeUser", "/users", {
    response: Schema.string,
    request: {
      body: Schema.struct({ name: Schema.string }),
    },
  }),
);

type Api = typeof api;
```

Now, let's implement a `UserRepository` interface abstracting the interaction with
our user storage. I'm also providing a mock implementation which will always return
the user already exists. We will plug the mock user repository into our server
so we can see the failure behavior.

```typescript
interface UserRepository {
  existsByName: (name: string) => Effect.Effect<never, never, boolean>;
  store: (user: string) => Effect.Effect<never, never, void>;
}

const UserRepository = Context.Tag<UserRepository>();

const mockUserRepository = UserRepository.of({
  existsByName: () => Effect.succeed(true),
  store: () => Effect.unit,
});
```

And finally, we have the actual `Server` implementation.

```typescript
const handleStoreUser = ({ body }: Http.Input<Api, "storeUser">) =>
  pipe(
    Effect.flatMap(UserRepository, (userRepository) =>
      userRepository.existsByName(body.name),
    ),
    Effect.filterOrFail(
      (alreadyExists) => !alreadyExists,
      () => Http.conflictError(`User "${body.name}" already exists.`),
    ),
    Effect.flatMap(() =>
      Effect.flatMap(UserRepository, (repository) =>
        repository.store(body.name),
      ),
    ),
    Effect.map(() => `User "${body.name}" stored.`),
  );

const server = pipe(
  api,
  Http.server,
  Http.handle("storeUser", handleStoreUser),
  Http.exhaustive,
);
```

To run the server, we will start the server using `Http.listen` and provide
the `mockUserRepository` service.

```typescript
pipe(
  server,
  Http.listen({ port: 3000 }),
  Effect.provideService(UserRepository, mockUserRepository),
  Effect.runPromise,
);
```

Try to run the server and call the `POST /user`.

_Server_

```bash
$ pnpm tsx examples/conflict-error-example.ts

16:53:55 (Fiber #0) INFO  Server listening on :::3000
16:54:14 (Fiber #8) WARN  POST /users failed
ᐉ { "errorTag": "ConflictError", "error": "User "milan" already exists." }
```

_Client_ (using [httpie cli](https://httpie.io/cli))

```bash
$ http localhost:3000/users name="patrik"

HTTP/1.1 409 Conflict
Connection: keep-alive
Content-Length: 68
Content-Type: application/json; charset=utf-8
Date: Sat, 15 Apr 2023 16:36:44 GMT
ETag: W/"44-T++MIpKSqscvfSu9Ed1oobwDDXo"
Keep-Alive: timeout=5
X-Powered-By: Express

{
    "details": "User \"patrik\" already exists.",
    "error": "ConflictError"
}
```

### Grouping endpoints

To create a new group of endpoints, use `Http.apiGroup("group name")`. This combinator
initializes new `ApiGroup` object. You can pipe it with combinators like `Http.get`,
`Http.post`, etc, as if were defining the `Api`. Api groups can be combined into an
`Api` using a `Http.addGroup` combinator which merges endpoints from the group
into the api in the type-safe manner while preserving group names for each endpoint.

This enables separability of concers for big APIs and provides information for
generation of tags for the OpenAPI specification.

```typescript
import * as Schema from "@effect/schema/Schema";
import { Effect, pipe } from "effect";

import * as Http from "effect-http";

const responseSchema = Schema.struct({ name: Schema.string });

const testApi = pipe(
  Http.apiGroup("test"),
  Http.get("test", "/test", { response: responseSchema }),
);

const userApi = pipe(
  Http.apiGroup("Users"),
  Http.get("getUser", "/user", { response: responseSchema }),
  Http.post("storeUser", "/user", { response: responseSchema }),
  Http.put("updateUser", "/user", { response: responseSchema }),
  Http.delete("deleteUser", "/user", { response: responseSchema }),
);

const categoriesApi = pipe(
  Http.apiGroup("Categories"),
  Http.get("getCategory", "/category", { response: responseSchema }),
  Http.post("storeCategory", "/category", { response: responseSchema }),
  Http.put("updateCategory", "/category", { response: responseSchema }),
  Http.delete("deleteCategory", "/category", { response: responseSchema }),
);

const api = pipe(
  Http.api(),
  Http.addGroup(testApi),
  Http.addGroup(userApi),
  Http.addGroup(categoriesApi),
);

pipe(api, Http.exampleServer, Http.listen({ port: 3000 }), Effect.runPromise);
```

_(This is a complete standalone code example)_

The OpenAPI UI will group endpoints according to the `api` and show
corresponding titles for each group.

![example-generated-open-api-ui](https://raw.githubusercontent.com/sukovanej/effect-http/master/assets/example-server-openapi-ui.png)

## Incremental adoption into existing express app

In `effect-http`, calling `Http.listen` under the hood performs the conversion of a
`Server` onto an `Express` app and then it immediately triggers `listen()` on the
generated app. This is very convenient because in the userland, you deal with the
app creation using `Effect` and don't need to care about details of the underlying
Express web framework.

This hiding might get in a way if you decide to adopt `effect-http` into an
existing application. Because of that, the Express app derivation is exposed
as a public API of the `effect-http`. The exposed function is `Http.express`
and it takes a configuration options and `Server` object (it's curried) as
inputs and returns the generated `Express` app.

As the [Express documentation](https://expressjs.com/en/guide/using-middleware.html)
describes, _an Express application is essentially a series of middleware function calls_.
This is perfect because if we decide to integrate an effect api into an express app,
we can do so by simply plugging the generated app as an application-level middleware
into the express app already in place.

Let's see it in action. Suppose we have the following existing express application.

```typescript
import express from "express";

const legacyApp = express();

legacyApp.get("/legacy-endpoint", (_, res) => {
  res.json({ hello: "world" });
});

app.listen(3000, () => console.log("Listening on 3000"));
```

Now, we'll create an `effect-http` api and server.

```typescript
import * as Schema from "@effect/schema/Schema";
import { Effect, pipe } from "effect";

import * as Http from "effect-http";

const api = pipe(
  Http.api(),
  Http.get("newEndpoint", "/new-endpoint", {
    response: Schema.struct({ hello: Schema.string }),
  }),
);

const server = pipe(
  Http.server(api),
  Http.handle("newEndpoint", () => Effect.succeed({ hello: "new world" })),
  Http.exhaustive,
);
```

In order to _merge_ these two applications, we use the aforementioned
`Http.express` function to convert the `server` into an Express app.
We'll receive the `Express` app in the success rail of the `Effect`
so we can map over it and attach the legacy app there. To start
the application, use `Http.listenExpress()` which has the same
signature as `Http.listen` but instead of `Http.Server` it takes
an `Express` instance.

```typescript
pipe(
  server,
  Http.express({ openapiPath: "/docs-new" }),
  Effect.map((app) => {
    app.use(legacyApp);
    return app;
  }),
  Effect.flatMap(Http.listenExpress()),
  Effect.runPromise,
);
```

There are some caveats we should be aware of. Middlewares and express in general
are imperative. Middlewares that execute first can end the request-response
cycle. Also, the `Server` doesn't have any information about the legacy express
application and the validation, logging and OpenAPI applies only for its
routes. That's why this approach should be generally considered a transition
state. It's in place mainly to allow an incremental rewrite.

If you already manage an OpenAPI in the express app, you can use the
options of `Http.express` to either disable the generated OpenAPI completely
or expose it through a different endpoint.

```typescript
// Disable the generated OpenAPI
Http.express({ openapiEnabled: false });

// Or, expose the new OpenAPI within a different route
Http.express({ openapiPath: "/docs-new" });
```

[See the full example](examples/incremental-adoption-express.ts)

## Cookbook

### Handler input type derivation

In non-trivial applications, it is expected the `Server` specification
and handlers are separated. If we define schemas purely for the purpose
of defining the `Api` we'd be forced to derive their type definitions
only for the type-safety of `Server` handlers. Instead, `Http` provides
the `Input` type helper which accepts a type of the `Api` and operation
id type and produces type covering query, params and body schema type.

```typescript
import * as Schema from "@effect/schema/Schema";
import { Effect, pipe } from "effect";

import * as Http from "effect-http";

const api = pipe(
  Http.api({ title: "My api" }),
  Http.get("stuff", "/stuff", {
    response: Schema.string,
    request: {
      query: Schema.struct({ value: Schema.string }),
    },
  }),
);

type Api = typeof api;

// Notice query has type { readonly value: string; }
const handleStuff = ({ query }: Http.Input<Api, "stuff">) =>
  pipe(
    Effect.fail(Http.notFoundError("I didnt find it")),
    Effect.tap(() => Effect.log(`Received ${query.value}`)),
  );

const server = pipe(
  api,
  Http.server,
  Http.handle("stuff", handleStuff),
  Http.exhaustive,
);

pipe(server, Http.listen({ port: 3000 }), Effect.runPromise);
```

_(This is a complete standalone code example)_

### Descriptions in OpenApi

The [schema-openapi](https://github.com/sukovanej/schema-openapi) library which is
used for OpenApi derivation from the `Schema` takes into account
[description](https://effect-ts.github.io/schema/modules/Schema.ts.html#description)
annotations and propagates them into the specification.

Some descriptions are provided from the built-in `@effect/schema/Schema` combinators.
For example, the usage of `Schema.int()` will result in "_a positive number_"
description in the OpenApi schema. One can also add custom description using the
`Schema.description` combinator.

On top of types descriptions which are included in the `schema` field, effect-http
also checks top-level schema descriptions and uses them for the parent object which
uses the schema. In the following example, the "_User_" description for the response
schema is used both as the schema description but also for the response itself. The
same holds for the `id` query paremeter.

For an operation-level description, call the API endpoint method (`Http.get`,
`Http.post` etc) with a 4th argument and set the `description` field to the
desired description.

```ts
import * as Schema from "@effect/schema/Schema";
import { pipe } from "effect";

import * as Http from "effect-http";

const responseSchema = pipe(
  Schema.struct({
    name: Schema.string,
    id: pipe(Schema.number, Schema.int(), Schema.positive()),
  }),
  Schema.description("User"),
);
const querySchema = Schema.struct({
  id: pipe(Schema.NumberFromString, Schema.description("User id")),
});

const api = pipe(
  Http.api({ title: "Users API" }),
  Http.get(
    "getUser",
    "/user",
    {
      response: responseSchema,
      request: { query: querySchema },
    },
    { description: "Returns a User by id" },
  ),
);
```

## API on the client side

While `effect-http` is intended to be primarly used on the server-side, i.e.
by developers providing the HTTP service, it is possible to use it also to
model, use and test against someone else's API. Out of box, you can make
us of the following combinators.

- `Http.client` - client for the real integration with the API.
- `Http.mockClient` - client for testing against the API interface.
- `Http.exampleServer` - server implementation derivation with example responses.

### Example server

`effect-http` has the ability to generate an example server
implementation based on the `Api` specification. This can be
helpful in the following and probably many more cases.

- You're in a process of designing an API and you want to have _something_
  to share with other people and have a discussion over before the actual
  implementation starts.
- You develop a fullstack application with frontend first approach
  you want to test the integration with a backend you haven't
  implemeted yet.
- You integrate a 3rd party HTTP API and you want to have an ability to
  perform integration tests without the need to connect to a real
  running HTTP service.

Use `Http.exampleServer` combinator to generate a `Server` from `Api`.

```typescript
import * as Schema from "@effect/schema/Schema";
import { Effect, pipe } from "effect";

import * as Http from "effect-http";

const responseSchema = Schema.struct({ name: Schema.string });

const api = pipe(
  Http.api(),
  Http.get("test", "/test", { response: responseSchema }),
);

pipe(api, Http.exampleServer, Http.listen({ port: 3000 }), Effect.runPromise);
```

_(This is a complete standalone code example)_

Go to [localhost:3000/docs](http://localhost:3000/docs) and try calling
endpoints. The exposed HTTP service conforms the `api` and will return
only valid example responses.

### Mock client

To performed quick tests against the API interface, `effect-http` has
the ability to generate a mock client which will return example or
specified responses. Suppose we are integrating a hypothetical API
with `/get-value` endpoint returning a number. We can model such
API as follows.

```typescript
import * as Schema from "@effect/schema/Schema";
import { pipe } from "effect";

import * as Http from "effect-http";

const api = pipe(
  Http.api(),
  Http.get("getValue", "/get-value", { response: Schema.number }),
);
```

In a real environment, we will probably use the derived client
using `Http.client`. But for tests, we probably want a dummy
client which will return values conforming the API. For such
a use-case, we can derive a mock client.

```typescript
const client = pipe(api, Http.mockClient());
```

Calling `getValue` on the client will perform the same client-side
validation as would be done by the real client. But it will return
an example response instead of calling the API. It is also possible
to enforce the value to be returned in a type-safe manner
using the option argument. The following client will always
return number `12` when calling the `getValue` operation.

```typescript
const client = pipe(api, Http.mockClient({ responses: { getValue: 12 } }));
```

### Common headers

On the client side, headers that have the same value for all request calls
(for example `USER-AGENT`) can be configured during a client creation. Such
headers can be completely omitted when an operation requiring these headers
is called. Common headers can be overriden during operation call.

Note that configuring common headers doesn't make them available
for all requests. Common header will be applied only if the given
endpoint declares it in its schema.

```typescript
import * as Schema from "@effect/schema/Schema";
import { Effect, pipe } from "effect";

import * as Http from "effect-http";

const api = pipe(
  Http.api(),
  Http.get("test", "/test", {
    response: Schema.struct({ name: Schema.string }),
    request: {
      headers: Schema.struct({ AUTHORIZATION: Schema.string }),
    },
  }),
);

const client = Http.client(api, new URL("http://my-url"), {
  headers: {
    authorization: "Basic aGVsbG8gcGF0cmlrLCBob3cgYXJlIHlvdSB0b2RheQ==",
  },
});

// "x-my-header" can be provided to override the default but it's not necessary
pipe(client.test(), Effect.runPromise);
```

_(This is a complete standalone code example)_

## Extensions

> [!warning]
> The extension API is very experimental.

Extensions allow the `Server` to be extended using effects that
are applied for all requests. The extension API is primarly
introduced to allow reusable implementations of authentication,
authorization, access logging, metrics collection, etc.
If you have a particular functionality that needs to be triggered
for every request on the server, extensions might be the way to go.

Extensions are somewhat similar to middlewares. They are represented
as a list of functions with additional metadata and they run
sequentially during every request handling. Contrary to middlewares,
extensions can't arbitrarly interupt the request-response chain, they can
only end it by failing. Also, extensions can't perform mutations of
the request object. The following extension types are supported.

- `BeforeHandlerExtension` - runs before every endpoint handler.
- `AfterHandlerExtension` - runs after every sucessful endpoint handler.
- `OnErrorExtension` - runs when the handling fails.

Failure of extension effects is propagated as the endpoint
error but success results of extensions are ignored. Therefore, extensions
don't allow modifications of inputs and outputs of endpoints.

There are built-in extensions in the `effect-http` in the `Extensions`
module.

| Extension                      | Description                                                                                   | Applied by default |
| ------------------------------ | --------------------------------------------------------------------------------------------- | :----------------: |
| `accessLogExtension`           | access logs for handled requests                                                              | :white_check_mark: |
| `errorLogExtension`            | failed requests are logged out                                                                | :white_check_mark: |
| `uuidLogAnnotationExtension`   | generates a UUID for every request and uses it in log annotations                             |        :x:         |
| `endpointCallsMetricExtension` | measures how many times each endpoint was called in a `server.endpoint_calls` counter metrics |        :x:         |
| `basicAuthExtension`           | authentication / authorization using basic auth                                               |        :x:         |

In the following example, `uuid-log-annotation`, `access-log`
and `endpoint-calls-metric` extensions are used. Collected metrics
are accesible as raw data through the _/metrics_ endpoint.

```ts
import * as Schema from "@effect/schema/Schema";
import { Effect, Metric, pipe } from "effect";

import * as Http from "effect-http";

const api = pipe(
  Http.api({ title: "Users API" }),
  Http.get(
    "getUser",
    "/user",
    { response: Schema.string },
    { description: "Returns a User by id" },
  ),
  Http.get("metrics", "/metrics", { response: Schema.any }),
);

const server = pipe(
  api,
  Http.server,
  Http.handle("getUser", () => Effect.succeed("Hello")),
  Http.handle("metrics", () => Metric.snapshot()),
  Http.prependExtension(Http.uuidLogAnnotationExtension()),
  Http.addExtension(Http.endpointCallsMetricExtension()),
  Http.exhaustive,
);

pipe(server, Http.listen({ port: 3000 }), Effect.runPromise);
```

Extensions can be constructed using the following constructors.

- `Http.afterHandlerExtension`
- `Http.beforeHandlerExtension`
- `Http.onHandlerErrorExtension`

The example bellow would fail every request with the authorization error.

```ts
const myExtension = Http.beforeHandlerExtension("example-auth", () =>
  Effect.fail(Http.unauthorizedError("sorry bro")),
);

const server = pipe(
  api,
  Http.server,
  Http.addExtension(myExtension),
  Http.exhaustive,
);
```

## Compatibility

This library is tested against nodejs 20.9.0.

---
_Source: https://npm.io/package/@rconjoe/effect-http · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
