npm package: @marcschauer/orpc-effect-next
Description: Effect integration for Next.js App Router - typed helpers for pages, layouts, server components, and actions
Author: Marc Schauer
Latest version: 0.1.0

# @orpc-effect/next

> Effect integration for Next.js App Router - typed helpers for building pages, layouts, server components, and actions with Effect.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Overview

`@orpc-effect/next` brings the power of [Effect](https://effect.website) to Next.js App Router. Build server components, pages, layouts, and server actions using Effect's functional programming model with full type safety, dependency injection, and composable middleware.

**Key Features:**

- 🎯 **Type-safe builders** for Next.js components with fluent API
- 🔌 **Dependency injection** through Effect's Layer system
- 🔗 **Composable middleware** with before/after logic
- ⚡ **HMR-friendly** runtime management for development
- 🚦 **Proper Next.js integration** with redirect/notFound support
- 🌊 **Effect-native wrappers** for Next.js APIs (cookies, headers, params)

## Installation

```bash
pnpm add @orpc-effect/next effect
```

**Peer Dependencies:**
- `effect` ^3.0.0
- `next` ^15.0.0 || ^16.0.0

## Quick Start

### 1. Create a Runtime

```typescript
// src/lib/runtime.ts
import { Next } from "@orpc-effect/next";
import { Layer } from "effect";
import { DatabaseServiceLive } from "./services";

export const BasePage = Next.make("BasePage", DatabaseServiceLive);
```

### 2. Build a Page

```typescript
// src/app/page.tsx
import { BasePage } from "@/lib/runtime";
import { DatabaseService } from "@/lib/services";
import { Effect } from "effect";

const HomePage = Effect.fn("HomePage")(function* () {
  const db = yield* DatabaseService;
  const users = yield* db.getAllUsers();
  
  return (
    <main>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </main>
  );
});

export default BasePage.build(HomePage);
```

That's it! Your page now has access to all services provided by the runtime, with full type safety and Effect's error handling.

## Core Concepts

### Next Builder

The `Next` builder is the main entry point. It manages middleware composition, runtime lifecycle, and type accumulation.

```typescript
import { Next } from "@orpc-effect/next";
import { Layer } from "effect";

// Stateless (recommended for most cases)
const BasePage = Next.make("BasePage", Layer.empty);

// Stateful (for persistent resources like DB connections)
const BasePage = Next.makeWithRuntime("BasePage", managedRuntime);
```

### Middleware System

Middlewares provide services or wrap handlers with cross-cutting concerns.

#### Standard Middleware

Provides a service to the handler:

```typescript
import { NextMiddleware } from "@orpc-effect/next";
import { Context, Effect, Layer, Schema } from "effect";

// Define service
export class CurrentUser extends Context.Tag("CurrentUser")<
  CurrentUser,
  { id: string; name: string }
>() {}

// Define middleware
export class AuthMiddleware extends NextMiddleware.Tag<AuthMiddleware>()(
  "AuthMiddleware",
  {
    provides: CurrentUser,
    failure: Schema.String,
  }
) {}

// Implement middleware
export const AuthMiddlewareLive = Layer.succeed(
  AuthMiddleware,
  AuthMiddleware.of(() =>
    Effect.gen(function* () {
      // Check auth (cookies, headers, etc.)
      return { id: "user_123", name: "Alice" };
    })
  )
);

// Use in pages
export const AuthPage = BasePage.middleware(AuthMiddleware);
```

#### Wrapped Middleware

Runs logic before and after the handler:

```typescript
export class LoggingMiddleware extends NextMiddleware.Tag<LoggingMiddleware>()(
  "LoggingMiddleware",
  { wrap: true }
) {}

export const LoggingLive = Layer.succeed(
  LoggingMiddleware,
  LoggingMiddleware.of(({ next }) =>
    Effect.gen(function* () {
      yield* Effect.log("Request started");
      const result = yield* next;
      yield* Effect.log("Request completed");
      return result;
    })
  )
);

const Page = BasePage.middleware(LoggingMiddleware);
```

### Chaining Middlewares

Middlewares are applied from left to right:

```typescript
const AuthLoggedPage = BasePage
  .middleware(LoggingMiddleware)  // Outermost
  .middleware(AuthMiddleware);    // Innermost
```

## Next.js API Wrappers

### Navigation

```typescript
import { Redirect, PermanentRedirect, NotFound } from "@orpc-effect/next/Navigation";

const handler = Effect.gen(function* () {
  const user = yield* db.getUser(id);
  
  if (!user) {
    yield* NotFound;  // Triggers not-found.tsx
  }
  
  if (!user.active) {
    yield* Redirect("/inactive");  // Redirects to another page
  }
  
  return user;
});
```

### Cache

```typescript
import { RevalidatePath, RevalidateTag } from "@orpc-effect/next/Cache";

const updatePost = Effect.gen(function* () {
  yield* db.updatePost(id, data);
  yield* RevalidatePath("/blog");
  yield* RevalidateTag("posts");
});
```

### Headers

```typescript
import { Headers, Cookies, DraftMode } from "@orpc-effect/next/Headers";

const handler = Effect.gen(function* () {
  const headers = yield* Headers;
  const userAgent = headers.get("user-agent");
  
  const cookies = yield* Cookies;
  const session = cookies.get("session");
  
  const draft = yield* DraftMode;
  if (draft.isEnabled) {
    // Show draft content
  }
});
```

### Params & SearchParams

```typescript
import { decodeParamsUnknown, decodeSearchParamsUnknown } from "@orpc-effect/next/Params";
import { Schema } from "effect";

const ParamsSchema = Schema.Struct({
  id: Schema.String,
  slug: Schema.optional(Schema.String)
});

const SearchParamsSchema = Schema.Struct({
  page: Schema.optional(Schema.NumberFromString),
  filter: Schema.optional(Schema.String)
});

const handler = Effect.fn("Handler")((props) =>
  Effect.gen(function* () {
    const params = yield* decodeParamsUnknown(ParamsSchema)(props.params);
    const search = yield* decodeSearchParamsUnknown(SearchParamsSchema)(props.searchParams);
    
    return <div>Post {params.id}, Page {search.page ?? 1}</div>;
  })
);
```

## Runtime Management

Understanding how `@orpc-effect/next` manages the Effect runtime is crucial for building robust applications, especially during development with Hot Module Replacement (HMR).

### Stateless Layers (Recommended)

For most use cases, use `Next.make()` with a Layer. This automatically creates and manages a `ManagedRuntime`:

```typescript
import { Next } from "@orpc-effect/next";
import { Layer } from "effect";
import { DatabaseServiceLive, LoggerServiceLive } from "./services";

const AppLayer = Layer.mergeAll(DatabaseServiceLive, LoggerServiceLive);

// Creates a ManagedRuntime internally
export const BasePage = Next.make("BasePage", AppLayer);
```

**How it works:**
- `Next.make()` creates a `ManagedRuntime` from your layer
- The runtime is created fresh on each module load
- Services are constructed when needed
- Resources are cleaned up automatically

**HMR Behavior:**
- On code changes, Next.js re-executes the module
- A new runtime is created with fresh services
- Old services are properly disposed
- Perfect for stateless services (config, logging, pure computations)

### Stateful Layers (Manual Lifecycle Management)

For persistent resources that should **not** be recreated on HMR (database pools, WebSocket connections, etc.), use `Next.makeWithRuntime()` with `globalValue`:

```typescript
// src/lib/runtime.ts
import { Next } from "@orpc-effect/next";
import { ManagedRuntime, Effect } from "effect";
import { globalValue } from "effect/GlobalValue";
import { DatabaseServiceLive } from "./services";

/**
 * Global runtime that persists across HMR updates
 * Only ONE instance exists during the entire dev server session
 */
export const statefulRuntime = globalValue("app/StatefulRuntime", () => {
  console.log("🚀 Creating stateful runtime (should only happen once)");
  
  const runtime = ManagedRuntime.make(DatabaseServiceLive);
  
  // Ensure graceful shutdown when process exits
  process.on("SIGINT", () => {
    console.log("⏹️ Disposing stateful runtime");
    runtime.dispose();
  });
  
  process.on("SIGTERM", () => {
    console.log("⏹️ Disposing stateful runtime");
    runtime.dispose();
  });
  
  return runtime;
});

// Use the stateful runtime
export const BasePage = Next.makeWithRuntime("BasePage", statefulRuntime);
```

**Why `globalValue`?**
- Without it, HMR would create a new database pool on every code change
- This leaks connections and eventually exhausts your connection limit
- `globalValue` ensures the runtime persists across module reloads
- The same runtime instance is reused until the dev server restarts

### Hybrid Approach: Stateful + Stateless

You can extract the context from a stateful runtime and use it in a stateless layer for HMR-friendly development:

```typescript
import { Layer, Effect } from "effect";

// Extract context from the stateful runtime
const EphemeralLayer = Layer.effectContext(
  statefulRuntime.runtimeEffect.pipe(
    Effect.map((runtime) => runtime.context)
  )
);

// Now you can still use Next.make() for HMR
export const BasePage = Next.make("BasePage", EphemeralLayer);
```

**Best of both worlds:**
- Database connection persists (from `statefulRuntime`)
- Other code changes trigger HMR properly
- No connection leaks during development

### Production Considerations

In production (when HMR is disabled), both approaches work identically. The choice between them is purely about development experience:

- **`Next.make()`**: Simple, automatic, works for 90% of cases
- **`Next.makeWithRuntime()`**: Explicit control, required for stateful resources

## Advanced Patterns

### Combining with ORPC

```typescript
import { effectHandler } from "orpc-effect/server";
import { BasePage } from "@/lib/runtime";

// Use in API routes
export const POST = effectHandler(
  ({ input }) => Effect.gen(function* () {
    const db = yield* DatabaseService;
    return yield* db.createUser(input);
  }),
  { layer: DatabaseServiceLive }
);

// Use in pages
const HomePage = Effect.gen(function* () {
  const db = yield* DatabaseService;
  const users = yield* db.getAllUsers();
  return <UserList users={users} />;
});

export default BasePage.build(HomePage);
```

### Error Handling

```typescript
const handler = Effect.gen(function* () {
  const result = yield* db.getUser(id).pipe(
    Effect.catchTag("UserNotFound", () => NotFound),
    Effect.catchTag("Unauthorized", () => Redirect("/login")),
    Effect.catchAll((error) => 
      Effect.succeed(<ErrorPage message={error.message} />)
    )
  );
  
  return <UserProfile user={result} />;
});
```

### Server Actions

```typescript
"use server";

import { BasePage } from "@/lib/runtime";
import { RevalidatePath } from "@orpc-effect/next/Cache";

export const createPost = BasePage.build((formData: FormData) =>
  Effect.gen(function* () {
    const db = yield* DatabaseService;
    const title = formData.get("title") as string;
    
    yield* db.createPost({ title });
    yield* RevalidatePath("/blog");
    
    yield* Redirect("/blog");
  })
);
```

### Layouts

```typescript
// app/layout.tsx
import { BasePage } from "@/lib/runtime";
import { Effect } from "effect";

const RootLayout = Effect.fn("RootLayout")(({ children }) =>
  Effect.gen(function* () {
    const config = yield* ConfigService;
    
    return (
      <html lang={config.locale}>
        <body>{children}</body>
      </html>
    );
  })
);

export default BasePage.build(RootLayout);
```

## TypeScript Integration

Works seamlessly with Next.js 15.5+ Route Props Helpers:

```typescript
import { Effect } from "effect";
import { Next } from "@orpc-effect/next";

const BlogPage = Effect.fn("BlogPage")(function* (props: PageProps<"/blog/[slug]">) {
  const { slug } = yield* Effect.promise(() => props.params);
  return <article>Blog Post: {slug}</article>;
});

export default Next.make("BlogPage", AppLayer).build(BlogPage);
```

## Architecture

Based on [effect-nextjs](https://github.com/mcrovero/effect-nextjs) by mcrovero, this package provides:

1. **Middleware Tag Factory** - Advanced TypeScript inference for compile-time validation
2. **Two Runtime Factories** - `make()` for stateless, `makeWithRuntime()` for stateful
3. **Effect Wrappers** - All Next.js functions wrapped for lazy evaluation
4. **unstable_rethrow Integration** - Proper Next.js control-flow error handling

## Examples

See the [nextjs-streaming example](../../examples/nextjs-streaming) for a complete working application demonstrating:

- Basic page with service injection
- Authenticated pages with middleware
- Wrapped middleware for logging
- Integration with ORPC
- Error handling patterns

## API Reference

### `Next.make(tag, layer)`

Create a Next builder with a Layer (stateless, HMR-friendly).

**Parameters:**
- `tag: string` - Identifier for this builder
- `layer: Layer.Layer<R, E, never>` - Effect Layer providing services

**Returns:** `Next<Tag, R, never, never, never>`

### `Next.makeWithRuntime(tag, runtime)`

Create a Next builder with a ManagedRuntime (stateful, manual lifecycle).

**Parameters:**
- `tag: string` - Identifier for this builder
- `runtime: ManagedRuntime.ManagedRuntime<R, E>` - ManagedRuntime instance

**Returns:** `Next<Tag, never, never, never, never>`

### `builder.middleware(middleware)`

Add a middleware to the builder chain.

**Parameters:**
- `middleware: NextMiddleware.TagClassAny` - Middleware tag to add

**Returns:** New builder with updated type signature

### `builder.build(handler)`

Build the final handler function.

**Parameters:**
- `handler: (...args) => Effect.Effect<A, E, R>` - Effect-based handler

**Returns:** `(...args) => Promise<A>` - Next.js-compatible async function

## Contributing

Contributions welcome! Please ensure:

1. All tests pass (`pnpm test`)
2. Code is properly typed
3. Effect APIs are used correctly
4. Examples are updated if APIs change

## License

MIT © Marc Schauer

## Credits

- Architecture based on [effect-nextjs](https://github.com/mcrovero/effect-nextjs) by [mcrovero](https://github.com/mcrovero)
- Built with [Effect](https://effect.website)
- Designed for [Next.js](https://nextjs.org)

---

**Part of the orpc-effect ecosystem:**
- [orpc-effect](../orpc-effect) - Effect integration for ORPC
- [@orpc-effect/react](../orpc-effect-react) - React hooks for ORPC Effect
- [@orpc-effect/next](../orpc-effect-next) - Next.js integration (this package)