Content APIs
REST vs GraphQL for Headless CMS Content Delivery
Compare REST and GraphQL for headless CMS delivery, including caching, query shape, relations, error handling, schema tooling, and when each approach is simpler.
REST and GraphQL are two delivery interfaces, not two levels of technical maturity. A straightforward page lookup may be clearer as a resource-oriented request. A screen that combines navigation, a page, and model-specific relations may benefit from a client-selected GraphQL document. The correct choice depends on the request shape, cache strategy, failure model, and tooling your team is prepared to operate.
Contoprix supports both approaches for published content. The JavaScript SDK wraps the REST delivery routes used for common page and content operations. The GraphQL endpoint exposes stable system fields plus tenant-generated fields based on the active content model.
Compare the actual request models#
With REST, the application asks for a known resource through a predictable endpoint. With GraphQL, it posts a query document describing the response fields.
| Decision area | REST | GraphQL |
|---|---|---|
| Request shape | Defined by the endpoint | Defined by the query selection |
| HTTP semantics | Resource URLs and status codes are direct | Application-level errors may accompany data in a successful HTTP response |
| Caching | Natural fit for URL-based HTTP and CDN caching | Usually needs query-aware server or application caching |
| Relations | Often requires expanded responses or additional requests | Can select related fields exposed by the schema |
| Type tooling | Endpoint DTOs or SDK types | Schema introspection/export and generated query types |
| Operational cost | Usually lower for simple reads | Higher when query governance, complexity, and schema tooling matter |
Neither column is universally better. A site can use both through separate server-side integration modules.
Where REST is strong#
REST works well when the content operation already has a clear resource boundary: fetch one page by path, list recent entries, get navigation, retrieve a media record, or execute search.
The current Contoprix client centralizes the API origin, delivery key, locale, timeout, and route encoding:
import { ContoprixClient } from "@contoprix/client";
export const contoprix = new ContoprixClient({
baseUrl: process.env.CONTOPRIX_BASE_URL!,
auth: {
type: "deliveryKey",
deliveryKey: process.env.CONTOPRIX_DELIVERY_KEY!,
},
languageCode: "en",
timeout: 10_000,
});Common calls then remain small and intention-revealing:
const about = await contoprix.pages.getBySlug("/about");
const navigation = await contoprix.navigation.get();
const result = await contoprix.content.list({
contentType: "article",
take: 10,
skip: 0,
sort: "newest",
});This approach is especially useful when a route needs one primary object and its ordinary delivery representation. Resource URLs are easy to inspect, endpoint-specific errors are familiar, and GET responses fit existing HTTP cache infrastructure.
REST still has costs. A screen that needs several unrelated resources may make multiple calls. A fixed response can include fields the current UI does not use. Those costs may be acceptable when the endpoint and cache behavior remain simple.
Where GraphQL is strong#
GraphQL is useful when a server-rendered screen needs a deliberate combination of fields or must traverse relations exposed by a content schema. The query becomes a visible contract between that screen and the content model.
Contoprix exposes a tenant-aware, read-only delivery endpoint at POST /graphql. These roots are stable for every tenant:
sitenavigation(locale)page(path, locale)pageById(id, locale)
The following query uses only fields present in the current stable schema:
query PageNavigation($path: String!, $locale: String) {
page(path: $path, locale: $locale) {
id
name
slug
locale
}
navigation(locale: $locale) {
id
name
url
openInNewTab
children {
id
name
url
}
}
}Content-type roots are different: Contoprix generates them from each tenant’s enabled model. A collection such as article returns a connection with nodes, totalCount, and pageInfo, but fields such as title, slug, or an author relation exist only if that tenant’s model defines them. Inspect the live schema or generate its types instead of copying assumed field names.
Use the GraphQL client at a server boundary#
The framework-neutral client appends /graphql, adds the correct authentication header, supports a timeout, and has no runtime dependency on Apollo or urql:
import { createContoprixGraphQLClient } from "@contoprix/graphql-client";
export const graph = createContoprixGraphQLClient({
endpoint: process.env.CONTOPRIX_BASE_URL!,
auth: {
type: "deliveryKey",
deliveryKey: process.env.CONTOPRIX_GRAPHQL_KEY!,
},
locale: "en",
timeout: 10_000,
});The GraphQL delivery key needs graphql:read. A REST delivery key with only delivery:read is rejected. Keep the key in server code.
Use graph.request() when any GraphQL error should fail the operation. It throws for non-successful HTTP responses and for a successful response containing GraphQL errors. Use requestWithErrors() only when the UI has an explicit policy for safely rendering partial data alongside field-level errors.
The convenience methods getSite(), getPage(), getPageById(), and getNavigation() cover stable system roots. Tenant-specific content fields should use a reviewed query document and generated types.
Relations change the calculation#
Relations are where GraphQL can become more expressive. If the generated tenant schema exposes an Article-to-Author relation, a screen can select only the related author fields it needs. That can make the screen’s dependency graph clearer than coordinating several resource requests.
It also creates responsibilities. The team must keep query depth and collection size bounded, understand nullable relation behavior, and update generated types when the content model changes. Deep traversal is not free simply because it fits in one HTTP request.
Contoprix caps collection page size and uses cursor pagination. Ask for the next page using pageInfo.endCursor; do not turn a graph query into an unbounded export.
Cacheability is not a slogan#
REST GET responses naturally map to URLs and shared HTTP caches. GraphQL commonly sends different documents to one POST endpoint, so a CDN cannot treat every request as the same object. Applications often cache the resolved view model, use framework data caching, or assign persisted-query identifiers.
GraphQL can reduce unused response fields, but that does not guarantee a faster request. Query parsing, validation, resolver work, relation access, response size, network conditions, and cache hit rate all matter. Measure the complete screen rather than assuming the interface determines performance.
Error handling differs#
A REST client usually starts with the HTTP status and an endpoint-specific response. A GraphQL response can contain both data and errors. That may mean a nullable field failed while sibling fields resolved correctly.
Choose the policy before building the UI:
- fail the whole screen when required content is incomplete;
- render reviewed partial data when optional fields fail;
- log error codes and field paths without exposing secrets;
- distinguish authentication, validation, timeout, and upstream failures.
The Contoprix GraphQL client deliberately offers strict and partial-result methods so that decision remains explicit.
Schema discovery and code generation#
Contoprix disables arbitrary public introspection outside development by default. The Graph Playground can explore a development schema and run known read-only queries. For production code generation, the CLI uses a separate schema export protected by schema:read:
npx contoprix graphql pull
npx contoprix graphql generate
# Or perform both steps
npx contoprix graphql syncThis separates application delivery credentials from schema tooling and prevents production code from depending on unrestricted introspection.
A practical selection rule#
Start with REST or the standard SDK when:
- the route needs one well-defined page, entry list, navigation tree, search result, or media record;
- conventional URL-based caching is valuable;
- the integration should remain accessible to any HTTP-capable runtime;
- the fixed delivery representation already matches the screen.
Consider GraphQL when:
- one server-rendered view needs a specific combination of stable and model-generated fields;
- relations are important to the view model;
- different clients genuinely need different shapes;
- the team will maintain schema export, generated types, query review, pagination, and error policies.
Choosing per screen is often more maintainable than making one API style a platform-wide rule. Read the /graphql-cms overview for Contoprix’s GraphQL delivery model, the GraphQL documentation for the current contract, and the API overview for supported REST routes.