Generate an OpenAPI 3.1 specification directly from Valibot schemas and minimal endpoint metadata. No runtime hooks. No hidden validation. No route magic. The output is a clean, deterministic OpenAPI document.
+server integrationcreateOpenApiSpec)This library is pure documentation generation, not a runtime validator or router. Still alpha.
pnpm add @uraniadev/sveltekit-valibot-openapi valibot @valibot/to-json-schema
Endpoints are declared by exporting a _openapi object from your SvelteKit route module.
Each key is an HTTP method, each value is created with defineEndpoint().
The object is later sanitized, validated, and deep-frozen by the generator to guarantee structural safety.
// src/routes/api/todos/+server.ts
import * as v from "valibot";
import { defineEndpoint } from "@uraniadev/sveltekit-valibot-openapi";
const Todo = v.object({
id: v.string(),
title: v.string(),
});
const TodoList = v.array(Todo);
const TodoCreate = v.object({ title: v.string() });
export const _openapi = {
GET: defineEndpoint({
method: "GET",
path: "/api/todos",
summary: "List todos",
query: v.object({
search: v.optional(v.string()),
}),
responses: {
200: {
description: "List of todos",
schema: TodoList,
},
},
}),
POST: defineEndpoint({
method: "POST",
path: "/api/todos",
summary: "Create a todo",
body: TodoCreate,
responses: {
201: {
description: "Created todo",
schema: Todo,
},
},
}),
} as const;
defineEndpoint() supportsquery: object-like only (object / optional / nullable / pipe / union-of-objects)
queryParams: extra documentation aligned with query
body:
application/json{ content: { "media/type": schema } } mapresponses:
{ schema }{ content: { "media/type": schema } }tags, summary, description, deprecated
per-endpoint security
createOpenApiSpec produces the OpenAPI spec object.
You expose it however you want.
// src/routes/openapi/+server.ts
import { json } from "@sveltejs/kit";
import { createOpenApiSpec } from "@uraniadev/sveltekit-valibot-openapi";
const modules = import.meta.glob("../api/**/+server.{ts,js}");
export const GET = async () => {
const spec = await createOpenApiSpec(modules, {
basePath: "/api",
info: {
title: "My API",
version: "1.0.0",
description: "Example SvelteKit API",
},
servers: [
{ url: "https://api.example.com", description: "Production" },
{ url: "http://localhost:5173", description: "Development" },
],
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
security: [{ bearerAuth: [] }],
});
return json(spec);
};
Visit:
/openapi
to see your OpenAPI 3.1 JSON.
import { createOpenApiSpec } from "@uraniadev/sveltekit-valibot-openapi";
const modules = import.meta.glob("./routes/**/route.{ts,js}");
async function build() {
const spec = await createOpenApiSpec(modules, {
info: { title: "My Service", version: "1.0.0" },
});
console.log(JSON.stringify(spec, null, 2));
}
Request bodies fully support multi-media content maps:
body: {
description: "Update profile",
required: false,
content: {
"application/json": v.object({ name: v.string() }),
"multipart/form-data": v.object({
name: v.string(),
avatar: v.string(),
}),
},
}
Shorthand (schema only):
body: v.object({ title: v.string() });
is documented as:
{
"content": {
"application/json": { "schema": { … } }
}
}
All body definitions undergo strict validation:
type/subtype)A response may define:
schema)content mapresponses: {
200: {
description: "Multiple formats",
content: {
"application/json": v.object({ ok: v.string() }),
"text/plain": v.string(),
"image/png": v.string(),
},
},
404: {
description: "Not found",
schema: v.object({ message: v.string() }),
},
}
Rules enforced by sanitization:
query must be object-like:
object(...)optional, nullable, nullish, pipe, brand, fallback, defaultUnsupported shapes (primitives, arrays) are rejected.
const Query = v.object({
search: v.optional(v.string()),
limit: v.number(),
sort: v.union([v.literal("asc"), v.literal("desc")]),
});
The generator:
in: "query" parametersqueryParamsArray-typed query values are permitted and documented normally.
Every _openapi module is sanitized before inclusion:
__proto__, constructor, prototypemethod and responsesThis prevents prototype pollution, malformed metadata, and unbounded structures from entering your spec.
Valibot schemas go through a full structural normalization step:
date() → { type: "string", format: "date-time" }never() removedTo prevent runaway or malicious schemas:
Invalid or pathological schemas fail early with explicit errors.
Schemas used in request/response bodies are automatically:
#/components/schemas/...This avoids excessive inlining and makes the generated spec tooling-friendly.
{
"components": {
"schemas": {
"User_1": { ... },
"Todo_2": { ... }
}
}
}
If an endpoint has tags, the generator aggregates them into a sorted list:
{
"tags": ["Users", "Todo"]
}
Your generator now:
[id] → {id})Example:
src/routes/api/users/[id]/+server.ts
↓
/api/users/{id}
const spec = await createOpenApiSpec(glob, {
securitySchemes: {
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
},
security: [{ bearerAuth: [] }],
});
defineEndpoint({
method: "GET",
path: "/api/public",
security: [], // override to no auth
responses: { 200: { description: "OK" } },
});
It is pure documentation generation, not a framework.
Import from the published package:
import { EndpointDef, OpenApiSpec } from "@uraniadev/sveltekit-valibot-openapi";
This library was not exactly “vibe-coded” or generated blindly. It was built through an iterative workflow where AI was used as a technical assistant, not as an author.
All architectural decisions, schema handling logic, and API design were intentionally crafted by the maintainer, with AI serving as a tool to accelerate refactoring, validate edge cases, and improve TypeScript ergonomics.
Every line of code was reviewed, tested, and integrated with a somehow clear understanding of SvelteKit, Valibot, and OpenAPI constraints.
So any mistake or naivety is purely mine, amplified by AI abuse 😉