Ir al contenido

API REST

Las operaciones de la API local expuestas por HTTP. Esta capa no decide nada: traduce una petición a una llamada de la API local y su resultado a una respuesta. Todo lo que decide qué documentos salen o qué escrituras valen está documentado en la otra página.

La integración de Astro monta estos seis endpoints por ti. La firma del final de la página solo importa si montas el CMS a mano.

Prefijo por defecto /api/cms, configurable con api.path.

Método Ruta Operación Éxito
GET /api/cms/:collection find 200
POST /api/cms/:collection create 201
GET /api/cms/:collection/count count 200
GET /api/cms/:collection/:id findByID 200
PATCH /api/cms/:collection/:id update 200
DELETE /api/cms/:collection/:id delete 200

count se resuelve antes que :id, así que nunca se interpreta como el id de un documento.

Las actualizaciones son PATCH y no PUT: son parciales, y PUT prometería un reemplazo total que no ocurre. findOne no tiene endpoint —?limit=1 sobre el listado da lo mismo— y la autenticación se sirve aparte, en /api/auth/*.

Las colecciones que declaran upload añaden dos rutas más. Están documentadas en Ficheros:

Método Ruta Qué hace Éxito
POST /api/cms/:collection/upload Sube un fichero con multipart/form-data 201
GET /api/cms/cdn/:collection/:id/:filename Devuelve el fichero 200
GET /api/cms/cdn/:collection/:id Redirige a la ruta canónica 302

upload se resuelve antes que :id, igual que count. El segmento cdn no colisiona con ninguna colección: las rutas de colección tienen uno o dos segmentos y las de fichero tres o cuatro, así que una colección llamada cdn sigue respondiendo con normalidad en /api/cms/cdn.

Subir es una escritura y exige sesión. Servir es una lectura y respeta el access.read de su colección.

Dos rutas más, sobre los ajustes del sitio guardados en KV:

Método Ruta Qué hace Éxito
GET /api/cms/settings Los cinco campos efectivos 200
PATCH /api/cms/settings Fusión superficial, devuelve el resultado 200

Las dos exigen sesión siempre, sin el matiz de access.read que tienen las colecciones. Un PATCH con una clave desconocida, un valor que no es cadena o un siteUrl que no es una URL absoluta responde 400 con su array issues.

A diferencia de cdn, este segmento sí colisionaría con una colección: /api/cms/settings tiene un solo segmento, igual que un listado. Por eso settings es un slug reservado y defineConfig rechaza al arrancar una colección que lo declare.

Cambiar el prefijo mueve los seis, y también las de ficheros y la de ajustes:

cms.config.ts
export default defineConfig({
collections: [posts],
api: { path: '/api/contenido' },
})
GET /api/cms/posts
?where[status][equals]=published
&where[publishedAt][less_than]=2026-01-01
&sort=-publishedAt
&limit=10
&page=2
&depth=1
&select=title,slug
Parámetro Por defecto Rango
limit 10 entero de 1 a 1000
page 1 entero ≥ 1
depth 1 entero de 0 a 10
sort -createdAt campos separados por comas, - para descendente
select todas las columnas nombres de campo separados por comas
where ver abajo

Cualquiera fuera de rango responde 400 sin haber lanzado una sola consulta.

select y depth valen también al leer un documento por id. count solo mira where.

Se codifica con la sintaxis de corchetes de qs, la misma que usa Payload:

Consulta Query string
{ status: { equals: 'published' } } where[status][equals]=published
{ readingTime: { greater_than: 5 } } where[readingTime][greater_than]=5
{ slug: { in: ['a', 'b'] } } where[slug][in][0]=a&where[slug][in][1]=b
{ or: [{ … }, { … }] } where[or][0][slug][equals]=a&where[or][1][slug][equals]=b

Los once operadores y sus tipos admitidos son los de la API local, sin recorte. Los valores viajan como cadenas y los convierte el campo, así que una fecha se escribe en ISO y un checkbox como true:

where[publishedAt][less_than]=2026-01-01
where[featured][equals]=true

in y not_in aceptan como mucho 100 valores, que es el tope de parámetros que D1 liga por consulta.

Un operador que no existe, un campo que no existe, un operador que no aplica al tipo del campo o un valor que el campo no sabe convertir responden 400. Ninguno se ignora en silencio: un filtro descartado devuelve filas que no deberían estar ahí y el fallo aparece tarde.

Listado — 200:

{
"docs": [{ "id": "01J8…", "title": "Hola" }],
"pagination": {
"totalDocs": 42,
"limit": 10,
"page": 1,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
}
}

Conteo — 200:

{ "totalDocs": 42 }

Documento único — 200 con el documento. Creación — 201 con el documento. Borrado — 200 con el documento borrado.

El cuerpo lleva siempre error y message, más issues cuando es de validación:

{
"error": "VALIDATION_ERROR",
"message": "El documento no es válido",
"issues": [{ "path": "title", "code": "REQUIRED", "message": "title es obligatorio" }]
}
Situación Status error
Colección o documento inexistente 404 NOT_FOUND
Cuerpo inválido 400 VALIDATION_ERROR
Consulta inválida 400 QUERY_ERROR
Sin sesión donde hace falta 401 UNAUTHORIZED
Con sesión pero sin permiso 403 FORBIDDEN
Método no soportado en la ruta 405 METHOD_NOT_ALLOWED
Cuerpo mayor de 1 MB 413 PAYLOAD_TOO_LARGE
Fallo de D1 500 INTERNAL_ERROR

Los issues de una validación son los mismos { path, code, message } de la API local.

El 403 no lo emite nadie todavía: v1 no tiene roles. Está en la tabla porque a tu cliente le conviene contemplarlo desde el principio.

El Content-Type no se mira: lo que decide es si el cuerpo parsea como objeto JSON. Si no parsea, es 400. Un POST con {} responde 400 con un issue por cada campo obligatorio que falta.

El límite es 1 MB. Por encima es 413, y se rechaza sin llegar a parsearlo.

El handler recibe una función getSession(request), que la ruta inyectada cablea a la autenticación. La sesión viaja en una cookie, así que desde el navegador basta con haber iniciado sesión.

Operación Regla
GET Pública si la colección declara access: { read: 'public' }; si no, requiere sesión
POST, PATCH, DELETE Requieren sesión siempre
Cualquiera sobre /settings Requiere sesión siempre, también el GET
defineCollection({
slug: 'posts',
access: { read: 'public' },
fields: [{ name: 'title', type: 'text', required: true }],
})

El método se resuelve antes que la sesión, así que un PUT responde 405 lleve o no credenciales: pedir autenticación para una ruta que no existe es confirmar que existe.

Desactivado por defecto: en el caso normal la API la consume el propio sitio, y el mismo origen no necesita CORS. Se activa listando orígenes:

cms(config, { cors: ['https://app.example.com'] })

Con la lista puesta, Access-Control-Allow-Origin se emite solo para los orígenes que estén en ella, siempre acompañado de Vary: Origin, y el preflight OPTIONS responde 204. Sin lista no se emite ninguna cabecera de CORS y un OPTIONS responde 405, porque no hay preflight que atender.

Access-Control-Allow-Credentials no se emite nunca, que es lo que hace inofensivo un * en la lista.

Lo que enruta la petición. La integración de Astro lo llama por ti desde la ruta que inyecta; esta firma solo importa si montas el CMS a mano.

function handle(
request: Request,
cms: { collections: Record<string, unknown> },
config: ResolvedConfig,
options: { getSession(request: Request): Promise<unknown> | unknown; cors?: string[] },
): Promise<Response>

El cliente de D1 y el objeto de operaciones se le pasan: son los que devuelve buildAPI. El handler no construye ninguno de los dos.