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

next-typesafe-api-route

v0.0.13

Published

Type-safe API handlers with OpenAPI documentation for Next.js Route Handlers (App Router)

Readme

next-typesafe-api-route

A type-safe API handler library for Next.js Route Handlers (App Router) with automatic OpenAPI documentation generation.

Features

  • 🔒 Type-safe API handlers for Next.js App Router
  • 📝 Automatic OpenAPI documentation generation
  • 🔍 Request validation using Zod schemas
  • 🔐 Modular authentication with built-in handlers
  • 🧩 Comprehensive parameter validation (body, query, path, headers)
  • 📊 Swagger UI integration for interactive API documentation

Installation

npm install next-typesafe-api-route zod @asteasolutions/zod-to-openapi swagger-ui-react

Quick Start

1. Create a Type-Safe API Handler

// app/api/products/route.ts
import { z } from 'zod';
import { createDocumentedApiHandler } from 'next-typesafe-api-route';

// Define request schema
const ProductCreateSchema = z.object({
  name: z.string().min(1).max(100).describe('Product name'),
  price: z.number().positive().describe('Product price in cents'),
  description: z.string().optional().describe('Product description'),
});

// Define response schema
const ProductResponseSchema = z.object({
  id: z.string().uuid().describe('Unique product ID'),
  name: z.string().describe('Product name'),
  price: z.number().describe('Product price in cents'),
  description: z.string().optional().describe('Product description'),
  createdAt: z.string().describe('Creation timestamp'),
});

// Create the POST handler with documentation
export const POST = createDocumentedApiHandler('post', '/api/products', {
  bodySchema: ProductCreateSchema,
  responseSchema: ProductResponseSchema,
  openapi: {
    summary: 'Create a new product',
    description: 'Creates a new product in the catalog',
    tags: ['Products'],
    operationId: 'createProduct',
  },
  handler: async ({ body }) => {
    // Create the product
    const product = {
      id: crypto.randomUUID(),
      name: body!.name,
      price: body!.price,
      description: body?.description,
      createdAt: new Date().toISOString(),
    };

    return product;
  },
});

2. Create a Swagger UI Page

// app/api/docs/page.tsx
'use client';

import { useEffect, useState } from 'react';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';

export default function ApiDocsPage() {
  const [spec, setSpec] = useState(null);

  useEffect(() => {
    fetch('/api/docs/openapi')
      .then((response) => response.json())
      .then((data) => setSpec(data))
      .catch((error) => console.error('Error loading OpenAPI spec:', error));
  }, []);

  return (
    <div className="container mx-auto py-8">
      <h1 className="text-3xl font-bold mb-8">API Documentation</h1>
      {spec ? <SwaggerUI spec={spec} /> : <p>Loading API documentation...</p>}
    </div>
  );
}

3. OpenAPI Documentation Generation

In Next.js development mode, route handlers aren't imported until they're accessed, which means your API endpoints won't be registered in the OpenAPI registry until they're called at least once. To solve this issue, next-typesafe-api-route provides a CLI tool to generate a registry file that imports all your API routes:

npx generate-api-registry

This will scan your app directory for route files and generate a registry file at lib/api-registry.ts that imports all your routes.

Options

  • -d, --dir <directory>: Root directory to scan for route files (default: ./app)
  • -o, --output <file>: Output file path (default: ./lib/api-registry.ts)
  • -v, --verbose: Enable verbose logging

Example Usage

# Generate with default options
npx generate-api-registry

# Specify custom directory and output file
npx generate-api-registry --dir ./src/app --output ./src/lib/api-registry.ts

# Enable verbose logging
npx generate-api-registry --verbose

Integration with Next.js

Add the following to your package.json:

{
  "scripts": {
    "dev": "generate-api-registry && next dev",
    "build": "generate-api-registry && next build"
  }
}

Then use the generated registry in your OpenAPI route:

// app/api/docs/openapi/route.ts
import { generateOpenApiDocument } from 'next-typesafe-api-route';
import { getSharedRegistry, importApiRoutes } from '@/lib/api-registry';

export async function GET() {
  // Get the registry
  await importApiRoutes();
  const registry = getSharedRegistry();

  // Generate the OpenAPI document
  const openApiDoc = generateOpenApiDocument(
    {
      title: 'My API',
      version: '1.0.0',
      description: 'API documentation for my Next.js application',
    },
    registry,
  );

  return Response.json(openApiDoc);
}

API Reference

createApiHandler

Creates a type-safe API route handler without OpenAPI documentation.

function createApiHandler<TBody, TQuery, TPathParams, THeaders, TUser, TOutput>(
  options: ApiHandlerOptions<
    TBody,
    TQuery,
    TPathParams,
    THeaders,
    TUser,
    TOutput
  >,
): (request: NextRequest, context?: any) => Promise<NextResponse>;

createDocumentedApiHandler

Creates a type-safe API route handler with OpenAPI documentation.

function createDocumentedApiHandler<
  TBody,
  TQuery,
  TPathParams,
  THeaders,
  TUser,
  TOutput,
>(
  method: HttpMethod,
  path: string,
  options: ApiHandlerOptions<
    TBody,
    TQuery,
    TPathParams,
    THeaders,
    TUser,
    TOutput
  >,
): (request: NextRequest, context?: any) => Promise<NextResponse>;

ApiHandlerOptions

Configuration options for the API handler.

interface ApiHandlerOptions<
  TBody,
  TQuery,
  TPathParams,
  THeaders,
  TUser,
  TOutput,
> {
  /** Schema for validating request body (optional) */
  bodySchema?: z.ZodType<TBody>;

  /** Schema for validating query parameters (optional) */
  querySchema?: z.ZodType<TQuery>;

  /** Schema for validating path parameters (optional) */
  pathParamsSchema?: z.ZodType<TPathParams>;

  /** Schema for validating headers (optional) */
  headerSchema?: z.ZodType<THeaders>;

  /** Schema for validating form data (optional) */
  formDataSchema?: z.ZodType<any>;

  /** Authentication handler (optional) */
  auth?: {
    /** Authentication handler function */
    handler: AuthHandler<TUser>;

    /** Options to pass to the auth handler */
    options?: any;

    /** Authentication scheme name for OpenAPI */
    scheme?: string;
  };

  /** Response schema for OpenAPI documentation */
  responseSchema?: z.ZodType<TOutput>;

  /** OpenAPI metadata */
  openapi?: OpenApiMetadata;

  /** Handler function that processes the validated inputs */
  handler: (params: {
    body: TBody | undefined;
    query: TQuery | undefined;
    pathParams: TPathParams | undefined;
    headers: THeaders | undefined;
    user: TUser | undefined;
    request: NextRequest;
  }) => Promise<TOutput>;
}

OpenApiMetadata

Metadata for OpenAPI documentation.

interface OpenApiMetadata {
  /** Operation summary */
  summary: string;

  /** Detailed operation description */
  description?: string;

  /** Operation tags for grouping */
  tags?: string[];

  /** Operation ID (unique identifier) */
  operationId?: string;

  /** Whether to include this endpoint in the OpenAPI docs */
  includeInDocs?: boolean;

  /** Custom request body for special cases like file uploads */
  requestBody?: any;

  /** Custom responses beyond the standard ones */
  responses?: Record<
    string,
    {
      description: string;
      content?: Record<string, { schema: z.ZodType<any> }>;
    }
  >;
}

Authentication Handlers

bearerAuth

Authenticates requests using Bearer token in Authorization header.

const bearerAuth: AuthHandler<User>;

Usage:

import { bearerAuth } from 'next-typesafe-api-route';
import { jwtVerify } from 'jose';

// JWT verification function
const verifyToken = async (token: string) => {
  const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
  const { payload } = await jwtVerify(token, secret);
  return payload;
};

export const GET = createDocumentedApiHandler('get', '/api/users/me', {
  auth: {
    handler: bearerAuth,
    options: {
      verifyToken,
    },
    scheme: 'bearerAuth',
  },
  // ...
});

apiKeyAuth

Authenticates requests using API key in headers.

const apiKeyAuth: AuthHandler<User>;

Usage:

import { apiKeyAuth } from 'next-typesafe-api-route';

export const POST = createDocumentedApiHandler(
  'post',
  '/api/webhooks/payment',
  {
    auth: {
      handler: apiKeyAuth,
      options: {
        headerName: 'x-payment-signature',
        keys: [process.env.PAYMENT_WEBHOOK_SECRET!],
      },
      scheme: 'apiKeyAuth',
    },
    // ...
  },
);

createAuthHandler

Creates a custom authentication handler.

function createAuthHandler<TUser, TOptions>(
  handler: (
    request: NextRequest,
    options: TOptions,
  ) => Promise<AuthResult<TUser>>,
): AuthHandler<TUser>;

Usage:

import { createAuthHandler } from 'next-typesafe-api-route';

// Custom auth handler for admin-only endpoints
const adminAuth = createAuthHandler(async (request) => {
  const authHeader = request.headers.get('authorization');

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return {
      success: false,
      error: 'Missing or invalid authorization header',
      status: 401,
    };
  }

  const token = authHeader.split(' ')[1];

  try {
    // Verify the token (simplified example)
    const user = { id: '123', email: '[email protected]', role: 'admin' };

    // Check if user is an admin
    if (user.role !== 'admin') {
      return {
        success: false,
        error: 'Insufficient permissions',
        status: 403,
      };
    }

    return { success: true, user };
  } catch (error) {
    return {
      success: false,
      error: 'Invalid or expired token',
      status: 401,
    };
  }
});

OpenAPI Generation

createOpenApiRoute

Creates an OpenAPI documentation route handler for Next.js.

function createOpenApiRoute(config?: Partial<OpenApiConfig>): () => Response;

generateOpenApiDocument

Generates the OpenAPI specification.

function generateOpenApiDocument(config?: Partial<OpenApiConfig>): any;

Examples

GET Request with Query Parameters

// app/api/users/route.ts
import { z } from 'zod';
import { createDocumentedApiHandler } from 'next-typesafe-api-route';

const UserQuerySchema = z.object({
  page: z.string().regex(/^\d+$/).transform(Number).optional().default('1'),
  limit: z.string().regex(/^\d+$/).transform(Number).optional().default('10'),
  sortBy: z
    .enum(['name', 'email', 'createdAt'])
    .optional()
    .default('createdAt'),
  order: z.enum(['asc', 'desc']).optional().default('desc'),
  search: z.string().optional(),
});

const UserResponseSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string(),
});

export const GET = createDocumentedApiHandler('get', '/api/users', {
  querySchema: UserQuerySchema,
  responseSchema: z.object({
    users: z.array(UserResponseSchema),
    pagination: z.object({
      total: z.number(),
      page: z.number(),
      limit: z.number(),
      totalPages: z.number(),
    }),
  }),
  openapi: {
    summary: 'List users',
    description: 'Get a paginated list of users',
    tags: ['Users'],
    operationId: 'listUsers',
  },
  handler: async ({ query }) => {
    // In a real app, you would fetch users from a database
    // based on the query parameters
    const { page, limit, sortBy, order, search } = query!;

    // Mock data
    const users = Array.from({ length: 3 }, (_, i) => ({
      id: crypto.randomUUID(),
      name: `User ${i + 1}`,
      email: `user${i + 1}@example.com`,
      createdAt: new Date().toISOString(),
    }));

    return {
      users,
      pagination: {
        total: 100,
        page,
        limit,
        totalPages: Math.ceil(100 / limit),
      },
    };
  },
});

Path Parameters

// app/api/users/[id]/route.ts
import { z } from 'zod';
import { createDocumentedApiHandler } from 'next-typesafe-api-route';

export const GET = createDocumentedApiHandler('get', '/api/users/{id}', {
  pathParamsSchema: z.object({
    id: z.string().uuid().describe('User ID'),
  }),
  responseSchema: z.object({
    id: z.string().uuid(),
    name: z.string(),
    email: z.string().email(),
    createdAt: z.string(),
  }),
  openapi: {
    summary: 'Get user by ID',
    description: 'Retrieves a specific user by their ID',
    tags: ['Users'],
    operationId: 'getUser',
  },
  handler: async ({ pathParams }) => {
    // In a real app, you would fetch the user from a database
    return {
      id: pathParams!.id,
      name: 'John Doe',
      email: '[email protected]',
      createdAt: new Date().toISOString(),
    };
  },
});

File Upload

// app/api/uploads/route.ts
import { z } from 'zod';
import { createDocumentedApiHandler } from 'next-typesafe-api-route';

export const POST = createDocumentedApiHandler('post', '/api/uploads', {
  formDataSchema: z.object({
    description: z.string().optional().describe('File description'),
  }),
  responseSchema: z.object({
    fileId: z.string().describe('Uploaded file ID'),
    url: z.string().describe('URL to access the file'),
    filename: z.string().describe('Original filename'),
    size: z.number().describe('File size in bytes'),
  }),
  openapi: {
    summary: 'Upload a file',
    description: 'Uploads a file to the server',
    tags: ['Files'],
    operationId: 'uploadFile',
    requestBody: {
      content: {
        'multipart/form-data': {
          schema: {
            type: 'object',
            properties: {
              file: {
                type: 'string',
                format: 'binary',
                description: 'File to upload',
              },
              description: {
                type: 'string',
                description: 'File description',
              },
            },
            required: ['file'],
          },
        },
      },
    },
  },
  handler: async ({ request }) => {
    const formData = await request.formData();
    const file = formData.get('file') as File;
    const description = formData.get('description') as string | null;

    // Process file upload
    return {
      fileId: crypto.randomUUID(),
      url: `https://example.com/files/${crypto.randomUUID()}`,
      filename: file.name,
      size: file.size,
    };
  },
});

Optional Authentication

// app/api/products/[id]/route.ts
import { z } from 'zod';
import {
  createDocumentedApiHandler,
  bearerAuth,
} from 'next-typesafe-api-route';
import { jwtVerify } from 'jose';

// JWT verification function
const verifyToken = async (token: string) => {
  const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
  const { payload } = await jwtVerify(token, secret);
  return payload;
};

export const GET = createDocumentedApiHandler('get', '/api/products/{id}', {
  pathParamsSchema: z.object({
    id: z.string().uuid().describe('Product ID'),
  }),
  auth: {
    handler: bearerAuth,
    options: {
      verifyToken,
      required: false, // Authentication is optional
    },
    scheme: 'bearerAuth',
  },
  responseSchema: z.object({
    id: z.string().uuid(),
    name: z.string(),
    price: z.number(),
    description: z.string().optional(),
    inventory: z.number().optional(),
  }),
  openapi: {
    summary: 'Get product by ID',
    description: 'Retrieves a specific product by its ID',
    tags: ['Products'],
    operationId: 'getProduct',
  },
  handler: async ({ pathParams, user }) => {
    // Fetch the product
    const product = {
      id: pathParams!.id,
      name: 'Sample Product',
      price: 2999,
      description: 'A great product',
      // Private field only visible to authenticated users
      inventory: user ? 150 : undefined,
    };

    return product;
  },
});

Version Compatibility

This package requires:

  • @asteasolutions/zod-to-openapi version 7.3.0 or higher
  • zod version 3.0.0 or higher
  • next version 13.0.0 or higher

Contributing

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

License

This project is licensed under the MIT License - see the LICENSE file for details.