svelte-reflector
v2.5.3
Published
Reflects types from openAPI schemas
Readme
Svelte Reflector
Turn your OpenAPI into a first-class Svelte 5 DX.
Svelte Reflector is a developer-experience-first code generator that converts OpenAPI specs into fully typed, reactive Svelte 5 modules — ready for production, forms included.
Features
- Automatic Type Generation - Generates TypeScript interfaces and classes from OpenAPI schemas
- Svelte 5 Runes Integration - Uses
$stateand$derivedfor reactive state management - Abstract Modules - Generated modules are abstract classes, ready to be extended with custom logic
- Per-Module Schemas - Each module gets its own schema file with only the types it needs (tree-shaking friendly)
- Form Handling - Auto-generates form schemas with
BuildedInput<T>wrappers and validation support - Type-Safe API Calls - Full TypeScript support for all API operations
- Query Parameter Sync -
QueryBuilderandEnumQueryBuilderkeep state synced with URL searchParams - Enum Support - Auto-generates enum types and array enum query builders
- OpenAPI/Swagger Compatible - Works with any backend that exposes OpenAPI specs
- Development Mode - Smart regeneration based on environment
- Validation Ready - Built-in support for custom field validators
- Vite Plugin - Can be used as a Vite plugin for automatic generation on build
Installation
npm install svelte-reflector
# or
yarn add svelte-reflector
# or
pnpm add svelte-reflectorNote:
prettier>= 3.0.0 is a required peer dependency. Make sure it's installed in your project.
Quick Start
1. Configure Environment Variables
Create a .env file in your project root:
# Required - Your backend URL
BACKEND_URL=https://api.example.com/
# or
PUBLIC_BACKEND=https://api.example.com/
# Optional - Environment (defaults to PROD)
ENVIRONMENT=DEV
# or
VITE_ENVIRONMENT=DEV2. Create Reflector Config (Optional)
Create a src/reflector.config.ts to define custom validators:
export const validators = [
{
fields: ["email", "userEmail"],
validator: "validateEmail",
},
{
fields: ["phone", "mobile"],
validator: "validatePhone",
},
];Validators are resolved from $lib/sanitizers/validateFormats — you need to implement and export them in your project.
3. Configure API Import Path (Optional)
Create a reflector.json in your project root to customize the API import path:
{
"api": "$lib/api"
}Defaults to $lib/api if not specified. This is the module that generated modules will import for making HTTP requests.
4. Run the Generator
# Manual generation
npx reflect
# Or programmatically as a Vite plugin
import { reflector } from "svelte-reflector";
await reflector(true); // true = force generation5. Use Generated Modules
Generated modules are abstract classes. Extend them to add custom logic or simply to instantiate:
import { UserModule } from "$reflector/controllers/user/user.module.svelte";
import type { User } from "$reflector/controllers/user/user.schema.svelte";
// Extend the abstract module
class UserService extends UserModule {}
const userService = new UserService();
// Access reactive state
console.log(userService.loading); // $state<boolean>
console.log(userService.list); // $state<User[]>
// Call API methods
await userService.listAll({
behavior: {
onSuccess: (response) => console.log(response),
onError: (error) => console.error(error),
},
});
// Work with forms
const userForm = userService.forms.createUser;
userForm.name.value = "John Doe";
userForm.email.value = "[email protected]";
// Submit form
await userService.createUser();Generated Structure
src/reflector/
├── controllers/
│ └── user/
│ ├── user.module.svelte.ts # Abstract API module with methods
│ └── user.schema.svelte.ts # Schemas & types used by this module
├── reflector.svelte.ts # Core utilities (build, isFormValid, QueryBuilder, etc.)
├── fields.ts # Field name constants
├── enums.ts # Enum type definitions
├── mocked-params.svelte.ts # Mocked path parameters ($state)
└── backup.json # Cached OpenAPI specEach module gets its own schema file (*.schema.svelte.ts) containing only the schemas it uses, with transitive dependencies automatically resolved.
Generated Module API
Each generated module is an abstract class that provides:
State Properties
| Property | Type | Description |
|----------|------|-------------|
| loading | $state<boolean> | Request loading state |
| list | $state<T[]> | List results (for list endpoints) |
| forms | $state<Record<string, T>> | Form instances |
| querys | Querys | Query parameter state (QueryBuilder instances) |
| headers | Headers | Header state |
| paths | Paths | Path parameter state |
Methods
// List all items (GET with page parameter)
async listAll(params?: { behavior?: Behavior }): Promise<T[]>
// Get single entity (GET without page parameter)
async get(params?: { behavior?: Behavior }): Promise<T>
// Create/Update (POST/PUT/PATCH)
async create(params?: { behavior?: Behavior }): Promise<T>
async update(params?: { behavior?: Behavior }): Promise<T>
// Delete (DELETE)
async delete(params?: { behavior?: Behavior }): Promise<void>
// Reset all state (protected)
protected reset(): void
// Clear forms (protected)
protected clearForms(): void
reset()andclearForms()areprotected— override them in your subclass if you need custom reset behavior.
QueryBuilder
Query parameters are wrapped in QueryBuilder / EnumQueryBuilder instances
that read directly from page.url.searchParams. There is no local cached
state — every read goes through the URL, so multiple instances with the same
key are always coherent.
// Single value query parameter
const querys = module.querys;
querys.status.value; // string | null — read-only getter, always
// reflects the current URL
querys.status.update("active"); // pushes ?status=active via goto()
// Array enum query parameter
const enumQuery = module.querys.roles; // EnumQueryBuilder<RoleType>
enumQuery.selected = "admin";
enumQuery.add(); // appends to URL searchParams
enumQuery.remove(0); // removes from URL searchParams
enumQuery.values; // $derived — always in sync with URLDefaults
default declared in the OpenAPI schema is propagated to the builder
constructor automatically:
parameters:
- name: limit
in: query
schema: { type: integer, default: 10 }
- name: tags
in: query
schema:
type: array
items: { type: string, enum: [hot, new, sale] }
default: [hot]generates:
class Querys {
readonly limit = new QueryBuilder({ key: 'limit', defaultValue: 10 });
readonly tags = new EnumQueryBuilder<'hot' | 'new' | 'sale'>({
key: 'tags',
defaultValues: ['hot'],
});
}When the URL has the param, the URL value wins. When the URL has no param, the default is returned. The URL stays clean until the user interacts.
Migrating from 1.x
value is no longer a setter. Two replacements:
// 1.x
querys.page.value = "1";
querys.page.value ??= "1"; // seed default
// 2.x — declarative default (preferred, set at codegen via OpenAPI)
new QueryBuilder({ key: "page", defaultValue: "1" });
// 2.x — imperative update (push to URL)
querys.page.update("1");The auto-injected setQueryGroup([...]) constructor on the generated
Querys class was removed — defaults now live on the builder. Import
setQueryGroup manually from $reflector/reflector.svelte if you still
need batch URL writes.
Ephemeral pagination (sidebar / widget)
Sometimes a list lives outside the canonical route — a global sidebar, a
widget, a paginated combobox in a modal — and should NOT mutate the
current URL. For those cases, every generated call() with query params
accepts an optional queryOverride:
const sidebar = new UserService();
// Current URL stays put. Request goes out with ?page=2&limit=10.
await sidebar.listAll({
queryOverride: { page: "2", limit: "10" },
});When queryOverride is passed, the method skips this.querys.bundle()
entirely and uses the override as queryData. Without it, the method
reads from QueryBuilder.value (the URL) as usual — so you can mix and
match on a per-call basis. Omit a key to drop it from the request; pass
null to send a literal null.
Configuration
Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| BACKEND_URL | Yes* | Backend API URL |
| PUBLIC_BACKEND | Yes* | Alternative to BACKEND_URL |
| ENVIRONMENT | No | DEV/PROD (defaults to PROD) |
| VITE_ENVIRONMENT | No | Vite-specific env var |
| NODE_ENV | No | Node environment |
* At least one of BACKEND_URL or PUBLIC_BACKEND is required.
Behavior Pattern
All API methods accept a Behavior object for callbacks:
class Behavior<TSuccess, TError> {
onSuccess?: (value: TSuccess) => Promise<void> | void;
onError?: (error: TError) => Promise<void> | void;
}
// Usage
await userService.createUser({
behavior: {
onSuccess: (user) => console.log("Created:", user),
onError: (err) => console.error("Failed:", err),
},
});Form Validation
Forms use BuildedInput class with validation:
class BuildedInput<T> {
value: T; // Current value ($state)
display: T; // Display value ($state)
required: boolean; // Is field required
placeholder: T; // Placeholder/example value
readonly kind: 'builded';
validator?: (v: T) => string | null; // Validation function
validate(): string | null; // Run validation
}
// Check if all form fields are valid
import { isFormValid } from "$reflector/reflector.svelte";
if (isFormValid(userService.forms.createUser)) {
await userService.createUser();
}TypeScript Configuration
Add path aliases to your tsconfig.json:
{
"compilerOptions": {
"paths": {
"$reflector/*": ["./src/reflector/*"],
"$lib/*": ["./src/lib/*"]
}
}
}For Vite projects, also update vite.config.ts:
export default defineConfig({
resolve: {
alias: {
$reflector: path.resolve("./src/reflector"),
$lib: path.resolve("./src/lib"),
},
},
});Workflow
Development Mode
In ENVIRONMENT=DEV:
- Schemas are NOT auto-regenerated on build
- Use
npx reflectto manually regenerate - Faster builds, manual control
Production Mode
In ENVIRONMENT=PROD:
- Schemas are auto-regenerated on each build
- Fresh types from latest OpenAPI spec
- Fallback to
backup.jsonif backend is unavailable
Advanced Usage
Extending Abstract Modules
Since modules are abstract, you can add custom logic:
import { UserModule } from "$reflector/controllers/user/user.module.svelte";
class UserService extends UserModule {
// Add custom computed state
get activeUsers() {
return this.list.filter(u => u.active);
}
// Override protected methods for custom behavior
protected override clearForms() {
super.clearForms();
// custom cleanup logic
}
// Add custom methods
async fetchAndFilter(status: string) {
this.querys.status.update(status);
await this.listAll();
}
}Manual Schema Access
import { User } from "$reflector/controllers/user/user.schema.svelte";
// Create instance
const user = new User({ name: "John", email: "[email protected]" });
// Get data bundle
const data = user.bundle(); // { name: "John", email: "[email protected]" }Batch Query Updates
import { setQueryGroup } from "$reflector/reflector.svelte";
// Update multiple query params at once
setQueryGroup([
{ key: "page", value: 1 },
{ key: "status", value: "active" },
{ key: "roles", value: ["admin", "editor"] }, // Array params supported
]);Troubleshooting
"BACKEND_URL vazio" Error
Ensure you have set BACKEND_URL or PUBLIC_BACKEND in your .env file.
Schemas Not Updating
In DEV mode, run npx reflect manually. Check that your backend's OpenAPI spec is accessible at {BACKEND_URL}openapi.json.
Type Errors After Generation
- Restart your TypeScript language server
- Check path aliases in
tsconfig.json - Ensure
$reflector/*alias is configured
License
MIT License - see LICENSE for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Links
Built with by the Pinaculo Digital team.
