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_KEYexport GRAPH=https://api.graphcms.io/v1# API info, no key neededcurl $GRAPH# Content types and their fieldscurl $GRAPH/content-types -H "Authorization: Bearer $GRAPH_API_KEY"# Published entries of one typecurl "$GRAPH/content/blog-posts?limit=5" -H "Authorization: Bearer $GRAPH_API_KEY"# Drafts of the same typecurl "$GRAPH/content/blog-posts?status=draft" -H "Authorization: Bearer $GRAPH_API_KEY"# One entry, translatedcurl "$GRAPH/content/blog-posts/hello-world?locale=fr-FR" -H "Authorization: Bearer $GRAPH_API_KEY"# The same from stagingcurl "$GRAPH/content/blog-posts?environment=staging" -H "Authorization: Bearer $GRAPH_API_KEY"# GraphQLcurl 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:
| Request | Expected for a read-only key (content.view) |
|---|---|
GET /v1/content-types | 200 with your types |
GET /v1/content/blog-posts | 200 with published entries |
GET /v1/content/no-such-type | 404 Content type not found |
GET /v1/content-types with no Authorization header | 401 |
POST /v1/content | 403 naming content.edit |
GET /v1/content-types?environment=prod | 400 Unknown environment "prod"… |
GET /v1/content-types?environment=staging | 200, 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:
1const API = process.env.GRAPH_API_URL ?? 'https://api.graphcms.io/v1';2const headers = { Authorization: `Bearer ${process.env.GRAPH_API_KEY}` };34async 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}1112const results = await Promise.all([13 check('/content-types'),14 check('/content/blog-posts?limit=1'),15 check('/content/does-not-exist', 404),16]);1718process.exit(results.every(Boolean) ? 0 : 1);GRAPH_API_KEY=graph_YOUR_API_KEY node scripts/check-graphcms.mjsNext.js (App Router)
Fetch in a server component, so the key never reaches the browser, and let Next.js cache it:
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}910export default async function BlogPage() {11 const { data: posts } = await getPosts();1213 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:
1const headers = { Authorization: `Bearer ${process.env.GRAPH_API_KEY}` };2const API = 'https://api.graphcms.io/v1/content/blog-posts';34export 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}89export 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();1314 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:
1import { useEffect, useState } from 'react';23export function BlogPosts() {4 const [posts, setPosts] = useState(null);56 useEffect(() => {7 fetch('/api/posts') // your server, which calls /v1/content/blog-posts8 .then((response) => response.json())9 .then(({ data }) => setPosts(data));10 }, []);1112 if (!posts) return <p>Loading…</p>;1314 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:
1<script setup>2import { ref, onMounted } from 'vue';34const posts = ref([]);56onMounted(async () => {7 const response = await fetch('/api/posts');8 posts.value = (await response.json()).data;9});10</script>1112<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>