Skip to main content

OpenAPI / Swagger Documentation

Both stacks produce an OpenAPI document from the schemas you already write, but by very different machinery:

  • Express: Efesto writes .yaml files to disk from your swaggerModel and _<verb>Swagger declarations. You bundle them into one document and serve a UI.
  • Bun/Elysia: Efesto writes nothing. @elysiajs/openapi builds the document in memory from your TypeBox schemas and each route's detail, and serves it.

Express

Where documentation comes from

Two declarations on a service feed the document:

  • swaggerModel — the route's category (modelName) and its shared schemas. 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:

ShorthandMeaning
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 .yaml per modelName (e.g. user.yaml) with that route's schemas and paths;
  • an index.yaml that 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));
Set modelName

Endpoints 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:

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"] },
}
);

To customize the OpenAPI plugin (title, version, path), pass an object as swagger instead of true, it is forwarded to @elysiajs/openapi.