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

@prism-engineer/router

v0.3.0

Published

Type-safe Express.js router with automatic client generation

Readme

@prism-engineer/router

A type-safe router library that provides Express.js integration with automatic API client code generation. This library serves as a wrapper around Express.js that adds:

⚠️ Breaking Change Notice: Starting from version 0.1.0, this package uses ES Modules (ESM) only. If you need CommonJS support, please use version 0.0.11 or earlier.

  1. Dynamic route loading from file patterns
  2. Automatic TypeScript API client generation
  3. Type-safe API consumption for frontends and backends

Table of Contents

Installation

# Install the router
npm install @prism-engineer/router

# Install required peer dependencies
npm install @sinclair/typebox express

Requirements:

  • Node.js 16+: Required for ES Module support
  • ES Modules: This package now uses ESM exclusively (version 0.1.0+)
  • TypeBox: Required for defining route schemas with runtime validation and type safety
  • Express.js: Required as the underlying web framework

Migration from CommonJS: If you're upgrading from version 0.0.11 or earlier, you'll need to:

  1. Update your package.json to include "type": "module"
  2. Change require() statements to import statements
  3. Change module.exports to export statements

Quick Start

1. Import and Initialize

import express from 'express';
import { router } from '@prism-engineer/router';

// Access the Express app instance
const app = router.app;

// Add middleware as needed
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

2. Create Route Files

Create route files using the createApiRoute helper. TypeBox is required for defining type-safe schemas:

// api/hello.ts - Simple GET route
import { createApiRoute } from '@prism-engineer/router';
import { Type } from '@sinclair/typebox';

export const helloRoute = createApiRoute({
  path: '/api/hello',
  method: 'GET',
  response: {
    200: {
      contentType: 'application/json',
      body: Type.Object({
        message: Type.String()
      })
    }
  },
  handler: async (req) => {
    return {
      status: 200 as const,
      body: { message: 'Hello, World!' }
    };
  }
});
// api/users.ts - POST route with request body
import { createApiRoute } from '@prism-engineer/router';
import { Type } from '@sinclair/typebox';

export const createUserRoute = createApiRoute({
  path: '/api/users',
  method: 'POST',
  request: {
    body: Type.Object({
      name: Type.String(),
      email: Type.String()
    })
  },
  response: {
    201: {
      contentType: 'application/json',
      body: Type.Object({
        id: Type.Number(),
        name: Type.String(),
        email: Type.String()
      })
    }
  },
  handler: async (req) => {
    const { name, email } = req.body;
    return {
      status: 201 as const,
      body: { id: 1, name, email }
    };
  }
});

3. Load Routes Dynamically

// Load all API routes using RegExp pattern matching
await router.loadRoutes('./api', /.*\.ts$/);

// Load specific route patterns
await router.loadRoutes('./api/v1', /.*\.ts$/);

// Load multiple directories and patterns
await router.loadRoutes('./api/v1', /users\.ts$/);
await router.loadRoutes('./api/v2', /.*\.ts$/);
await router.loadRoutes('./admin', /.*\.ts$/);

4. Start the Server

// Start the server
app.listen(3000, () => {
  console.log('Server running on port 3000');
});

API Client Generation

Configuration File

Create config.prism.router.ts in your project root:

export default {
  outputDir: './generated',
  name: 'ApiClient',
  baseUrl: 'http://localhost:3000',
  routes: {
    directory: './api',
    pattern: /.*\.ts$/
  }
} as const;

Programmatic Compilation

import { router } from '@prism-engineer/router';

// Compile with custom config
await router.compile({
  outputDir: './src/generated',
  name: 'MyApiClient',
  baseUrl: 'http://localhost:3000',
  routes: {
    directory: './api',
    pattern: /.*\.ts$/
  }
});

CLI Usage

The CLI provides a simple way to generate API clients from your route definitions:

# Generate API client using config file
npx @prism-engineer/router compile

# Alternative: use the binary name directly (after installation)
npx prism-router compile

# Show CLI help
npx @prism-engineer/router help

Configuration Options: The CLI looks for configuration files in this order:

  • config.prism.router.ts
  • config.prism.router.js
  • prism.config.ts
  • prism.config.js

Example output:

🔍 Loading configuration...
📁 Output directory: ./generated
🏷️  Client name: ApiClient
🌐 Base URL: http://localhost:3000
📂 Routes directory: ./api
🔍 Pattern: /.*\.ts$/

⚡ Compiling API client...
✅ API client generated successfully!
📄 Generated file: ./generated/ApiClient.generated.ts

Using Generated Client

The generated client mirrors your API structure using actual paths:

import { createApiClient } from './generated/ApiClient.generated';

const client = createApiClient('http://localhost:3000');

// GET /api/hello -> client.api.hello.get()
const hello = await client.api.hello.get();

// POST /api/users -> client.api.users.post({ body: {...} })
const newUser = await client.api.users.post({ 
  body: { name: 'John', email: '[email protected]' } 
});

Query Parameters

Add query parameters to GET requests for filtering, pagination, etc:

// Route definition
export const getUsersRoute = createApiRoute({
  path: '/api/users',
  method: 'GET',
  request: {
    query: Type.Object({
      page: Type.Optional(Type.Number()),
      limit: Type.Optional(Type.Number()),
      search: Type.Optional(Type.String())
    })
  },
  response: {
    200: {
      contentType: 'application/json',
      body: Type.Array(Type.Object({
        id: Type.Number(),
        name: Type.String(),
        email: Type.String()
      }))
    }
  },
  handler: async (req) => {
    const { page = 1, limit = 10, search } = req.query;
    return {
      status: 200 as const,
      body: [{ id: 1, name: 'John', email: '[email protected]' }]
    };
  }
});
// Client usage
const users = await client.api.users.get({ 
  query: { page: 1, limit: 10, search: 'john' } 
});

Path Parameters

Use {paramName} syntax in routes. Client uses underscore notation _paramName_:

// Route definition
export const getUserByIdRoute = createApiRoute({
  path: '/api/users/{userId}',
  method: 'GET',
  response: {
    200: {
      contentType: 'application/json',
      body: Type.Object({
        id: Type.Number(),
        name: Type.String(),
        email: Type.String()
      })
    }
  },
  handler: async (req) => {
    const { userId } = req.params;
    return {
      status: 200 as const,
      body: { id: Number(userId), name: 'John', email: '[email protected]' }
    };
  }
});
// Client usage
const user = await client.api.users._userId_.get('123');

// Multiple path parameters (passed in order)
const comment = await client.api.users._userId_.posts._postId_.get('123', '456');

Headers

Define expected headers for validation and typing:

// Route definition
export const protectedRoute = createApiRoute({
  path: '/api/protected',
  method: 'GET',
  request: {
    headers: Type.Object({
      authorization: Type.String(),
      'x-api-version': Type.Optional(Type.String())
    })
  },
  response: {
    200: {
      contentType: 'application/json',
      body: Type.Object({
        message: Type.String()
      })
    }
  },
  handler: async (req) => {
    const { authorization } = req.headers;
    return {
      status: 200 as const,
      body: { message: 'Access granted' }
    };
  }
});
// Client usage
const result = await client.api.protected.get({
  headers: { 
    authorization: 'Bearer token123',
    'x-api-version': 'v1' 
  }
});

Authentication

Define reusable authentication schemes and use them in routes:

Step 1: Define Authentication Schemes

// auth/schemes.ts
import { createAuthScheme } from '@prism-engineer/router';
import express from 'express';

export const bearerAuth = createAuthScheme({
  name: 'bearer',
  validate: async (req: express.Request) => {
    const authHeader = req.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
      const token = authHeader.substring(7);
      const user = await validateJWT(token);
      return { user, scopes: user.permissions };
    }
    throw new Error('Missing or invalid bearer token');
  }
});

export const apiKeyAuth = createAuthScheme({
  name: 'apiKey',
  validate: async (req: express.Request) => {
    const key = req.headers['x-api-key'] as string;
    if (key) {
      const client = await validateApiKey(key);
      return { client, scopes: ['read', 'write'] };
    }
    throw new Error('Missing API key');
  }
});

// Placeholder functions for the example
declare function validateJWT(token: string): Promise<{ id: string; permissions: string[] }>;
declare function validateApiKey(key: string): Promise<{ id: string }>;

Step 2: Use Auth Schemes in Routes

// api/users.ts
import { bearerAuth } from '../auth/schemes';

export const getUsersRoute = createApiRoute({
  path: '/api/users',
  method: 'GET',
  auth: bearerAuth, // Single auth scheme
  response: {
    200: {
      contentType: 'application/json',
      body: Type.Array(Type.Object({
        id: Type.Number(),
        name: Type.String()
      }))
    }
  },
  handler: async (req) => {
    // req.auth contains { name: <scheme>, context: <auth result> }
    const { user } = req.auth.context;
    const authScheme = req.auth.name; // 'bearer' in this case
    return {
      status: 200 as const,
      body: [{ id: 1, name: 'John' }]
    };
  }
});

// Multiple auth schemes (OR logic)
export const flexibleRoute = createApiRoute({
  path: '/api/flexible',
  method: 'GET',
  auth: [bearerAuth, apiKeyAuth], // Either bearer OR API key
  handler: async (req) => {
    // req.auth is a union type that preserves which scheme was used
    if (req.auth.name === 'bearer') {
      // TypeScript knows context contains user data
      const { user } = req.auth.context;
      console.log('Authenticated user:', user.id);
    } else if (req.auth.name === 'apiKey') {
      // TypeScript knows context contains client data
      const { client } = req.auth.context;
      console.log('Authenticated client:', client.id);
    }
    return { status: 200 as const, body: { success: true } };
  }
});

Authentication Context Structure

The req.auth object now has a strongly-typed structure that preserves both the authentication scheme name and the validated result:

// req.auth structure (for single auth scheme)
{
  name: 'your-scheme-name',              // Literal type of your auth scheme name
  context: <YourValidateReturnType>      // Exactly what your validate function returns
}

// req.auth structure (for multiple auth schemes - union type)
{
  name: 'scheme1',
  context: <Scheme1ValidateReturnType>
} | {
  name: 'scheme2', 
  context: <Scheme2ValidateReturnType>
} | ...

Key Features:

  • Full Request Access: Your validate function receives the complete express.Request object
  • Type Safety: req.auth.context is strongly typed based on your validate function's return type
  • Scheme Discrimination: req.auth.name lets you identify which auth scheme was used
  • Maximum Flexibility: Extract authentication data from headers, query params, cookies, or anywhere in the request

Example with Custom Return Types:

const customAuth = createAuthScheme({
  name: 'custom-jwt',
  validate: async (req: express.Request) => {
    // Access any part of the request
    const token = req.headers.authorization?.replace('Bearer ', '') || 
                  req.query.token as string ||
                  req.cookies?.jwt;
    
    if (!token) throw new Error('No token provided');
    
    // Your validate function can return ANY type
    const decoded = await verifyJWT(token);
    return { userId: decoded.sub, permissions: decoded.scope.split(' ') };
  }
});

const route = createApiRoute({
  path: '/api/custom',
  method: 'GET',
  auth: customAuth,
  response: {
    200: {
      contentType: 'application/json',
      body: Type.Object({
        message: Type.String()
      })
    }
  },
  handler: async (req) => {
    // req.auth.context is typed as { userId: string, permissions: string[] }
    const userId: string = req.auth.context.userId;
    const permissions: string[] = req.auth.context.permissions;
    const schemeName: 'custom-jwt' = req.auth.name;
    // ...
    return { status: 200 as const, body: { message: 'Success' } };
  }
});

// Placeholder function for the example
declare function verifyJWT(token: string): Promise<{ sub: string; scope: string }>;

// Multiple auth schemes create union types
const multiAuthRoute = createApiRoute({
  path: '/api/multi',
  method: 'GET',
  auth: [bearerAuth, apiKeyAuth], // Union of different schemes
  handler: async (req) => {
    // req.auth is union type with proper discrimination
    if (req.auth.name === 'bearer') {
      // TypeScript knows this is bearer auth result
      const user = req.auth.context.user; // Typed correctly
      const scopes = req.auth.context.scopes;
    } else if (req.auth.name === 'apiKey') {
      // TypeScript knows this is API key auth result  
      const client = req.auth.context.client; // Typed correctly
      const scopes = req.auth.context.scopes;
    }
    // Full type safety with literal string discrimination!
  }
});

Step 3: Client Authentication

The generated client automatically handles authentication:

import { createApiClient } from './generated/ApiClient.generated';

// Initialize client with auth schemes
const client = createApiClient('http://localhost:3000', {
  auth: {
    bearer: () => localStorage.getItem('token'),
    apiKey: 'your-api-key'
  }
});

// Auth headers automatically added to protected endpoints
const users = await client.api.users.get(); // Adds Bearer header
const data = await client.api.flexible.get(); // Adds appropriate auth header

Advanced Authentication Patterns

// Note: Advanced authentication patterns like getToken and onUnauthorized
// are not currently implemented in the generated client code.
// The generated client currently supports simple string tokens or functions
// that return tokens.

// Dynamic token management (not currently implemented)
const client = createApiClient(baseUrl, {
  auth: {
    bearer: () => authStore.getAccessToken(),
    apiKey: 'your-api-key'
  }
});

// Multiple auth schemes for different endpoint types
const client = createApiClient(baseUrl, {
  auth: {
    bearer: userToken,      // For user endpoints
    apiKey: serviceKey,     // For service endpoints
    oauth: oauthToken       // For OAuth endpoints
  }
});

Request Options

All HTTP methods support the same options structure:

interface RequestOptions {
  query?: Record<string, any>;
  body?: any;
  headers?: Record<string, string>;
}

// Example with query parameters
await client.api.users.get({ 
  query: { page: 1, limit: 10 }
});

// Example with request body and headers
await client.api.users.post({ 
  body: { name: 'John', email: '[email protected]' },
  headers: { 'content-type': 'application/json' }
});

Key Features

Core Benefits

  • Runtime Validation: Automatic request/response validation using TypeBox schemas
  • Type Safety: TypeScript types automatically inferred from schemas
  • Path-Based Client: Generated client mirrors your API structure (client.api.users.get())
  • JSON Schema Output: Generate OpenAPI/Swagger documentation automatically

Handler Functions

Route handlers are async functions that receive a typed request object and return a response object:

handler: async (req) => {
  // req.body - typed request body (if defined)
  // req.query - typed query parameters (if defined) 
  // req.headers - typed headers (if defined)
  // req.params - extracted path parameters
  
  return {
    status: 200 as const,
    body: { /* response data */ },
    headers?: { /* optional response headers */ }
  };
}

Response Types

The router supports two types of responses based on the contentType you specify:

JSON Responses

For JSON content types (application/json, application/vnd.api+json, application/ld+json, text/json), return a body object:

export const jsonRoute = createApiRoute({
  path: '/api/data',
  method: 'GET',
  response: {
    200: {
      contentType: 'application/json',
      body: Type.Object({
        message: Type.String(),
        timestamp: Type.Number()
      })
    }
  },
  handler: async (req) => {
    return {
      status: 200 as const,
      body: {
        message: 'Hello, World!',
        timestamp: Date.now()
      }
    };
  }
});

Custom Content Types

For non-JSON content types (like text/plain, text/html, image/png, etc.), return a custom function that receives the Express response object:

export const customRoute = createApiRoute({
  path: '/api/download',
  method: 'GET',
  response: {
    200: {
      contentType: 'text/plain'
    },
    404: {
      contentType: 'application/json',
      body: Type.Object({
        error: Type.String()
      })
    }
  },
  handler: async (req) => {
    const data = await getFileData();
    
    if (!data) {
      return {
        status: 404 as const,
        body: { error: 'File not found' }
      };
    }
    
    return {
      status: 200 as const,
      custom: (res) => {
        res.setHeader('Content-Disposition', 'attachment; filename="data.txt"');
        res.send(data);
      }
    };
  }
});

// Example: Streaming response for large files or real-time data
export const streamRoute = createApiRoute({
  path: '/api/stream/{fileId}',
  method: 'GET',
  response: {
    200: {
      contentType: 'application/octet-stream'
    },
    404: {
      contentType: 'application/json',
      body: Type.Object({
        error: Type.String()
      })
    }
  },
  handler: async (req) => {
    const { fileId } = req.params;
    const fileStream = await getFileStream(fileId);
    
    if (!fileStream) {
      return {
        status: 404 as const,
        body: { error: 'File not found' }
      };
    }
    
    return {
      status: 200 as const,
      custom: (res) => {
        res.setHeader('Content-Disposition', `attachment; filename="${fileId}"`);
        res.setHeader('Transfer-Encoding', 'chunked');
        
        // Stream the file directly to the response
        fileStream.pipe(res);
        
        // Handle stream errors
        fileStream.on('error', (err) => {
          console.error('Stream error:', err);
          if (!res.headersSent) {
            res.status(500).json({ error: 'Stream error' });
          }
        });
      }
    };
  }
});

// Example: Server-Sent Events (SSE) for real-time updates
export const sseRoute = createApiRoute({
  path: '/api/events',
  method: 'GET',
  response: {
    200: {
      contentType: 'text/event-stream'
    }
  },
  handler: async (req) => {
    return {
      status: 200 as const,
      custom: (res) => {
        // Set SSE headers
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Connection', 'keep-alive');
        res.setHeader('Access-Control-Allow-Origin', '*');
        
        // Send initial connection message
        res.write('data: {"type":"connected","timestamp":' + Date.now() + '}\n\n');
        
        // Set up interval to send periodic updates
        const interval = setInterval(() => {
          const data = {
            type: 'update',
            timestamp: Date.now(),
            data: Math.random()
          };
          res.write(`data: ${JSON.stringify(data)}\n\n`);
        }, 1000);
        
        // Clean up when client disconnects
        req.on('close', () => {
          clearInterval(interval);
          console.log('SSE connection closed');
        });
        
        // Handle errors
        res.on('error', (err) => {
          console.error('SSE error:', err);
          clearInterval(interval);
        });
      }
    };
  }
});

// Placeholder functions for the examples
declare function getFileData(): Promise<string | null>;
declare function getFileStream(fileId: string): Promise<NodeJS.ReadableStream | null>;

Key Points:

  • JSON responses: Use body property with TypeBox schema validation
  • Custom responses: Use custom function for full control over the response
  • Content-Type: Automatically set by the framework based on your schema
  • Mixed responses: You can mix JSON and custom content types in the same route (different status codes)

Complete Example

import express from 'express';
import { router } from '@prism-engineer/router';

async function main() {
  // Get the Express app
  const app = router.app;
  
  // Add middleware
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  
  // Load routes from multiple directories
  await router.loadRoutes('./api', /.*\.ts$/);
  await router.loadRoutes('./admin', /.*\.ts$/);
  
  // Start server
  app.listen(3000, () => {
    console.log('🚀 Server running on port 3000');
  });
  
  // Generate API client
  await router.compile({
    outputDir: './generated',
    name: 'ApiClient',
    baseUrl: 'http://localhost:3000',
    routes: {
      directory: './api',
      pattern: /.*\.ts$/
    }
  });
  console.log('✅ API client generated');
}

main().catch(console.error);

Configuration Options

The config.prism.router.ts file supports these options:

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | outputDir | string | ✅ | - | Output directory for generated client | | name | string | ✅ | - | Name of the generated client function | | baseUrl | string | ✅ | - | Base URL for API calls | | routes.directory | string | ✅ | - | Directory to scan for route files | | routes.pattern | RegExp | ✅ | - | RegExp pattern to match route files |

export default {
  outputDir: './generated',
  name: 'ApiClient',
  baseUrl: 'http://localhost:3000',
  routes: {
    directory: './api',
    pattern: /.*\.ts$/
  }
} as const;

Development

Dependencies

  • Use npm install {package} to add dependencies
  • Compatible with any Node.js project

Testing

  • Run tests: npm test
  • Watch mode: npm run test:watch
  • Coverage: npm run test:coverage

Package Info

  • Package name: @prism-engineer/router
  • Type: ES Module library for Express.js applications
  • Current version: 0.1.0 (ESM-only)
  • Last CommonJS version: 0.0.11
  • Node.js compatibility: >= 16.0.0 (ES Modules required)