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';
typescript

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", ... }
//   }
// }
typescript

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

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}`);
    }
  }
}
typescript

Dynamic Import

Load schemas on demand:

export async function loadSchemas() {
  const { default: schemaPaths } = await import(
    'virtual:sveltekit-auto-openapi/schema-paths'
  );

  return schemaPaths;
}
typescript

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;
}
typescript

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)
  };
}
typescript

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();
        }
      }
    }
  });
});
typescript

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');
typescript

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);
}
typescript

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)
);
typescript

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
    };
  }
}
typescript

Caching Mechanism

The virtual module is cached until invalidated:

Cache invalidation triggers:

  • Any +server.ts file changes
  • _config export 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);
    }
  }
}
typescript

Module Resolution

Vite resolves virtual modules through a two-step process:

  1. resolveId: virtual:*\0virtual:* (null byte prefix)
  2. 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;
typescript

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;
}
typescript

Handle Missing Operations

Always check if operations exist:

const operation = schemaPaths['/api/users']?.GET;

if (operation && 'summary' in operation) {
  console.log(operation.summary);
}
typescript

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';
typescript

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 showDebugLogs to see what's discovered

Stale data

Problem: Changes not reflected in virtual module

Solutions:

  • Restart dev server
  • Clear .svelte-kit directory
  • Check HMR is working

Related Documentation