Routes API des plugins

Sur cette page

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 pluginNom de routeURL
formsstatus/_emdash/api/plugins/forms/status
formssubmissions/_emdash/api/plugins/forms/submissions
seosettings/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 :

  1. POST/PUT/PATCH — Corps de la requête (JSON)
  2. 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;
}