Installation
Efesto ships two stacks from one package. Pick the tab for your runtime, the rest of
the docs use the same groupId, so your choice follows you across pages.
Prerequisites
- Express
- Bun/Elysia
Install
- Express
- Bun/Elysia
npm install efesto express
# or: yarn add efesto express / pnpm add efesto express
npm install -D typescript @types/node @types/express
Elysia and the OpenAPI plugin are optional peer dependencies, install them alongside Efesto:
bun add efesto elysia @elysiajs/openapi
Project structure
The only hard requirement is a routes directory whose files map to paths. A common layout:
my-efesto-api/
├── src/ (or lib/)
│ ├── routes/ # endpoints — file path becomes the URL path
│ │ └── users/
│ │ └── index.ts
│ ├── server.ts # or app.ts
│ └── ...
├── tsconfig.json
└── package.json
The Express stack additionally writes generated OpenAPI into a
swagger-declarations/ folder (configurable). The Elysia stack serves OpenAPI from
memory and writes nothing.
Your first endpoint
- Express
- Bun/Elysia
src/routes/users/index.ts — a service class. The file location (users/index.ts)
becomes the path /users:
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",
},
},
],
};
_getSwagger: SwaggerOptions = {
operationId: "getUsers",
summary: "List users",
responses: { 200: "@User[]" }, // shorthand: an array of User
};
async _get(req, res) {
return res.json([{ id: 1, name: "Ada", email: "ada@example.com" }]);
}
}
export default Users;
src/app.ts — mount Efesto as Express middleware:
import express from "express";
import efesto from "efesto";
import path from "path";
const app = express();
app.use(express.json());
app.use(
"/api/v1",
efesto({
authMiddleware: (req, res, next) => next(), // your auth here
errorMiddleware: (req, res, next) => next(), // your error handling here
isProduction: process.env.NODE_ENV === "production",
options: {
absoluteDirRoutes: path.join(__dirname, "routes"),
relativeDirSwaggerDeclarationsPath: "swagger-declarations",
},
})
);
app.listen(3000, () => console.log("http://localhost:3000/api/v1"));
Run it:
npx ts-node src/app.ts # dev
# or: tsc && node dist/app.js
GET http://localhost:3000/api/v1/users returns the list.
src/routes/user/index.ts — a native Elysia module. The file location
(user/index.ts) becomes the prefix /user:
import { Elysia, t } from "efesto/elysia"; // re-exports Elysia's `Elysia` and `t`
const users = [{ id: 1, name: "Ada", surname: "Lovelace" }];
export default new Elysia()
.get("/", () => users, {
detail: { summary: "List users", tags: ["User"] },
})
.post("/", ({ body }) => ({ id: users.length + 1, ...body }), {
body: t.Object({ name: t.String(), surname: t.String() }),
detail: { summary: "Create user", tags: ["User"] },
});
src/server.ts — create the app:
import efesto from "efesto/elysia";
const app = efesto({
isProduction: process.env.NODE_ENV === "production",
routesDir: `${import.meta.dir}/routes`,
prefix: "/api/v1",
swagger: true, // native OpenAPI at /api/v1/openapi
}).listen(2014);
console.log(`http://localhost:${app.server?.port}/api/v1`);
Run it:
bun run src/server.ts
GET http://localhost:2014/api/v1/user returns the list; OpenAPI is at
/api/v1/openapi.
Viewing the documentation
- Express
- Bun/Elysia
Efesto writes OpenAPI .yaml files into swagger-declarations/. Bundle them and
serve a UI with swagger-ui-express:
import swaggerUi from "swagger-ui-express";
// after the Efesto middleware (apis.json is the bundled document)
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(require("../swagger-declarations/apis.json")));
See Swagger Documentation for the full pipeline.
Nothing to set up: @elysiajs/openapi serves an interactive document at
<prefix>/openapi (here /api/v1/openapi) whenever swagger is not false.
Troubleshooting
- Routes not found — check
absoluteDirRoutes(Express) /routesDir(Elysia) points at the real folder, and thatisProductionmatches your files: production scans.js, development scans.ts. - Express: nothing exported — a route file must
export defaulta class extendingBaseApiService. - Elysia: module skipped — a route file must
export defaulta native Elysia instance or a class/instance extendingBaseApiService; anything else is skipped with a warning. - Elysia: peer deps —
elysiaand@elysiajs/openapiare optional peers; if you use the Elysia stack you must install them yourself.
Next, learn the endpoint model or the full configuration.