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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mcrovero/effect-nextjs

v0.31.0

Published

A library to work with Next.js in Effect

Readme

@mcrovero/effect-nextjs

npm version license: MIT

Write your Next.js App Router pages, layouts, server components, routes, and actions with Effect without losing the Next.js developer experience.

  • End-to-end Effect: Write your app logic as Effect while keeping familiar Next.js ergonomics.
  • Composable middlewares: Add auth and other cross‑cutting concerns in a clear, reusable way.
  • Works with Next.js: redirect, notFound, and other control‑flow behaviors just work. Also provides Effect versions of the utilities.
  • Safe routing: Decode route params and search params with Effect Schema for safer handlers.
  • Cache‑ready: Plays well with @mcrovero/effect-react-cache (react-cache wrapper) across pages, layouts, and components.

[!WARNING] This library is in early alpha and is not ready for production use.

Getting Started

  1. Install effect and the library in an existing Next.js 15+ application
pnpm add @mcrovero/effect-nextjs effect

or create a new Next.js application first:

pnpx create-next-app@latest
  1. Define Next effect runtime
// lib/runtime.ts
import { Next } from "@mcrovero/effect-nextjs"
import { Layer } from "effect"

const AppLive = Layer.empty // Your stateless layers
export const BasePage = Next.make("BasePage", AppLive)

[!WARNING] It is important that all layers passed to the runtime are stateless. If you need to use a stateful layer like a database connection read below. (see Stateful layers)

  1. Write your first page
// app/page.tsx
import { BasePage } from "@/lib/runtime"
import { Effect } from "effect"

const HomePage = Effect.fn("HomePage")(function* () {
  return <div>Hello World</div>
})

export default BasePage.build(HomePage)

When using Effect.fn you'll get automatic telemetry spans for the page load and better stack traces.

  1. Define a middleware
// lib/auth-runtime.ts
import { Next, NextMiddleware } from "@mcrovero/effect-nextjs"
import { Layer, Schema } from "effect"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"

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
export const AuthLive = Layer.succeed(
  AuthMiddleware,
  AuthMiddleware.of(() => Effect.succeed({ id: "123", name: "Ada" }))
)

// Create a typed page handler
export const AuthenticatedPage = Next.make("BasePage", AuthLive).middleware(AuthMiddleware)
  1. Use the middleware in a page and get the CurrentUser value
// app/page.tsx
import { AuthenticatedPage, CurrentUser } from "@/lib/auth-runtime" // wherever you defined it
import { Effect } from "effect"

const HomePage = () =>
  Effect.gen(function* () {
    const user = yield* CurrentUser
    return <div>Hello {user.name}</div>
  })

export default AuthenticatedPage.build(HomePage)

You can provide as many middlewares as you want.

const HomePage = AuthenticatedPage.middleware(LocaleMiddleware).middleware(TimezoneMiddleware).build(HomePage)

[!WARNING] The middleware order is important. The middleware will be executed in the order they are provided from left to right.

Effect Next.js utilities

When you need to use nextjs utilities like redirect, notFound, etc. you need to call them using Effect.sync. Code with side effects should always be lazy in Effect.

import { Effect } from "effect"
import { redirect } from "next/navigation"

const HomePage = Effect.fn("HomePage")(function* () {
  yield* Effect.sync(() => redirect("/somewhere"))
})
export default BasePage.build(HomePage)

Or you can use the Effect version of the utility functions like Redirect or NotFound.

import { Effect } from "effect"
import { Redirect } from "@mcrovero/effect-nextjs/Navigation"

const HomePage = Effect.fn("HomePage")(function* () {
  yield* Redirect("/somewhere")
})
export default BasePage.build(HomePage)

Navigation:

import { Redirect, PermanentRedirect, NotFound } from "@mcrovero/effect-nextjs/Navigation"

const HomePage = Effect.fn("HomePage")(function* () {
  yield* Redirect("/somewhere")
  yield* PermanentRedirect("/somewhere")
  yield* NotFound
})

Cache:

import { RevalidatePath, RevalidateTag } from "@mcrovero/effect-nextjs/Cache"

const HomePage = Effect.fn("HomePage")(function* () {
  yield* RevalidatePath("/")
  yield* RevalidateTag("tag")
})

Headers:

import { Headers, Cookies, DraftMode } from "@mcrovero/effect-nextjs/Headers"
Ø
const HomePage = Effect.fn("HomePage")(function* () {
  const headers = yield* Headers
  const cookies = yield* Cookies
  const draftMode = yield* DraftMode
})

Params and Search Params

You should always validate the params and search params with Effect Schema.

import { BasePage } from "@/lib/runtime"
import { decodeParamsUnknown, decodeSearchParamsUnknown } from "@mcrovero/effect-nextjs/Params"
import { Effect, Schema } from "effect"

const HomePage = Effect.fn("HomePage")((props) =>
  Effect.all([
    decodeParamsUnknown(Schema.Struct({ id: Schema.optional(Schema.String) }))(props.params),
    decodeSearchParamsUnknown(Schema.Struct({ name: Schema.optional(Schema.String) }))(props.searchParams)
  ]).pipe(
    Effect.map(([params, searchParams]) => (
      <div>
        Id: {params.id} Name: {searchParams.name}
      </div>
    )),
    Effect.catchTag("ParseError", () => Effect.succeed(<div>Error decoding params</div>))
  )
)

export default BasePage.build(HomePage)

Wrapped middlewares

You can use wrapped middlewares (wrap: true) to run before and after the handler.

import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import { Layer, Schema } from "effect"
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* () {
      yield* Effect.log("before")
      // pre logic...
      const out = yield* Effect.provideService(next, CurrentUser, { id: "u1", name: "Ada" })
      // post logic...
      yield* Effect.log("after")
      return out
    })
  )
)

const AppLive = Layer.mergeAll(WrappedLive)
const Page = Next.make("Home", AppLive).middleware(Wrapped)

Stateful layers

When using a stateful layer there is no clean way to dispose it safely on HMR in development. You should define the Next runtime globally using globalValue from effect/GlobalValue.

import { Next } from "@mcrovero/effect-nextjs"
import { Effect, ManagedRuntime } from "effect"
import { globalValue } from "effect/GlobalValue"

export class StatefulService extends Effect.Service<StatefulService>()("app/StatefulService", {
  scoped: Effect.gen(function* () {
    yield* Effect.log("StatefulService scoped")
    yield* Effect.addFinalizer(() => Effect.log("StatefulService finalizer"))
    return {}
  })
}) {}

export const statefulRuntime = globalValue("BasePage", () => {
  const managedRuntime = ManagedRuntime.make(StatefulService.Default)
  process.on("SIGINT", () => {
    managedRuntime.dispose()
  })
  process.on("SIGTERM", () => {
    managedRuntime.dispose()
  })
  return managedRuntime
})

Then you can use it directly using Next.makeWithRuntime.

export const BasePage = Next.makeWithRuntime("BasePage", statefulRuntime)

Or you can extract the context you need from the stateful runtime and using it in a stateless layer. This way you'll get HMR for the stateless layer and clean disposal of the stateful runtime.

const EphemeralLayer = Layer.effectContext(statefulRuntime.runtimeEffect.pipe(Effect.map((runtime) => runtime.context)))

export const BasePage = Next.make("BasePage", EphemeralLayer)

Next.js Route Props Helpers Integration

With Next.js 15.5, you can now use the globally available PageProps and LayoutProps types for fully typed route parameters without manual definitions. You can use them with this library as follows:

import * as Effect from "effect/Effect"
import { Next } from "@mcrovero/effect-nextjs"

// Page with typed route parameters
const BlogPage = Effect.fn("BlogHandler")(function* (props: PageProps<"/blog/[slug]">) {
  const { slug } = yield* Effect.promise(() => props.params)
  return (
    <article>
      <h1>Blog Post: {slug}</h1>
      <p>Content for {slug}</p>
    </article>
  )
})

export default Next.make("BlogPage", AppLive).build(BlØogPage)

// Layout with parallel routes support
const DashboardLayout = Effect.fn("DashboardLayout")(function* (props: LayoutProps<"/dashboard">) {
  // Fully typed parallel route slots
  return (
    <div>
      {props.children}
      {props.analytics} {/* Fully typed */}
      {props.team} {/* Fully typed */}
    </div>
  )
})
export default Next.make("DashboardLayout", AppLive).build(DashboardLayout)

See the official documentation: - Next.js 15.5 – Route Props Helpers

OpenTelemetry

Setup nextjs telemetry following official documentation: - OpenTelemetry

Then install @effect/opentelemetry

pnpm add @effect/opentelemetry

Create the tracer layer

import { Tracer as OtelTracer, Resource } from "@effect/opentelemetry"
import { Effect, Layer, Option } from "effect"

export const layerTracer = OtelTracer.layerGlobal.pipe(
  Layer.provide(
    Layer.unwrapEffect(
      Effect.gen(function* () {
        const resource = yield* Effect.serviceOption(Resource.Resource)
        if (Option.isSome(resource)) {
          return Layer.succeed(Resource.Resource, resource.value)
        }
        return Resource.layerFromEnv()
      })
    )
  )
)

and provide it to the Next runtime

export const AppLiveWithTracer = AppLive.pipe(Layer.provideMerge(layerTracer))
export const BasePage = Next.make("BasePage", AppLiveWithTracer)