Skip to content

OpenAPI

Every Slingshot app ships an OpenAPI document and a browser UI automatically.

  • GET /openapi.json returns the generated spec.
  • GET /docs serves the Scalar API reference.

The framework currently emits OpenAPI 3.0.0, not 3.1, and the docs UI is Scalar rather than Swagger UI.

Use createRoute() to declare request and response schemas, then mount the route with router.openapi(). Slingshot registers the route with Hono, validates the request at runtime, and includes the route in the generated spec.

src/routes/products.ts
import {
createRoute,
createRouter,
cursorParams,
getActor,
withSecurity,
} from '@lastshotlabs/slingshot';
export const router = createRouter();
router.openapi(
createRoute({
method: 'get',
path: '/products',
tags: ['Products'],
summary: 'List products',
request: {
query: cursorParams({ limit: 25, maxLimit: 100 }),
},
responses: {
200: {
description: 'Cursor-paginated product list',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
price: { type: 'number' },
},
required: ['id', 'name', 'price'],
},
},
nextCursor: { type: 'string', nullable: true },
hasMore: { type: 'boolean' },
},
required: ['items', 'nextCursor', 'hasMore'],
},
},
},
},
},
}),
c => {
void c.req.valid('query');
return c.json({ items: [], nextCursor: null, hasMore: false }, 200) as never;
},
);
router.openapi(
withSecurity(
createRoute({
method: 'get',
path: '/me',
tags: ['Account'],
summary: 'Get the current user',
responses: {
200: {
description: 'Current user',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
userId: { type: 'string' },
},
required: ['userId'],
},
},
},
},
},
}),
{ cookieAuth: [] },
{ userToken: [] },
),
c => c.json({ userId: getActor(c).id ?? 'anonymous' }, 200) as never,
);

Routes discovered from routesDir are added automatically. You do not need to manually register them with the spec generator.

For shared schemas, prefer registerSchema() or registerSchemas() so the same component name is reused across every route that references it.

src/schemas/errors.ts
import { z } from 'zod';
export const ErrorResponse = z.object({
error: z.string(),
});
export const ValidationErrorResponse = z.object({
error: z.string(),
details: z.array(
z.object({
path: z.string(),
message: z.string(),
}),
),
requestId: z.string(),
});

Unnamed schemas still work. createRoute() auto-registers JSON request and response schemas under generated component names when it can.

withSecurity() only annotates the OpenAPI document. It does not enforce access control by itself. Pair it with real middleware such as userAuth, requireRole(...), or framework-generated auth routes.

The built-in security scheme names are:

  • cookieAuth for the session cookie
  • userToken for the x-user-token header
  • bearerAuth for bearer-token middleware

Routes generated by createEntityPlugin() also appear in the OpenAPI output. The framework derives request and response schemas from the entity field definitions.

import { defineEntity, field } from '@lastshotlabs/slingshot';
export const Post = defineEntity('Post', {
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
body: field.string(),
},
routes: {
defaults: { auth: 'userAuth' },
get: { auth: 'none' },
list: { auth: 'none' },
},
});

For a default entity mount, Slingshot generates POST, GET list, GET :id, PATCH :id, and DELETE :id routes and includes all of them in the spec.

When you enable versioning, Slingshot mounts a separate spec and docs UI per API version.

app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({
meta: {
name: 'Slingshot Example API',
version: '1.0.0',
},
routesDir: `${import.meta.dir}/routes`,
versioning: {
versions: ['v1', 'v2'],
defaultVersion: 'v2',
},
});

That produces:

GET /v1/openapi.json
GET /v1/docs
GET /v2/openapi.json
GET /v2/docs
GET /openapi.json -> redirects to the default version
GET /docs -> version selector page

Customizing the document title and version

Section titled “Customizing the document title and version”

The generated OpenAPI document reads its info.title and info.version from meta.name and meta.version.

app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({
meta: {
name: 'My API',
version: '2026-04-12',
},
routesDir: `${import.meta.dir}/routes`,
});