Passa al contenuto principale

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.

Parametri dinamici

Un file [id].ts espone id come parametro di rotta.

  • Express: leggilo da req.params.id. Il suo tipo documentato proviene dalla configurazione dynamicParameterType ("string" di default).
  • Elysia: il segmento diventa :id; leggilo dal contesto params tipizzato.

Definire una rotta

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.

GET non ha corpo della richiesta

_getSwagger è tipizzato senza requestBody (una GET non porta corpo). Usa invece i parametri di query, dichiarati in parameters.

Aspetti trasversali

Ogni aspetto ha una guida dedicata con entrambi gli stack. Ecco dove viene dichiarato ciascuno:

AspettoExpressBun/ElysiaGuida
Documentazione_<verb>Swagger + swaggerModeldocs / detail nativoSwagger
Validazione_<verb>Validation (express-validator)body/query/params (TypeBox)Validazione
Permessi_<verb>Swagger.permissionpermission sul verboABAC
Autenticazioneglobale + _<verb>OverrideAuthsetup (onBeforeHandle)Autenticazione
Caching_<verb>Swagger.cache/purgeKeynativo (plugin Elysia)Caching
Upload di fileflag _<verb>Multert.File() nativoUpload 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 categoria n-a nella 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 @User ovunque. Vedi Magic Types.
  • Mantieni gli handler snelli. Validazione, permessi e documentazione sono dichiarativi; l'handler dovrebbe contenere solo logica di business.