# next-route-compose

[![npm version](https://img.shields.io/npm/v/next-route-compose.svg)](https://www.npmjs.com/package/next-route-compose)
[![CI](https://github.com/patchybean/next-route-compose/actions/workflows/ci.yml/badge.svg)](https://github.com/patchybean/next-route-compose/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Composable utilities for Next.js Route Handlers** — auth, CORS, rate limiting, validation & more. Following official Next.js patterns.

## ✨ Features

- 🔗 **Composable** — Chain utilities with functional composition
- 🔒 **Auth** — JWT, session, API key verification
- 🌐 **CORS** — Flexible origin configuration
- 🚦 **Rate Limiting** — IP-based with custom store support
- ✅ **Validation** — Zod integration with type inference
- 📝 **Logging** — Request/response with timing
- 🎯 **Type-Safe** — Full TypeScript with auto-complete
- ⚡ **Zero Config** — Works out of the box

## 📦 Installation

```bash
pnpm add next-route-compose
# or
npm install next-route-compose
# or
yarn add next-route-compose
```

## 🚀 Quick Start

```typescript
// app/api/users/route.ts
import { compose, withCors, withAuth, withRateLimit } from "next-route-compose";

export const GET = compose(
  withCors({ origin: "*" }),
  withAuth({ verify: async (ctx) => verifyToken(ctx.request) }),
  withRateLimit({ limit: 100, window: 60 })
)(async (ctx) => {
  return Response.json({ user: ctx.user });
});
```

## 📖 API Reference

### Core

#### `compose(...utilities)`

Compose multiple utilities into a single route handler.

```typescript
import { compose, withCors, withAuth } from "next-route-compose";

export const GET = compose(
  withCors({ origin: "https://example.com" }),
  withAuth({ verify: verifyToken })
)(async (ctx) => {
  return Response.json({ data: "Hello!" });
});
```

#### `createHandler()`

Builder pattern alternative for cleaner syntax.

```typescript
import { createHandler, withCors, withAuth } from "next-route-compose";

export const GET = createHandler()
  .use(withCors({ origin: "*" }))
  .use(withAuth({ verify: verifyToken }))
  .handle(async (ctx) => {
    return Response.json({ user: ctx.user });
  });
```

### Utilities

#### `withCors(options)`

CORS handling with flexible origin configuration.

```typescript
withCors({
  origin: "*", // or ["https://a.com", "https://b.com"] or (origin) => boolean
  methods: ["GET", "POST"],
  credentials: true,
  maxAge: 86400,
});
```

#### `withAuth(options)`

Authentication with custom verification.

```typescript
withAuth({
  verify: async (ctx) => {
    const token = ctx.request.headers.get("Authorization")?.replace("Bearer ", "");
    return token ? await decodeJWT(token) : null;
  },
  onUnauthorized: (ctx) => Response.json({ error: "Unauthorized" }, { status: 401 }),
});
```

#### `withRateLimit(options)`

Rate limiting with customizable key generation.

```typescript
withRateLimit({
  limit: 100,
  window: 60, // seconds
  keyGenerator: (ctx) => ctx.user?.id ?? getIP(ctx.request),
});
```

#### `withValidation(options)`

Request validation with Zod.

```typescript
import { z } from "zod";

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

withValidation({ schema });
// ctx.body is typed as { name: string; email: string }
```

#### `withLogging(options)`

Request/response logging.

```typescript
withLogging({
  level: "info",
  logger: (msg, data) => console.log(msg, data),
  includeBody: true,
});
```

### Error Classes

```typescript
import {
  UnauthorizedError,
  ForbiddenError,
  ValidationError,
  RateLimitError,
  NotFoundError,
  BadRequestError,
} from "next-route-compose";

// Throw anywhere in your handler
throw new UnauthorizedError("Invalid token");
// Automatically converts to JSON response with correct status
```

## 🔧 Advanced

### Custom Utility

Create your own utility following the same pattern:

```typescript
import type { HandlerUtility, HandlerContext } from "next-route-compose";

export function withCustomHeader(name: string, value: string): HandlerUtility {
  return (handler) => {
    return async (context: HandlerContext) => {
      const response = await handler(context);
      const headers = new Headers(response.headers);
      headers.set(name, value);
      return new Response(response.body, { ...response, headers });
    };
  };
}
```

### Custom Rate Limit Store (Redis)

```typescript
import { createRateLimiter } from "next-route-compose";
import { redis } from "./redis";

const withRedisRateLimit = createRateLimiter({
  get: async (key) => {
    const data = await redis.get(key);
    return data ? JSON.parse(data) : null;
  },
  set: async (key, value) => {
    await redis.setex(key, 60, JSON.stringify(value));
  },
});

export const GET = compose(
  withRedisRateLimit({ limit: 100, window: 60 })
)(handler);
```

## 📄 License

MIT © Patchy Bean
