npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@effect/platform

v0.77.2

Published

Unified interfaces for common platform-specific services

Downloads

3,026,170

Readme

Introduction

Welcome to the documentation for @effect/platform, a library designed for creating platform-independent abstractions (Node.js, Bun, browsers).

[!WARNING] This documentation focuses on unstable modules. For stable modules, refer to the official website documentation.

Running Your Main Program with runMain

runMain helps you execute a main effect with built-in error handling, logging, and signal management. You can concentrate on your effect while runMain looks after finalizing resources, logging errors, and setting exit codes.

  • Exit Codes If your effect fails or is interrupted, runMain assigns a suitable exit code (for example, 1 for errors and 0 for success).
  • Logs By default, it records errors. This can be turned off if needed.
  • Pretty Logging By default, error messages are recorded using a "pretty" format. You can switch this off when required.
  • Interrupt Handling If the application receives SIGINT (Ctrl+C) or a similar signal, runMain will interrupt the effect and still run any necessary teardown steps.
  • Teardown Logic You can rely on the default teardown or define your own. The default sets an exit code of 1 for a non-interrupted failure.

Usage Options

When calling runMain, pass in a configuration object with these fields (all optional):

  • disableErrorReporting: If true, errors are not automatically logged.
  • disablePrettyLogger: If true, it avoids adding the "pretty" logger.
  • teardown: Provide a custom function for finalizing the program. If missing, the default sets exit code 1 for a non-interrupted failure.

Example (Running a Successful Program)

import { NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const success = Effect.succeed("Hello, World!")

NodeRuntime.runMain(success)
// No Output

Example (Running a Failing Program)

import { NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const failure = Effect.fail("Uh oh!")

NodeRuntime.runMain(failure)
/*
Output:
[12:43:07.186] ERROR (#0):
  Error: Uh oh!
*/

Example (Running a Failing Program Without Pretty Logger)

import { NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const failure = Effect.fail("Uh oh!")

NodeRuntime.runMain(failure, { disablePrettyLogger: true })
/*
Output:
timestamp=2025-01-14T11:43:46.276Z level=ERROR fiber=#0 cause="Error: Uh oh!"
*/

Example (Running a Failing Program Without Error Reporting)

import { NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const failure = Effect.fail("Uh oh!")

NodeRuntime.runMain(failure, { disableErrorReporting: true })
// No Output

Example (Running a Failing Program With Custom Teardown)

import { NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const failure = Effect.fail("Uh oh!")

NodeRuntime.runMain(failure, {
  teardown: function customTeardown(exit, onExit) {
    if (exit._tag === "Failure") {
      console.error("Program ended with an error.")
      onExit(1)
    } else {
      console.log("Program finished successfully.")
      onExit(0)
    }
  }
})
/*
Output:
[12:46:39.871] ERROR (#0):
  Error: Uh oh!
Program ended with an error.
*/

HTTP API

Overview

The HttpApi* modules offer a flexible and declarative way to define HTTP APIs.

To define an API, create a set of HttpEndpoints. Each endpoint is described by a path, a method, and schemas for the request and response.

Collections of endpoints are grouped in an HttpApiGroup, and multiple groups can be merged into a complete HttpApi.

HttpApi
├── HttpGroup
│   ├── HttpEndpoint
│   └── HttpEndpoint
└── HttpGroup
    ├── HttpEndpoint
    ├── HttpEndpoint
    └── HttpEndpoint

Once your API is defined, the same definition can be reused for multiple purposes:

  • Starting a Server: Use the API definition to implement and serve endpoints.
  • Generating Documentation: Create a Swagger page to document the API.
  • Deriving a Client: Generate a fully-typed client for your API.

Benefits of a Single API Definition:

  • Consistency: A single definition ensures the server, documentation, and client remain aligned.
  • Reduced Maintenance: Changes to the API are reflected across all related components.
  • Simplified Workflow: Avoids duplication by consolidating API details in one place.

Hello World

Defining and Implementing an API

This example demonstrates how to define and implement a simple API with a single endpoint that returns a string response. The structure of the API is as follows:

HttpApi ("MyApi)
└── HttpGroup ("Greetings")
    └── HttpEndpoint ("hello-world")

Example (Hello World Definition)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"

// Define our API with one group named "Greetings" and one endpoint called "hello-world"
const MyApi = HttpApi.make("MyApi").add(
  HttpApiGroup.make("Greetings").add(
    HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
  )
)

// Implement the "Greetings" group
const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
  handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
)

// Provide the implementation for the API
const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))

// Set up the server using NodeHttpServer on port 3000
const ServerLive = HttpApiBuilder.serve().pipe(
  Layer.provide(MyApiLive),
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

// Launch the server
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)

After running the code, open a browser and navigate to http://localhost:3000. The server will respond with:

Hello, World!

Serving The Auto Generated Swagger Documentation

You can enhance your API by adding auto-generated Swagger documentation using the HttpApiSwagger module. This makes it easier for developers to explore and interact with your API.

To include Swagger in your server setup, provide the HttpApiSwagger.layer when configuring the server.

Example (Serving Swagger Documentation)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSwagger
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"

const MyApi = HttpApi.make("MyApi").add(
  HttpApiGroup.make("Greetings").add(
    HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
  )
)

const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
  handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
)

const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))

const ServerLive = HttpApiBuilder.serve().pipe(
  // Provide the Swagger layer so clients can access auto-generated docs
  Layer.provide(HttpApiSwagger.layer()),
  Layer.provide(MyApiLive),
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(ServerLive).pipe(NodeRuntime.runMain)

After running the server, open your browser and navigate to http://localhost:3000/docs.

This URL will display the Swagger documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively.

Swagger Documentation

Deriving a Client

Once you have defined your API, you can generate a client to interact with it using the HttpApiClient module. This allows you to call your API endpoints without manually handling HTTP requests.

Example (Deriving and Using a Client)

import {
  FetchHttpClient,
  HttpApi,
  HttpApiBuilder,
  HttpApiClient,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSwagger
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"

const MyApi = HttpApi.make("MyApi").add(
  HttpApiGroup.make("Greetings").add(
    HttpApiEndpoint.get("hello-world")`/`.addSuccess(Schema.String)
  )
)

const GreetingsLive = HttpApiBuilder.group(MyApi, "Greetings", (handlers) =>
  handlers.handle("hello-world", () => Effect.succeed("Hello, World!"))
)

const MyApiLive = HttpApiBuilder.api(MyApi).pipe(Layer.provide(GreetingsLive))

const ServerLive = HttpApiBuilder.serve().pipe(
  Layer.provide(HttpApiSwagger.layer()),
  Layer.provide(MyApiLive),
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(ServerLive).pipe(NodeRuntime.runMain)

// Create a program that derives and uses the client
const program = Effect.gen(function* () {
  // Derive the client
  const client = yield* HttpApiClient.make(MyApi, {
    baseUrl: "http://localhost:3000"
  })
  // Call the "hello-world" endpoint
  const hello = yield* client.Greetings["hello-world"]()
  console.log(hello)
})

// Provide a Fetch-based HTTP client and run the program
Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer)))
// Output: Hello, World!

Defining a HttpApiEndpoint

An HttpApiEndpoint represents a single endpoint in your API. Each endpoint is defined with a name, path, HTTP method, and optional schemas for requests and responses. This allows you to describe the structure and behavior of your API.

Below is an example of a simple CRUD API for managing users, which includes the following endpoints:

  • GET /users - Retrieve all users.
  • GET /users/:userId - Retrieve a specific user by ID.
  • POST /users - Create a new user.
  • DELETE /users/:userId - Delete a user by ID.
  • PATCH /users/:userId - Update a user by ID.

GET

The HttpApiEndpoint.get method allows you to define a GET endpoint by specifying its name, path, and optionally, a schema for the response.

To define the structure of successful responses, use the .addSuccess method. If no schema is provided, the default response status is 204 No Content.

Example (Defining a GET Endpoint to Retrieve All Users)

import { HttpApiEndpoint } from "@effect/platform"
import { Schema } from "effect"

// Define a schema representing a User entity
const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

// Define the "getUsers" endpoint, returning a list of users
const getUsers = HttpApiEndpoint
  //      ┌─── Endpoint name
  //      │            ┌─── Endpoint path
  //      ▼            ▼
  .get("getUsers", "/users")
  // Define the success schema for the response (optional).
  // If no response schema is specified, the default response is `204 No Content`.
  .addSuccess(Schema.Array(User))

Path Parameters

Path parameters allow you to include dynamic segments in your endpoint's path. There are two ways to define path parameters in your API.

Using setPath

The setPath method allows you to explicitly define path parameters by associating them with a schema.

Example (Defining Parameters with setPath)

import { HttpApiEndpoint } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

// Define a GET endpoint with a path parameter ":id"
const getUser = HttpApiEndpoint.get("getUser", "/user/:id")
  .setPath(
    Schema.Struct({
      // Define a schema for the "id" path parameter
      id: Schema.NumberFromString
    })
  )
  .addSuccess(User)

Using Template Strings

You can also define path parameters by embedding them in a template string with the help of HttpApiSchema.param.

Example (Defining Parameters using a Template String)

import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

// Create a path parameter using HttpApiSchema.param
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

// Define the GET endpoint using a template string
const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
  User
)

POST

The HttpApiEndpoint.post method is used to define an endpoint for creating resources. You can specify a schema for the request body (payload) and a schema for the successful response.

Example (Defining a POST Endpoint with Payload and Success Schemas)

import { HttpApiEndpoint } from "@effect/platform"
import { Schema } from "effect"

// Define a schema for the user object
const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

// Define a POST endpoint for creating a new user
const createUser = HttpApiEndpoint.post("createUser", "/users")
  // Define the request body schema (payload)
  .setPayload(
    Schema.Struct({
      name: Schema.String
    })
  )
  // Define the schema for a successful response
  .addSuccess(User)

DELETE

The HttpApiEndpoint.del method is used to define an endpoint for deleting a resource.

Example (Defining a DELETE Endpoint with Path Parameters)

import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

// Define a path parameter for the user ID
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

// Define a DELETE endpoint to delete a user by ID
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`

PATCH

The HttpApiEndpoint.patch method is used to define an endpoint for partially updating a resource. This method allows you to specify a schema for the request payload and a schema for the successful response.

Example (Defining a PATCH Endpoint for Updating a User)

import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

// Define a schema for the user object
const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

// Define a path parameter for the user ID
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

// Define a PATCH endpoint to update a user's name by ID
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
  // Specify the schema for the request payload
  .setPayload(
    Schema.Struct({
      name: Schema.String // Only the name can be updated
    })
  )
  // Specify the schema for a successful response
  .addSuccess(User)

Catch-All Endpoints

The path can also be "*" to match any incoming path. This is useful for defining a catch-all endpoint to handle unmatched routes or provide a fallback response.

Example (Defining a Catch-All Endpoint)

import { HttpApiEndpoint } from "@effect/platform"

const catchAll = HttpApiEndpoint.get("catchAll", "*")

Setting URL Parameters

The setUrlParams method allows you to define the structure of URL parameters for an endpoint. You can specify the schema for each parameter and include metadata such as descriptions to provide additional context.

Example (Defining URL Parameters with Metadata)

import { HttpApiEndpoint } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const getUsers = HttpApiEndpoint.get("getUsers", "/users")
  // Specify the URL parameters schema
  .setUrlParams(
    Schema.Struct({
      // Parameter "page" for pagination
      page: Schema.NumberFromString,
      // Parameter "sort" for sorting options with an added description
      sort: Schema.String.annotations({
        description: "Sorting criteria (e.g., 'name', 'date')"
      })
    })
  )
  .addSuccess(Schema.Array(User))

Defining an Array of Values for a URL Parameter

When defining a URL parameter that accepts multiple values, you can use the Schema.Array combinator. This allows the parameter to handle an array of items, with each item adhering to a specified schema.

Example (Defining an Array of String Values for a URL Parameter)

import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
import { Schema } from "effect"

const api = HttpApi.make("myApi").add(
  HttpApiGroup.make("group").add(
    HttpApiEndpoint.get("get", "/")
      .setUrlParams(
        Schema.Struct({
          // Define "a" as an array of strings
          a: Schema.Array(Schema.String)
        })
      )
      .addSuccess(Schema.String)
  )
)

You can test this endpoint by passing an array of values in the query string. For example:

curl "http://localhost:3000/?a=1&a=2"

The query string sends two values (1 and 2) for the a parameter. The server will process and validate these values according to the schema.

Status Codes

By default, the success status code is 200 OK. You can change it by annotating the schema with a custom status.

Example (Defining a GET Endpoint with a custom status code)

import { HttpApiEndpoint } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const getUsers = HttpApiEndpoint.get("getUsers", "/users")
  // Override the default success status
  .addSuccess(Schema.Array(User), { status: 206 })

Handling Multipart Requests

To support file uploads, you can use the HttpApiSchema.Multipart API. This allows you to define an endpoint's payload schema as a multipart request, specifying the structure of the data, including file uploads, with the Multipart module.

Example (Defining an Endpoint for File Uploads)

In this example, the HttpApiSchema.Multipart function marks the payload as a multipart request. The files field uses Multipart.FilesSchema to handle uploaded file data automatically.

import { HttpApiEndpoint, HttpApiSchema, Multipart } from "@effect/platform"
import { Schema } from "effect"

const upload = HttpApiEndpoint.post("upload", "/users/upload").setPayload(
  // Specify that the payload is a multipart request
  HttpApiSchema.Multipart(
    Schema.Struct({
      // Define a "files" field to handle file uploads
      files: Multipart.FilesSchema
    })
  ).addSuccess(Schema.String)
)

You can test this endpoint by sending a multipart request with a file upload. For example:

echo "Sample file content" | curl -X POST -F "files=@-" http://localhost:3000/users/upload

Changing the Request Encoding

By default, API requests are encoded as JSON. If your application requires a different format, you can customize the request encoding using the HttpApiSchema.withEncoding method. This allows you to define the encoding type and content type of the request.

Example (Customizing Request Encoding)

import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

const createUser = HttpApiEndpoint.post("createUser", "/users")
  // Set the request payload as a string encoded with URL parameters
  .setPayload(
    Schema.Struct({
      a: Schema.String // Parameter "a" must be a string
    })
      // Specify the encoding as URL parameters
      .pipe(HttpApiSchema.withEncoding({ kind: "UrlParams" }))
  )

Changing the Response Encoding

By default, API responses are encoded as JSON. If your application requires a different format, you can customize the encoding using the HttpApiSchema.withEncoding API. This method lets you define the type and content type of the response.

Example (Returning Data as text/csv)

import { HttpApiEndpoint, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

const csv = HttpApiEndpoint.get("csv")`/users/csv`
  // Set the success response as a string with CSV encoding
  .addSuccess(
    Schema.String.pipe(
      HttpApiSchema.withEncoding({
        // Specify the type of the response
        kind: "Text",
        // Define the content type as text/csv
        contentType: "text/csv"
      })
    )
  )

Setting Request Headers

The HttpApiEndpoint.setHeaders method allows you to define the expected structure of request headers. You can specify the schema for each header and include additional metadata, such as descriptions.

Example (Defining Request Headers with Metadata)

import { HttpApiEndpoint } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const getUsers = HttpApiEndpoint.get("getUsers", "/users")
  // Specify the headers schema
  .setHeaders(
    Schema.Struct({
      // Header must be a string
      "X-API-Key": Schema.String,
      // Header must be a string with an added description
      "X-Request-ID": Schema.String.annotations({
        description: "Unique identifier for the request"
      })
    })
  )
  .addSuccess(Schema.Array(User))

Defining a HttpApiGroup

You can group related endpoints under a single entity by using HttpApiGroup.make. This can help organize your code and provide a clearer structure for your API.

Example (Creating a Group for User-Related Endpoints)

import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
  Schema.Array(User)
)

const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
  User
)

const createUser = HttpApiEndpoint.post("createUser", "/users")
  .setPayload(
    Schema.Struct({
      name: Schema.String
    })
  )
  .addSuccess(User)

const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`

const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
  .setPayload(
    Schema.Struct({
      name: Schema.String
    })
  )
  .addSuccess(User)

// Group all user-related endpoints
const usersGroup = HttpApiGroup.make("users")
  .add(getUsers)
  .add(getUser)
  .add(createUser)
  .add(deleteUser)
  .add(updateUser)

If you would like to create a more opaque type for the group, you can extend HttpApiGroup with a class.

Example (Creating a Group with an Opaque Type)

// Create an opaque class extending HttpApiGroup
class UsersGroup extends HttpApiGroup.make("users").add(getUsers).add(getUser) {
  // Additional endpoints or methods can be added here
}

Creating the Top-Level HttpApi

After defining your groups, you can combine them into one HttpApi representing your entire set of endpoints.

Example (Combining Groups into a Top-Level API)

import {
  HttpApi,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema
} from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
  Schema.Array(User)
)

const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(
  User
)

const createUser = HttpApiEndpoint.post("createUser", "/users")
  .setPayload(
    Schema.Struct({
      name: Schema.String
    })
  )
  .addSuccess(User)

const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`

const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
  .setPayload(
    Schema.Struct({
      name: Schema.String
    })
  )
  .addSuccess(User)

const usersGroup = HttpApiGroup.make("users")
  .add(getUsers)
  .add(getUser)
  .add(createUser)
  .add(deleteUser)
  .add(updateUser)

// Combine the groups into one API
const api = HttpApi.make("myApi").add(usersGroup)

// Alternatively, create an opaque class for your API
class MyApi extends HttpApi.make("myApi").add(usersGroup) {}

Adding errors

Error responses allow your API to handle different failure scenarios. These responses can be defined at various levels:

  • Endpoint-level errors: Use HttpApiEndpoint.addError to add errors specific to an endpoint.
  • Group-level errors: Use HttpApiGroup.addError to add errors applicable to all endpoints in a group.
  • API-level errors: Use HttpApi.addError to define errors that apply to every endpoint in the API.

Group-level and API-level errors are useful for handling shared issues like authentication failures, especially when managed through middleware.

Example (Defining Error Responses for Endpoints and Groups)

import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

// Define error schemas
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
  "UserNotFound",
  {}
) {}

class Unauthorized extends Schema.TaggedError<Unauthorized>()(
  "Unauthorized",
  {}
) {}

const getUsers = HttpApiEndpoint.get("getUsers", "/users").addSuccess(
  Schema.Array(User)
)

const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
  .addSuccess(User)
  // Add a 404 error response for this endpoint
  .addError(UserNotFound, { status: 404 })

const usersGroup = HttpApiGroup.make("users")
  .add(getUsers)
  .add(getUser)
  // ...etc...
  // Add a 401 error response for the entire group
  .addError(Unauthorized, { status: 401 })

You can assign multiple error responses to a single endpoint by calling HttpApiEndpoint.addError multiple times. This is useful when different types of errors might occur for a single operation.

Example (Adding Multiple Errors to an Endpoint)

const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
  // Add a 404 error response for when the user is not found
  .addError(UserNotFound, { status: 404 })
  // Add a 401 error response for unauthorized access
  .addError(Unauthorized, { status: 401 })

Predefined Empty Error Types

The HttpApiError module provides a set of predefined empty error types that you can use in your endpoints. These error types help standardize common HTTP error responses, such as 404 Not Found or 401 Unauthorized. Using these predefined types simplifies error handling and ensures consistency across your API.

Example (Adding a Predefined Error to an Endpoint)

import { HttpApiEndpoint, HttpApiError, HttpApiSchema } from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const getUser = HttpApiEndpoint.get("getUser")`/user/${idParam}`
  .addSuccess(User)
  .addError(HttpApiError.NotFound)

| Name | Status | Description | | --------------------- | ------ | -------------------------------------------------------------------------------------------------- | | HttpApiDecodeError | 400 | Represents an error where the request did not match the expected schema. Includes detailed issues. | | BadRequest | 400 | Indicates that the request was malformed or invalid. | | Unauthorized | 401 | Indicates that authentication is required but missing or invalid. | | Forbidden | 403 | Indicates that the client does not have permission to access the requested resource. | | NotFound | 404 | Indicates that the requested resource could not be found. | | MethodNotAllowed | 405 | Indicates that the HTTP method used is not allowed for the requested resource. | | NotAcceptable | 406 | Indicates that the requested resource cannot be delivered in a format acceptable to the client. | | RequestTimeout | 408 | Indicates that the server timed out waiting for the client request. | | Conflict | 409 | Indicates a conflict in the request, such as conflicting data. | | Gone | 410 | Indicates that the requested resource is no longer available and will not return. | | InternalServerError | 500 | Indicates an unexpected server error occurred. | | NotImplemented | 501 | Indicates that the requested functionality is not implemented on the server. | | ServiceUnavailable | 503 | Indicates that the server is temporarily unavailable, often due to maintenance or overload. |

Prefixing

Prefixes can be added to endpoints, groups, or an entire API to simplify the management of common paths. This is especially useful when defining multiple related endpoints that share a common base URL.

Example (Using Prefixes for Common Path Management)

import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
import { Schema } from "effect"

const api = HttpApi.make("api")
  .add(
    HttpApiGroup.make("group")
      .add(
        HttpApiEndpoint.get("getRoot", "/")
          .addSuccess(Schema.String)
          // Prefix for this endpoint
          .prefix("/endpointPrefix")
      )
      .add(HttpApiEndpoint.get("getA", "/a").addSuccess(Schema.String))
      // Prefix for all endpoints in the group
      .prefix("/groupPrefix")
  )
  // Prefix for the entire API
  .prefix("/apiPrefix")

Implementing a Server

After defining your API, you can implement a server to handle its endpoints. The HttpApiBuilder module provides tools to help you connect your API's structure to the logic that serves requests.

Here, we will create a simple example with a getUser endpoint organized within a users group.

Example (Defining the users Group and API)

import {
  HttpApi,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema
} from "@effect/platform"
import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
)

const api = HttpApi.make("myApi").add(usersGroup)

Implementing a HttpApiGroup

The HttpApiBuilder.group API is used to implement a specific group of endpoints within an HttpApi definition. It requires the following inputs:

| Input | Description | | --------------------------------- | ----------------------------------------------------------------------- | | The complete HttpApi definition | The overall API structure that includes the group you are implementing. | | The name of the group | The specific group you are focusing on within the API. | | A function to add handlers | A function that defines how each endpoint in the group is handled. |

Each endpoint in the group is connected to its logic using the HttpApiBuilder.handle method, which maps the endpoint's definition to its corresponding implementation.

The HttpApiBuilder.group API produces a Layer that can later be provided to the server implementation.

Example (Implementing a Group with Endpoint Logic)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema
} from "@effect/platform"
import { DateTime, Effect, Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
)

const api = HttpApi.make("myApi").add(usersGroup)

// --------------------------------------------
// Implementation
// --------------------------------------------

//      ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">>
//      ▼
const usersGroupLive =
  //                    ┌─── The Whole API
  //                    │      ┌─── The Group you are implementing
  //                    ▼      ▼
  HttpApiBuilder.group(api, "users", (handlers) =>
    handlers.handle(
      //  ┌─── The Endpoint you are implementing
      //  ▼
      "getUser",
      // Provide the handler logic for the endpoint.
      // The parameters & payload are passed to the handler function.
      ({ path: { id } }) =>
        Effect.succeed(
          // Return a mock user object with the provided ID
          {
            id,
            name: "John Doe",
            createdAt: DateTime.unsafeNow()
          }
        )
    )
  )

Using HttpApiBuilder.group, you connect the structure of your API to its logic, enabling you to focus on each endpoint's functionality in isolation. Each handler receives the parameters and payload for the request, making it easy to process input and generate a response.

Using Services Inside a HttpApiGroup

If your handlers need to use services, you can easily integrate them because the HttpApiBuilder.group API allows you to return an Effect. This ensures that external services can be accessed and utilized directly within your handlers.

Example (Using Services in a Group Implementation)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema
} from "@effect/platform"
import { Context, Effect, Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
)

const api = HttpApi.make("myApi").add(usersGroup)

// --------------------------------------------
// Implementation
// --------------------------------------------

type User = typeof User.Type

// Define the UsersRepository service
class UsersRepository extends Context.Tag("UsersRepository")<
  UsersRepository,
  {
    readonly findById: (id: number) => Effect.Effect<User>
  }
>() {}

// Implement the `users` group with access to the UsersRepository service
//
//      ┌─── Layer<HttpApiGroup.ApiGroup<"myApi", "users">, never, UsersRepository>
//      ▼
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
  Effect.gen(function* () {
    // Access the UsersRepository service
    const repository = yield* UsersRepository
    return handlers.handle("getUser", ({ path: { id } }) =>
      repository.findById(id)
    )
  })
)

Implementing a HttpApi

Once all your groups are implemented, you can create a top-level implementation to combine them into a unified API. This is done using the HttpApiBuilder.api API, which generates a Layer. You then use Layer.provide to include the implementations of all the groups into the top-level HttpApi.

Example (Combining Group Implementations into a Top-Level API)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema
} from "@effect/platform"
import { DateTime, Effect, Layer, Schema } from "effect"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
)

const api = HttpApi.make("myApi").add(usersGroup)

const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
  handlers.handle("getUser", ({ path: { id } }) =>
    Effect.succeed({
      id,
      name: "John Doe",
      createdAt: DateTime.unsafeNow()
    })
  )
)

// Combine all group implementations into the top-level API
//
//      ┌─── Layer<HttpApi.Api, never, never>
//      ▼
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))

Serving the API

You can serve your API using the HttpApiBuilder.serve function. This utility builds an HttpApp from an HttpApi instance and uses an HttpServer to handle requests. Middleware can be added to customize or enhance the server's behavior.

Example (Setting Up and Serving an API with Middleware)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema,
  HttpMiddleware,
  HttpServer
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { DateTime, Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
)

const api = HttpApi.make("myApi").add(usersGroup)

const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
  handlers.handle("getUser", ({ path: { id } }) =>
    Effect.succeed({
      id,
      name: "John Doe",
      createdAt: DateTime.unsafeNow()
    })
  )
)

const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))

// Configure and serve the API
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
  // Add CORS middleware to handle cross-origin requests
  Layer.provide(HttpApiBuilder.middlewareCors()),
  // Provide the API implementation
  Layer.provide(MyApiLive),
  // Log the server's listening address
  HttpServer.withLogAddress,
  // Set up the Node.js HTTP server
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

// Launch the server
Layer.launch(HttpLive).pipe(NodeRuntime.runMain)

Accessing the HttpServerRequest

In some cases, you may need to access details about the incoming HttpServerRequest within an endpoint handler. The HttpServerRequest module provides access to the request object, allowing you to inspect properties such as the HTTP method or headers.

Example (Accessing the Request Object in a GET Endpoint)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpMiddleware,
  HttpServer,
  HttpServerRequest
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"

const api = HttpApi.make("myApi").add(
  HttpApiGroup.make("group").add(
    HttpApiEndpoint.get("get", "/").addSuccess(Schema.String)
  )
)

const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
  handlers.handle("get", () =>
    Effect.gen(function* () {
      // Access the incoming request
      const req = yield* HttpServerRequest.HttpServerRequest

      // Log the HTTP method for demonstration purposes
      console.log(req.method)

      // Return a response
      return "Hello, World!"
    })
  )
)

const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))

const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
  Layer.provide(HttpApiBuilder.middlewareCors()),
  Layer.provide(MyApiLive),
  HttpServer.withLogAddress,
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(HttpLive).pipe(NodeRuntime.runMain)

Streaming Requests

Streaming requests allow you to send large or continuous data streams to the server. In this example, we define an API that accepts a stream of binary data and decodes it into a string.

Example (Handling Streaming Requests)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema,
  HttpMiddleware,
  HttpServer
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"

const api = HttpApi.make("myApi").add(
  HttpApiGroup.make("group").add(
    HttpApiEndpoint.post("acceptStream", "/stream")
      // Define the payload as a Uint8Array with a specific encoding
      .setPayload(
        Schema.Uint8ArrayFromSelf.pipe(
          HttpApiSchema.withEncoding({
            kind: "Uint8Array",
            contentType: "application/octet-stream"
          })
        )
      )
      .addSuccess(Schema.String)
  )
)

const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
  handlers.handle("acceptStream", (req) =>
    // Decode the incoming binary data into a string
    Effect.succeed(new TextDecoder().decode(req.payload))
  )
)

const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))

const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
  Layer.provide(HttpApiBuilder.middlewareCors()),
  Layer.provide(MyApiLive),
  HttpServer.withLogAddress,
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(HttpLive).pipe(NodeRuntime.runMain)

You can test the streaming request using curl or any tool that supports sending binary data. For example:

echo "abc" | curl -X POST 'http://localhost:3000/stream' --data-binary @- -H "Content-Type: application/octet-stream"
# Output: abc

Streaming Responses

To handle streaming responses in your API, you can use handleRaw. The HttpServerResponse.stream function is designed to return a continuous stream of data as the response.

Example (Implementing a Streaming Endpoint)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema,
  HttpMiddleware,
  HttpServer,
  HttpServerResponse
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Layer, Schedule, Schema, Stream } from "effect"
import { createServer } from "node:http"

// Define the API with a single streaming endpoint
const api = HttpApi.make("myApi").add(
  HttpApiGroup.make("group").add(
    HttpApiEndpoint.get("getStream", "/stream").addSuccess(
      Schema.String.pipe(
        HttpApiSchema.withEncoding({
          kind: "Text",
          contentType: "application/octet-stream"
        })
      )
    )
  )
)

// Simulate a stream of data
const stream = Stream.make("a", "b", "c").pipe(
  Stream.schedule(Schedule.spaced("500 millis")),
  Stream.map((s) => new TextEncoder().encode(s))
)

const groupLive = HttpApiBuilder.group(api, "group", (handlers) =>
  handlers.handleRaw("getStream", () => HttpServerResponse.stream(stream))
)

const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive))

const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
  Layer.provide(HttpApiBuilder.middlewareCors()),
  Layer.provide(MyApiLive),
  HttpServer.withLogAddress,
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(HttpLive).pipe(NodeRuntime.runMain)

You can test the streaming response using curl or any similar HTTP client that supports streaming:

curl 'http://localhost:3000/stream' --no-buffer

The response will stream data (a, b, c) with a 500ms interval between each item.

Middlewares

Defining Middleware

The HttpApiMiddleware module allows you to add middleware to your API. Middleware can enhance your API by introducing features like logging, authentication, or additional error handling.

You can define middleware using the HttpApiMiddleware.Tag class, which lets you specify:

| Option | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | failure | A schema that describes any errors the middleware might return. | | provides | A Context.Tag representing the resource or data the middleware will provide to subsequent handlers. | | security | Definitions from HttpApiSecurity that the middleware will implement, such as authentication mechanisms. | | optional | A boolean indicating whether the request should continue if the middleware fails with an expected error. When optional is set to true, the provides and failure options do not affect the final error type or handlers. |

Example (Defining a Logger Middleware)

import {
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiMiddleware,
  HttpApiSchema
} from "@effect/platform"
import { Schema } from "effect"

// Define a schema for errors returned by the logger middleware
class LoggerError extends Schema.TaggedError<LoggerError>()(
  "LoggerError",
  {}
) {}

// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
  // Optionally define the error schema for the middleware
  failure: LoggerError
}) {}

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users")
  .add(
    HttpApiEndpoint.get("getUser")`/user/${idParam}`
      .addSuccess(User)
      // Apply the middleware to a single endpoint
      .middleware(Logger)
  )
  // Or apply the middleware to the entire group
  .middleware(Logger)

Implementing HttpApiMiddleware

Once you have defined your HttpApiMiddleware, you can implement it as a Layer. This allows the middleware to be applied to specific API groups or endpoints, enabling modular and reusable behavior.

Example (Implementing and Using Logger Middleware)

import { HttpApiMiddleware, HttpServerRequest } from "@effect/platform"
import { Effect, Layer } from "effect"

class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger") {}

const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    yield* Effect.log("creating Logger middleware")

    // Middleware implementation as an Effect
    // that can access the `HttpServerRequest` context.
    return Effect.gen(function* () {
      const request = yield* HttpServerRequest.HttpServerRequest
      yield* Effect.log(`Request: ${request.method} ${request.url}`)
    })
  })
)

After implementing the middleware, you can attach it to your API groups or specific endpoints using the Layer APIs.

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiMiddleware,
  HttpApiSchema,
  HttpServerRequest
} from "@effect/platform"
import { DateTime, Effect, Layer, Schema } from "effect"

// Define a schema for errors returned by the logger middleware
class LoggerError extends Schema.TaggedError<LoggerError>()(
  "LoggerError",
  {}
) {}

// Extend the HttpApiMiddleware.Tag class to define the logger middleware tag
class Logger extends HttpApiMiddleware.Tag<Logger>()("Http/Logger", {
  // Optionally define the error schema for the middleware
  failure: LoggerError
}) {}

const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    yield* Effect.log("creating Logger middleware")

    // Middleware implementation as an Effect
    // that can access the `HttpServerRequest` context.
    return Effect.gen(function* () {
      const request = yield* HttpServerRequest.HttpServerRequest
      yield* Effect.log(`Request: ${request.method} ${request.url}`)
    })
  })
)

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users")
  .add(
    HttpApiEndpoint.get("getUser")`/user/${idParam}`
      .addSuccess(User)
      // Apply the middleware to a single endpoint
      .middleware(Logger)
  )
  // Or apply the middleware to the entire group
  .middleware(Logger)

const api = HttpApi.make("myApi").add(usersGroup)

const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
  handlers.handle("getUser", (req) =>
    Effect.succeed({
      id: req.path.id,
      name: "John Doe",
      createdAt: DateTime.unsafeNow()
    })
  )
).pipe(
  // Provide the Logger middleware to the group
  Layer.provide(LoggerLive)
)

Defining security middleware

The HttpApiSecurity module enables you to add security annotations to your API. These annotations specify the type of authorization required to access specific endpoints.

Supported authorization types include:

| Authorization Type | Description | | ------------------------ | ---------------------------------------------------------------- | | HttpApiSecurity.apiKey | API key authorization via headers, query parameters, or cookies. | | HttpApiSecurity.basic | HTTP Basic authentication. | | HttpApiSecurity.bearer | Bearer token authentication. |

These security annotations can be used alongside HttpApiMiddleware to create middleware that protects your API endpoints.

Example (Defining Security Middleware)

import {
  HttpApi,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiMiddleware,
  HttpApiSchema,
  HttpApiSecurity
} from "@effect/platform"
import { Context, Schema } from "effect"

// Define a schema for the "User"
class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}

// Define a schema for the "Unauthorized" error
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
  "Unauthorized",
  {},
  // Specify the HTTP status code for unauthorized errors
  HttpApiSchema.annotations({ status: 401 })
) {}

// Define a Context.Tag for the authenticated user
class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}

// Create the Authorization middleware
class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
  "Authorization",
  {
    // Define the error schema for unauthorized access
    failure: Unauthorized,
    // Specify the resource this middleware will provide
    provides: CurrentUser,
    // Add security definitions
    security: {
      // ┌─── Custom name for the security definition
      // ▼
      myBearer: HttpApiSecurity.bearer
      // Additional security definitions can be added here.
      // They will attempt to be resolved in the order they are defined.
    }
  }
) {}

const api = HttpApi.make("api")
  .add(
    HttpApiGroup.make("group")
      .add(
        HttpApiEndpoint.get("get", "/")
          .addSuccess(Schema.String)
          // Apply the middleware to a single endpoint
          .middleware(Authorization)
      )
      // Or apply the middleware to the entire group
      .middleware(Authorization)
  )
  // Or apply the middleware to the entire API
  .middleware(Authorization)

Implementing HttpApiSecurity middleware

When using HttpApiSecurity in your middleware, the implementation involves creating a Layer with security handlers tailored to your requirements. Below is an example demonstrating how to implement middleware for HttpApiSecurity.bearer authentication.

Example (Implementing Bearer Token Authentication Middleware)

import {
  HttpApiMiddleware,
  HttpApiSchema,
  HttpApiSecurity
} from "@effect/platform"
import { Context, Effect, Layer, Redacted, Schema } from "effect"

class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}

class Unauthorized extends Schema.TaggedError<Unauthorized>()(
  "Unauthorized",
  {},
  HttpApiSchema.annotations({ status: 401 })
) {}

class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}

class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
  "Authorization",
  {
    failure: Unauthorized,
    provides: CurrentUser,
    security: {
      myBearer: HttpApiSecurity.bearer
    }
  }
) {}

const AuthorizationLive = Layer.effect(
  Authorization,
  Effect.gen(function* () {
    yield* Effect.log("creating Authorization middleware")

    // Return the security handlers for the middleware
    return {
      // Define the handler for the Bearer token
      // The Bearer token is redacted for security
      myBearer: (bearerToken) =>
        Effect.gen(function* () {
          yield* Effect.log(
            "checking bearer token",
            Redacted.value(bearerToken)
          )
          // Return a mock User object as the CurrentUser
          return new User({ id: 1 })
        })
    }
  })
)

Adding Descriptions to Security Definitions

The HttpApiSecurity.annotate function allows you to add metadata, such as a description, to your security definitions. This metadata is displayed in the Swagger documentation, making it easier for developers to understand your API's security requirements.

Example (Adding a Description to a Bearer Token Security Definition)

import {
  HttpApiMiddleware,
  HttpApiSchema,
  HttpApiSecurity,
  OpenApi
} from "@effect/platform"
import { Context, Schema } from "effect"

class User extends Schema.Class<User>("User")({ id: Schema.Number }) {}

class Unauthorized extends Schema.TaggedError<Unauthorized>()(
  "Unauthorized",
  {},
  HttpApiSchema.annotations({ status: 401 })
) {}

class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}

class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
  "Authorization",
  {
    failure: Unauthorized,
    provides: CurrentUser,
    security: {
      myBearer: HttpApiSecurity.bearer.pipe(
        // Add a description to the security definition
        HttpApiSecurity.annotate(OpenApi.Description, "my description")
      )
    }
  }
) {}

Setting HttpApiSecurity cookies

To set a security cookie from within a handler, you can use the HttpApiBuilder.securitySetCookie API. This method sets a cookie with default properties, including the HttpOnly and Secure flags, ensuring the cookie is not accessible via JavaScript and is transmitted over secure connections.

Example (Setting a Security Cookie in a Login Handler)

// Define the security configuration for an API key stored in a cookie
const security = HttpApiSecurity.apiKey({
   // Specify that the API key is stored in a cookie
  in: "cookie"
   // Define the cookie name,
  key: "token"
})

const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
  handlers.handle("login", () =>
    // Set the security cookie with a redacted value
    HttpApiBuilder.securitySetCookie(security, Redacted.make("keep me secret"))
  )
)

Serving Swagger documentation

You can add Swagger documentation to your API using the HttpApiSwagger module. This integration provides an interactive interface for developers to explore and test your API. To enable Swagger, you simply provide the HttpApiSwagger.layer to your server implementation.

Example (Adding Swagger Documentation to an API)

import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApiSchema,
  HttpApiSwagger,
  HttpMiddleware,
  HttpServer
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { DateTime, Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"

const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  createdAt: Schema.DateTimeUtc
})

const idParam = HttpApiSchema.param("id", Schema.NumberFromString)

const usersGroup = HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("getUser")`/user/${idParam}`.addSuccess(User)
)

const api = HttpApi.make("myApi").add(usersGroup)

const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
  handlers.handle("getUser", ({ path: { id } }) =>
    Effect.succeed({
      id,
      name: "John Doe",
      createdAt: DateTime.unsafeNow()
    })
  )
)

const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))

const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
  // Add the Swagger documentation layer
  Layer.provide(
    HttpApiSwagger.layer({
      // Specify the Swagger documentation path.
      // "/docs" is the default path.
      path: "/docs"
    })
  ),
  Layer.provide(HttpApiBuilder.middlewareCors()),
  Layer.provide(MyApiLive),
  HttpServer.withLogAddress,
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(HttpLive).pipe(NodeRuntime.runMain)

Swagger Documentation

Adding OpenAPI Annotations

You can add OpenAPI annotations to your API to include metadata such as titles, descriptions, and more. These annotations help generate richer API documentation.

HttpApi

Below is a list of available annotations for a top-level HttpApi. They can be added using the .annotate method:

| Annotation | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | | HttpApi.AdditionalSchemas | Adds custom schemas to the final OpenAPI specification. Only schemas with an identifier annotation are included. | | OpenApi.Description | Sets a general description for the API. | | OpenApi.License | Defines the license used by the API. | | OpenApi.Summary | Provides a brief summary of the API. | | OpenApi.Servers | Lists server URLs and optional metadata such as variables. | | OpenApi.Override | Merges the supplied fields into the resulting specification. | | OpenApi.Transform | Allows you to modify the final specification with a custom function. |

Example (Annotating the Top-Level API)

import { HttpApi, OpenApi } from "@effect/platform"
import { Schema } from "effect"

const api = HttpApi.make("api")
  // Provide additional schemas
  .annotate(HttpApi.AdditionalSchemas, [
    Schema.String.annotations({ identifier: "MyString" })
  ])
  // Add a description
  .annotate(OpenApi.Description, "my description")
  // Set license information
  .annotate(OpenApi.License, { name: "MIT", url: "http://example.com" })
  // Provide a summary
  .annotate(OpenApi.Summary, "my summary")
  // Define servers
  .annotate(OpenApi.Servers, [
    {
      url: "http://example.com",
      description: "example",
      variables: { a: { default: "b", enum: ["c"], description: "d" } }
    }
  ])
  // Override parts of the generated specification
  .annotate(OpenApi.Override, {
    tags: [{ name: "a", description: "a-description" }]
  })
  // Apply a transform function to the final specification
  .annotate(OpenApi.Transform, (spec) => ({
    ...spec,
    tags: [...spec.tags, { name: "b", description: "b-description" }]
  }))

// Generate the OpenAPI specification from the annotated API
const spec = OpenApi.fromApi(api)

console.log(JSON.stringify(spec, null, 2))
/*
Output:
{
  "openapi": "3.1.0",
  "info": {
    "title": "Api",
    "version": "0.0.1",
    "description": "my description",
    "license": {
      "name": "MIT",
      "url": "http://example.com"
    },
    "summary": "my summary"
  },
  "paths": {},
  "tags": [
    { "name": "a", "description": "a-description" },
    { "name": "b", "description": "b-description" }
  ],
  "components": {
    "schemas": {
      "MyString": {
        "type": "string"
      }
    },
    "securitySchemes": {}
  },
  "security": [],
  "servers": [
    {
      "url": "http://example.com",
      "description": "example",
      "variables": {
        "a": {
          "default": "b",
          "enum": [
            "c"
          ],
          "description": "d"
        }
      }
    }
  ]
}
*/

HttpApiGroup

The following annotations can be added to an HttpApiGroup:

| Annotation | Description | | ---------------------- | --------------------------------------------------------------------- | | OpenApi.Description | Sets a description for this group. | | OpenApi.ExternalDocs | Provides external documentation links for the group. | | OpenApi.Override | Merges specified fields into the resulting specification. | | OpenApi.Transform | Lets you modify the final group specification with a custom function. | | OpenApi.Exclude | Excludes the group from the final OpenAPI specification. |

Example (Annotating a Group)

import { HttpApi, HttpApiGroup, OpenApi } from "@effect/platform"

const api = HttpApi.make("api")
  .add(
    HttpApiGroup.make("group")
      // Add a description for the group
      .annotate(OpenApi.Description, "my description")
      // Provide external documentation links
      .annotate(OpenApi.ExternalDocs, {
        url: "http://example.com",
        description: "example"
      })
      // Override parts of the final output
      .annotate(OpenApi.Override, { name: "my name" })
      // Transform the final specification for this group
      .annotate(OpenApi.Transform, (spec) => ({
        ...spec,
        name: spec.name + "-transformed"
      }))
  )
  .add(
    HttpApiGroup.make("excluded")
      // Exclude the group from the final specification
      .annotate(OpenApi.Exclude, true)
  )

// Generate the OpenAPI spec
const spec = OpenApi.fromApi(api)

console.log(JSON.stringify(spec, null, 2))
/*
Output:
{
  "openapi": "3.1.0",
  "info": {
    "title": "Api",
    "version": "0.0.1"
  },
  "paths": {},
  "tags": [
    {
      "name": "my name-transformed",
      "description": "my description",
      "externalDocs": {
        "url": "http://example.com",
        "description": "example"
      }
    }
  ],
  "components": {
    "schemas": {},
    "securitySchemes": {}
  },
  "security": []
}
*/

HttpApiEndpoint

For an HttpApiEndpoint, you can use the following annotations:

| Annotation | Description | | ---------------------- | --------------------------------------------------------------------------- | | OpenApi.Description | Adds a description for this endpoint. | | OpenApi.Summary | Provides a short summary of the endpoint's purpose. | | OpenApi.Deprecated | Marks the endpoint as deprecated. | | OpenApi.ExternalDocs | Supplies external documentation links for the endpoint. | | OpenApi.Override | Merges specified fields into the resulting specification for this endpoint. | | OpenApi.Transform | Lets you modify the final endpoint specification with a custom function. | | OpenApi.Exclude | Excludes the endpoint from the final OpenAPI specification. |

Example (Annotating an Endpoint)

import {
  HttpApi,
  HttpApiEndpoint,
  HttpApiGroup,
  OpenApi
} from "@effect/platform"
import { Schema } from "effect"

const api = HttpApi.make("api").add(
  HttpApiGroup.make("group")
    .add(
      HttpApiEndpoint.get("get", "/")
        .addSuccess(Schema.String)
        // Add a description
        .annotate(OpenApi.Description, "my description")
        // Provide a summary
        .annotate(OpenApi.Summary, "my summary")
        // Mark the endpoint as deprecated
        .annotate(OpenApi.Deprecated, true)
        // Provide external documentation