GraphQL
The GraphQL endpoint is auto-generated from the project's content types. One endpoint per project at /api/graphql/{slug}.
Endpoint
POST /api/graphql/{slug}
Headers:
Content-Type: application/json
Authorization: Bearer <api-key> # delivery / preview / management
Body:
{
"query": "{ entry(id: \"ckl_…\") { ... on ArticlePage { title body { html } } } }",
"variables": {}
}
entry and entryBySlug return the ContentEntry interface, so typed fields are read through an inline fragment (... on ArticlePage { … }).
Schema generation
For each content type Krios emits:
- A type named in PascalCase of the content type's
apiName(e.g.articlePage→ArticlePage); its fields are the fieldapiNames verbatim. - A list query
{apiName}Collection(e.g.articlePageCollection). There is no per-type single query — fetch one entry with the genericentry(id)orentryBySlug(slug, site, locale). - Reference / blocks fields resolve to the linked types (or a synthesized union when multiple types are allowed).
- Rich text fields surface as
{ raw, html, text }. - Custom field types resolve to the JSON scalar by default; setting
graphqlTypeNameon the registration emits a named type derived from the registration's JSON Schema (V3.9).
Run an introspection query with a management or preview key to see the live shape — introspection is disabled for delivery keys.
Root queries
entry(id: ID!): ContentEntry
entryBySlug(slug: String!, site: String!, locale: String!, preview: Boolean): ContentEntry
{apiName}Collection(site: String, locale: String, preview: Boolean, limit: Int, skip: Int, where: …, orderBy: …): {Type}Collection!
resolveRoute(site: String!, locale: String!, path: String!): RouteResolution
asset(id: ID!, locale: String): MediaAsset
Each collection returns { items, total, skip, limit }.
where and orderBy are present in the generated SDL but the resolver does not apply them yet — collections always sort by _updatedAt descending. Filter and sort client-side for now. (Full enforcement is roadmap.)
DataLoader
Every reference / media / block field uses a per-request DataLoader. A query that pulls 100 entries with their author and category does 3 round-trips: 1 for entries, 1 for unique authors, 1 for unique categories. No N+1.
Depth + complexity limits
- Depth limit: 10 (
MAX_QUERY_DEPTH). - Complexity limit: 1000 (
MAX_COMPLEXITY). A hard limit today; per-API-key overrides are planned (V1.5). - Reference recursion: capped at 3 hops; in GraphQL, deeper traversals resolve to
null(single reference) or[](list). (The{ _ref, _type }placeholder form is REST-delivery only.)
The validator runs before resolvers — over-deep / over-complex queries return 400 with the violation in the body.
Example queries
# Single article with rich-text projections
query GetArticle($id: ID!) {
entry(id: $id) {
_id
_publishedAt
... on ArticlePage {
title
body { raw html text }
}
}
}
# Paginated list (skip/limit). orderBy is not yet honored — sort client-side.
query ListArticles {
articlePageCollection(limit: 25, skip: 0) {
total
skip
limit
items {
_id
title
excerpt
_updatedAt
}
}
}
# Block-composed landing page (fetch by slug, then narrow with a fragment)
query Landing($slug: String!, $site: String!, $locale: String!) {
entryBySlug(slug: $slug, site: $site, locale: $locale) {
... on LandingPage {
title
blocks {
__typename
... on HeroBlock { headline ctaText }
... on CtaBlock { headline buttonText }
}
}
}
}
Cache
Same headers as REST:
Cache-Control: public, max-age=60, stale-while-revalidate=3600
Surrogate-Key: entry:{id} type:{apiName} site:{slug} project:{slug}
POST is treated as cacheable when the body is read-only (no mutations). Preview-key responses are private, no-store.
Type generation
krios types generate --output ./src/krios.types.ts
Emits TypeScript interfaces matching the project's GraphQL schema. Re-run after a schema change.