Headless CMS’es with auto-generated API’s are popular. They separate content management from your front-end, giving you extra freedom. But this disconnect also means your code editor doesn’t know about your data structure and can’t help you out. How can we fix this?
We’ve worked with different headless CMS’es over the years, with Contentful and DatoCMS being our favourites. These modern CMS-as-a-service typically generate a complete GraphQL API for all your data models automatically. GraphQL is a strongly typed language. So if we can bring these types to our code editor, we’ll be able to get IntelliSense - like autocomplete and instant validation - for data from our CMS. Let’s explore how to achieve this for local GraphQL, JavaScript and TypeScript files.
Introspecting our CMS API
We can ask our CMS what types, queries and mutations it supports using GraphQL’s introspection. With introspection we call the same GraphQL endpoint we use to query data from our CMS. But instead of querying data, we fetch the schema which describes the types of our data models. An introspection query typically looks something like this:
query IntrospectionQuery {
__schema {
types { ...FullType }
queryType { name }
mutationType { name }
subscriptionType { name }
directives {
name, description, locations
args { ...InputValue }
}
}
}
fragment FullType on __Type { ... }
fragment TypeRef on __Type { ... }
fragment InputValue on __InputValue { ... }
It returns a very lengthy response containing the entire schema with all types. The one from this website is about 50K lines of code. It contains types like a BlogPostRecord which in turn links to a FileField for its image and a PersonRecord for its authors field:
{
"__schema": {
"types": [{
"kind": "OBJECT",
"name": "BlogPostRecord",
"description": "Record of type Blog Post",
"fields": [{
"name": "title",
"type": { "kind": "SCALAR", "name": "String" },
...
},{
"name": "image",
"type": { "kind": "OBJECT", "name": "FileField" },
...
},{
"name": "authors",
"type": {
"kind": "NON_NULL"
"ofType": {
"kind": "LIST"
"ofType": {
"kind": "NON_NULL"
"ofType": { "kind": "OBJECT", "name": "PersonRecord" },
...
This schema is what enables us to validate our code and add autocomplete to our editors.
We can use a tool like GraphQL Code Generator to automatically fetch and save the schema, by pointing to our CMS and enabling introspection in a standardised GraphQL Config:
/* ./.graphqlrc.js */
require('dotenv-safe').config()
const { DATO_API_TOKEN } = process.env
module.exports = {
schema: {
'https://graphql.datocms.com': {
headers: { Authorization: DATO_API_TOKEN },
},
},
extensions: {
codegen: {
generates: {
'src/lib/dato.schema.json': {
plugins: ['introspection'],
}
} } } }
Using a schema to validate GraphQL files
In our blog post Componentize data with GraphQL Fragments we described how you can structure your GraphQL queries and fragments in .graphql
files. The good news is that we can use our GraphQL schema from above to validate these GraphQL files.
We can enable validation by configuring the GraphQL loader from our previous blog post to use the schema:
webpackConfig.module.rules.push({
test: /\.graphql?$/,
use: [{
loader: 'webpack-graphql-loader',
options: {
validate: true,
schema: './lib/dato.schema.json'
}
}]
})
Note that other more elaborate GraphQL tools like Apollo may do this for you under the hood. But if you want a lightweight setup without the overhead of libraries, this gives you validation for free.
Using a schema for autocomplete in code editors
The setup above gives us validation during run and build-time. But depending on your editor we can do even better and enable direct validation and autocomplete while we’re writing our GraphQL code. For example the VSCode GraphQL extensionautomatically picks up on our .graphqlrc.yaml
, understands the custom imports, and brings IntelliSense directly to our .graphql
files:
Note: restart VSCode after installing the extension for the IntelliSense to kick in.
Turning a GraphQL schema into TypeScript typings
Finally we can bring our content types into our TypeScript and JavaScript files by converting GraphQL types to TypeScript typings and importing them in our code. To generate the TypeScript typings we extend our GraphQL config file using the @graphql-codegen/typescript
plugin:
codegen: {
generates: {
'src/lib/dato.schema.json': {
plugins: ['introspection'],
},
'src/lib/dato.d.ts': {
plugins: ['typescript'],
}
Naturally you can use the dato.d.ts
typings directly in your TypeScript files. But you can also use them in your JavaScript files to document your code and benefit from IntelliSense. The trick to achieve this is using TypeScript infused JSDoc syntax:
/**
* @param {object} props
* @param {string} props.locale
* @param {import('../lib/dato').PageRecord} props.page
*/
export default function Blog({ locale, page }) {
// ...
Enjoy the enhanced GraphQL authoring experience.