Virtual Modules
Virtual Modules
Virtual modules are a Vite feature that allows the plugin to expose dynamically generated content as importable modules. SvelteKit Auto OpenAPI uses this to provide access to generated OpenAPI schemas.
What Are Virtual Modules?
Virtual modules are JavaScript/TypeScript modules that don't exist as physical files but are generated by Vite plugins at build time. They can be imported like regular modules:
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths'; Key characteristics:
- No physical file on disk
- Generated during build
- Cached and invalidated on changes
- Fully typed with TypeScript declarations
- Support Hot Module Replacement (HMR)
Available Virtual Modules
virtual:sveltekit-auto-openapi/schema-paths
The main virtual module exposing all generated OpenAPI paths.
Type: OpenAPIV3.PathsObject
Content: A complete OpenAPI PathsObject with all discovered routes and their operations.
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
console.log(schemaPaths);
// {
// "/api/users": {
// "GET": { summary: "List users", ... },
// "POST": { summary: "Create user", ... }
// },
// "/api/users/{id}": {
// "GET": { summary: "Get user", ... },
// "PUT": { summary: "Update user", ... }
// }
// } Importing Virtual Modules
Basic Import
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
// Access specific path
const usersPath = schemaPaths['/api/users'];
const getUserOperation = usersPath?.GET;
console.log(getUserOperation?.summary); // "List users" TypeScript Support
The virtual module is fully typed:
import type { OpenAPIV3 } from 'openapi-types';
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
// Type is inferred as OpenAPIV3.PathsObject
const paths: OpenAPIV3.PathsObject = schemaPaths;
// Navigate with type safety
for (const [path, pathItem] of Object.entries(schemaPaths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if ('summary' in operation) {
console.log(`${method} ${path}: ${operation.summary}`);
}
}
} Dynamic Import
Load schemas on demand:
export async function loadSchemas() {
const { default: schemaPaths } = await import(
'virtual:sveltekit-auto-openapi/schema-paths'
);
return schemaPaths;
} Use Cases
Custom Documentation
Generate custom documentation formats:
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
export function generateMarkdownDocs(): string {
let markdown = '# API Documentation\n\n';
for (const [path, pathItem] of Object.entries(schemaPaths)) {
markdown += `## \`${path}\`\n\n`;
for (const [method, operation] of Object.entries(pathItem)) {
if ('summary' in operation) {
markdown += `### ${method}\n\n`;
markdown += `${operation.summary}\n\n`;
if (operation.description) {
markdown += `${operation.description}\n\n`;
}
// Add parameters
if (operation.parameters) {
markdown += '**Parameters:**\n\n';
for (const param of operation.parameters) {
if ('name' in param) {
markdown += `- \`${param.name}\` (${param.in}): ${param.description || ''}\n`;
}
}
markdown += '\n';
}
}
}
}
return markdown;
} Schema Introspection
Analyze your API structure:
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
export function getApiStats() {
let totalEndpoints = 0;
let totalOperations = 0;
const methodCounts: Record<string, number> = {};
for (const pathItem of Object.values(schemaPaths)) {
totalEndpoints++;
for (const [method, operation] of Object.entries(pathItem)) {
if ('summary' in operation) {
totalOperations++;
methodCounts[method] = (methodCounts[method] || 0) + 1;
}
}
}
return {
totalEndpoints,
totalOperations,
methodCounts,
paths: Object.keys(schemaPaths)
};
} Testing
Test generated schemas:
import { describe, test, expect } from 'bun:test';
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
describe('OpenAPI Schemas', () => {
test('all operations have summaries', () => {
for (const [path, pathItem] of Object.entries(schemaPaths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if ('summary' in operation) {
expect(operation.summary).toBeTruthy();
expect(operation.summary).not.toBe('');
}
}
}
});
test('all POST operations have request bodies', () => {
for (const [path, pathItem] of Object.entries(schemaPaths)) {
if (pathItem.POST && 'requestBody' in pathItem.POST) {
expect(pathItem.POST.requestBody).toBeDefined();
}
}
});
test('all operations have 200 response', () => {
for (const [path, pathItem] of Object.entries(schemaPaths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if ('responses' in operation) {
expect(operation.responses?.['200'] || operation.responses?.['201']).toBeDefined();
}
}
}
});
}); Type-Safe API Client
Generate a client with types from schemas:
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
import type { OpenAPIV3 } from 'openapi-types';
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async request<T>(
path: keyof typeof schemaPaths,
method: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
method
});
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
}
}
// Usage with autocomplete for paths
const client = new ApiClient('https://api.example.com');
const users = await client.request('/api/users', 'GET'); Schema Validation
Validate custom schemas against generated ones:
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
export function validateCustomPath(
path: string,
method: string,
customOperation: unknown
): boolean {
const existingOperation = schemaPaths[path]?.[method];
if (!existingOperation) {
console.warn(`Path ${method} ${path} not found in generated schemas`);
return false;
}
// Compare schemas
return JSON.stringify(existingOperation) === JSON.stringify(customOperation);
} Export OpenAPI Spec
Generate a complete OpenAPI document:
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
import type { OpenAPIV3 } from 'openapi-types';
export function generateOpenAPIDocument(): OpenAPIV3.Document {
return {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'Auto-generated API documentation'
},
servers: [
{
url: 'https://api.example.com',
description: 'Production server'
}
],
paths: schemaPaths,
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
};
}
// Save to file
import { writeFile } from 'node:fs/promises';
const spec = generateOpenAPIDocument();
await writeFile(
'openapi.json',
JSON.stringify(spec, null, 2)
); Internal Details
How They're Generated
The plugin's load() hook generates the virtual module:
// Inside the plugin
load(id) {
if (id === RESOLVED_SCHEMA_PATHS_ID) {
const paths = await generateSchemaPaths();
return {
code: `export default ${JSON.stringify(paths, null, 2)}`,
map: null
};
}
} Caching Mechanism
The virtual module is cached until invalidated:
Cache invalidation triggers:
- Any
+server.tsfile changes _configexport changes- Route files added/removed
HMR support:
handleHotUpdate({ file, server }) {
if (file.endsWith('+server.ts')) {
// Invalidate virtual module
const module = server.moduleGraph.getModuleById(RESOLVED_SCHEMA_PATHS_ID);
if (module) {
server.moduleGraph.invalidateModule(module);
}
}
} Module Resolution
Vite resolves virtual modules through a two-step process:
- resolveId:
virtual:*→\0virtual:*(null byte prefix) - load: Generate content for
\0virtual:*
The null byte prefix prevents conflicts with real files.
Best Practices
Use Type Imports
Import types separately for better tree-shaking:
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
import type { OpenAPIV3 } from 'openapi-types';
const paths: OpenAPIV3.PathsObject = schemaPaths; Cache Results
Virtual module access is fast but cache results when used repeatedly:
let cachedStats: ReturnType<typeof getApiStats> | null = null;
export function getApiStats() {
if (!cachedStats) {
cachedStats = computeStats();
}
return cachedStats;
} Handle Missing Operations
Always check if operations exist:
const operation = schemaPaths['/api/users']?.GET;
if (operation && 'summary' in operation) {
console.log(operation.summary);
} Server-Side Only
Virtual modules should only be imported server-side:
// ✅ Good - server-side
// src/routes/api/docs/+server.ts
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths';
export async function GET() {
return json(schemaPaths);
}
// ❌ Bad - client-side
// src/routes/+page.svelte
import schemaPaths from 'virtual:sveltekit-auto-openapi/schema-paths'; Troubleshooting
Module not found
Problem: Import error for virtual module
Solutions:
- Ensure plugin is registered in
vite.config.ts - Restart dev server
- Check TypeScript declarations are installed
Empty schemas
Problem: Virtual module returns empty object
Solutions:
- Verify routes exist in
src/routes/ - Check files are named
+server.ts - Enable
showDebugLogsto see what's discovered
Stale data
Problem: Changes not reflected in virtual module
Solutions:
- Restart dev server
- Clear
.svelte-kitdirectory - Check HMR is working
Related Documentation
- Plugin Configuration - Configure schema generation
- Scalar Module - Uses virtual module for docs
- Advanced Overview - Other advanced features