Creare endpoint
Gli endpoint sono file. Dove metti il file decide l'URL; ciò che esporti decide il comportamento. Questa pagina copre entrambi gli stack affiancati: le regole di routing sono condivise, la definizione della rotta differisce.
Routing basato sui file (entrambi gli stack)
Una cartella è un segmento di percorso. Un file index mappa la sua cartella. Un file
[param] diventa un parametro di rotta. L'estensione del file viene rimossa (.ts in
sviluppo, .js in produzione, selezionata da isProduction).
routes/
├── users/
│ ├── index.ts # /users
│ ├── [id].ts # /users/:id
│ └── [id]/
│ └── posts.ts # /users/:id/posts
└── health.ts # /health
Con un prefisso di mount /api/v1, routes/users/[id].ts serve
/api/v1/users/:id. Vedi Routing da filesystem per le
regole esatte e una mappa interattiva.
Un file [id].ts espone id come parametro di rotta.
- Express: leggilo da
req.params.id. Il suo tipo documentato proviene dalla configurazionedynamicParameterType("string"di default). - Elysia: il segmento diventa
:id; leggilo dal contestoparamstipizzato.
Definire una rotta
- Express
- Bun/Elysia
Un file di rotta fa export default di una classe che estende BaseApiService. Il
nome della classe non conta; il costruttore deve chiamare super(__filename) così che
Efesto possa risolvere i parametri di percorso del file.
Ogni verbo HTTP è composto da due membri: un handler _<verb>(req, res, next) e un
oggetto di documentazione opzionale _<verb>Swagger. L'handler è puro Express:
restituisci un valore o chiama res.json(...).
import { BaseApiService, SwaggerModel, SwaggerOptions } from "efesto";
class Users extends BaseApiService {
constructor() {
super(__filename);
}
// Schemi condivisi per questa rotta, raggruppati sotto una "categoria" di modello.
swaggerModel: SwaggerModel = {
modelName: "User",
schemas: [
{
name: "User",
properties: {
id: "number",
name: "string",
email: "string::email",
},
},
],
};
// GET /users
_getSwagger: SwaggerOptions = {
operationId: "getUsers",
summary: "List users",
responses: { 200: "@User[]" },
};
async _get(req, res) {
return res.json(await listUsers());
}
// POST /users
_postSwagger: SwaggerOptions = {
operationId: "createUser",
requestBody: "@User",
responses: { 201: "@User" },
};
async _post(req, res) {
return res.status(201).json(await createUser(req.body));
}
}
export default Users;
Verbi supportati: _get, _post, _put, _patch, _delete, _head,
_options, _trace, _connect. Ciascuno può dichiarare il proprio _<verb>Swagger,
_<verb>Validation e _<verb>OverrideAuth.
_getSwagger è tipizzato senza requestBody (una GET non porta corpo). Usa invece i
parametri di query, dichiarati in parameters.
Un file di rotta fa export default di una istanza Elysia nativa oppure di una
classe che estende BaseApiService. Usa percorsi relativi all'interno
dell'istanza; Efesto la monta al prefisso ricavato dal file. Le due forme possono
coesistere in un solo progetto.
Elysia nativo — accesso completo all'API di Elysia:
import { Elysia, t } from "efesto/elysia";
const users = [{ id: 1, name: "Ada" }];
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() }),
detail: { summary: "Create user", tags: ["User"] },
});
BaseApiService basato su classe — mantiene documentazione,
validazione/permessi e implementazione visivamente separati, scritto esattamente
come il BaseApiService di Express (differiscono solo handler e validazione):
import { BaseApiService, type Context, RouteValidation, SwaggerOptions, t } from "efesto/elysia";
const users = [{ id: 1, name: "Ada" }];
export default class extends BaseApiService {
_getSwagger: SwaggerOptions = { operationId: "listUsers", summary: "List users" }; // -> OpenAPI
_get() {
return users;
}
_postSwagger: SwaggerOptions = { operationId: "createUser", summary: "Create user" };
_postValidation: RouteValidation = { body: t.Object({ name: t.String() }) }; // TypeBox
_post({ body }: Context) {
return { id: users.length + 1, name: (body as { name: string }).name };
}
}
Verbi supportati: get, post, put, patch, delete, head, options. Per
ogni verbo dichiara _<verb> (handler, sintassi a metodo), _<verb>Validation
(schemi TypeBox { body, query, params, response }), _<verb>Swagger
(SwaggerOptions: metadati OpenAPI + permission) e _<verb>Multer /
_<verb>MultipleMulter per gli upload (post/patch/put). Efesto traduce ciascuno
in una rotta Elysia nativa, quindi la forma a classe non ha costo a runtime rispetto a
quella nativa.
Aspetti trasversali
Ogni aspetto ha una guida dedicata con entrambi gli stack. Ecco dove viene dichiarato ciascuno:
| Aspetto | Express | Bun/Elysia | Guida |
|---|---|---|---|
| Documentazione | _<verb>Swagger + swaggerModel | docs / detail nativo | Swagger |
| Validazione | _<verb>Validation (express-validator) | body/query/params (TypeBox) | Validazione |
| Permessi | _<verb>Swagger.permission | permission sul verbo | ABAC |
| Autenticazione | globale + _<verb>OverrideAuth | setup (onBeforeHandle) | Autenticazione |
| Caching | _<verb>Swagger.cache/purgeKey | nativo (plugin Elysia) | Caching |
| Upload di file | flag _<verb>Multer | t.File() nativo | Upload di file |
Un servizio Express completo
Mettendo documentazione, validazione, permessi e caching su un'unica rotta:
import { BaseApiService, SwaggerModel, SwaggerOptions } from "efesto";
import { body, query } from "express-validator";
class Users extends BaseApiService {
constructor() {
super(__filename);
}
swaggerModel: SwaggerModel = {
modelName: "User",
schemas: [
{
name: "User",
properties: {
id: "number",
name: "string!", // obbligatorio
email: "string::email!", // obbligatorio, formato email
age: "number?", // opzionale
},
},
],
};
_getValidation = [query("page").optional().isInt({ min: 1 })];
_getSwagger: SwaggerOptions = {
operationId: "getUsers",
summary: "List users",
permission: ["readAll", "User"],
cache: { key: "`users`", expiresInSeconds: 120 },
responses: { 200: "@User[]" },
};
async _get(req, res) {
return res.json(await listUsers(req.query));
}
_postValidation = [body("name").isString(), body("email").isEmail()];
_postSwagger: SwaggerOptions = {
operationId: "createUser",
permission: ["create", "User"],
purgeKey: ["`users`"], // invalida la cache della lista alla creazione
requestBody: "@User",
responses: { 201: "@User" },
};
async _post(req, res) {
return res.status(201).json(await createUser(req.body));
}
}
export default Users;
Un servizio Elysia completo
La stessa forma sullo stack Elysia, con TypeBox che fa validazione e tipizzazione:
import { BaseApiService, type Context, RouteValidation, SwaggerOptions, t } from "efesto/elysia";
const User = t.Object({
name: t.String(),
email: t.String({ format: "email" }),
age: t.Optional(t.Number()),
});
export default class extends BaseApiService {
_getSwagger: SwaggerOptions = { summary: "List users", permission: ["readAll", "User"] };
_getValidation: RouteValidation = { query: t.Object({ page: t.Optional(t.Numeric()) }) };
_get({ query }: Context) {
return listUsers(query);
}
_postSwagger: SwaggerOptions = { summary: "Create user", permission: ["create", "User"] };
_postValidation: RouteValidation = { body: User };
_post({ body }: Context) {
return createUser(body);
}
}
Buone pratiche
- Imposta
modelName. Senza, gli endpoint vengono raggruppati sotto la categorian-anella documentazione generata. Usa un sostantivo chiaro ("User","Product"). - Riusa gli schemi con
@. Efesto condivide gli schemi nell'intero progetto: dichiara un modello una volta e referenzialo come@Userovunque. Vedi Magic Types. - Mantieni gli handler snelli. Validazione, permessi e documentazione sono dichiarativi; l'handler dovrebbe contenere solo logica di business.