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

@yigityalim/next-router

v1.0.1

Published

Type-safe Next.js API route handler with composable middleware, plugins, and OpenAPI support

Readme

@yigityalim/next-router

Type-safe Next.js API route handler with composable middleware, plugins, and OpenAPI support

npm version License: MIT Node.js Version

✨ Features

  • 🔒 Type-Safe: Full TypeScript support with Zod validation and conditional types
  • 🧩 Composable: Schema-agnostic plugin system for reusable route logic
  • 🎯 Flexible Plugins: Plugins work across different schemas - create once, use everywhere
  • 🔑 Multi-Auth: Session, API key, and bearer token authentication out of the box
  • 🚀 Performance: Built-in rate limiting, caching, and response optimization
  • 📝 OpenAPI: Optional automatic OpenAPI 3.1 spec generation
  • 🎨 DX: Intuitive builder pattern with full IntelliSense support
  • 🔌 Extensible: 9 built-in plugins + 7 route presets + custom plugin support

📦 Installation

pnpm add @yigityalim/next-router zod next
# or
npm install @yigityalim/next-router zod next
# or
yarn add @yigityalim/next-router zod next

Peer Dependencies:

  • next >= 14.0.0
  • zod >= 3.0.0
  • Node.js >= 18.0.0

🚀 Quick Start

Basic Route

// app/api/hello/route.ts
import { route } from "@yigityalim/next-router";
import { z } from "zod";

const querySchema = z.object({
  name: z.string(),
});

export const GET = route()
  .query(querySchema)
  .handler(async (req, ctx) => {
    const { name } = ctx.query; // Type-safe! No undefined
    return ctx.json({ message: `Hello, ${name}!` });
  });

Authenticated Route

// app/api/users/route.ts
import { authenticatedRoute } from "@yigityalim/next-router";
import { z } from "zod";

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

export const POST = authenticatedRoute()
  .body(userSchema)
  .handler(async (req, ctx) => {
    const user = ctx.body; // Fully typed!
    const currentUser = ctx.user; // From auth plugin
    
    // Create user logic...
    
    return ctx.json({ success: true, user });
  });

API Key Authentication

// app/api/webhooks/route.ts
import { apiKeyRoute, envApiKeyValidator } from "@yigityalim/next-router";

export const POST = apiKeyRoute({
  validator: envApiKeyValidator(process.env.API_KEYS!)
})
  .body(webhookSchema)
  .handler(async (req, ctx) => {
    const payload = ctx.body;
    const clientId = ctx.clientId; // From API key plugin
    
    // Process webhook...
    
    return ctx.json({ received: true });
  });

🧩 Plugin System

Built-in Plugins

Use pre-built plugins for common functionality:

import {
  timestampPlugin,      // Adds ctx.timestamp
  requestIdPlugin,      // Adds ctx.requestId
  correlationIdPlugin,  // Adds ctx.correlationId
  userIdPlugin,         // Adds ctx.userId
  ipAddressPlugin,      // Adds ctx.ipAddress
  userAgentPlugin,      // Adds ctx.userAgent
  performancePlugin,    // Adds ctx.performance
  devicePlugin,         // Adds ctx.device
  geoPlugin,           // Adds ctx.geo
} from "@yigityalim/next-router";

export const GET = route()
  .extend(timestampPlugin)
  .extend(requestIdPlugin)
  .extend(performancePlugin)
  .handler(async (req, ctx) => {
    ctx.logger.info("Request received", {
      requestId: ctx.requestId,
      timestamp: ctx.timestamp,
    });
    
    // Your logic...
    
    ctx.performance.end();
    return ctx.json({ 
      duration: ctx.performance.duration 
    });
  });

Route Presets

Pre-configured routes for common patterns:

import {
  authenticatedRoute,      // Requires authentication
  publicRoute,             // No auth required
  adminRoute,              // Requires admin role
  apiKeyRoute,             // API key authentication
  publicRateLimitedRoute,  // Public with rate limiting
  cachedRoute,             // Response caching
  webhookRoute,            // Webhook signature validation
} from "@yigityalim/next-router";

// Admin-only endpoint
export const DELETE = adminRoute()
  .params(z.object({ id: z.string() }))
  .handler(async (req, ctx) => {
    const { id } = ctx.params;
    
    // Only admins can access this
    await deleteUser(id);
    
    return ctx.json({ success: true });
  });

// Public endpoint with rate limiting
export const GET = publicRateLimitedRoute({
  maxRequests: 100,
  windowMs: 60000,
})
  .query(searchSchema)
  .handler(async (req, ctx) => {
    const results = await search(ctx.query);
    return ctx.json(results);
  });

Custom Plugins

Create your own reusable plugins:

import { createPlugin, route } from "@yigityalim/next-router";

const tenantPlugin = createPlugin("tenant", async (ctx) => {
  const tenantId = ctx.req.headers.get("x-tenant-id");
  if (!tenantId) {
    throw new Error("Missing tenant ID");
  }
  
  return {
    tenantId,
    isolate: (query: any) => ({ ...query, tenant_id: tenantId }),
  };
});

// Use in any route - works across different schemas!
export const GET = route()
  .extend(tenantPlugin)
  .query(z.object({ status: z.string() }))
  .handler(async (req, ctx) => {
    const { tenantId, isolate } = ctx.tenant; // Fully typed!
    const data = await db.query(isolate(ctx.query));
    return ctx.json(data);
  });

export const POST = route()
  .extend(tenantPlugin) // Same plugin, different schema!
  .body(z.object({ name: z.string() }))
  .handler(async (req, ctx) => {
    const { tenantId, isolate } = ctx.tenant;
    const result = await db.insert(isolate(ctx.body));
    return ctx.json(result);
  });

🔑 Authentication

Session-Based Auth

export const GET = route()
  .auth({ 
    type: "session",
    required: true 
  })
  .handler(async (req, ctx) => {
    const user = ctx.user; // Authenticated user from session
    return ctx.json({ user });
  });

API Key Auth

import { 
  envApiKeyValidator, 
  createMultiKeyValidator,
  createDatabaseValidator 
} from "@yigityalim/next-router";

// Simple: Environment variable
export const GET = route()
  .auth({
    type: "api-key",
    required: true,
    apiKey: {
      validator: envApiKeyValidator(process.env.API_KEYS!)
    }
  })
  .handler(async (req, ctx) => {
    return ctx.json({ message: "Authenticated!" });
  });

// Advanced: Multiple keys with metadata
const validator = createMultiKeyValidator({
  "key_prod_abc": { 
    clientId: "client1", 
    scopes: ["read", "write"] 
  },
  "key_prod_xyz": { 
    clientId: "client2", 
    scopes: ["read"] 
  },
});

export const POST = route()
  .auth({
    type: "api-key",
    required: true,
    apiKey: { validator }
  })
  .handler(async (req, ctx) => {
    const clientId = ctx.clientId; // From validated API key
    const auth = ctx.auth; // Full auth context
    return ctx.json({ clientId, scopes: auth.scopes });
  });

// Database validation
export const DELETE = route()
  .auth({
    type: "api-key",
    required: true,
    apiKey: {
      validator: createDatabaseValidator(async (key) => {
        const apiKey = await db.apiKeys.findUnique({ 
          where: { key } 
        });
        return apiKey ? {
          userId: apiKey.userId,
          permissions: apiKey.permissions,
        } : null;
      })
    }
  })
  .handler(async (req, ctx) => {
    return ctx.json({ success: true });
  });

Bearer Token

export const GET = route()
  .auth({
    type: "bearer",
    required: true,
    bearer: {
      validator: async (token) => {
        const payload = await verifyJWT(token);
        return { 
          userId: payload.sub,
          email: payload.email 
        };
      }
    }
  })
  .handler(async (req, ctx) => {
    const user = ctx.auth; // From validated token
    return ctx.json({ user });
  });

📝 OpenAPI Support (Optional)

OpenAPI is optional - routes work perfectly without it:

// Without OpenAPI - works fine!
export const GET = route()
  .query(listSchema)
  .handler(async (req, ctx) => {
    return ctx.json({ data: [] });
  });

// With OpenAPI - auto-generates documentation
export const GET = route()
  .query(listSchema)
  .openapi({
    method: "GET",
    path: "/api/posts",
    summary: "List all posts",
    description: "Returns a paginated list of posts",
    tags: ["Posts"],
  })
  .handler(async (req, ctx) => {
    return ctx.json({ data: [] });
  });

// Generate OpenAPI spec
import { generateOpenApiSpec } from "@yigityalim/next-router";

export async function GET() {
  const spec = generateOpenApiSpec({
    info: {
      title: "My API",
      version: "1.0.0",
      description: "API documentation",
    },
    servers: [
      { url: "https://api.example.com", description: "Production" },
    ],
  });
  
  return Response.json(spec);
}

🎯 Response Helpers

Convenient response helpers with proper status codes:

export const POST = route()
  .body(userSchema)
  .handler(async (req, ctx) => {
    try {
      const user = await createUser(ctx.body);
      
      // Success responses
      return ctx.json({ user });
      // or
      return ctx.response.created(user, `/api/users/${user.id}`);
      
    } catch (error) {
      // Error responses
      if (error.code === "DUPLICATE_EMAIL") {
        return ctx.response.error(
          "Email already exists", 
          "DUPLICATE", 
          409
        );
      }
      
      // Or use specific helpers
      return ctx.response.badRequest("Invalid input");
      // return ctx.response.unauthorized("Login required");
      // return ctx.response.forbidden("Access denied");
      // return ctx.response.notFound("User not found");
    }
  });

// All available response helpers
ctx.json(data);                           // 200 OK
ctx.json(data, { status: 201 });          // Custom status
ctx.response.success(data);               // 200 OK
ctx.response.created(data, location);     // 201 Created
ctx.response.noContent();                 // 204 No Content
ctx.response.badRequest(message);         // 400 Bad Request
ctx.response.unauthorized(message);       // 401 Unauthorized
ctx.response.forbidden(message);          // 403 Forbidden
ctx.response.notFound(message);           // 404 Not Found
ctx.response.error(message, code, 500);   // 500 Internal Server Error
ctx.response.paginated(data, pagination); // 200 with pagination

⚙️ Configuration

Rate Limiting

import { publicRateLimitedRoute } from "@yigityalim/next-router";

export const GET = publicRateLimitedRoute({
  maxRequests: 100,
  windowMs: 60000, // 1 minute
})
  .handler(async (req, ctx) => {
    return ctx.json({ message: "Success" });
  });

Caching

import { cachedRoute } from "@yigityalim/next-router";

export const GET = cachedRoute({ ttl: 300 }) // 5 minutes
  .handler(async (req, ctx) => {
    const data = await fetchExpensiveData();
    return ctx.json(data);
  });

CORS

export const GET = route()
  .cors({
    origin: ["https://example.com"],
    methods: ["GET", "POST"],
    credentials: true,
  })
  .handler(async (req, ctx) => {
    return ctx.json({ data });
  });

💡 Advanced Examples

Multi-Plugin Composition

import { 
  timestampPlugin, 
  requestIdPlugin, 
  performancePlugin,
  ipAddressPlugin 
} from "@yigityalim/next-router";

export const POST = route()
  .extend(timestampPlugin)
  .extend(requestIdPlugin)
  .extend(performancePlugin)
  .extend(ipAddressPlugin)
  .body(searchSchema)
  .handler(async (req, ctx) => {
    ctx.logger.info("Search request", {
      requestId: ctx.requestId,
      timestamp: ctx.timestamp,
      ip: ctx.ipAddress,
      query: ctx.body.query,
    });
    
    const results = await searchDatabase(ctx.body);
    
    ctx.performance.end();
    
    return ctx.json({
      results,
      meta: {
        duration: ctx.performance.duration,
        requestId: ctx.requestId,
        timestamp: ctx.timestamp,
      },
    });
  });

Complex Validation & Error Handling

import { route, ValidationError } from "@yigityalim/next-router";

const complexSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  age: z.number().min(18).max(120),
  terms: z.boolean().refine(val => val === true),
});

export const POST = route()
  .body(complexSchema)
  .handler(async (req, ctx) => {
    // Body is guaranteed to be valid!
    const { email, password, age } = ctx.body;
    
    try {
      const user = await registerUser({ email, password, age });
      return ctx.response.created(user, `/api/users/${user.id}`);
    } catch (error) {
      if (error instanceof ValidationError) {
        return ctx.response.validationError(
          error.message, 
          error.details
        );
      }
      throw error; // Auto-handled by framework
    }
  });

Webhook with Signature Validation

import { webhookRoute } from "@yigityalim/next-router";
import { verifySignature } from "./crypto";

export const POST = webhookRoute({
  signatureHeader: "x-webhook-signature",
  validator: async (body, signature, secret) => {
    return verifySignature(body, signature, secret);
  },
})
  .body(webhookPayloadSchema)
  .handler(async (req, ctx) => {
    const payload = ctx.body; // Validated and signature verified!
    
    // Process webhook
    await processWebhook(payload);
    
    return ctx.json({ received: true });
  });

📦 Package Exports

// Main package - core functionality
import { 
  route, 
  createPlugin,
  RouteBuilder,
  createRoute
} from "@yigityalim/next-router";

// Plugins subpath - all plugins and presets
import { 
  timestampPlugin,
  requestIdPlugin,
  authenticatedRoute,
  publicRoute,
  apiKeyRoute,
  envApiKeyValidator,
  createApiKeyPlugin
} from "@yigityalim/next-router/plugins";

// You can also import from main package (re-exported)
import { timestampPlugin } from "@yigityalim/next-router";

📚 Type Safety

Conditional Types

Body, query, and params are properly typed based on schema:

// With schema → guaranteed values
export const POST = route()
  .body(z.object({ title: z.string() }))
  .handler(async (req, ctx) => {
    ctx.body.title; // ✅ string (no undefined!)
  });

// Without schema → undefined
export const GET = route()
  .handler(async (req, ctx) => {
    ctx.body; // ✅ undefined (type-safe)
  });

Plugin Type Inference

Plugins are fully type-safe with zero type assertions:

const myPlugin = createPlugin("myData", async (ctx) => {
  return { value: 42, name: "test" };
});

export const GET = route()
  .extend(myPlugin)
  .handler(async (req, ctx) => {
    ctx.myData.value; // ✅ number (inferred!)
    ctx.myData.name;  // ✅ string (inferred!)
  });

🛠️ CLI Tools

# Initialize configuration
npx @yigityalim/next-router-cli init

# Generate a new route
npx @yigityalim/next-router-cli generate api/users --methods GET POST

# Scan routes and generate OpenAPI
npx @yigityalim/next-router-cli scan

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

MIT © myy

🔗 Links

  • Changelog
  • Package Size: ~105 KB (minified, tree-shakeable)
  • License: MIT

Made with ❤️ for Next.js developers