Graphdocs

Reference

Examples & testing

Copy-paste requests and framework recipes to test against.

Ready-to-run requests for checking a key and a workspace, and recipes for common frameworks. Replace blog-posts with one of your own content type slugs.

Testing from the terminal

Put your key in the shell once, then run each request:

export GRAPH_API_KEY=graph_YOUR_API_KEY
export GRAPH=https://api.graphcms.io/v1
# API info, no key needed
curl $GRAPH
# Content types and their fields
curl $GRAPH/content-types -H "Authorization: Bearer $GRAPH_API_KEY"
# Published entries of one type
curl "$GRAPH/content/blog-posts?limit=5" -H "Authorization: Bearer $GRAPH_API_KEY"
# Drafts of the same type
curl "$GRAPH/content/blog-posts?status=draft" -H "Authorization: Bearer $GRAPH_API_KEY"
# One entry, translated
curl "$GRAPH/content/blog-posts/hello-world?locale=fr-FR" -H "Authorization: Bearer $GRAPH_API_KEY"
# The same from staging
curl "$GRAPH/content/blog-posts?environment=staging" -H "Authorization: Bearer $GRAPH_API_KEY"
# GraphQL
curl https://api.graphcms.io/graphql \
-H "Authorization: Bearer $GRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"{ contentTypes { name slug } }"}'

Add -i to any of them to see the status code and headers.

What to expect

A quick way to confirm a key is set up the way you intended:

RequestExpected for a read-only key (content.view)
GET /v1/content-types200 with your types
GET /v1/content/blog-posts200 with published entries
GET /v1/content/no-such-type404 Content type not found
GET /v1/content-types with no Authorization header401
POST /v1/content403 naming content.edit
GET /v1/content-types?environment=prod400 Unknown environment "prod"…
GET /v1/content-types?environment=staging200, or 404 if staging isn't set up yet

A smoke test script

Run it in CI or after rotating keys to make sure a site's key can still read what it needs. Node 20+, no dependencies:

scripts/check-graphcms.mjs
1const API = process.env.GRAPH_API_URL ?? 'https://api.graphcms.io/v1';
2const headers = { Authorization: `Bearer ${process.env.GRAPH_API_KEY}` };
3
4async function check(path, expect = 200) {
5 const response = await fetch(`${API}${path}`, { headers });
6 const ok = response.status === expect;
7 const body = await response.json().catch(() => ({}));
8 console.log(`${ok ? 'ok ' : 'FAIL'} ${response.status} ${path}${ok ? '' : ` ${body.error ?? ''}`}`);
9 return ok;
10}
11
12const results = await Promise.all([
13 check('/content-types'),
14 check('/content/blog-posts?limit=1'),
15 check('/content/does-not-exist', 404),
16]);
17
18process.exit(results.every(Boolean) ? 0 : 1);
GRAPH_API_KEY=graph_YOUR_API_KEY node scripts/check-graphcms.mjs

Next.js (App Router)

Fetch in a server component, so the key never reaches the browser, and let Next.js cache it:

app/blog/page.jsx
1async function getPosts() {
2 const response = await fetch('https://api.graphcms.io/v1/content/blog-posts?limit=20', {
3 headers: { Authorization: `Bearer ${process.env.GRAPH_API_KEY}` },
4 next: { revalidate: 60 },
5 });
6 if (!response.ok) throw new Error(`Graph ${response.status}`);
7 return response.json();
8}
9
10export default async function BlogPage() {
11 const { data: posts } = await getPosts();
12
13 return (
14 <main>
15 <h1>Blog</h1>
16 {posts.map((post) => (
17 <article key={post.id}>
18 <h2>{post.title}</h2>
19 <time dateTime={post.publishedAt}>{new Date(post.publishedAt).toLocaleDateString()}</time>
20 <p>{post.fields.excerpt}</p>
21 </article>
22 ))}
23 </main>
24 );
25}

One page per entry, prerendered from the list:

app/blog/[slug]/page.jsx
1const headers = { Authorization: `Bearer ${process.env.GRAPH_API_KEY}` };
2const API = 'https://api.graphcms.io/v1/content/blog-posts';
3
4export async function generateStaticParams() {
5 const { data } = await fetch(`${API}?limit=100`, { headers }).then((r) => r.json());
6 return data.map((post) => ({ slug: post.slug }));
7}
8
9export default async function PostPage({ params }) {
10 const { slug } = await params;
11 const response = await fetch(`${API}/${slug}`, { headers, next: { revalidate: 60 } });
12 const { data: post } = await response.json();
13
14 return (
15 <article>
16 <h1>{post.title}</h1>
17 <div dangerouslySetInnerHTML={{ __html: post.fields.body }} />
18 </article>
19 );
20}

Only render html fields this way if you trust everyone who can edit content.

React (single-page app)

A browser app shouldn't hold a key. Give it a small endpoint on your own server that calls Graph, and fetch that:

src/BlogPosts.jsx
1import { useEffect, useState } from 'react';
2
3export function BlogPosts() {
4 const [posts, setPosts] = useState(null);
5
6 useEffect(() => {
7 fetch('/api/posts') // your server, which calls /v1/content/blog-posts
8 .then((response) => response.json())
9 .then(({ data }) => setPosts(data));
10 }, []);
11
12 if (!posts) return <p>Loading…</p>;
13
14 return posts.map((post) => (
15 <article key={post.id}>
16 <h2>{post.title}</h2>
17 <p>{post.fields.excerpt}</p>
18 </article>
19 ));
20}

Vue

The same pattern in Vue 3, again through your own endpoint:

src/components/BlogPosts.vue
1<script setup>
2import { ref, onMounted } from 'vue';
3
4const posts = ref([]);
5
6onMounted(async () => {
7 const response = await fetch('/api/posts');
8 posts.value = (await response.json()).data;
9});
10</script>
11
12<template>
13 <article v-for="post in posts" :key="post.id">
14 <h2>{{ post.title }}</h2>
15 <p>{{ post.fields.excerpt }}</p>
16 </article>
17</template>