Les plugins peuvent exposer des routes API pour leurs composants d’UI d’administration ou des intégrations externes. Les routes reçoivent le contexte complet du plugin et peuvent accéder au stockage, au KV, au contenu et aux médias.
Définir des routes
Définissez les routes dans l’objet routes :
import { definePlugin } from "emdash";
import { z } from "astro/zod";
export default definePlugin({
id: "forms",
version: "1.0.0",
storage: {
submissions: {
indexes: ["formId", "status", "createdAt"],
},
},
routes: {
// Simple route
status: {
handler: async (ctx) => {
return { ok: true, plugin: ctx.plugin.id };
},
},
// Route with input validation
submissions: {
input: z.object({
formId: z.string().optional(),
limit: z.number().default(50),
cursor: z.string().optional(),
}),
handler: async (ctx) => {
const { formId, limit, cursor } = ctx.input;
const result = await ctx.storage.submissions!.query({
where: formId ? { formId } : undefined,
orderBy: { createdAt: "desc" },
limit,
cursor,
});
return {
items: result.items,
cursor: result.cursor,
hasMore: result.hasMore,
};
},
},
},
});
URLs des routes
Les routes sont montées sous /_emdash/api/plugins/<plugin-id>/<route-name> :
| ID plugin | Nom de route | URL |
|---|---|---|
forms | status | /_emdash/api/plugins/forms/status |
forms | submissions | /_emdash/api/plugins/forms/submissions |
seo | settings/save | /_emdash/api/plugins/seo/settings/save |
Les noms de route peuvent inclure des barres obliques pour des chemins imbriqués.
Gestionnaire de route
Le gestionnaire reçoit un RouteContext avec le contexte du plugin et des données spécifiques à la requête :
interface RouteContext extends PluginContext {
input: TInput; // Validated input (from body or query params)
request: Request; // Original Request object
}
Valeurs de retour
Retournez une valeur sérialisable en JSON :
// Object
return { success: true, data: items };
// Array
return items;
// Primitive
return 42;
Erreurs
Lancez une exception pour renvoyer une réponse d’erreur :
handler: async (ctx) => {
const item = await ctx.storage.items!.get(ctx.input.id);
if (!item) {
throw new Error("Item not found");
// Returns: { "error": "Item not found" } with 500 status
}
return item;
};
Pour des codes de statut personnalisés, lancez un Response :
handler: async (ctx) => {
const item = await ctx.storage.items!.get(ctx.input.id);
if (!item) {
throw new Response(JSON.stringify({ error: "Not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return item;
};
Validation des entrées
Utilisez des schémas Zod pour valider et analyser les entrées :
import { z } from "astro/zod";
routes: {
create: {
input: z.object({
title: z.string().min(1).max(200),
email: z.string().email(),
priority: z.enum(["low", "medium", "high"]).default("medium"),
tags: z.array(z.string()).optional()
}),
handler: async (ctx) => {
// ctx.input is typed and validated
const { title, email, priority, tags } = ctx.input;
await ctx.storage.items!.put(`item_${Date.now()}`, {
title,
email,
priority,
tags: tags ?? [],
createdAt: new Date().toISOString()
});
return { success: true };
}
}
}
Une entrée invalide renvoie une erreur 400 avec des détails de validation.
Sources d’entrée
Les entrées sont analysées depuis :
- POST/PUT/PATCH — Corps de la requête (JSON)
- GET/DELETE — Paramètres de requête dans l’URL
// POST /plugins/forms/create
// Body: { "title": "Hello", "email": "[email protected]" }
// GET /plugins/forms/list?limit=20&status=pending
Méthodes HTTP
Les routes répondent à toutes les méthodes HTTP. Vérifiez ctx.request.method pour les traiter différemment :
routes: {
item: {
input: z.object({
id: z.string()
}),
handler: async (ctx) => {
const { id } = ctx.input;
switch (ctx.request.method) {
case "GET":
return await ctx.storage.items!.get(id);
case "DELETE":
await ctx.storage.items!.delete(id);
return { deleted: true };
default:
throw new Response("Method not allowed", { status: 405 });
}
}
}
}
Accéder à la requête
L’objet Request complet est disponible pour les cas d’usage avancés :
handler: async (ctx) => {
const { request } = ctx;
// Headers
const auth = request.headers.get("Authorization");
// URL parameters
const url = new URL(request.url);
const page = url.searchParams.get("page");
// Method
if (request.method !== "POST") {
throw new Response("POST required", { status: 405 });
}
// Body (if not using input schema)
const body = await request.json();
};
Modèles courants
Routes de paramètres
Exposer et mettre à jour les paramètres du plugin :
routes: {
settings: {
handler: async (ctx) => {
const settings = await ctx.kv.list("settings:");
const result: Record<string, unknown> = {};
for (const entry of settings) {
result[entry.key.replace("settings:", "")] = entry.value;
}
return result;
}
},
"settings/save": {
input: z.object({
enabled: z.boolean().optional(),
apiKey: z.string().optional(),
maxItems: z.number().optional()
}),
handler: async (ctx) => {
const input = ctx.input;
for (const [key, value] of Object.entries(input)) {
if (value !== undefined) {
await ctx.kv.set(`settings:${key}`, value);
}
}
return { success: true };
}
}
}
Liste paginée
Renvoyer des résultats paginés avec navigation par curseur :
routes: {
list: {
input: z.object({
limit: z.number().min(1).max(100).default(50),
cursor: z.string().optional(),
status: z.string().optional()
}),
handler: async (ctx) => {
const { limit, cursor, status } = ctx.input;
const result = await ctx.storage.items!.query({
where: status ? { status } : undefined,
orderBy: { createdAt: "desc" },
limit,
cursor
});
return {
items: result.items.map(item => ({
id: item.id,
...item.data
})),
cursor: result.cursor,
hasMore: result.hasMore
};
}
}
}
Proxy d’API externe
Relayer les requêtes vers des services externes (nécessite la capability network:fetch) :
definePlugin({
id: "weather",
version: "1.0.0",
capabilities: ["network:fetch"],
allowedHosts: ["api.weather.example.com"],
routes: {
forecast: {
input: z.object({
city: z.string(),
}),
handler: async (ctx) => {
const apiKey = await ctx.kv.get<string>("settings:apiKey");
if (!apiKey) {
throw new Error("API key not configured");
}
const response = await ctx.http!.fetch(
`https://api.weather.example.com/forecast?city=${ctx.input.city}`,
{
headers: { "X-API-Key": apiKey },
},
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
return response.json();
},
},
},
});
Point de terminaison d’action
Déclencher une action ponctuelle :
routes: {
sync: {
handler: async (ctx) => {
ctx.log.info("Starting sync...");
const startTime = Date.now();
let synced = 0;
// Do work...
const items = await fetchExternalItems(ctx);
for (const item of items) {
await ctx.storage.items!.put(item.id, item);
synced++;
}
const duration = Date.now() - startTime;
ctx.log.info("Sync complete", { synced, duration });
return {
success: true,
synced,
duration,
};
};
}
}
Appeler les routes depuis l’UI d’administration
Utilisez le hook usePluginAPI() dans les composants d’administration :
import { usePluginAPI } from "@emdash-cms/admin";
function SettingsPage() {
const api = usePluginAPI();
const handleSave = async (settings) => {
await api.post("settings/save", settings);
};
const loadSettings = async () => {
return api.get("settings");
};
}
Le hook préfixe automatiquement l’ID du plugin aux URLs des routes.
Appeler les routes de l’extérieur
Les routes sont accessibles à leur URL complète :
# GET request
curl https://your-site.com/_emdash/api/plugins/forms/submissions?limit=10
# POST request
curl -X POST https://your-site.com/_emdash/api/plugins/forms/create \
-H "Content-Type: application/json" \
-d '{"title": "Hello", "email": "[email protected]"}'
Référence RouteContext
interface RouteContext<TInput = unknown> extends PluginContext {
/** Validated input from request body or query params */
input: TInput;
/** Original request object */
request: Request;
/** Plugin metadata */
plugin: { id: string; version: string };
/** Plugin storage collections */
storage: Record<string, StorageCollection>;
/** Key-value store */
kv: KVAccess;
/** Content access (if capability declared) */
content?: ContentAccess;
/** Media access (if capability declared) */
media?: MediaAccess;
/** HTTP client (if capability declared) */
http?: HttpAccess;
/** Structured logger */
log: LogAccess;
}