@yigityalim/next-router
v1.0.1
Published
Type-safe Next.js API route handler with composable middleware, plugins, and OpenAPI support
Maintainers
Readme
@yigityalim/next-router
Type-safe Next.js API route handler with composable middleware, plugins, and OpenAPI support
✨ 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 nextPeer Dependencies:
next>= 14.0.0zod>= 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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
MIT © myy
🔗 Links
- Changelog
- Package Size: ~105 KB (minified, tree-shakeable)
- License: MIT
Made with ❤️ for Next.js developers
