Building a TypeSafe SDK with TypeScript for a Headless Statamic website using Astro

August 25, 2025 · 5 min read

‘So, you’ve decided to go headless with ‘Statamic‘, and you’re using ‘Astro to build your shiny frontend. Now comes the fun part: gluing everything together in a way that’s type-safe, scalable, and won’t make you cry at 2 AM when you rename a field in your CMS.
In this guide, we’ll walk through building a TypeScript SDK powered by graphql-codegen. This SDK will automatically generate fully typed queries and mutations, so you can stop guessing what shape your data has and start building like a pro.
Do you prefer visual learning? We recorded a detailed step-by-step video explaining many important techniques for type safety, as well as some core concepts of using Headless Statamic with GraphQL and Astro.
Why Type Safety Matters in Headless CMS Projects
Let’s be real: working with a headless CMS is awesome until you rename a field and your frontend explodes. Without type safety, you’re left with runtime errors, broken builds, and an endless game of “guess the data shape.”
TypeScript + GraphQL changes that. With generated types, your editor becomes your best friend, auto-completing fields, catching errors before they hit production, and making refactors painless.
Think of it like seatbelts in a car. You could drive without them, but why would you?

Prerequisites
Before we dive in, we need the following:
Node.js installation.
A Statamic site set up with GraphQL enabled.
Familiarity with TypeScript and Astro.
AstroStarter kit from lucky-media/astrostarter.
Getting started with AstroStarter
We start by cloning the AstroStarter repository.
git clone https://github.com/lucky-media/astrostarter.gitAstroStarter provides a solid foundation with Astro's best practices, allowing us to focus on building a type-safe SDK without reinventing the wheel.
Setting up a Statamic website
Assuming you already have a Statamic website with GraphQL enabled, you're on the right track. If not, make sure that GraphQL is set up and functioning correctly. This tutorial is designed with Statamic in mind, but the principles can be applied to other headless CMSs with minor adjustments.
Installing necessary packages
To generate TypeScript types from a GraphQL schema, we'll use graphql-codegen. Additionally, we'll install some supporting packages to simplify the process.
Run the following commands:
npm install graphql
npm install -D @graphql-codegen/cli @graphql-codegen/typescript-graphql-requestConfiguring TypeScript code generation
Next, let's set up a package.json to automate type generation during development.
Open the package.json and add the following scripts:
{
"name": "astrostarter",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "npm run codegen:watch && astro dev",
"build": "astro build",
"preview": "astro preview",
"prettier": "prettier . --write",
"lint": "eslint . --fix",
"astro": "astro",
"prepare": "husky",
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
},
"dependencies": {
"@astrojs/alpinejs": "^0.4.8",
"@astrojs/sitemap": "^3.4.2",
"@tailwindcss/vite": "^4.1.11",
"@types/alpinejs": "^3.13.11",
"alpinejs": "^3.14.9",
"astro": "^5.12.8",
"astro-seo": "^0.8.4",
"class-variance-authority": "^0.7.1",
"graphql": "^16.11.0",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@graphql-codegen/cli": "^5.0.7",
"@graphql-codegen/typescript-graphql-request": "^6.3.0",
"@typescript-eslint/parser": "^8.39.0",
"clsx": "^2.1.1",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"husky": "^9.1.7",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwind-merge": "^3.3.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.0"
}
}Note: At this stage, running these commands might throw an error. We'll fix this later in this post!
Creating environment variables
Securing a GraphQL endpoint is crucial. Let's create a .env file in the root directory to store environment variables.
GRAPHQL_API_URL=http://statamicapi.test/graphqlTip: Never commit the
.envfile to version control.

Generating TypeScript types
We create a codegen.ts file to configure how the TypeScript types are generated from the GraphQL schema.
Create the file at the root of the project:
import type { CodegenConfig } from "@graphql-codegen/cli";
import { loadEnv } from "vite";
const { GRAPHQL_API_URL } = loadEnv(process.env.NODE_ENV || "development", process.cwd(), "");
const config: CodegenConfig = {
schema: GRAPHQL_API_URL,
documents: ["src/**/*.graphql"],
ignoreNoDocuments: true,
generates: {
"./src/generated/graphql.ts": {
plugins: ["typescript", "typescript-operations", "typescript-graphql-request"],
config: {
preset: "client",
rawRequest: true,
useTypeImports: true,
strictScalars: false,
},
},
},
config: {
skipDocumentsValidation: true,
},
hooks: {
afterAllFileWrite: ["prettier --write"],
},
};
export default config;Schema: Points to the GraphQL API URL.
Documents: Specifies where the GraphQL queries are located.
Generates: Defines the output file and plugins used.
Hooks: Runs Prettier to keep the code tidy after generation.
For a deeper dive, check out the graphql-codegen documentation.
Building the SDK client
With the types ready, let's create the client that will serve as the backbone of the SDK.
Create a client.ts file inside the utils folder:
import { GraphQLClient } from "graphql-request";
import { getSdk } from "@/generated/graphql";
import { loadEnv } from "vite";
const { GRAPHQL_API_URL } = loadEnv(process.env.NODE_ENV || "development", process.cwd(), "");
const client = new GraphQLClient(GRAPHQL_API_URL);
export const sdk = getSdk(client);GraphQLClient: Connects to the GraphQL API.
getSdk: Generated by
graphql-codegen, providing typed methods based on the queries.sdk: The exported SDK instance we'll use across the application.
Now, every time you add a GraphQL query, graphql-codegen will generate a nice sdk.myQueryName() method for you. No guessing, no boilerplate.

Writing GraphQL queries
Organizing the GraphQL queries is essential for maintainability. Let's create a dedicated folder for them.
Create the
graphqlfolder:mkdir src/graphqlAdd a
pages.graphqlfile:
query pages {
entries(collection: "pages") {
data {
id
title
slug
uri
status
date
__typename
... on Entry_Pages_Page {
title
tagline
page_description
blocks {
...CtaBlock
...HeroSectionBlock
...FullImageBlock
}
}
}
}
}Why use Fragments?
Fragments like ...CtaBlock modularize the queries, making them reusable and easier to manage. Plus, they enhance type safety by ensuring consistency across different parts of the application.
Here's an example of a fragment:
fragment HeroSectionBlock on Set_Blocks_HeroSection {
id
type
title
__typename
}Fun Fact: Think of fragments as the LEGO blocks of the GraphQL queries. Assemble them as needed!
Implementing the SDK in Astro
Now that the queries and client are set up, let's integrate everything into Astro to dynamically generate pages.
Create a dynamic Astro route
Inside the src/pages, create a file named [...uri].astro. The uri parameter captures all URL patterns, allowing dynamic page generation.
---
import { sdk } from "@/utils/client";
import Builder from "@/components/Builder.astro";
import Layout from "@/layouts/Layout.astro";
export async function getStaticPaths() {
const { data } = await sdk.pages();
if (!data || !data.entries) {
return [];
}
return data.entries.data.map((entry) => {
return {
params: {
uri: entry.uri,
},
props: {
entry,
},
};
});
}
const { entry } = Astro.props;
---
<Layout>
<h1>{entry.title}</h1>
<div>
{
entry.__typename === "Entry_Pages_Page" &&
entry.blocks &&
entry.blocks.map((block) => <Builder block={block} />)
}
</div>
</Layout>getStaticPaths(): Fetches all page entries to generate static paths.
Layout: Wraps the page with a consistent layout.
Builder component: Dynamically renders different blocks based on the page data.
Notice how we call sdk.pages()? That’s a fully typed method generated from our query. If the schema changes, TypeScript will scream at us immediately.
Creating the Builder component
The Builder.astro component maps each Statamic block to its corresponding Astro component, ensuring dynamic and type-safe rendering.
---
import type { Sets_Blocks, Maybe } from "@/generated/graphql";
import CTABlock from "./blocks/CTABlock.astro";
import HeroSection from "./blocks/HeroSection.astro";
import FullImage from "./blocks/FullImage.astro";
const BLOCK_MAP = {
Set_Blocks_Cta: CTABlock,
Set_Blocks_HeroSection: HeroSection,
Set_Blocks_FullImage: FullImage,
};
function renderBlock(block: Maybe<Sets_Blocks>) {
if (!block || !block.__typename || !BLOCK_MAP[block.__typename as keyof typeof BLOCK_MAP]) {
console.warn("Missing block type on page:", Astro.params.uri);
return null;
}
const template = BLOCK_MAP[block.__typename as keyof typeof BLOCK_MAP];
return template;
}
const { block } = Astro.props as { block: Maybe<Sets_Blocks> };
const Template = renderBlock(block) ?? Fragment;
---
<Template block={block} />BLOCK_MAP: Associates GraphQL types with Astro components.
renderBlock(): Determines which component to render based on the block type.
Type safety: Makes sure that each block is handled correctly, preventing runtime errors.
This maps each Statamic block type to its Astro component. Think of it as a router for components.
Enforcing type safety
One of the standout features of this setup is the type safety it offers. Let's see this in action with the Hero Section component.
---
import type { HeroSectionBlockFragment } from "@/generated/graphql";
type Props = {
block: HeroSectionBlockFragment;
};
// block prop is type safe!
const { block } = Astro.props;
---
<h1>{block.title}</h1>Conclusion
By combining Astro, Statamic, GraphQL, and TypeScript, we’ve built a type-safe SDK that makes working with headless CMS data a joy instead of a headache. Type safety isn’t just for big enterprise apps; it’s for anyone who wants to ship reliable code faster.

Bring Your Ideas to Life 🚀
If you need help with a Statamic or Astro project, let's get in touch.
Lucky Media is proud to be recognized as the #1 Statamic agency and a leading Astro partner agency.
FAQs
1. Can I use this setup with a different headless CMS other than Statamic?
While this tutorial uses Statamic as the CMS, the principles remain the same for other headless CMSs. As long as the CMS supports GraphQL, you can adapt the steps accordingly. Just ensure the GraphQL queries align with the CMS's schema.
2. What if my GraphQL endpoint changes? How do I handle that?
If the GraphQL endpoint changes, simply update the GRAPHQL_API_URL in the .env file. Since we load this variable dynamically in the configuration, the SDK will automatically point to the new endpoint upon restarting the development server or rebuilding the project.
3. How do I add new GraphQL queries to the SDK?
Create a new .graphql file inside the src/graphql folder (or add to an existing one), define the query, and run npm run codegen to generate the corresponding TypeScript types. The SDK will automatically include the new methods based on the updated queries.
4. Is it necessary to keep the generated folder in version control?
Yes, it's recommended to keep the generated folder in a version control system (like Git). This makes sure that everyone on your team has access to the latest types without needing to run the code generation process themselves. Just remember to regenerate and commit the types whenever your GraphQL schema changes.
5. How can I improve build performance with this setup?
To enhance build performance, consider:
Caching: Implement caching strategies for GraphQL requests and type generation.
Selective Codegen: Only run graphql-codegen for the parts of the schema that have changed.
Parallel Builds: Use parallel processing where possible to speed up tasks.
Optimizing Queries: Keep the GraphQL queries lean to reduce payload sizes and processing times.
Technologies

Stay up-to-date
Be updated with all news, products and tips we share!
On this page
- Why Type Safety Matters in Headless CMS Projects
- Prerequisites
- Getting started with AstroStarter
- Setting up a Statamic website
- Installing necessary packages
- Configuring TypeScript code generation
- Creating environment variables
- Generating TypeScript types
- Building the SDK client
- Writing GraphQL queries
- Implementing the SDK in Astro
- Creating the Builder component
- Enforcing type safety
- Conclusion
- Bring Your Ideas to Life 🚀
- FAQs

