OpenAPI / Swagger Documentation
Both stacks produce an OpenAPI document from the schemas you already write, but by very different machinery:
- Express: Efesto writes
.yamlfiles to disk from yourswaggerModeland_<verb>Swaggerdeclarations. You bundle them into one document and serve a UI. - Bun/Elysia: Efesto writes nothing.
@elysiajs/openapibuilds the document in memory from your TypeBox schemas and each route'sdetail, and serves it.
Express
Where documentation comes from
Two declarations on a service feed the document:
swaggerModel— the route's category (modelName) and its sharedschemas. Schemas are global across the whole project and referenced anywhere with@Name._<verb>Swagger— per-endpoint metadata:operationId,summary,parameters,requestBody,responses.
import { BaseApiService, SwaggerModel, SwaggerOptions } from "efesto";
class Users extends BaseApiService {
constructor() {
super(__filename);
}
swaggerModel: SwaggerModel = {
modelName: "User",
schemas: [
{
name: "User",
properties: {
id: "number",
name: "string!",
email: "string::email!",
},
},
{
name: "CreateUser",
properties: { name: "string!", email: "string::email!" },
},
],
};
_postSwagger: SwaggerOptions = {
operationId: "createUser",
summary: "Create a user",
requestBody: "@CreateUser", // shorthand
responses: { 201: "@User" }, // shorthand
};
async _post(req, res) {
return res.status(201).json(await createUser(req.body));
}
}
export default Users;
Shorthands
Writing raw OpenAPI is verbose; Efesto's Magic Types collapse it. All four below are equivalent to the long object form:
| Shorthand | Meaning |
|---|---|
name: "string" | a property of type string |
"string::email" | string with email format |
"string|Joe" | string with example Joe |
requestBody: "@CreateUser" | a body of application/json with that schema |
responses: { 200: "@User" } | a 200 of application/json with that schema |
responses: { 200: "@User[]" } | a 200 array of User |
See Magic Types for the complete grammar.
Parameters
Path, query, and header parameters go in parameters (the schema accepts the Magic
Type shorthand too):
_getSwagger: SwaggerOptions = {
operationId: "getUsers",
parameters: [
{ in: "query", name: "page", schema: "number?" },
{ in: "header", name: "X-Request-Id", schema: "string" },
],
responses: { 200: "@User[]" },
};
A [id].ts file's path parameter is added for you; document extra ones here.
Generated files and serving the UI
When Efesto starts it writes, into relativeDirSwaggerDeclarationsPath (default
swagger-declarations/):
- one
.yamlpermodelName(e.g.user.yaml) with that route's schemas and paths; - an
index.yamlthat merges your base file with$refs to those per-model files.
You provide one file yourself: baseIndex.yaml, the document's base (OpenAPI
version, servers, info, securitySchemes, shared components, security, and an
empty paths: {}):
openapi: "3.0.0"
servers:
- url: http://localhost:3000/api/v1
info:
title: My API
version: 1.0.0
components:
securitySchemes:
headerAuth: { type: apiKey, in: header, name: X-Auth-Token }
security:
- headerAuth: []
paths: {}
Then bundle index.yaml into a single document and serve it with
swagger-ui-express:
import SwaggerParser from "@apidevtools/swagger-parser";
import swaggerUi from "swagger-ui-express";
import fs from "fs";
// after the Efesto middleware: merge all .yaml into one apis.json
const bundled = await SwaggerParser.bundle("swagger-declarations/index.yaml");
fs.writeFileSync("swagger-declarations/apis.json", JSON.stringify(bundled));
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(bundled));
modelNameEndpoints whose service omits modelName are grouped under the n-a category. Give
each route a clear modelName to keep the document organized.
Bun/Elysia
There is no swaggerModel, no _<verb>Swagger, and no file generation. The OpenAPI
document is built from your TypeBox schemas (body/query/params/response)
and each route's detail, then served by @elysiajs/openapi.
Enable it with the swagger option (on by default); the document is served at
<prefix>/openapi:
const app = efesto({
isProduction: false,
routesDir: `${import.meta.dir}/routes`,
prefix: "/api/v1",
swagger: true, // -> GET /api/v1/openapi
});
Document an endpoint inline. detail maps to OpenAPI metadata; the schemas document
the shapes:
- Native Elysia
- Class-based
import { Elysia, t } from "efesto/elysia";
export default new Elysia().post(
"/",
({ body }) => createUser(body),
{
body: t.Object({ name: t.String(), email: t.String({ format: "email" }) }),
response: { 201: t.Object({ id: t.Number() }) },
detail: { summary: "Create a user", tags: ["User"] },
}
);
import { BaseApiService, type Context, RouteValidation, SwaggerOptions, t } from "efesto/elysia";
export default class extends BaseApiService {
_postSwagger: SwaggerOptions = { operationId: "createUser", summary: "Create a user" }; // -> OpenAPI
_postValidation: RouteValidation = {
body: t.Object({ name: t.String(), email: t.String({ format: "email" }) }),
};
_post({ body }: Context) {
return createUser(body);
}
}
To customize the OpenAPI plugin (title, version, path), pass an object as swagger
instead of true, it is forwarded to @elysiajs/openapi.