Warning
This library is in early alpha and is not ready for production use.
Typed helpers to build Next.js App Router pages, layouts, server components, and server actions with Effect. Compose middlewares as Context.Tag
s, validate params/search params/input with Schema
, and build your Effect
programs with a single call.
- Install
pnpm add @mcrovero/effect-nextjs effect next
- Define a service and a middleware
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { Next, NextMiddleware } from "@mcrovero/effect-nextjs"
// A simple service
export class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, { id: string; name: string }>() {}
// Middleware that provides CurrentUser and can fail with a string
export class AuthMiddleware extends NextMiddleware.Tag<AuthMiddleware>()("AuthMiddleware", {
provides: CurrentUser,
failure: Schema.String
}) {}
// Live implementation for the middleware (typed R is inferred)
export const AuthLive = NextMiddleware.layer(AuthMiddleware, () => Effect.succeed({ id: "123", name: "Ada" }))
// Combine all lives you need
const AppLive = Layer.mergeAll(AuthLive)
// Create a typed page handler
export const page = Next.make(AppLive)
.page("HomePage")
.setParamsSchema(Schema.Struct({ id: Schema.String }))
.middleware(AuthMiddleware)
.build(({ params }) =>
Effect.gen(function* () {
const user = yield* CurrentUser
return <div>Hello {user.name}</div>
})
)
- Use it in a Next.js App Router file
// app/[id]/page.tsx
import { page } from "@/lib/app" // wherever you defined it
// Use it directly
export default page
// Or use it in a Next.js page
export default async function Page(props: {
params: Promise<{ id: string }>
searchParams: Promise<Record<string, string>>
}) {
return await page({ params: props.params, searchParams: props.searchParams })
}
Notes
- Use
.layout(tag)
,.component(tag)
, and.action(tag)
for layouts, server components, and server actions. - Validate search params with
.setSearchParamsSchema(...)
on pages, and action input with.setInputSchema(...)
on actions. - You can add multiple middlewares with
.middleware(...)
. Middlewares can be markedwrap
via the tag options to run before/after the handler. - Server actions: due to Next.js restrictions, the action handler must be declared with the
async
keyword. In this API, that means the function you pass to.build(...)
for actions must beasync
, returning a Promise of an Effect. - You can use this together with
@mcrovero/effect-react-cache
to cacheEffect
-based functions between pages, layouts, and components.
Use NextMiddleware.layer(tag, impl)
when your middleware needs other services. The layer will carry the implementation's environment in its R
type so you can compose it safely.
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { Next, NextMiddleware } from "@mcrovero/effect-nextjs"
// Dependencies
export class Other extends Context.Tag("Other")<Other, { id: string; name: string }>() {}
export class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, { id: string; name: string }>() {}
// Auth requires `Other` to compute `CurrentUser`
export class AuthMiddleware extends NextMiddleware.Tag<AuthMiddleware>()("AuthMiddleware", {
provides: CurrentUser,
failure: Schema.String
}) {}
// Implementation reads from `Other` (its requirement is reflected in the Layer type)
const AuthLive = NextMiddleware.layer(AuthMiddleware, () =>
Effect.gen(function* () {
const other = yield* Other
return { id: "123", name: other.name }
})
)
// Provide the dependency
const OtherLive = Layer.succeed(Other, { id: "999", name: "Jane" })
const AppLive = Layer.mergeAll(OtherLive, AuthLive)
// Use in a page
const page = Next.make(AppLive)
.page("Home")
.middleware(AuthMiddleware)
.build(() =>
Effect.gen(function* () {
const user = yield* CurrentUser
return user
})
)
Wrapped middlewares (wrap: true
) receive a next
Effect to run when they decide.
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { Next, NextMiddleware } from "@mcrovero/effect-nextjs"
export class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, { id: string; name: string }>() {}
export class Wrapped extends NextMiddleware.Tag<Wrapped>()("Wrapped", {
provides: CurrentUser,
failure: Schema.String,
wrap: true
}) {}
const WrappedLive = Layer.succeed(
Wrapped,
Wrapped.of(({ next }) =>
Effect.gen(function* () {
// pre logic...
const out = yield* Effect.provideService(next, CurrentUser, { id: "u1", name: "Ada" })
// post logic...
return out
})
)
)
const AppLive = Layer.mergeAll(WrappedLive)
const page = Next.make(AppLive)
.page("Home")
.middleware(Wrapped)
.build(() => Effect.succeed("ok"))
Use Schema
to validate/transform values automatically before your handler runs.
import * as Schema from "effect/Schema"
import { Next } from "@mcrovero/effect-nextjs"
// Params and searchParams (Page)
const page = Next.make(AppLive)
.page("Home")
.setParamsSchema(Schema.Struct({ id: Schema.String }))
.setSearchParamsSchema(Schema.Struct({ q: Schema.optional(Schema.String) }))
.build(({ params, searchParams }) => Effect.succeed({ params, searchParams }))
// Input (Action)
// IMPORTANT: The action handler must be async because of Next.js server action requirements
const action = Next.make(AppLive)
.action("DoSomething")
.setInputSchema(Schema.Struct({ count: Schema.Number, tags: Schema.Array(Schema.String) }))
.build(async ({ input }) => Effect.succeed({ ok: true, input }))
// Server Component (with props):
export default Next.make(AppLive)
.component("ServerInfo")
.build(({ time }: { time: { now: number } }) =>
Effect.succeed({
time
})
)
// Or, no props:
export const component = Next.make(AppLive)
.component("ServerInfo")
.build(() => Effect.succeed({ ok: true }))
The library automatically creates spans around pages, layouts, actions, server components, and middlewares.