Skip to content

add HttpLayerRouter module #5117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/cool-chairs-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@effect/platform": patch
---

add HttpLayerRouter module

The experimental HttpLayerRouter module provides a simplified way to create HTTP servers.

You can read more in the /platform README:

https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#httplayerrouter
19 changes: 19 additions & 0 deletions packages/ai/ai/src/McpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @since 1.0.0
*/
import * as Headers from "@effect/platform/Headers"
import type * as HttpLayerRouter from "@effect/platform/HttpLayerRouter"
import type * as HttpRouter from "@effect/platform/HttpRouter"
import type { RpcMessage } from "@effect/rpc"
import type * as Rpc from "@effect/rpc/Rpc"
Expand Down Expand Up @@ -574,6 +575,24 @@ export const layerHttp = <I = HttpRouter.Default>(options: {
Layer.provide(RpcSerialization.layerJsonRpc())
)

/**
* Run the McpServer, using HTTP for input and output.
*
* Uses a `HttpLayerRouter` to register the McpServer routes.
*
* @since 1.0.0
* @category Layers
*/
export const layerHttpRouter = (options: {
readonly name: string
readonly version: string
readonly path: HttpRouter.PathInput
}): Layer.Layer<McpServer | McpServerClient, never, HttpLayerRouter.HttpRouter> =>
layer(options).pipe(
Layer.provide(RpcServer.layerProtocolHttpRouter(options)),
Layer.provide(RpcSerialization.layerJsonRpc())
)

/**
* Register an AiToolkit with the McpServer.
*
Expand Down
51 changes: 51 additions & 0 deletions packages/platform-node/test/HttpServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HttpClient,
HttpClientRequest,
HttpClientResponse,
HttpLayerRouter,
HttpMultiplex,
HttpPlatform,
HttpRouter,
Expand Down Expand Up @@ -52,6 +53,27 @@ describe("HttpServer", () => {
expect(todo).toEqual({ id: 1, title: "test" })
}).pipe(Effect.provide(NodeHttpServer.layerTest)))

it.scoped("schema HttpLayerRouter", () =>
Effect.gen(function*() {
const handler = yield* HttpLayerRouter.toHttpEffect(HttpLayerRouter.use(Effect.fnUntraced(function*(router) {
yield* router.add(
"GET",
"/todos/:id",
Effect.flatMap(
HttpLayerRouter.schemaParams(IdParams),
({ id }) => todoResponse({ id, title: "test" })
)
)
})))

yield* HttpServer.serveEffect(handler)

const todo = yield* HttpClient.get("/todos/1").pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
)
expect(todo).toEqual({ id: 1, title: "test" })
}).pipe(Effect.provide(NodeHttpServer.layerTest)))

it.scoped("formData", () =>
Effect.gen(function*() {
yield* HttpRouter.empty.pipe(
Expand Down Expand Up @@ -696,4 +718,33 @@ describe("HttpServer", () => {
maxParamLength: 5
})
))

it.scoped("HttpLayerRouter prefixed", () =>
Effect.gen(function*() {
const handler = yield* HttpLayerRouter.toHttpEffect(HttpLayerRouter.use(Effect.fnUntraced(function*(router_) {
const router = router_.prefixed("/todos")
yield* router.add(
"GET",
"/:id",
Effect.flatMap(
HttpLayerRouter.schemaParams(IdParams),
({ id }) => todoResponse({ id, title: "test" })
)
)
yield* router.addAll([
HttpLayerRouter.route("GET", "/", Effect.succeed(HttpServerResponse.text("root")))
])
})))

yield* HttpServer.serveEffect(handler)

const todo = yield* HttpClient.get("/todos/1").pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
)
expect(todo).toEqual({ id: 1, title: "test" })
const root = yield* HttpClient.get("/todos").pipe(
Effect.flatMap((r) => r.text)
)
expect(root).toEqual("root")
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
})
271 changes: 271 additions & 0 deletions packages/platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4828,3 +4828,274 @@ Output:
}
*/
```

# HttpLayerRouter

The experimental `HttpLayerRouter` module provides a simplified way to create HTTP servers.
It aims to simplify the process of defining routes and registering other HTTP
services like `HttpApi` or `RpcServer`'s.

## Registering routes

```ts
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter"
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { createServer } from "http"

// Here is how you can register a simple GET route
const HelloRoute = Layer.effectDiscard(
Effect.gen(function* () {
// First, we need to access the `HttpRouter` service
const router = yield* HttpLayerRouter.HttpRouter

// Then, we can add a new route to the router
yield* router.add("GET", "/hello", HttpServerResponse.text("Hello, World!"))
})
)

// You can also use the `HttpLayerRouter.use` function to register a route
const GoodbyeRoute = HttpLayerRouter.use(
Effect.fn(function* (router) {
// The `router` parameter is the `HttpRouter` service
yield* router.add(
"GET",
"/goodbye",
HttpServerResponse.text("Goodbye, World!")
)
})
)

const AllRoutes = Layer.mergeAll(HelloRoute, GoodbyeRoute)

// To start the server, we use `HttpLayerRouter.serve` with the routes layer
HttpLayerRouter.serve(AllRoutes).pipe(
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
Layer.launch,
NodeRuntime.runMain
)
```

## Applying middleware

```ts
import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter"
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"

// Here is a service that we want to provide to every HTTP request
class CurrentSession extends Context.Tag("CurrentSession")<
CurrentSession,
{
readonly token: string
}
>() {}

// Using the `HttpLayerRouter.middleware` function, we can create a middleware
// that provides the `CurrentSession` service to every HTTP request.
const SessionMiddleware = HttpLayerRouter.middleware<{
provides: CurrentSession
}>()(
Effect.gen(function* () {
yield* Effect.log("SessionMiddleware initialized")

return (httpEffect) =>
Effect.provideService(httpEffect, CurrentSession, {
token: "dummy-token"
})
})
)

// And here is a CORS middleware that modifies the HTTP response
const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors())
// You can also use `HttpLayerRouter.cors()` to create a CORS middleware

const HelloRoute = Layer.effectDiscard(
Effect.gen(function* () {
const router = yield* HttpLayerRouter.HttpRouter
yield* router.add(
"GET",
"/hello",
Effect.gen(function* () {
// We can now access the `CurrentSession` service in our route handler
const session = yield* CurrentSession
return HttpServerResponse.text(
`Hello, World! Your session token is: ${session.token}`
)
})
)
})
).pipe(
// We can provide the `SessionMiddleware.layer` to the `HelloRoute` layer
Layer.provide(SessionMiddleware.layer),
// And we can also provide the `CorsMiddleware` layer to handle CORS
Layer.provide(CorsMiddleware.layer)
)
```

## Interdependent middleware

If middleware depends on another middleware, you can use the `.combine` api to
combine them.

```ts
import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter"
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"

class CurrentSession extends Context.Tag("CurrentSession")<
CurrentSession,
{
readonly token: string
}
>() {}

const SessionMiddleware = HttpLayerRouter.middleware<{
provides: CurrentSession
}>()(
Effect.gen(function* () {
yield* Effect.log("SessionMiddleware initialized")

return (httpEffect) =>
Effect.provideService(httpEffect, CurrentSession, {
token: "dummy-token"
})
})
)

// Here is a middleware that uses the `CurrentSession` service
const LogMiddleware = HttpLayerRouter.middleware(
Effect.gen(function* () {
yield* Effect.log("LogMiddleware initialized")

return Effect.fn(function* (httpEffect) {
const session = yield* CurrentSession
yield* Effect.log(`Current session token: ${session.token}`)
return yield* httpEffect
})
})
)

// We can then use the .combine method to combine the middlewares
const LogAndSessionMiddleware = LogMiddleware.combine(SessionMiddleware)

const HelloRoute = Layer.effectDiscard(
Effect.gen(function* () {
const router = yield* HttpLayerRouter.HttpRouter
yield* router.add(
"GET",
"/hello",
Effect.gen(function* () {
const session = yield* CurrentSession
return HttpServerResponse.text(
`Hello, World! Your session token is: ${session.token}`
)
})
)
})
).pipe(Layer.provide(LogAndSessionMiddleware.layer))
```

## Registering a HttpApi

```ts
import {
HttpApi,
HttpApiBuilder,
HttpApiEndpoint,
HttpApiGroup,
HttpApiScalar,
HttpLayerRouter
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer } from "effect"
import { createServer } from "http"

// First, we define our HttpApi
class MyApi extends HttpApi.make("api").add(
HttpApiGroup.make("users")
.add(HttpApiEndpoint.get("me", "/me"))
.prefix("/users")
) {}

// Implement the handlers for the API
const UsersApiLayer = HttpApiBuilder.group(MyApi, "users", (handers) =>
handers.handle("me", () => Effect.void)
)

// Use `HttpLayerRouter.addHttpApi` to register the API with the router
const HttpApiRoutes = HttpLayerRouter.addHttpApi(MyApi, {
openapiPath: "/docs/openapi.json"
}).pipe(
// Provide the api handlers layer
Layer.provide(UsersApiLayer)
)

// Create a /docs route for the API documentation
const DocsRoute = HttpApiScalar.layerHttpLayerRouter({
api: MyApi,
path: "/docs"
})

// Finally, we merge all routes and serve them using the Node HTTP server
const AllRoutes = Layer.mergeAll(HttpApiRoutes, DocsRoute).pipe(
Layer.provide(HttpLayerRouter.cors())
)

HttpLayerRouter.serve(AllRoutes).pipe(
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
Layer.launch,
NodeRuntime.runMain
)
```

## Registering a RpcServer

```ts
import { HttpLayerRouter } from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Rpc, RpcGroup, RpcSerialization, RpcServer } from "@effect/rpc"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "http"

export class User extends Schema.Class<User>("User")({
id: Schema.String,
name: Schema.String
}) {}

// Define a group of RPCs
export class UserRpcs extends RpcGroup.make(
Rpc.make("UserById", {
success: User,
error: Schema.String, // Indicates that errors, if any, will be returned as strings
payload: {
id: Schema.String
}
})
) {}

const UserHandlers = UserRpcs.toLayer({
UserById: ({ id }) => Effect.succeed(new User({ id, name: "John Doe" }))
})

// Use `HttpLayerRouter` to register the rpc server
const RpcRoute = RpcServer.layerHttpRouter({
group: UserRpcs,
path: "/rpc"
}).pipe(Layer.provide(UserHandlers), Layer.provide(RpcSerialization.layerJson))

// Start the HTTP server with the RPC route
HttpLayerRouter.serve(RpcRoute).pipe(
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
Layer.launch,
NodeRuntime.runMain
)
```
Loading