0.35.1-a • Published 7 months ago

@rconjoe/effect-http v0.35.1-a

Weekly downloads
-
License
MIT
Repository
-
Last release
7 months ago

effect-http

download badge

High-level declarative HTTP API for 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

Install together with effect using

pnpm add effect effect-http

Bootstrap a simple API specification.

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.

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.

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

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

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. How awesome is that!

open api ui

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

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.

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.

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.

{ "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

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.

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.

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

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.

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.

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.

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.

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.

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.

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

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.

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.

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.

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.

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

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

Server

$ 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)

$ 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.

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

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

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.

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.

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.

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

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.

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 library which is used for OpenApi derivation from the Schema takes into account 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.

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.

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

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.

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.

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.

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.

ExtensionDescriptionApplied by default
accessLogExtensionaccess logs for handled requests:white_check_mark:
errorLogExtensionfailed requests are logged out:white_check_mark:
uuidLogAnnotationExtensiongenerates a UUID for every request and uses it in log annotations:x:
endpointCallsMetricExtensionmeasures how many times each endpoint was called in a server.endpoint_calls counter metrics:x:
basicAuthExtensionauthentication / 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.

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.

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.

0.35.1-a

7 months ago

0.33.1-a

7 months ago

0.30.2-c

7 months ago

0.30.2-b

8 months ago

0.30.2-b-rc1

8 months ago

0.30.2-a

8 months ago

0.30.2

8 months ago