effect-http

repository·master·Indexed 43 days ago

https://github.com/sukovanej/effect-http

A high-level, declarative HTTP library for the Effect-TS ecosystem. It allows developers to define an API specification to automatically derive type-safe clients, OpenAPI documentation, mock clients, and server implementations with built-in runtime validation. Note: As of 2024-08-30, this package is deprecated in favor of @effect/platform (HttpApi modules) or @effect/rpc.

Tokens
18.2K
Snippets
57
Records
82
Agent score
55%

What's inside effect-http

  1. Overview of effect-http features

    master

    effect-http is a high-level declarative HTTP library for Effect-TS built on top of @effect/platform. It provides several automated capabilities:

    • Client derivation: Generate a type-safe client with runtime validation from a single API specification.
    • OpenAPI derivation: Automatically provides a /docs endpoint with an OpenAPI UI.
    • Batteries included server implementation: Provides automatic runtime validation for both requests and responses.
    • Example server derivation: Automatically derives an example server implementation from your specification.
    • Mock client derivation: Allows for safe testing against a specified API using a mock client.

    Warning: This project is under development. Releases may introduce breaking changes as the public API and internals are still evolving.

  2. Group endpoints using ApiGroup

    master

    For large APIs, use ApiGroup to organize endpoints into logical sections. This improves code maintainability and provides automatic tagging in the generated OpenAPI specification.

    1. Create a group with ApiGroup.make("name", { description, externalDocs }).
    2. Add endpoints to the group using ApiGroup.addEndpoint.
    3. Merge groups into the main API using Api.addGroup(group).

    The OpenAPI UI will display these groups with their respective titles and descriptions.

    import { Api, ApiGroup } from "effect-http";
    
    const userApi = ApiGroup.make("Users", { description: "All about users" }).pipe(
      ApiGroup.addEndpoint(
        ApiGroup.get("getUser", "/user").pipe(Api.setResponseBody(Response))
      )
    );
    
    const api = Api.make().pipe(
      Api.addGroup(userApi)
    );
  3. Implement Security and Authentication

    master

    Use the Security module to document and enforce authentication/authorization.

    Core Concepts:

    • Api.setSecurity(security): Attaches a security mechanism to an endpoint.
    • Security.basic(): Provides basic authentication. The handler receives BasicCredentials as the second argument.
    • Security.or(sec1, sec2): Combines multiple security mechanisms (e.g., making auth optional).
    • Security.make(effect, openApiSpec): Creates custom security logic. The effect accesses the ServerRequest, and the openApiSpec documents the mechanism for OpenAPI.

    Client-side Security: Use Client.setBasic(user, pass) or Client.setBearer(token) to configure authorization headers on the generated client.

    import { NodeRuntime } from "@effect/platform-node";
    import { Effect, Schema } from "effect";
    import { Api, RouterBuilder, Security } from "effect-http";
    import { NodeServer } from "effect-http-node";
    
    const api = Api.make().pipe(
      Api.addEndpoint(
        Api.post("mySecuredEndpoint", "/my-secured-endpoint").pipe(
          Api.setResponseBody(Schema.String),
          Api.setSecurity(Security.basic())
        )
      )
    );
    
    const app = RouterBuilder.make(api).pipe(
      RouterBuilder.handle("mySecuredEndpoint", (_, security) =>
        Effect.succeed(`Accessed as ${security.user}`)
      ),
      RouterBuilder.build
    );
    
    app.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain);
  4. Configure response representations (Content-Type)

    master

    By default, effect-http uses Representation.json to serialize/deserialize messages as JSON with content-type: application/json.

    You can change this using Api.setResponseRepresentations. If you provide a list of representations, the server will respect the incoming Accept header to select the appropriate one. If no match is found, it defaults to the first representation in the list.

    Common representations include Representation.json and Representation.plainText.

    import { Api, Representation } from "effect-http";
    import { Schema } from "effect";
    
    // Example: Setting response to plain text
    export const api2 = Api.make().pipe(
      Api.addEndpoint(
        Api.get("myHandler", "/test").pipe(
          Api.setResponseBody(Schema.String),
          Api.setResponseRepresentations([Representation.plainText])
        )
      )
    );
    
    // Example: Multiple representations (order matters for fallback)
    export const api = Api.make().pipe(
      Api.addEndpoint(
        Api.get("root", "/").pipe(
          Api.setResponseBody(Schema.Unknown),
          Api.setResponseRepresentations([
            Representation.plainText,
            Representation.json,
          ])
        )
      )
    );
  5. Quickstart: Bootstrap, implement, and run an API

    master

    Follow these steps to create a complete API lifecycle:

    1. Bootstrap the API specification using Api.make and Api.addEndpoint. Define your schemas using effect's Schema module.
    2. Implement the app using RouterBuilder.make(api). Use RouterBuilder.handle to map endpoint names to logic (returning an Effect).
    3. Generate a client using Client.make(api, { baseUrl: ... }) for type-safe communication.
    4. Spawn the server using NodeServer.listen and NodeRuntime.runMain (for Node.js).

    An OpenAPI UI is automatically generated and available at http://localhost:3000/docs/.

    import { Schema } from "effect";
    import { Api, QuerySchema } from "effect-http";
    import { Effect } from "effect";
    import { RouterBuilder } from "effect-http";
    import { NodeRuntime } from "@effect/platform-node";
    import { NodeServer } from "effect-http-node";
    import { Client } from "effect-http";
    
    // 1. Specification
    const UserResponse = Schema.Struct({
      name: Schema.String,
      id: Schema.Int.pipe(Schema.positive()),
    });
    const GetUserQuery = Schema.Struct({ id: QuerySchema.Number });
    
    const api = Api.make({ title: "Users API" }).pipe(
      Api.addEndpoint(
        Api.get("getUser", "/user").pipe(
          Api.setResponseBody(UserResponse),
          Api.setRequestQuery(GetUserQuery)
        )
      )
    );
    
    // 2. Implementation
    const app = RouterBuilder.make(api).pipe(
      RouterBuilder.handle("getUser", ({ query }) =>
        Effect.succeed({ name: "milan", id: query.id })
      ),
      RouterBuilder.build
    );
    
    // 3. Client
    const client = Client.make(api, { baseUrl: "http://localhost:3000" });
    
    // 4. Server
    app.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain);
    
    // Usage
    const response = client.getUser({ query: { id: 12 } }).pipe(
      Effect.flatMap((user) => Effect.log(`Got ${user.name}, nice!`)),
    );
  6. Add descriptions to OpenAPI specifications

    master

    You can enrich your OpenAPI documentation by adding descriptions to schemas and endpoints.

    1. Schema Descriptions: Use Schema.annotations({ description: '...' }) on your schemas. Built-in effect/Schema combinators (like Schema.positive()) also automatically propagate descriptions.
    2. Parameter Descriptions: Descriptions applied to query parameters or path parameters are propagated to the OpenAPI spec.
    3. Operation Descriptions: To describe an entire endpoint, pass a 4th argument to the API method (e.g., Api.get, Api.post) containing a description field.
    import { Effect, Schema } from "effect";
    import { Api, QuerySchema, RouterBuilder } from "effect-http";
    
    const Response = Schema.Struct({
      name: Schema.String,
      id: Schema.Number.pipe(Schema.int(), Schema.positive()),
    }).pipe(Schema.annotations({ description: "User" }));
    
    const Query = Schema.Struct({
      id: QuerySchema.Number.pipe(Schema.annotations({ description: "User id" })),
    });
    
    const api = Api.make({ title: "Users API" }).pipe(
      Api.addEndpoint(
        Api.get("getUser", "/user", { description: "Returns a User by id" }).pipe(
          Api.setResponseBody(Response),
          Api.setRequestQuery(Query)
        )
      )
    );
  7. Test the server with NodeTesting

    master

    For integration or contract testing, use NodeTesting.make(app, api) from effect-http-node. This generates a testing client that runs your endpoint handlers in place.

    Note: If your server uses Dependency Injection (DI) services, you must provide them in your test code using Effect.provide or Effect.provideService to satisfy the requirements of the handlers.

    import { it } from "@effect/vitest";
    import { NodeTesting } from "effect-http-node";
    import { Effect } from "effect";
    
    it.scoped("test /hello endpoint", () =>
      Effect.gen(function* () {
        const response = yield* NodeTesting.make(app, api).pipe(
          Effect.flatMap((client) => client.hello({ query: { input: 12 } }))
        );
    
        expect(response).toEqual("13");
      })
    );
  8. Generate an Example Server from an API specification

    master

    If you are designing an API or want to test a frontend against a mock backend, you can use ExampleServer.make to generate a RouterBuilder that returns valid example responses based on your Api definition. This is useful for prototyping or integration testing without a real service.

    import { NodeRuntime } from "@effect/platform-node";
    import { Effect, Schema } from "effect";
    import { Api, ExampleServer, RouterBuilder } from "effect-http";
    import { NodeServer } from "effect-http-node";
    
    const Response = Schema.Struct({
      name: Schema.String,
      value: Schema.Number,
    });
    
    const api = Api.make({
      servers: ["http://localhost:3000", { description: "hello", url: "/api/" }],
    }).pipe(
      Api.addEndpoint(Api.get("test", "/test").pipe(Api.setResponseBody(Response)))
    );
    
    ExampleServer.make(api).pipe(
      RouterBuilder.build,
      NodeServer.listen({ port: 3000 }),
      NodeRuntime.runMain
    );
  9. Scale up APIs using ApiGroups and RouterBuilder merging

    master

    For large applications, organize your API into logical modules using ApiGroup.

    1. Define Endpoints: Declare endpoints independently using Api.get, Api.post, etc.
    2. Group Endpoints: Use ApiGroup.make to collect related endpoints into a group.
    3. Compose API: Combine multiple ApiGroups into a single Api using Api.addGroup.
    4. Implement Handlers: Use Handler.make to create handlers for your endpoints. You can use the data-first style Handler.make(endpoint, implementation) or the pipe style endpoint.pipe(Handler.make(implementation)).
    5. Merge Routers: Use RouterBuilder.merge to combine multiple RouterBuilder instances into a single application.
    import { Api, ApiGroup, Handler, RouterBuilder } from 'effect-http';
    import { Effect } from 'effect';
    
    // 1. Define endpoints
    export const getArticleEndpoint = Api.get("getArticle", "/article").pipe(
      Api.setResponseBody(Response)
    );
    
    // 2. Group them
    export const articleApi = ApiGroup.make("Articles").pipe(
      ApiGroup.addEndpoint(getArticleEndpoint)
    );
    
    // 3. Combine into main API
    export const api = Api.make().pipe(
      Api.addGroup(articleApi)
    );
    
    // 4. Implement handlers
    const getArticleHandler = getArticleEndpoint.pipe(
      Handler.make(() => Effect.succeed(someResponse))
    );
    
    // 5. Build and merge routers
    const articleRouterBuilder = RouterBuilder.make(api).pipe(
      RouterBuilder.handle(getArticleHandler)
    );
    
    const userRouterBuilder = /* ... */; 
    
    const app = RouterBuilder.make(api).pipe(
      RouterBuilder.merge(userRouterBuilder),
      RouterBuilder.merge(articleRouterBuilder),
      RouterBuilder.build
    );
  10. Install effect-http and effect-http-node

    master

    To use effect-http, install the platform-agnostic package. If you intend to run an HTTP server in a Node.js environment, you must also install effect-http-node.

    Peer Dependencies: You must have effect, @effect/platform, and @effect/platform-node installed. If you don't have them, install them using:

    pnpm add effect @effect/platform @effect/platform-node
    pnpm add effect-http effect-http-node
  11. Define API paths and operations

    master

    API endpoints are defined using OpenAPISpecPaths, which is a record where keys are path strings (e.g., /users) and values are OpenAPISpecPathItem objects.

    A OpenAPISpecPathItem can contain multiple HTTP methods (defined by OpenAPISpecMethodName) and common metadata like summary, description, or parameters that apply to all methods on that path.

    Supported methods (OpenAPISpecMethodName):

    • get, put, post, delete, options, head, patch, trace.
  12. Understand the OpenAPISpec structure

    master

    The OpenAPISpec interface defines the root structure for an OpenAPI 3.0.3 specification used within effect-http. It includes metadata about the API, server information, available paths, shared components, and security requirements.

    Key top-level properties:

    • openapi: Must be exactly "3.0.3".
    • info: An OpenAPISpecInfo object containing the title and version.
    • paths: An OpenAPISpecPaths object mapping URL paths to operations.
    • components: An OpenAPIComponents object for reusable schemas and security schemes.
    • servers: An optional array of OpenAPISpecServer objects.
    import { OpenAPISpec } from 'effect-http';
    
    const spec: OpenAPISpec = {
      openapi: "3.0.3",
      info: {
        title: "My API",
        version: "1.0.0"
      },
      paths: {
        "/hello": {
          get: {
            summary: "Say hello",
            responses: {
              200: {
                description: "Successful response",
                content: {
                  "application/json": {
                    schema: { type: "string" }
                  }
                }
              }
            }
          }
        }
      }
    };