@ndriadev/vite-plugin-universal-api
v2.3.0
Published
Plugin Mock WebSocket, REST and File-based APIs for Vite
Downloads
89
Maintainers
Readme
vite-plugin-universal-api
Accelerate Your Development Journey with Mock APIs without changing your client code.
Built with:
📋 Table of Contents
- Overview
- Features
- Installation
- Quick Start
- Configuration
- How it works
- Usage Examples
- API Reference
- WebSocket API
- Middleware System
- Advanced Features
- Important Notes
- Troubleshooting
- Contributing
- License
🎯 Overview
vite-plugin-universal-api is a comprehensive Vite plugin that transforms your development server into a powerful mock backend. It provides three complementary approaches to handle API requests:
- 📁 File-System Based API - Automatically serve mock data from your file system
- 🔄 REST API Handlers - Define custom programmatic handlers for dynamic responses
- ⚡ WebSocket Support - Real-time bidirectional communication with rooms and broadcast capabilities
Perfect for frontend developers who need to:
- Develop without waiting for backend APIs
- Test edge cases and error scenarios
- Work offline or with unreliable backend connections
- Prototype and demo features quickly
- Simulate real-time features with WebSocket
✨ Features
🎨 File-System Based Mocking
- Zero Configuration - Point to a directory and start serving files
- Smart Path Mapping - Automatic mapping of URL paths to file paths
- Multiple File Formats - JSON, HTML, XML, text files, and binary data
- Directory Index - Automatic
index.jsonlookup for directory requests - Built-in Pagination - Automatic pagination for JSON arrays via query params or body
- Advanced Filtering - Filter JSON arrays by field values with type-safe comparisons
- CRUD Operations - Full support for GET, POST, PUT, PATCH, DELETE on files
🔧 REST API Handlers
- Flexible Routing - Ant-style path patterns (
/users/**,/items/{id}) - HTTP Method Support - GET, POST, PUT, PATCH, DELETE, HEAD
- Dynamic Responses - Programmatic handlers with full request/response control
- Express-like Middleware - Pre-processing, authentication, logging
- Custom Parsers - Compatible with Express body parsers
- Error Handling - Dedicated error middleware chain
- Hybrid Approach - Mix file-based and programmatic handlers
⚡ WebSocket Support
- RFC 6455 Compliant - Full WebSocket protocol implementation
- Room System - Group connections and broadcast to specific rooms
- Compression - permessage-deflate extension support (RFC 7692)
- Heartbeat/Keep-alive - Configurable ping/pong mechanism
- Inactivity Timeout - Automatic connection cleanup
- Event Handlers - onConnect, onMessage, onClose, onError, onPing, onPong
- Pattern Matching - Ant-style patterns for WebSocket endpoints
- Authentication - Custom authentication hook before upgrade
- Sub-protocols - WebSocket sub-protocol negotiation
🛠️ Development Utilities
- Simulated Latency - Add delays to test loading states
- Gateway Timeout - Simulate server timeouts
- Detailed Logging - Configurable log levels (debug, info, warn, error)
- Hot Reload - Changes reflected immediately during development
- TypeScript Support - Full type definitions included
📦 Installation
# pnpm (recommended)
pnpm add -D @ndriadev/vite-plugin-universal-api
# npm
npm install -D @ndriadev/vite-plugin-universal-api
# yarn
yarn add -D @ndriadev/vite-plugin-universal-apiRequirements
- Node.js:
^16.0.0 || ^18.0.0 || >=20.0.0 - Vite:
^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || >=8.0.0
🚀 Quick Start
Minimal Setup
// vite.config.ts
import { defineConfig } from 'vite';
// import mockApi from '@ndriadev/vite-plugin-universal-api' //Default export
import { universalApi } from '@ndriadev/vite-plugin-universal-api' // Named export ;
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
fsDir: 'mock'
})
]
});Create a mock file:
// mock/users.json
[
{ "id": 1, "name": "John Doe", "email": "[email protected]" },
{ "id": 2, "name": "Jane Smith", "email": "[email protected]" }
]Access it:
curl http://localhost:5173/api/users
# Returns the JSON array⚙️ Configuration
Basic Options
interface UniversalApiOptions {
/**
* Disable the entire plugin
* @default false
*/
disable?: boolean;
/**
* Logging verbosity level
* @default 'info'
*/
logLevel?: 'debug' | 'info' | 'warn' | 'error';
/**
* URL prefix(es) for API endpoints
* Can be a single string or array of prefixes
* @example '/api' or ['/api', '/mock']
*/
endpointPrefix: string | string[];
/**
* Directory path for file-based mocking (relative to project root)
* Set to null to disable file-based routing
* @example 'mock' or 'src/mocks'
*/
fsDir?: string | null;
/**
* Enable WebSocket support
* When true, wsHandlers option becomes required
* @default false
*/
enableWs?: boolean;
/**
* Simulated response delay in milliseconds
* Useful for testing loading states
* @default 0
*/
delay?: number;
/**
* Timeout for long-running handlers (in ms)
* Returns 504 Gateway Timeout if exceeded
* @default 30000 (30 seconds)
*/
gatewayTimeout?: number;
/**
* Behavior for unmatched requests
* - '404': Return 404 Not Found
* - 'forward': Pass to next Vite middleware (e.g., serve static files)
* @default '404'
*/
noHandledRestFsRequestsAction?: '404' | 'forward';
/**
* Request body parsing configuration
* @default true (built-in parser)
*/
parser?: boolean | {
parser: ParserFunction | ParserFunction[];
transform: (req: IncomingMessage) => {
body?: any;
files?: { name: string; content: Buffer; contentType: string }[];
query?: URLSearchParams;
}
};
/**
* Global middleware executed before all handlers
* Similar to Express middleware
*/
handlerMiddlewares?: MiddlewareFunction[];
/**
* Error handling middleware
*/
errorMiddlewares?: ErrorHandlerFunction[];
/**
* REST API handler configurations
*/
handlers?: RestHandler[];
/**
* WebSocket handler configurations (required when enableWs is true)
*/
wsHandlers?: WebSocketHandler[];
/**
* Global pagination configuration for file-based endpoints
*/
pagination?: Partial<Record<'ALL' | 'GET' | 'POST' | 'DELETE', PaginationConfig>>;
/**
* Global filter configuration for file-based endpoints
*/
filters?: Partial<Record<'ALL' | 'GET' | 'POST' | 'DELETE', FilterConfig>>;
}REST API Handlers
interface RestHandler {
/**
* URL pattern with Ant-style syntax
* - * matches one path segment
* - ** matches zero or more path segments
* - {param} extracts a path parameter
* @example '/users/{id}' or '/posts/**'
*/
pattern: string;
/**
* HTTP method to handle
*/
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
/**
* Handler function or 'FS' for file-system routing
*/
handle: 'FS' | ((req: UniversalApiRequest, res: ServerResponse) => void | Promise<void>);
/**
* Disable this specific handler
* @default false
*/
disabled?: boolean;
/**
* Response delay override for this handler
*/
delay?: number;
/**
* For 'FS' handlers: function called before reading the file
* Can modify path, check permissions, etc.
*/
preHandle?: (req: UniversalApiRequest, res: ServerResponse) => {
continueHandle: boolean;
path?: string;
} | Promise<{...}>;
/**
* For 'FS' handlers: function called after reading the file
* Can transform data, add headers, etc.
*/
postHandle?: (req: UniversalApiRequest, res: ServerResponse, data: any) => {
continueHandle: boolean;
data?: any;
} | Promise<{...}>;
/**
* Authentication check performed before the handler runs.
*
* - `false` — No check. Request always passes through. (default)
* - `true` — The `authorization` header must be present and non-empty.
* - `string` — The specified header must be present and non-empty.
* - `function` — Custom predicate; receives the request and must return
* `true` to allow or `false` to reject.
* Can be async for token validation, DB lookups, etc.
*
* A rejected request receives `401 Unauthorized`.
* If the function throws, the response is `500 Internal Server Error`.
*
* @default false
*
* @example
* // Require the Authorization header to be present
* authenticate: true
*
* @example
* // Require a custom header (e.g. an API key)
* authenticate: 'x-api-key'
*
* @example
* // Custom async validation
* authenticate: async (req) => {
* const token = req.headers['authorization']?.replace('Bearer ', '');
* if (!token) return false;
* return await verifyToken(token);
* }
*/
authenticate?: false | true | string | ((req: IncomingMessage) => boolean | Promise<boolean>);
/**
* Pagination config for this handler (overrides global)
*/
pagination?: PaginationConfig;
/**
* Filter config for this handler (overrides global)
*/
filters?: FilterConfig;
}Pagination Configuration
type PaginationConfig = {
/**
* Where to look for pagination params
* - 'query-param': URL query string (?limit=10&skip=20)
* - 'body': Request body
*/
type: 'query-param' | 'body';
/**
* For body type: nested object path
* @example 'pagination' for { pagination: { limit: 10 } }
*/
root?: string;
/**
* Parameter name for limit/page size
* @default 'limit'
*/
limit?: string;
/**
* Parameter name for skip/offset
* @default 'skip'
*/
skip?: string;
/**
* Parameter name for sort field
* @default 'sort'
*/
sort?: string;
/**
* Parameter name for sort order ('asc' | 'desc')
* @default 'order'
*/
order?: string;
}Filter Configuration
type FilterConfig = {
type: 'query-param' | 'body';
root?: string;
filters: Array<{
/**
* Query param or body field name
*/
key: string;
/**
* Field in the JSON object to filter by
*/
field?: string;
/**
* Expected value type
*/
valueType: 'string' | 'number' | 'boolean';
/**
* Comparison operator
* - eq: equals
* - ne: not equals
* - gt: greater than
* - gte: greater than or equal
* - lt: less than
* - lte: less than or equal
* - in: value in array
* - nin: value not in array
* - regex: regular expression match (use `regexFlags` for flags, e.g. `"i"` for case-insensitive)
*/
comparison: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'regex';
}>;
}WebSocket Handlers
interface WebSocketHandler {
/**
* URL pattern for WebSocket upgrade requests
* @example '/ws/chat' or '/ws/**'
*/
pattern: string;
/**
* Disable this handler
* @default false
*/
disabled?: boolean;
/**
* Authentication check executed before the WebSocket handshake completes.
*
* Accepts the same values as the REST handler `authenticate` option:
*
* - `false` — No check. Upgrade always proceeds. (default)
* - `true` — The `authorization` header must be present and non-empty.
* - `string` — The specified header must be present and non-empty.
* - `function` — Custom predicate; receives the HTTP upgrade request and must
* return `true` to allow the connection, `false` to reject it
* with `401 Unauthorized`. Can be async.
*
* If the function throws, the connection is rejected with `500 Internal Server Error`.
*
* @default false
*
* @example
* // Require the Authorization header
* authenticate: true
*
* @example
* // Require a custom header
* authenticate: 'x-api-key'
*
* @example
* // Token-based async validation
* authenticate: async (req) => {
* const token = req.headers['authorization']?.replace('Bearer ', '');
* if (!token) return false;
* try {
* return (await verifyToken(token)) !== null;
* } catch {
* return false;
* }
* }
*/
authenticate?: false | true | string | ((req: IncomingMessage) => boolean | Promise<boolean>);
/**
* Default room to join on connection
*/
defaultRoom?: string;
/**
* Heartbeat interval in milliseconds
* Sends ping frames to keep connection alive
* Connection closed after 3 missed pongs
*/
heartbeat?: number;
/**
* Inactivity timeout in milliseconds
* Closes connection if no data received within this time
*/
inactivityTimeout?: number;
/**
* WebSocket sub-protocols to accept
* @example ['chat', 'v2.chat']
*/
subprotocols?: string[];
/**
* permessage-deflate compression configuration
* @default false (disabled)
*/
perMessageDeflate?: boolean | {
clientNoContextTakeover?: boolean;
serverNoContextTakeover?: boolean;
clientMaxWindowBits?: 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
serverMaxWindowBits?: 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
strict?: boolean;
};
/**
* Custom data transformation function
* Override default JSON/text parsing
*/
transformRawData?: (data: Buffer) => any | Promise<any>;
/**
* Delay before processing messages (ms)
*/
delay?: number;
/**
* Pattern-based automatic responses
* Messages matching conditions trigger automatic replies
*/
responses?: Array<{
/**
* Function to test if message matches
*/
match: (connection: IWebSocketConnection, message: any) => boolean;
/**
* Response data or function to generate response
*/
response: any | ((connection: IWebSocketConnection, message: any) => any | Promise<any>);
/**
* Broadcast response instead of sending to sender
*/
broadcast?: boolean | {
room?: string;
includeSelf?: boolean;
};
}>;
/** Called when connection is established */
onConnect?: (connection: IWebSocketConnection, request: IncomingMessage) => void | Promise<void>;
/** Called when message is received */
onMessage?: (connection: IWebSocketConnection, message: any) => void | Promise<void>;
/** Called when connection is closed */
onClose?: (connection: IWebSocketConnection, code: number, reason: string, initiatedByClient: boolean) => void | Promise<void>;
/** Called on errors */
onError?: (connection: IWebSocketConnection, error: Error) => void | Promise<void>;
/** Called on ping frame received */
onPing?: (connection: IWebSocketConnection, payload: Buffer) => void | Promise<void>;
/** Called on pong frame received */
onPong?: (connection: IWebSocketConnection, payload: Buffer) => void | Promise<void>;
}⚙️ How it works
vite-plugin-universal-api only affects development. In production, your application performs real HTTP requests.
In a real application, you typically want to:
- use local APIs during development
- call real APIs in production
You can achieve this with a simple base URL configuration:
// api.ts
export const API_BASE_URL = import.meta.env.PROD
? 'https://api.example.com'
: '/api'
// usage.ts
import {API_BASE_URL }from'./api'
export async function getUsers() {
const res = await fetch(`${API_BASE_URL}/users`)
return res.json()
}- In development
/api/usersis intercepted byvite-plugin-universal-api- you can return mocked or locally defined responses
- In production
- requests go directly to
https://api.example.com/users
- requests go directly to
This means you can develop without a backend and switch to real APIs without changing your code.
Vite proxy
Instead of defining local API endpoints, you can forward requests to a real backend during development using Vite's proxy.
// vite.config.ts
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}You can then call your API as usual:
// in your file
...
fetch('/api/users')When to use what
vite-plugin-universal-api→ mock or local APIs- Vite proxy → real backend during development
Both approaches work with the same client code.
Environment variables
For more flexibility, use .env files:
# .env.development
VITE_API_BASE_URL=/api
# .env.production
VITE_API_BASE_URL=https://api.example.com// api.ts
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
// services/users.ts
import {API_BASE_URL }from'../api'
export async function getUsers() {
const res = await fetch(`${API_BASE_URL}/users`)
return res.json()
}Result
Development→ mocked response from local filesProduction→ real API calls
💡 Usage Examples
File-Based Mocking
Basic File Structure
project/
├── mock/
│ ├── users.json # GET /api/users
│ ├── users/
│ │ ├── index.json # GET /api/users/ (directory index)
│ │ └── profile.json # GET /api/users/profile
│ ├── posts/
│ │ └── {id}.json # GET /api/posts/123 (dynamic parameter)
│ └── data.xml # GET /api/data (XML response)
└── vite.config.tsConfiguration
// vite.config.ts
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
fsDir: 'mock',
// Global pagination for GET requests
pagination: {
GET: {
type: 'query-param',
limit: 'limit',
skip: 'skip',
sort: 'sortBy',
order: 'order'
}
},
// Global filters
filters: {
GET: {
type: 'query-param',
filters: [
{ key: 'status', valueType: 'string', comparison: 'eq' },
{ key: 'age', valueType: 'number', comparison: 'gte' }
]
}
}
})
]
});Requests
# Basic request
GET /api/users
# Returns: mock/users.json
# With pagination
GET /api/users?limit=10&skip=20&sortBy=name&order=desc
# Returns: Paginated and sorted array from users.json
# With filters
GET /api/users?status=active&age=25
# Returns: Filtered array (status === 'active' AND age >= 25)
# Dynamic path parameter
GET /api/posts/123
# Returns: mock/posts/123.json
# Directory index
GET /api/users/
# Returns: mock/users/index.json
# POST with body (creates/updates file)
POST /api/users
Content-Type: application/json
{"name": "New User", "email": "[email protected]"}
# Writes to: mock/users.json (appends to array if file exists)
# PUT (replaces file content)
PUT /api/users/123
# Writes to: mock/users/123.json (creates if not exists)
# DELETE (removes file)
DELETE /api/users/123
# Deletes: mock/users/123.jsonCustom REST Handlers
Basic Handler
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
handlers: [
{
pattern: '/users/{id}',
method: 'GET',
handle: async (req, res) => {
const userId = req.params?.id;
// Simulate database lookup
const user = await db.findUser(userId);
if (!user) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(user));
}
}
]
})
]
});Hybrid: Custom + File-System
handlers: [
{
pattern: '/users',
method: 'GET',
handle: 'FS', // Use file-system
preHandle: async (req, res) => {
// Check authentication before reading file
if (!req.headers.authorization) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unauthorized' }));
return { continueHandle: false };
}
return { continueHandle: true };
},
postHandle: async (req, res, data) => {
// Transform data after reading file
const transformedData = data.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName}`
}));
return { continueHandle: true, data: transformedData };
}
},
{
pattern: '/users',
method: 'POST',
handle: async (req, res) => {
// Custom validation
const { email, name } = req.body;
if (!email || !email.includes('@')) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid email' }));
return;
}
const newUser = { id: Date.now(), email, name };
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(newUser));
}
}
]With Middleware
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
// Global middleware for all handlers
handlerMiddlewares: [
// Logger
async (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
},
// Authentication
async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'No token provided' }));
return;
}
try {
req.body.user = await verifyToken(token);
next();
} catch (err) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid token' }));
}
}
],
// Error middleware
errorMiddlewares: [
(err, req, res, next) => {
console.error('API Error:', err);
if (err.name === 'ValidationError') {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error' }));
}
}
],
handlers: [
{
pattern: '/protected/data',
method: 'GET',
handle: async (req, res) => {
// req.body.user available from auth middleware
const user = req.body.user;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: `Hello ${user.name}` }));
}
}
]
})
]
});WebSocket Real-Time Communication
Basic Chat Server
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
enableWs: true,
wsHandlers: [
{
pattern: '/ws/chat',
defaultRoom: 'lobby',
heartbeat: 30000, // Send ping every 30 seconds
inactivityTimeout: 60000, // Close after 1 minute of inactivity
onConnect: (conn, req) => {
console.log(`User connected: ${conn.id}`);
// Send welcome message
conn.send({
type: 'system',
message: 'Welcome to the chat!'
});
// Notify others
conn.broadcast({
type: 'system',
message: 'A user joined the chat'
}, { includeSelf: false });
},
onMessage: (conn, msg) => {
console.log('Received:', msg);
if (msg.type === 'chat') {
// Broadcast to all in same room
conn.broadcast({
type: 'chat',
user: msg.user,
message: msg.message,
timestamp: Date.now()
}, { includeSelf: true });
}
if (msg.type === 'join-room') {
conn.leaveRoom('lobby');
conn.joinRoom(msg.room);
conn.send({
type: 'system',
message: `Joined room: ${msg.room}`
});
}
},
onClose: (conn, code, reason) => {
console.log(`User disconnected: ${conn.id}`);
conn.broadcast({
type: 'system',
message: 'A user left the chat'
}, { includeSelf: false });
},
onError: (conn, error) => {
console.error('WebSocket error:', error);
}
}
]
})
]
});Advanced: Game Server with Rooms
wsHandlers: [
{
pattern: '/ws/game',
authenticate: async (req) => {
const token = new URLSearchParams(req.url?.split('?')[1]).get('token');
return token === 'valid-token';
},
perMessageDeflate: {
serverNoContextTakeover: true,
clientNoContextTakeover: true,
serverMaxWindowBits: 15,
clientMaxWindowBits: 15
},
heartbeat: 20000,
// Automatic responses
responses: [
{
match: (conn, msg) => msg.type === 'ping',
response: { type: 'pong', timestamp: Date.now() }
},
{
match: (conn, msg) => msg.type === 'get-rooms',
response: (conn) => ({
type: 'rooms',
rooms: conn.getRooms()
})
}
],
onConnect: (conn, req) => {
// Extract game room from query params
const url = new URL(req.url!, `http://${req.headers.host}`);
const gameRoom = url.searchParams.get('room') || 'default';
conn.joinRoom(gameRoom);
conn.metadata.gameRoom = gameRoom;
conn.metadata.username = url.searchParams.get('username') || 'Anonymous';
// Send current game state
conn.send({
type: 'game-state',
state: getGameState(gameRoom)
});
// Notify room members
conn.broadcast({
type: 'player-joined',
username: conn.metadata.username
}, { room: gameRoom, includeSelf: false });
},
onMessage: (conn, msg) => {
const gameRoom = conn.metadata.gameRoom;
switch (msg.type) {
case 'move':
// Update game state
updateGameState(gameRoom, conn.metadata.username, msg.move);
// Broadcast to all players in the same room
conn.broadcast({
type: 'player-moved',
username: conn.metadata.username,
move: msg.move
}, { room: gameRoom, includeSelf: true });
break;
case 'chat':
// Room-specific chat
conn.broadcast({
type: 'chat-message',
username: conn.metadata.username,
message: msg.message,
timestamp: Date.now()
}, { room: gameRoom, includeSelf: true });
break;
case 'leave-game':
conn.leaveRoom(gameRoom);
conn.send({ type: 'left-game' });
break;
}
},
onClose: (conn, code, reason, initiatedByClient) => {
const gameRoom = conn.metadata.gameRoom;
const username = conn.metadata.username;
// Notify remaining players
conn.broadcast({
type: 'player-left',
username: username,
reason: reason || 'Connection closed'
}, { room: gameRoom });
// Cleanup game state
removePlayerFromGame(gameRoom, username);
}
}
]Client-Side Example
// Frontend WebSocket client
const ws = new WebSocket('ws://localhost:5173/api/ws/chat');
ws.onopen = () => {
console.log('Connected to chat');
// Send message
ws.send(JSON.stringify({
type: 'chat',
user: 'John',
message: 'Hello everyone!'
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
if (data.type === 'chat') {
displayMessage(data.user, data.message);
} else if (data.type === 'system') {
displaySystemMessage(data.message);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Disconnected from chat');
};Advanced Patterns
Custom Parser (Express Integration)
import express from 'express';
import multer from 'multer';
const upload = multer();
export default defineConfig({
plugins: [
universalApi({
endpointPrefix: '/api',
parser: {
// Use Express parsers
parser: [
express.json(),
express.urlencoded({ extended: true }),
upload.any() // Handle multipart/form-data
],
// Transform Express request to plugin format
transform: (req: any) => ({
body: req.body,
files: req.files?.map((f: any) => ({
name: f.originalname,
content: f.buffer,
contentType: f.mimetype
})),
query: new URLSearchParams(req.url.split('?')[1])
})
},
handlers: [
{
pattern: '/upload',
method: 'POST',
handle: async (req, res) => {
const files = req.files;
if (files && files.length > 0) {
console.log(`Received ${files.length} files`);
files.forEach(file => {
console.log(`- ${file.name} (${file.contentType})`);
});
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
filesReceived: files?.length || 0
}));
}
}
]
})
]
});Dynamic Mock Data Generation
import { faker } from '@faker-js/faker';
handlers: [
{
pattern: '/users/random',
method: 'GET',
handle: async (req, res) => {
const count = parseInt(req.query.get('count') || '10');
const users = Array.from({ length: count }, () => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
avatar: faker.image.avatar(),
address: {
street: faker.location.streetAddress(),
city: faker.location.city(),
country: faker.location.country()
}
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
}
}
]Stateful Mock Server
// Maintain state across requests
const mockDatabase = {
users: new Map<string, any>(),
posts: new Map<string, any>()
};
handlers: [
{
pattern: '/users',
method: 'GET',
handle: async (req, res) => {
const users = Array.from(mockDatabase.users.values());
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
}
},
{
pattern: '/users',
method: 'POST',
handle: async (req, res) => {
const newUser = {
id: Date.now().toString(),
...req.body,
createdAt: new Date().toISOString()
};
mockDatabase.users.set(newUser.id, newUser);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(newUser));
}
},
{
pattern: '/users/{id}',
method: 'GET',
handle: async (req, res) => {
const user = mockDatabase.users.get(req.params!.id);
if (!user) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(user));
}
},
{
pattern: '/users/{id}',
method: 'DELETE',
handle: async (req, res) => {
const deleted = mockDatabase.users.delete(req.params!.id);
if (!deleted) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
res.writeHead(204);
res.end();
}
}
]📚 API Reference
Request Object (UniversalApiRequest)
Extended IncomingMessage with additional properties:
interface UniversalApiRequest extends IncomingMessage {
/** Parsed request body (JSON, form data, etc.) */
body: any;
/** Extracted route parameters from pattern */
params: Record<string, string> | null;
/** Parsed URL query parameters */
query: URLSearchParams;
/** Uploaded files (multipart/form-data) */
files: Array<{
name: string;
content: Buffer<ArrayBuffer>;
contentType: string;
}> | null;
}Examples
// Access parsed body
const { username, password } = req.body;
// Access route parameters
// Pattern: /users/{userId}/posts/{postId}
// URL: /users/123/posts/456
const userId = req.params.userId; // "123"
const postId = req.params.postId; // "456"
// Access query parameters
// URL: /search?q=typescript&page=2
const searchQuery = req.query.get('q'); // "typescript"
const page = parseInt(req.query.get('page') || '1'); // 2
// Access uploaded files
if (req.files && req.files.length > 0) {
req.files.forEach(file => {
console.log(`File: ${file.name}`);
console.log(`Type: ${file.contentType}`);
// Save file.content to disk
});
}⚡ WebSocket API
Connection Object (IWebSocketConnection)
interface IWebSocketConnection {
/** Unique connection identifier */
id: string;
/** Request path that initiated the connection */
path: string;
/** Whether connection is closed */
closed: boolean;
/** Negotiated sub-protocol */
subprotocol?: string;
/** Custom metadata storage */
metadata: Record<string, any>;
/** Set of rooms this connection belongs to */
rooms: Set<string>;
/**
* Send message to this connection
* Automatically JSON-stringifies objects
*/
send(data: any): Promise<void>;
/**
* Broadcast message to other connections
* @param data Message to send
* @param options Filtering options
*/
broadcast(data: any, options?: {
room?: string;
includeSelf?: boolean;
}): void;
/**
* Broadcast to all rooms this connection is in
*/
broadcastAllRooms(data: any, includeSelf: boolean): void;
/**
* Join a room
*/
joinRoom(room: string): void;
/**
* Leave a room
*/
leaveRoom(room: string): void;
/**
* Check if in a room
*/
isInRoom(room: string): boolean;
/**
* Get all rooms
*/
getRooms(): string[];
/**
* Send ping frame
*/
ping(payload?: string | Buffer): void;
/**
* Send pong frame
*/
pong(payload?: string | Buffer): void;
/**
* Close connection
* @param code WebSocket close code (default 1000)
* @param reason Close reason string
* @param initiatedByClient Whether client initiated close
*/
close(code?: number, reason?: string, initiatedByClient?: boolean): Promise<void>;
/**
* Force close without handshake
*/
forceClose(): void;
/**
* Reset missed pong counter
*/
resetMissedPong(): void;
/**
* Decompress data (if compression enabled)
*/
decompressData(data: Buffer): Promise<Buffer>;
}WebSocket Close Codes
Standard close codes according to RFC 6455:
| Code | Name | Description | |------|------|-------------| | 1000 | Normal Closure | Successful operation / regular socket shutdown | | 1001 | Going Away | Server/client going down or navigating away | | 1002 | Protocol Error | Endpoint terminating due to protocol error | | 1003 | Unsupported Data | Received data type that cannot be accepted | | 1007 | Invalid Payload | Received inconsistent data (e.g., non-UTF-8) | | 1008 | Policy Violation | Received message violating policy | | 1009 | Message Too Big | Message too large to process | | 1010 | Mandatory Extension | Client requires extensions server doesn't support | | 1011 | Internal Error | Server encountered unexpected condition | | 3000-3999 | Reserved | Framework/library codes | | 4000-4999 | Reserved | Application codes |
REST API Request Handling
Comprehensive table showing how different HTTP methods are handled in File-System mode and with handlers:
| Method | File Exists | Body Allowed | Files Allowed | Pagination | Filters | Behavior | Status Code | Notes |
|--------|-------------|--------------|---------------|------------|---------|----------|-------------|-------|
| GET | ✅ Yes | ❌ No | ❌ No | ✅ Yes (JSON arrays) | ✅ Yes (JSON arrays) | Returns file content | 200 | • Supports pagination/filters for JSON arrays• Binary files returned as-is• Directory lookup for index.json |
| GET | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | Error | 404 | File not found |
| HEAD | ✅ Yes | ❌ No | ❌ No | ✅ Yes (JSON arrays) | ✅ Yes (JSON arrays) | Returns headers only | 200 | Same as GET but without body |
| HEAD | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | Error | 404 | File not found |
| POST | ❌ No | ✅ Yes | ✅ Yes (single) | ❌ No | ❌ No | Creates new file | 201 | • Creates file with body or first file• Only first file is written |
| POST | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | Error | 400 | No data provided |
| POST | ✅ Yes (JSON) | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes | Returns filtered data | 200 | • File not modified• With pagination/filters: returns query results• Without pagination/filters and with body: returns 409 Conflict |
| POST | ✅ Yes (JSON) | ❌ No | ❌ No | ✅ Yes | ✅ Yes | Returns filtered data | 200 | No modification, returns data with pagination/filters applied |
| POST | ✅ Yes (non-JSON) | - | - | ❌ No | ❌ No | Error | 400 | POST not allowed for non-JSON files |
| POST | - | ✅ Yes | ✅ Yes | - | - | Error | 400 | Cannot send both body and files |
| POST | - | - | ✅ Yes (multiple) | - | - | Error | 400 | Only single file allowed |
| PUT | ❌ No | ✅ Yes | ✅ Yes (single) | ❌ No | ❌ No | Creates file | 201 | • Body or first file becomes file content• Only first file is written |
| PUT | ✅ Yes | ✅ Yes | ✅ Yes (single) | ❌ No | ❌ No | Replaces file | 200 | Completely replaces file content |
| PUT | - | ❌ No | ❌ No | ❌ No | ❌ No | Error | 400 | No data provided |
| PUT | - | - | ✅ Yes (multiple) | - | - | Error | 400 | Only single file allowed |
| PATCH | ✅ Yes (JSON) | ✅ Yes (JSON) | ❌ No | ❌ No | ❌ No | Merges/patches file | 200 | • Supports application/json (merge)• Supports application/json-patch+json (JSON Patch RFC 6902)• Supports application/merge-patch+json (Merge Patch RFC 7396) |
| PATCH | ❌ No | - | - | ❌ No | ❌ No | Error | 404 | Resource not found |
| PATCH | ✅ Yes (non-JSON) | - | - | ❌ No | ❌ No | Error | 400 | Only JSON files can be patched |
| PATCH | - | ✅ (non-JSON) | - | - | - | Error | 415 | Unsupported Content-Type |
| DELETE | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | Deletes file | 204 | • Removes entire file• Returns X-Deleted-Count: 1 header |
| DELETE | ✅ Yes (JSON) | ❌ No | ❌ No | ✅ Yes | ✅ Yes | Partial delete | 204 | • With pagination/filters: deletes matched items from array• If all items deleted: removes file• If some items remain: updates file• Returns X-Deleted-Count: N header |
| DELETE | ❌ No | - | - | ❌ No | ❌ No | Error | 404 | Resource not found |
| DELETE | - | ✅ Yes | - | - | - | Error | 400 | Body not allowed in DELETE |
| OPTIONS | - | - | - | ❌ No | ❌ No | Error | 405 | Method not allowed in FS mode |
Legend
- ✅ Yes: Feature is supported/allowed
- ❌ No: Feature is not supported/will cause error
- -: Not applicable for this scenario
- Status Codes: HTTP status returned for the operation
Special Headers
The plugin uses custom headers for metadata:
| Header | Usage | Example |
|--------|-------|---------|
| X-Total-Count | Total number of elements (before pagination) | X-Total-Count: 150 |
| X-Deleted-Count | Number of elements deleted | X-Deleted-Count: 5 |
Content-Type Requirements
| Method | Content-Type | Required | Notes |
|--------|--------------|----------|-------|
| POST | application/json | No | Auto-detected for JSON body |
| POST | multipart/form-data | Yes | When sending files |
| POST | Other | Yes | Must match file content |
| PUT | Any | Yes | Must match file content |
| PATCH | application/json | Yes | Merge patch |
| PATCH | application/json-patch+json | Yes | JSON Patch (RFC 6902) |
| PATCH | application/merge-patch+json | Yes | Merge Patch (RFC 7396) |
Pagination & Filters Scope
Pagination and Filters work ONLY when ALL of these conditions are met:
- ✅ File exists and contains JSON array
- ✅ Method is GET, POST, HEAD, or DELETE
- ✅ Pagination/filters are configured (globally or per-handler)
- ✅ File is a valid JSON file
Pagination & Filters do NOT work for:
- ❌ Non-JSON files (binary, XML, HTML, etc.)
- ❌ JSON objects (not arrays)
- ❌ PUT or PATCH methods
- ❌ Custom programmatic handlers (unless explicitly implemented)
Example scenarios:
// ✅ Works with pagination/filters
[
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"}
]
// ❌ Does NOT work (object, not array)
{
"users": [
{"id": 1, "name": "John"}
]
}
// ❌ Does NOT work (not JSON)
<users>
<user id="1">John</user>
</users>POST Method Special Behavior
The POST method has complex behavior depending on file existence and request content:
File Does NOT Exist:
POST /api/users
Body: {"name": "John"}
→ Creates new file with body content
→ Status: 201 CreatedFile Exists (JSON) WITHOUT Pagination/Filters:
POST /api/users
Body: {"name": "John"}
→ Error: File already exists
→ Status: 409 ConflictFile Exists (JSON) WITH Pagination/Filters and NO Body:
POST /api/users?status=active&limit=10
(no body)
→ Returns filtered/paginated data
→ File is NOT modified
→ Status: 200 OKFile Exists (JSON) WITH Pagination/Filters and Body:
POST /api/users?status=active
Body: {"name": "John"}
→ If body contains ONLY pagination/filter params: returns filtered data
→ If body contains OTHER data: Error 409 Conflict
→ File is NOT modified⚠️ Important POST Notes:
- When sending files (
multipart/form-data), only the FIRST file is written. All other files are ignored. - You cannot send both
bodyandfilesin the same POST request (Error 400). - Multiple files in a single POST request are not allowed (Error 400).
- For non-JSON files, POST is only allowed when file doesn't exist (Error 400 if file exists).
DELETE Method Special Behavior
DELETE behavior varies based on pagination/filters configuration:
Without Pagination/Filters:
DELETE /api/users/123
→ Deletes entire file
→ Status: 204 No Content
→ Header: X-Deleted-Count: 1With Pagination/Filters (JSON Array):
DELETE /api/users?status=inactive
→ Deletes matching items from array
→ If array becomes empty: deletes file
→ If items remain: updates file with remaining items
→ Status: 204 No Content
→ Header: X-Deleted-Count: 5File Lookup Behavior
When a request path doesn't exactly match a file, the plugin tries multiple strategies:
- Exact file match:
/api/users→mock/users(if exists) - Directory with index:
/api/users/→mock/users/index.json(if exists) - File with extension:
/api/data→mock/data.json,mock/data.xml, etc.
Example:
Request: GET /api/users
Tries in order:
1. mock/users (exact match)
2. mock/users/index.json (directory index)
3. mock/users.json, mock/users.xml, etc. (with extensions)🔧 Middleware System
Middleware Execution Order
Request arrives
↓
[handlerMiddlewares] (in order)
↓
[parser] (if enabled)
↓
[handler function or FS routing]
↓
Response sent
↓
(If error occurs at any step)
↓
[errorMiddlewares] (in order)Middleware Types
1. Handler Middleware
type MiddlewareFunction = (
req: UniversalApiRequest,
res: ServerResponse,
next: () => void
) => void | Promise<void>;Example use cases:
- Authentication/Authorization
- Request logging
- Rate limiting
- Request validation
- Adding custom headers
- Request timing
2. Error Middleware
type ErrorHandlerFunction = (
err: any,
req: UniversalApiRequest | IncomingMessage,
res: ServerResponse,
next: (err?: any) => void
) => void | Promise<void>;Example use cases:
- Error logging
- Error transformation
- Custom error responses
- Error monitoring/tracking
Middleware Examples
// Request logger
const loggerMiddleware: MiddlewareFunction = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
});
next();
};
// API key authentication
const apiKeyMiddleware: MiddlewareFunction = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || !isValidApiKey(apiKey)) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid API key' }));
return;
}
next();
};
// Request validation
const validateMiddleware: MiddlewareFunction = async (req, res, next) => {
if (req.method === 'POST' || req.method === 'PUT') {
if (!req.body) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Request body required' }));
return;
}
try {
await validateSchema(req.body);
next();
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
} else {
next();
}
};
// CORS middleware
const corsMiddleware: MiddlewareFunction = (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
next();
};
// Error handler
const errorHandler: ErrorHandlerFunction = (err, req, res, next) => {
console.error('Error:', err);
// Don't send response if already sent
if (res.writableEnded) {
return;
}
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal server error';
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}));
};🎓 Advanced Features
Pattern Matching
The plugin uses Ant-style path patterns for flexible routing:
| Pattern | Matches | Example |
|---------|---------|---------|
| /api/users | Exact match | /api/users |
| /api/* | Single segment wildcard | /api/users, /api/posts |
| /api/** | Multi-segment wildcard | /api/users/123, /api/posts/456/comments |
| /api/{id} | Path parameter | /api/123 (params.id = "123") |
| /api/users/{userId}/posts/{postId} | Multiple parameters | /api/users/1/posts/2 |
| /api/**.json | Extension match | /api/data.json, /api/sub/data.json |
Pagination Details
When pagination is enabled for a file-based handler:
- File must contain a JSON array
- Query params or body fields are extracted based on configuration
- Operations applied in order:
- Filtering (if configured)
- Sorting (if sort field provided)
- Pagination (skip and limit)
Response format:
{
"data": [...],
"pagination": {
"total": 100,
"skip": 20,
"limit": 10,
"returned": 10
}
}Filtering Details
Supported comparison operators:
eq: Equals (==)ne: Not equals (!=)gt: Greater than (>)gte: Greater than or equal (>=)lt: Less than (<)lte: Less than or equal (<=)in: Value in arraynin: Value not in arrayregex: regular expression match (useregexFlagsfor flags, e.g."i"for case-insensitive)
Example:
filters: {
GET: {
type: 'query-param',
filters: [
{ key: 'status', valueType: 'string', comparison: 'eq' },
{ key: 'minAge', valueType: 'number', comparison: 'gte' },
{ key: 'categories', valueType: 'string', comparison: 'in' }
]
}
}
// Request: GET /api/users?status=active&minAge=18&categories=admin,moderator
// Filters: status === 'active' AND age >= 18 AND category IN ['admin', 'moderator']WebSocket Compression
When perMessageDeflate is enabled:
- Messages are compressed using DEFLATE algorithm (RFC 1951)
- Reduces bandwidth usage for text-heavy messages
- Configurable compression parameters
- Note: Adds CPU overhead, use for text > 1KB
Compression Options:
perMessageDeflate: {
// Client doesn't reuse compression context between messages
clientNoContextTakeover: false,
// Server doesn't reuse compression context between messages
serverNoContextTakeover: false,
// LZ77 sliding window size for client (8-15, higher = better compression)
clientMaxWindowBits: 15,
// LZ77 sliding window size for server
serverMaxWindowBits: 15,
// Reject handshake if client doesn't support these exact parameters
strict: false
}File-System Handler Details
POST Request Behavior
When handling POST to a file-based endpoint:
- If file exists and contains JSON array: Append new item
- If file exists and contains JSON object: Replace with new object
- If file doesn't exist: Create new file with body content
- If
req.filesexists: Write first file to the path (other files ignored)
⚠️ Important: When handling file-system POST requests, only the first file in req.files is written. Other files are ignored.
PUT/PATCH Request Behavior
- PUT: Replace entire file content
- PATCH: Merge with existing JSON object (if file exists)
DELETE Request Behavior
- Deletes the file at the matched path
- Returns 404 if file doesn't exist
Response Streaming
When manually handling responses with streams:
handle: async (req, res) => {
const fileStream = fs.createReadStream('/path/to/large/file.zip');
res.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="file.zip"'
});
fileStream.pipe(res);
// ⚠️ IMPORTANT: Wait for stream to finish before returning
await new Promise((resolve, reject) => {
fileStream.on('end', resolve);
fileStream.on('error', reject);
});
// Plugin checks res.writableEnded to determine if response is complete
}⚠️ Important: If you manually use streams in your response, you must wait for them to finish before the handler function returns. Otherwise, the plugin might interfere with the response, causing unexpected behavior.
🔐 Authentication
The authenticate option is available on both REST and WebSocket handlers. It is evaluated before the handler runs (or before the WebSocket upgrade completes) and rejects unauthenticated requests automatically.
Possible Values
| Value | Behaviour |
|-------|-----------|
| false | No check — every request is allowed through. Default. |
| true | The authorization header must be present and non-empty. |
| string | The named header (case-insensitive) must be present and non-empty. |
| function | Custom predicate (req: IncomingMessage) => boolean \| Promise<boolean>. Return true to allow, false to reject. |
A rejected request receives 401 Unauthorized.
If the function throws, the response is 500 Internal Server Error.
Usage Examples
Require the Authorization header (REST)
handlers: [
{
pattern: '/api/profile',
method: 'GET',
authenticate: true, // 401 if Authorization header is missing or empty
handle: async (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ user: 'me' }));
}
}
]Require a custom header (REST)
handlers: [
{
pattern: '/api/admin',
method: 'GET',
authenticate: 'x-api-key', // 401 if x-api-key header is missing or empty
handle: async (req, res) => { /* ... */ }
}
]Custom async function (REST)
handlers: [
{
pattern: '/api/orders',
method: 'GET',
authenticate: async (req) => {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) return false;
try {
return (await verifyToken(token)) !== null;
} catch {
return false;
}
},
handle: async (req, res) => { /* ... */ }
}
]Require the Authorization header (WebSocket)
wsHandlers: [
{
pattern: '/ws/private',
authenticate: true,
onConnect: (conn) => conn.send({ type: 'welcome' })
}
]Custom async function (WebSocket)
wsHandlers: [
{
pattern: '/ws/game',
authenticate: async (req) => {
const token = new URLSearchParams(req.url?.split('?')[1]).get('token');
return token === 'valid-token';
},
onConnect: (conn) => { /* ... */ }
}
]⚠️ Important Notes
Handler Middleware Scope
handlerMiddlewares are executed only for handlers defined in the handlers array. They are NOT executed for:
- Pure file-system requests (when no custom handler matches)
- WebSocket requests
- Requests forwarded via
noHandledRestFsRequestsAction: 'forward'
Parser Scope
The parser option applies only to REST API requests. It is NOT used for:
- WebSocket messages (use
transformRawDatainstead)
Pagination and Filters Scope
Pagination and filters work only with:
- File-based handlers returning JSON arrays
- Handlers defined with
handle: 'FS'
They do NOT work with:
- Custom programmatic handlers
- Non-JSON files
- JSON objects (not arrays)
Pattern Matching Priority
When multiple handlers match a request:
- More specific patterns have priority
- First matching handler in the array is used
handlersarray order matters
WebSocket Pattern Collisions
Each WebSocket handler must have a unique pattern. Duplicate patterns will cause an error at startup.
Performance Considerations
- File I/O: File-based routing reads from disk on each request. For production, use a real backend.
- Compression: WebSocket compression adds CPU overhead. Enable only for text-heavy messages.
- Heartbeat: Lower heartbeat intervals increase network traffic.
- Large Files: Use streams for large file responses to avoid memory issues.
🔍 Troubleshooting
Common Issues
1. Plugin Not Working
Symptoms: Requests return 404 or are handled by Vite's default handler
Solutions:
- Check
endpointPrefixmatches your request URL - Verify
disableoption is not set totrue - Ensure
fsDirpath exists (if using file-based routing) - Check Vite server logs for error messages
// Enable debug logging
universalApi({
logLevel: 'debug',
// ...
})2. WebSocket Connection Fails
Symptoms: WebSocket upgrade fails with 401, 404, or 500
Solutions:
- Ensure
enableWs: trueis set - Check
wsHandlerspattern matches your WebSocket URL - Verify
authenticateoption (if set) allows the request — useauthenticate: falseto bypass it during debugging - Check browser console and Vite server logs
// Test authentication
wsHandlers: [{
pattern: '/ws/test',
authenticate: async (req) => {
console.log('Auth check:', req.headers);
return true; // Allow all during debugging
}
}]3. Request Body is Undefined
Symptoms: req.body is undefined or null
Solutions:
- Ensure
parseris not disabled - Check
Content-Typeheader is set correctly - Verify request body is valid JSON (for built-in parser)
- Try custom parser with logging
parser: {
parser: (req, res, next) => {
console.log('Content-Type:', req.headers['content-type']);
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
console.log('Raw body:', body);
try {
req.body = JSON.parse(body);
} catch (e) {
console.error('Parse error:', e);
}
next();
});
},
transform: (req: any) => ({ body: req.body })
}4. File Not Found
Symptoms: 404 error when accessing file-based endpoint
Solutions:
- Check file path relative to
fsDir - Verify file extension matches request
- Check file permissions
- Try absolute path in logs
handlers: [{
pattern: '/test/**',
method: 'GET',
handle: 'FS',
preHandle: (req, res) => {
console.log('Looking for file:', req.url);
console.log('fsDir:', options.fsDir);
return { continueHandle: true };
}
}]5. CORS Errors
Symptoms: Browser blocks requests with CORS policy error
Solutions:
- Add CORS middleware
handlerMiddlewares: [
(req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
next();
}
]6. Pagination Not Working
Symptoms: Pagination returns entire array
Solutions:
- Ensure file contains a JSON array, not an object
- Check pagination parameter names match query params
- Verify file contains more items than
limit
// Debug pagination
pagination: {
GET: {
type: 'query-param',
limit: 'limit',
skip: 'skip'
}
}
// Request: GET /api/users?limit=5&skip=0
// Should return first 5 items7. WebSocket Messages Not Received
Symptoms: onMessage not called or messages lost
Solutions:
- Ensure messages are valid JSON (if not using
transformRawData) - Check WebSocket is fully connected before sending
- Verify no errors in browser console
- Add logging to
onMessage
onMessage: (conn, msg) => {
console.log('Received message:', msg);
// Your logic here
}Debug Mode
Enable maximum verbosity for troubleshooting:
universalApi({
logLevel: 'debug',
// ... other options
})This will log:
- Plugin initialization
- Request matching attempts
- File system operations
- WebSocket lifecycle events
- Middleware execution
- Parser operations
📄 License
🙏 Acknowledgments
📞 Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: [email protected]
If you find this plugin useful, please consider giving it a ⭐ on GitHub!
