{"id":27302366,"url":"https://github.com/sukovanej/effect-http","last_synced_at":"2025-04-12T02:18:33.032Z","repository":{"id":152934029,"uuid":"626601290","full_name":"sukovanej/effect-http","owner":"sukovanej","description":"Declarative HTTP API library for effect-ts","archived":false,"fork":false,"pushed_at":"2025-03-22T13:19:06.000Z","size":3765,"stargazers_count":261,"open_issues_count":20,"forks_count":21,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-12T02:17:47.913Z","etag":null,"topics":["api","effect-ts","http","openapi","swagger","typescript"],"latest_commit_sha":null,"homepage":"https://sukovanej.github.io/effect-http","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sukovanej.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-04-11T19:50:10.000Z","updated_at":"2025-04-09T20:37:23.000Z","dependencies_parsed_at":null,"dependency_job_id":"b7be44bb-6847-4929-9ea8-07361a421f64","html_url":"https://github.com/sukovanej/effect-http","commit_stats":null,"previous_names":[],"tags_count":277,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sukovanej%2Feffect-http","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sukovanej%2Feffect-http/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sukovanej%2Feffect-http/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sukovanej%2Feffect-http/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sukovanej","download_url":"https://codeload.github.com/sukovanej/effect-http/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248505919,"owners_count":21115354,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","effect-ts","http","openapi","swagger","typescript"],"created_at":"2025-04-12T02:18:32.452Z","updated_at":"2025-04-12T02:18:33.014Z","avatar_url":"https://github.com/sukovanej.png","language":"TypeScript","readme":"# effect-http\n\n\u003e [!WARNING]\n\u003e\n\u003e [@effect/platform@0.63.0](https://github.com/Effect-TS/effect/releases/tag/%40effect%2Fplatform%400.63.0) introduced a new\n\u003e set of `HttpApi` modules that are meant to be an effect official continuation of this project.\n\u003e\n\u003e Starting today (2024-08-30), the `effect-http` will be receiving only effect updates and critical patches to give\n\u003e the current users enough time to migrate. Consider the `effect-http` package to be deprecated in favour of the\n\u003e `@effect/platform`.\n\n![download badge](https://img.shields.io/npm/dm/effect-http.svg)\n\nHigh-level declarative HTTP library for [Effect-TS](https://github.com/Effect-TS) built on top of\n[@effect/platform](https://github.com/Effect-TS/effect/tree/main/packages/platform).\n\n- :star: **Client derivation**. Write the api specification once, get the type-safe client with runtime validation for free.\n- :rainbow: **OpenAPI derivation**. `/docs` endpoint with OpenAPI UI out of box.\n- :battery: **Batteries included server implementation**. Automatic runtime request and response validation.\n- :crystal_ball: **Example server derivation**. Automatic derivation of example server implementation.\n- :bug: **Mock client derivation**. Test safely against a specified API.\n\n**Under development.** Please note that currently any release might introduce\nbreaking changes and the internals and the public API are still evolving and changing.\n\n\u003e [!NOTE]\n\u003e This is an unofficial community package. You might benefit from checking the `@effect/platform`\n\u003e and `@effect/rpc` packages as they are the official Effect packages. The `effect-http` package strongly\n\u003e relies on `@effect/platform`, and knowledge of it can be beneficial for understanding what\n\u003e the `effect-http` does under the hood.\n\n## Quickstart\n\n- [Quickstart](#quickstart)\n- [Request validation](#request-validation)\n  - [Example](#example)\n  - [Optional path parameters](#optional-path-parameters)\n- [Headers](#headers)\n- [Security](#security)\n- [Responses](#responses)\n- [Testing the server](#testing-the-server)\n- [Error handling](#error-handling)\n  - [Reporting errors in handlers](#reporting-errors-in-handlers)\n  - [Example API with conflict API error](#example-api-with-conflict-api-error)\n- [Grouping endpoints](#grouping-endpoints)\n- [Descriptions in OpenApi](#descriptions-in-openapi)\n- [Representations](#representations)\n- [API on the client side](#api-on-the-client-side)\n  - [Example server](#example-server)\n  - [Mock client](#mock-client)\n- [Router handlers](#router-handlers)\n- [Compatibility](#compatibility)\n- [Scaling up](#scaling-up)\n\nInstall\n\n- `effect-http` - platform-agnostic, this one is enough if you intend to use it in browser only\n- `effect-http-node` - if you're planning to run a HTTP server on a node\n\n```bash\npnpm add effect-http effect-http-node\n```\n\nNote that `effect`, `@effect/platform` and `@effect/platform-node` are requested as peer dependencies.\nYou very probably have them already. If not, install them using\n\n```bash\npnpm add effect @effect/platform @effect/platform-node\n```\n\nThe `@effect/platform-node` is needed only for the node version.\n\nBootstrap a simple API specification.\n\n```typescript\nimport { Schema } from \"effect\";\nimport { Api, QuerySchema } from \"effect-http\";\n\nconst UserResponse = Schema.Struct({\n  name: Schema.String,\n  id: Schema.Int.pipe(Schema.positive()),\n});\nconst GetUserQuery = Schema.Struct({ id: QuerySchema.Number });\n\nconst api = Api.make({ title: \"Users API\" }).pipe(\n  Api.addEndpoint(\n    Api.get(\"getUser\", \"/user\").pipe(\n      Api.setResponseBody(UserResponse),\n      Api.setRequestQuery(GetUserQuery)\n    )\n  )\n);\n```\n\nCreate the app implementation.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { RouterBuilder } from \"effect-http\";\n\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"getUser\", ({ query }) =\u003e\n    Effect.succeed({ name: \"milan\", id: query.id })\n  ),\n  RouterBuilder.build\n);\n```\n\nNow, we can generate an object providing the HTTP client interface using `Client.make`.\n\n```typescript\nimport { Client } from \"effect-http\";\n\nconst client = Client.make(api, { baseUrl: \"http://localhost:3000\" });\n```\n\nSpawn the server on port 3000,\n\n```typescript\nimport { NodeRuntime } from \"@effect/platform-node\";\nimport { NodeServer } from \"effect-http-node\";\n\napp.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain);\n```\n\nand call it using the `client`.\n\n```ts\nconst response = client.getUser({ query: { id: 12 } }).pipe(\n  Effect.flatMap((user) =\u003e Effect.log(`Got ${user.name}, nice!`)),\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/readme-quickstart.ts)\n\nAlso, check the auto-generated OpenAPI UI running on\n[localhost:3000/docs](http://localhost:3000/docs/). How awesome is that!\n\n![open api ui](https://raw.githubusercontent.com/sukovanej/effect-http/master/assets/example-openapi-ui.png)\n\n### Request validation\n\nEach endpoint can declare expectations on the request format. Specifically,\n\n- `body` - request body\n- `query` - query parameters\n- `path` - path parameters\n- `headers` - request headers\n\nThey are specified in the input schemas object (3rd argument of `Api.get`, `Api.post`, ...).\n\n#### Example\n\n```typescript\nimport { Schema } from \"effect\";\nimport { Api } from \"effect-http\";\n\nconst Stuff = Schema.Struct({ value: Schema.Number });\nconst StuffRequest = Schema.Struct({ field: Schema.Array(Schema.String) });\nconst StuffQuery = Schema.Struct({ value: Schema.String });\nconst StuffPath = Schema.Struct({ param: Schema.String });\n\nexport const api = Api.make({ title: \"My api\" }).pipe(\n  Api.addEndpoint(\n    Api.post(\"stuff\", \"/stuff/:param\").pipe(\n      Api.setRequestBody(StuffRequest),\n      Api.setRequestQuery(StuffQuery),\n      Api.setRequestPath(StuffPath),\n      Api.setResponseBody(Stuff)\n    )\n  )\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/request-validation.ts)\n\n#### Optional path parameters\n\nOptional parameter is denoted using a question mark in the path\nmatch pattern. In the request param schema, use `Schema.optional(\u003cschema\u003e)`.\n\nIn the following example the last `:another` path parameter can be\nommited on the client side.\n\n```typescript\nimport { Schema } from \"effect\";\nimport { Api } from \"effect-http\";\n\nconst Stuff = Schema.Struct({ value: Schema.Number });\nconst StuffParams = Schema.Struct({\n  param: Schema.String,\n  another: Schema.optional(Schema.String),\n});\n\nexport const api = Api.make({ title: \"My api\" }).pipe(\n  Api.addEndpoint(\n    Api.get(\"stuff\", \"/stuff/:param/:another?\").pipe(\n      Api.setResponseBody(Stuff),\n      Api.setRequestPath(StuffParams)\n    )\n  )\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/request-validation-optional-parameter.ts)\n\n### Headers\n\nRequest headers are part of input schemas along with the request body or query parameters.\nTheir schema is specified similarly to query parameters and path parameters, i.e. using\na mapping from header names onto their schemas. The example below shows an API with\na single endpoint `/hello` which expects a header `X-Client-Id` to be present.\n\n```typescript\nimport { NodeRuntime } from \"@effect/platform-node\";\nimport { Schema } from \"effect\";\nimport { Api, ExampleServer, RouterBuilder } from \"effect-http\";\nimport { NodeServer } from \"effect-http-node\";\n\nconst api = Api.make().pipe(\n  Api.addEndpoint(\n    Api.get(\"hello\", \"/hello\").pipe(\n      Api.setResponseBody(Schema.String),\n      Api.setRequestHeaders(Schema.Struct({ \"x-client-id\": Schema.String }))\n    )\n  )\n);\n\nExampleServer.make(api).pipe(\n  RouterBuilder.build,\n  NodeServer.listen({ port: 3000 }),\n  NodeRuntime.runMain\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/readme-headers.ts)\n\nServer implementation deals with the validation the usual way. For example, if we try\nto call the endpoint without the header we will get the following error response.\n\n```json\n{\n  \"error\": \"Request validation error\",\n  \"location\": \"headers\",\n  \"message\": \"x-client-id is missing\"\n}\n```\n\nAnd as usual, the information about headers will be reflected in the generated\nOpenAPI UI.\n\n![example-headers-openapi-ui](https://raw.githubusercontent.com/sukovanej/effect-http/master/assets/example-headers-openapi-ui.png)\n\n**Important!** Use a lowercase form of header names.\n\n### Security\n\nTo deal with authentication / authorization, the `effect-http` exposes the `Security` module. `Security.Security\u003cA, E, R\u003e`\nis a structure capturing information how to document the security mechanism within the OpenAPI and how to parse the\nincomming server request to produce a value `A` available for the endpoint handler.\n\nTo to secure an endpoint, use the `Api.setSecurity` combinator. Let's see an example of a secured endpoint\nusing the basic auth.\n\n```ts\nimport { NodeRuntime } from \"@effect/platform-node\";\nimport { Effect, Schema } from \"effect\";\nimport { Api, RouterBuilder, Security } from \"effect-http\";\nimport { NodeServer } from \"effect-http-node\";\n\nconst api = Api.make().pipe(\n  Api.addEndpoint(\n    Api.post(\"mySecuredEndpoint\", \"/my-secured-endpoint\").pipe(\n      Api.setResponseBody(Schema.String),\n      Api.setSecurity(Security.basic())\n    )\n  )\n);\n\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"mySecuredEndpoint\", (_, security) =\u003e\n    Effect.succeed(`Accessed as ${security.user}`)\n  ),\n  RouterBuilder.build\n);\n\napp.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/readme-security-basic.ts)\n\nIn the example, we use the `Security.basic()` constructor which produces a new security of type\n`Security\u003cBasicCredentials, never, never\u003e`. In the second argument of our handler\nfunction, we receive the value of `BasicCredentials` if the request contains a valid\nauthorization header with the basic auth credentials.\n\nIn case the request doesn't include valid authorization, the client will get a `401 Unauthorized` response\nwith a JSON body containing the error message.\n\n#### Optional security\n\nImplementation-wise, the `Security\u003cA, E, R\u003e` contains an `Effect\u003cA, E | HttpError, R | ServerRequest\u003e`.\nTherefore, we can combine multiple security mechanisms similarly as if we were combining effects.\n\nFor instance, we could make the authentication optional using the `Security.or` combinator.\n\n```ts\nconst mySecurity = Security.or(\n  Security.asSome(Security.basic()),\n  Security.as(Security.unit, Option.none())\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/readme-security.ts)\n\nThe `Security.asSome`, `Security.as` and `Security.unit` behave the same way as their `Effect` counterparts.\n\n#### Constructing more complex security\n\nThe following example show-cases how to construct a security mechanism that validates\nthe basic auth credentials and then fetches the user information from the `UserStorage` service.\n\n```ts\nimport { Effect, Layer } from \"effect\";\nimport { Security } from \"effect-http\";\n\ninterface UserInfo {\n  email: string;\n}\n\nclass UserStorage extends Effect.Tag(\"UserStorage\")\u003c\n  UserStorage,\n  { getInfo: (user: string) =\u003e Effect.Effect\u003cUserInfo\u003e }\n\u003e() {\n  static dummy = Layer.succeed(\n    UserStorage,\n    UserStorage.of({\n      getInfo: (_: string) =\u003e Effect.succeed({ email: \"email@gmail.com\" }),\n    })\n  );\n}\n\nconst mySecurity = Security.basic({ description: \"My basic auth\" }).pipe(\n  Security.map((creds) =\u003e creds.user),\n  Security.mapEffect((user) =\u003e UserStorage.getInfo(user))\n);\n```\n\nIn the handler implementation, we obtain the `security` argument typed as `UserInfo`.\n\n```ts\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"endpoint\", (_, security) =\u003e\n    Effect.succeed(`Logged as ${security.email}`)\n  ),\n  RouterBuilder.build,\n  Middlewares.errorLog\n);\n```\n\nAnd finally, because we made use of the effect context, we are forced to provide the `UserStorage`\nwhen running the server.\n\n```ts\napp.pipe(\n  NodeServer.listen({ port: 3000 }),\n  Effect.provide(UserStorage.dummy),\n  NodeRuntime.runMain\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/readme-security-complex.ts)\n\n#### Security on the client side\n\nEach endpoint method accepts an optional second argument of type `(request: ClientRequest) =\u003e ClientRequest`\nused to map internally produced `HttpClient.request.ClientRequest`. We can provide the header mapping\nto set the appropriate header. Additionally, the `Client` module exposes `Client.setBasic` and `Client.setBearer`\ncombinators that produce setter functions configuring the `Authorization` header.\n\n```ts\nimport { Client } from \"effect-http\";\n\nconst client = Client.make(api);\n\nclient.endpoint({}, Client.setBasic(\"user\", \"pass\"));\n```\n\n#### Custom security\n\nA primitive security is constructed using `Security.make` function.\n\nIt accepts a handler effect which is expected to access the `ServerRequest`\nand possibly fail with a `HttpError`.\n\nIf we want to document the authorization mechanism in the OpenAPI, we must also provide the second argument\nof the `Security.make` which is a mapping of the auth identifier and actual security scheme spec.\n\nHere is an example of a security validating a `X-API-KEY` header.\n\n```ts\nimport { HttpServer } from \"@effect/platform\";\nimport { Effect, pipe, Schema } from \"effect\";\nimport { Security, HttpError } from \"effect-http\";\n\nconst customSecurity = Security.make(\n  pipe(\n    HttpServer.request.schemaHeaders(\n      Schema.Struct({ \"x-api-key\": Schema.String })\n    ),\n    Effect.mapError(() =\u003e\n      HttpError.unauthorizedError(\"Expected valid X-API-KEY header\")\n    ),\n    Effect.map((headers) =\u003e headers[\"x-api-key\"])\n  ),\n  {\n    myApiKey: {\n      name: \"x-api-key\",\n      type: \"apiKey\",\n      in: \"header\",\n      description: \"My API key\",\n    },\n  }\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/readme-security-custom.ts)\n\nIf the client doesn't provide the `X-API-KEY` header, the server will respond with `401 Unauthorized` status\nand the given message.\n\n\u003e [!NOTE]\n\u003e In this particular case, you can use `Security.apiKey({ key: \"X-API-KEY\", in: \"header\" })` instead\n\u003e of a custom security.\n\n### Responses\n\nEvery new endpoint has default response with status code 200 with ignored\nresponse and headers.\n\nIf you want to customize the default response, use the `Api.setResponseStatus`,\n`Api.setResponseBody` or `Api.setResponseHeaders` combinators. The following\nexample shows how to enforce (both for types and runtime) that returned status,\nbody and headers conform the specified response.\n\n```ts\nimport { Schema } from \"effect\";\nimport { Api } from \"effect-http\";\n\nconst api = Api.make().pipe(\n  Api.addEndpoint(\n    Api.get(\"hello\", \"/hello\").pipe(\n      Api.setResponseStatus(201),\n      Api.setResponseBody(Schema.Number),\n      Api.setResponseHeaders(Schema.Struct({ \"x-hello-world\": Schema.String }))\n    )\n  )\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/custom-response.ts)\n\nIt is also possible to specify multiple response schemas. Use the `Api.addResponse`\ncombinator to another possible response of an endpoint. The `Api.addResponse` accepts\neither an `ApiResponse` object created using `ApiResponse.make` or a plain object of\nform `{ status; headers; body}`.\n\n```ts\nimport { Effect, Schema } from \"effect\";\nimport { Api, ApiResponse, RouterBuilder } from \"effect-http\";\n\nconst helloEndpoint = Api.post(\"hello\", \"/hello\").pipe(\n  Api.setResponseBody(Schema.Number),\n  Api.setResponseHeaders(\n    Schema.Struct({\n      \"my-header\": Schema.NumberFromString.pipe(\n        Schema.annotations({ description: \"My header\" })\n      ),\n    })\n  ),\n  Api.addResponse(ApiResponse.make(201, Schema.Number)),\n  Api.addResponse({\n    status: 204,\n    headers: Schema.Struct({ \"x-another\": Schema.NumberFromString }),\n  })\n);\n\nconst api = Api.make().pipe(Api.addEndpoint(helloEndpoint));\n```\n\nThe server implemention is type-checked against the api responses\nand one of the specified response objects must be returned.\n\nNote: the `status` needs to be `as const` because without it Typescript\nwill infere the `number` type.\n\n```ts\nimport { Effect } from \"effect\";\nimport { Api, RouterBuilder } from \"effect-http\";\n\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"hello\", () =\u003e\n    Effect.succeed({\n      body: 12,\n      headers: { \"my-header\": 69 },\n      status: 201 as const,\n    })\n  ),\n  RouterBuilder.build\n);\n```\n\n### Testing the server\n\nYou need to install `effect-http-node`.\n\nWhile most of your tests should focus on the functionality independent\nof HTTP exposure, it can be beneficial to perform integration or\ncontract tests for your endpoints. The `NodeTesting` module offers a\n`NodeTesting.make` combinator that generates a testing client from\nthe Server. This derived testing client has a similar interface\nto the one derived by `Client.make`.\n\nNow, let's write an example test for the following server.\n\n```ts\nconst api = Api.api().pipe(\n  Api.get(\"hello\", \"/hello\", {\n    response: Schema.String,\n  })\n);\n\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"hello\", ({ query }) =\u003e\n    Effect.succeed(`${query.input + 1}`)\n  ),\n  RouterBUilder.build\n);\n```\n\nThe test might look as follows.\n\n```ts\nimport { it } from \"@effect/vitest\";\nimport { NodeTesting } from \"effect-http-node\";\n\nit.scoped(\"test /hello endpoint\", () =\u003e\n  Effect.gen(function* () {\n    const response = yield* NodeTesting.make(app, api).pipe(\n      Effect.flatMap((client) =\u003e client.hello({ query: { input: 12 } }))\n    );\n\n    expect(response).toEqual(\"13\");\n  })\n);\n```\n\nIn comparison to the `Client` we need to run our endpoint handlers\nin place. Therefore, in case your server uses DI services, you need to\nprovide them in the test code. This contract is type safe and you'll be\nnotified by the type-checker if the `Effect` isn't invoked with all\nthe required services.\n\n### Error handling\n\nValidation of query parameters, path parameters, body and even responses is\nhandled for you out of box. By default, failed validation will be reported\nto clients in the response body. On the server side, you get warn logs with\nthe same information.\n\n#### Reporting errors in handlers\n\nOn top of the automatic input and output validation, handlers can fail for variety\nof different reasons.\n\nSuppose we're creating user management API. When persisting a new user, we want\nto guarantee we don't attempt to persist a user with an already taken name.\nIf the user name check fails, the API should return `409 CONFLICT` error because the client\nis attempting to trigger an operatin conflicting with the current state of the server.\nFor these cases, `effect-http` provides error types and corresponding creational\nfunctions we can use in the error rail of the handler effect.\n\n#### Example API with conflict API error\n\nLet's see it in action and implement the mentioned user management API. The\nAPI will look as follows.\n\n```typescript\nimport { Context, Effect, pipe, Schema } from \"effect\";\nimport { Api, RouterBuilder, HttpError } from \"effect-http\";\nimport { NodeServer } from \"effect-http-node\";\n\nconst api = Api.make({ title: \"Users API\" }).pipe(\n  Api.addEndpoint(\n    Api.post(\"storeUser\", \"/users\").pipe(\n      Api.setResponseBody(Schema.String),\n      Api.setRequestBody(Schema.Struct({ name: Schema.String }))\n    )\n  )\n);\n```\n\nNow, let's implement a `UserRepository` interface abstracting the interaction with\nour user storage. I'm also providing a mock implementation which will always return\nthe user already exists. We will plug the mock user repository into our server\nso we can see the failure behavior.\n\n```typescript\ninterface UserRepository {\n  userExistsByName: (name: string) =\u003e Effect.Effect\u003cboolean\u003e;\n  storeUser: (user: string) =\u003e Effect.Effect\u003cvoid\u003e;\n}\n\nconst UserRepository = Context.GenericTag\u003cUserRepository\u003e(\"UserRepository\");\n\nconst mockUserRepository = UserRepository.of({\n  userExistsByName: () =\u003e Effect.succeed(true),\n  storeUser: () =\u003e Effect.unit,\n});\n\nconst { userExistsByName, storeUser } = Effect.serviceFunctions(UserRepository);\n```\n\nAnd finally, we have the actual `HttpApp.HttpApp` implementation.\n\n```typescript\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"storeUser\", ({ body }) =\u003e\n    pipe(\n      userExistsByName(body.name),\n      Effect.filterOrFail(\n        (alreadyExists) =\u003e !alreadyExists,\n        () =\u003e HttpError.conflict(`User \"${body.name}\" already exists.`)\n      ),\n      Effect.andThen(storeUser(body.name)),\n      Effect.map(() =\u003e `User \"${body.name}\" stored.`)\n    )\n  ),\n  RouterBuilder.build\n);\n```\n\nTo run the server, we will start the server using `NodeServer.listen` and provide\nthe `mockUserRepository` service.\n\n```typescript\napp.pipe(\n  NodeServer.listen({ port: 3000 }),\n  Effect.provideService(UserRepository, mockUserRepository),\n  NodeRuntime.runMain\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/conflict-error-example.ts)\n\nTry to run the server and call the `POST /user`.\n\n_Server_\n\n```bash\n$ pnpm tsx examples/conflict-error-example.ts\n\n22:06:00 (Fiber #0) DEBUG Static swagger UI files loaded (1.7MB)\n22:06:00 (Fiber #0) INFO  Listening on :::3000\n22:06:01 (Fiber #8) WARN  POST /users client error 409\n```\n\n_Client_ (using [httpie cli](https://httpie.io/cli))\n\n```bash\n$ http localhost:3000/users name=\"patrik\"\n\nHTTP/1.1 409 Conflict\nContent-Length: 68\nContent-Type: application/json; charset=utf-8\n\nUser \"patrik\" already exists.\n```\n\n### Grouping endpoints\n\nTo create a new group of endpoints, use `ApiGroup.apiGroup(\"group name\")`. This combinator\ninitializes new `ApiGroup` object. You can pipe it with combinators like `ApiGroup.addEndpoint`,\nfollowed by `ApiGroup.get`, `Api.post`, etc, as if were defining the `Api`. Api groups can be combined into an\n`Api` using a `Api.addGroup` combinator which merges endpoints from the group\ninto the api in the type-safe manner while preserving group names for each endpoint.\n\nThis enables separability of concers for big APIs and provides information for\ngeneration of tags for the OpenAPI specification.\n\n```typescript\nimport { NodeRuntime } from \"@effect/platform-node\";\nimport { Effect, Schema } from \"effect\";\nimport { Api, ApiGroup, ExampleServer, RouterBuilder } from \"effect-http\";\n\nimport { NodeServer } from \"effect-http-node\";\n\nconst Response = Schema.Struct({ name: Schema.String });\n\nconst testApi = ApiGroup.make(\"test\", {\n  description: \"Test description\",\n  externalDocs: {\n    description: \"Test external doc\",\n    url: \"https://www.google.com/search?q=effect-http\",\n  },\n}).pipe(\n  ApiGroup.addEndpoint(\n    ApiGroup.get(\"test\", \"/test\").pipe(Api.setResponseBody(Response))\n  )\n);\n\nconst userApi = ApiGroup.make(\"Users\", {\n  description: \"All about users\",\n  externalDocs: {\n    url: \"https://www.google.com/search?q=effect-http\",\n  },\n}).pipe(\n  ApiGroup.addEndpoint(\n    ApiGroup.get(\"getUser\", \"/user\").pipe(Api.setResponseBody(Response))\n  ),\n  ApiGroup.addEndpoint(\n    ApiGroup.post(\"storeUser\", \"/user\").pipe(Api.setResponseBody(Response))\n  ),\n  ApiGroup.addEndpoint(\n    ApiGroup.put(\"updateUser\", \"/user\").pipe(Api.setResponseBody(Response))\n  ),\n  ApiGroup.addEndpoint(\n    ApiGroup.delete(\"deleteUser\", \"/user\").pipe(Api.setResponseBody(Response))\n  )\n);\n\nconst categoriesApi = ApiGroup.make(\"Categories\").pipe(\n  ApiGroup.addEndpoint(\n    ApiGroup.get(\"getCategory\", \"/category\").pipe(Api.setResponseBody(Response))\n  ),\n  ApiGroup.addEndpoint(\n    ApiGroup.post(\"storeCategory\", \"/category\").pipe(\n      Api.setResponseBody(Response)\n    )\n  ),\n  ApiGroup.addEndpoint(\n    ApiGroup.put(\"updateCategory\", \"/category\").pipe(\n      Api.setResponseBody(Response)\n    )\n  ),\n  ApiGroup.addEndpoint(\n    ApiGroup.delete(\"deleteCategory\", \"/category\").pipe(\n      Api.setResponseBody(Response)\n    )\n  )\n);\n\nconst api = Api.make().pipe(\n  Api.addGroup(testApi),\n  Api.addGroup(userApi),\n  Api.addGroup(categoriesApi)\n);\n\nExampleServer.make(api).pipe(\n  RouterBuilder.build,\n  NodeServer.listen({ port: 3000 }),\n  NodeRuntime.runMain\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/groups.ts)\n\nThe OpenAPI UI will group endpoints according to the `api` and show\ncorresponding titles for each group.\n\n![example-generated-open-api-ui](https://raw.githubusercontent.com/sukovanej/effect-http/master/assets/example-server-openapi-ui.png)\n\n## Descriptions in OpenApi\n\nThe OpenAPI logic takes into account\n[description](https://effect-ts.github.io/schema/modules/Schema.ts.html#description)\nannotations and propagates them into the specification.\n\nSome descriptions are provided from the built-in `effect/Schema` combinators.\nFor example, the usage of `Schema.Int.pipe(Schema.positive())` will result in \"_a positive number_\"\ndescription in the OpenApi schema. One can also add custom description using\n`Schema.annotations({ description:  ... })`.\n\nOn top of types descriptions which are included in the `schema` field, effect-http\nalso checks top-level schema descriptions and uses them for the parent object which\nuses the schema. In the following example, the \"_User_\" description for the response\nschema is used both as the schema description but also for the response itself. The\nsame holds for the `id` query paremeter.\n\nFor an operation-level description, call the API endpoint method (`Api.get`,\n`Api.post` etc) with a 4th argument and set the `description` field to the\ndesired description.\n\n```ts\nimport { NodeRuntime } from \"@effect/platform-node\";\nimport { Effect, Schema } from \"effect\";\nimport { Api, QuerySchema, RouterBuilder } from \"effect-http\";\nimport { NodeServer } from \"effect-http-node\";\n\nconst Response = Schema.Struct({\n  name: Schema.String,\n  id: pipe(Schema.Number, Schema.int(), Schema.positive()),\n}).pipe(Schema.annotations({ description: \"User\" }));\n\nconst Query = Schema.Struct({\n  id: QuerySchema.Number.pipe(Schema.annotations({ description: \"User id\" })),\n});\n\nconst api = Api.make({ title: \"Users API\" }).pipe(\n  Api.addEndpoint(\n    Api.get(\"getUser\", \"/user\", { description: \"Returns a User by id\" }).pipe(\n      Api.setResponseBody(Response),\n      Api.setRequestQuery(Query)\n    )\n  )\n);\n\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"getUser\", ({ query }) =\u003e\n    Effect.succeed({ name: \"mike\", id: query.id })\n  ),\n  RouterBuilder.build\n);\n\napp.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/description.ts)\n\n## Representations\n\nBy default, the `effect-http` client and server will attempt the serialize/deserialize\nmessages as JSONs. This means that whenever you return something from a handler, the\ninternal logic will serialize it as a JSON onto a string and send the response along\nwith `content-type: application/json` header.\n\nThis behaviour is a result of a default [Representation.json](https://sukovanej.github.io/effect-http/modules/Representation.ts.html#json).\nThe default representation of the content can be changed using `Api.setResponseRepresentations`\ncombinator.\n\nFor example, the following API specification states that the response of `/test` endpoint\nwill be always a string represent as a plain text. Therefore, the HTTP message\nwill contain `content-type: text/plain` header.\n\n```ts\nexport const api2 = Api.make().pipe(\n  Api.addEndpoint(\n    Api.get(\"myHandler\", \"/test\").pipe(\n      Api.setResponseBody(Schema.String),\n      Api.setResponseRepresentations([Representation.plainText])\n    )\n  )\n);\n```\n\nThe `representations` is a list and if it contains multiple possible representations\nof the data it internal server logic will respect incomming `Accept` header to decide\nwhich representation to use.\n\nThe following example uses `plainText` and `json` representations. The order of\nrepresentations is respected by the logic that decides which representation should\nbe used, and if there is no representation matching the incomming `Accept` media type,\nit will choose the first representation in the list.\n\n```ts\nimport { NodeRuntime } from \"@effect/platform-node\";\nimport { Effect, Schema } from \"effect\";\nimport { Api, Representation, RouterBuilder } from \"effect-http\";\nimport { NodeServer } from \"effect-http-node\";\n\nexport const api = Api.make({ title: \"Example API\" }).pipe(\n  Api.addEndpoint(\n    Api.get(\"root\", \"/\").pipe(\n      Api.setResponseBody(Schema.Unknown),\n      Api.setResponseRepresentations([\n        Representation.plainText,\n        Representation.json,\n      ])\n    )\n  )\n);\n\nexport const app = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(\"root\", () =\u003e\n    Effect.succeed({ content: { hello: \"world\" }, status: 200 as const })\n  ),\n  RouterBuilder.build\n);\n\napp.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/plain-text.ts)\n\nTry running the server above and call the root path with different\n`Accept` headers. You should see the response content-type reflecting\nthe incomming `Accept` header.\n\n```bash\n# JSON\ncurl localhost:3000/ -H 'accept: application/json' -v\n\n# Plain text\ncurl localhost:3000/ -H 'accept: text/plain' -v\n```\n\n## API on the client side\n\nWhile `effect-http` is intended to be primarly used on the server-side, i.e.\nby developers providing the HTTP service, it is possible to use it also to\nmodel, use and test against someone else's API. Out of box, you can make\nus of the following combinators.\n\n- `Client` - client for the real integration with the API.\n- `MockClient` - client for testing against the API interface.\n- `ExampleServer` - server implementation derivation with example responses.\n\n### Example server\n\n`effect-http` has the ability to generate an example server\nimplementation based on the `Api` specification. This can be\nhelpful in the following and probably many more cases.\n\n- You're in a process of designing an API and you want to have _something_\n  to share with other people and have a discussion over before the actual\n  implementation starts.\n- You develop a fullstack application with frontend first approach\n  you want to test the integration with a backend you haven't\n  implemeted yet.\n- You integrate a 3rd party HTTP API and you want to have an ability to\n  perform integration tests without the need to connect to a real\n  running HTTP service.\n\nUse `ExampleServer.make` combinator to generate a `RouterBuilder` from an `Api`.\n\n```typescript\nimport { NodeRuntime } from \"@effect/platform-node\";\nimport { Effect, Schema } from \"effect\";\nimport { Api, ExampleServer, RouterBuilder } from \"effect-http\";\nimport { NodeServer } from \"effect-http-node\";\n\nconst Response = Schema.Struct({\n  name: Schema.String,\n  value: Schema.Number,\n});\n\nconst api = Api.make({\n  servers: [\"http://localhost:3000\", { description: \"hello\", url: \"/api/\" }],\n}).pipe(\n  Api.addEndpoint(Api.get(\"test\", \"/test\").pipe(Api.setResponseBody(Response)))\n);\n\nExampleServer.make(api).pipe(\n  RouterBuilder.build,\n  NodeServer.listen({ port: 3000 }),\n  NodeRuntime.runMain\n);\n```\n\n[\\[Source code\\]](./packages/effect-http-node/examples/example-server.ts)\n\nGo to [localhost:3000/docs](http://localhost:3000/docs) and try calling\nendpoints. The exposed HTTP service conforms the `api` and will return\nonly valid example responses.\n\n### Mock client\n\nTo performed quick tests against the API interface, `effect-http` has\nthe ability to generate a mock client which will return example or\nspecified responses. Suppose we are integrating a hypothetical API\nwith `/get-value` endpoint returning a number. We can model such\nAPI as follows.\n\n```typescript\nimport { Schema } from \"effect\";\nimport { Api } from \"effect-http\";\n\nconst api = Api.make().pipe(\n  Api.addEndpoint(\n    Api.get(\"getValue\", \"/value\").pipe(Api.setResponseBody(Schema.Number))\n  )\n);\n```\n\nIn a real environment, we will probably use the derived client\nusing `Client.make`. But for tests, we probably want a dummy\nclient which will return values conforming the API. For such\na use-case, we can derive a mock client.\n\n```typescript\nconst client = MockClient.make(api);\n```\n\nCalling `getValue` on the client will perform the same client-side\nvalidation as would be done by the real client. But it will return\nan example response instead of calling the API. It is also possible\nto enforce the value to be returned in a type-safe manner\nusing the option argument. The following client will always\nreturn number `12` when calling the `getValue` operation.\n\n```typescript\nconst client = MockClient.make(api, { responses: { getValue: 12 } });\n```\n\n## Scaling up\n\nFor bigger applications, you might want to separate the logic of endpoints\nor groups of endpoints into separate modules. This section shows how to do\nthat. Firstly, it is possible to declare endpoints independently of the `Api`\nor `ApiGroup` the're part of. Suppose we are creating a CMS system with\narticles, users, categories, etc. The API group responsible for management\nof articles would schematically look as follows.\n\n```ts\nimport { Api, ApiGroup } from \"effect-http\";\n\nexport const getArticleEndpoint = Api.get(\"getArticle\", \"/article\").pipe(\n  Api.setResponseBody(Response)\n);\nexport const storeArticleEndpoint = Api.post(\"storeArticle\", \"/article\").pipe(\n  Api.setResponseBody(Response)\n);\nexport const updateArticleEndpoint = Api.put(\"updateArticle\", \"/article\").pipe(\n  Api.setResponseBody(Response)\n);\nexport const deleteArticleEndpoint = Api.delete(\n  \"deleteArticle\",\n  \"/article\"\n).pipe(Api.setResponseBody(Response));\n\nexport const articleApi = ApiGroup.make(\"Articles\").pipe(\n  ApiGroup.addEndpoint(getArticleEndpoint),\n  ApiGroup.addEndpoint(storeArticleEndpoint),\n  ApiGroup.addEndpoint(updateArticleEndpoint),\n  ApiGroup.addEndpoint(deleteArticleEndpoint)\n);\n```\n\nSimilarly, we'd define the API group for user management, categories and others.\nLet's combine these groups into our API definition.\n\n```ts\nexport const api = Api.make().pipe(\n  Api.addGroup(articleApi),\n  Api.addGroup(userApi),\n  Api.addGroup(categoryApi)\n  // ...\n);\n```\n\nEach one of `deleteUserEndpoint`, `storeUserEndpoint`, ..., is an object\nof type `ApiEndpoint\u003cId, Request, Response, Security\u003e`. They are a full type\nand runtime declarations of your endpoints. You can use these objects to\nimplement the handlers for these endpoints. Produced handlers are objects\nof type `Handler\u003cA, E, R\u003e` (where `A` is a description of an endpoint\n`ApiEndpoint\u003cId, Request, Response, Security\u003e`). Handlers are combined\ninto a router using a `RouterBuilder`. You'd implement the handlers\nof the article API group as follows.\n\n```ts\nimport { Handler, RouterBuilder } from 'effect-http';\nimport { api, getArticleEndpoint, storeArticleEndpoint, updateArticleEndpoint, deleteArticleEndpoint } from 'path-to-your-api';\n\nconst getArticleHandler = Handler.make(getArticleEndpoint, () =\u003e Effect.succeed(...))\nconst storeArticleHandler = Handler.make(storeArticleEndpoint, () =\u003e Effect.succeed(...))\nconst updateArticleHandler = Handler.make(updateArticleEndpoint, () =\u003e Effect.succeed(...))\nconst deleteArticleHandler = Handler.make(deleteArticleEndpoint, () =\u003e Effect.succeed(...))\n\nexport const articleRouterBuilder = RouterBuilder.make(api).pipe(\n  RouterBuilder.handle(getArticleHandler),\n  RouterBuilder.handle(storeArticleHandler),\n  RouterBuilder.handle(updateArticleHandler),\n  RouterBuilder.handle(deleteArticleHandler),\n)\n```\n\n\u003e [!NOTE]\n\u003e The `Handler.make` function has both data-first and data-last overloads.\n\u003e If you prefer the pipe style, you can also do the following.\n\u003e\n\u003e ```ts\n\u003e const getArticleHandler = getArticleEndpoint.pipe(\n\u003e   Handler.make(() =\u003e Effect.succeed(...)\n\u003e )\n\u003e ```\n\nFinally, you merge all the router builders and build the app.\n\n```ts\nimport { RouterBuilder } from \"effect-http\";\nimport { userRouterBuilder } from \"path-to-your-router-builder\";\n\nconst app = RouterBuilder.make(api).pipe(\n  RouterBuilder.merge(userRouterBuilder),\n  RouterBuilder.merge(articleRouterBuilder),\n  RouterBuilder.merge(categoryRouterBuilder),\n  // ...\n  RouterBuilder.build\n);\n```\n\n## Compatibility\n\nThis library is tested against nodejs 21.5.0.\n","funding_links":[],"categories":["\u003ca name=\"TypeScript\"\u003e\u003c/a\u003eTypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsukovanej%2Feffect-http","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsukovanej%2Feffect-http","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsukovanej%2Feffect-http/lists"}