Using TypeScript with the GraphQL API

This article explains how to make the most out of Prepr GraphQL API features when using TypeScript in your front-end app.

Introduction

TypeScript is a strongly typed programming language that builds on JavaScript. Developers choose to use TypeScript instead of JavaScript in their projects to take advantage of cleaner and more scalable code, better code readability, type safety, and code that's easier to debug and test.

Use case

By using TypeScript in Strict Mode and a GraphQL code generator, a developer will get stronger guarantees of program correctness for types and null checks.

Prepr delivers strict types in the GraphQL API schema so that TypeScript in strict mode can support the following situations in the front end:

  • To process content fields in their expected data types.
  • To make sure that some fields in content items are not null before rendering the content in the front end.

Use strict mode in Prepr to generate strong types

Follow the steps below to use the strict mode feature in Prepr CMS to process an API response in your front end with strong TypeScript types.

Note

The steps below are based on a simple Next 13 project that connects to Prepr using the Apollo client for GraphQL API requests.

Step 1: Mark the fields that are required

Generally, one identifies required fields while modeling content. When it's time to set up the Schema in Prepr, configure these fields with the following steps:

  1. Click the Schema tab to open the Schema Editor and click the applicable model.
  2. Click the field that needs to be set as required, choose the Validation tab and enable the This field is required toggle.

validation

Step 2: Set workflow stages to trigger the required validation

Content editors collaborate with each other by setting different workflow stages on content items such as In progress or Review before publishing them.

To make sure that content editors always fill the required fields at the correct workflow stage, go to the Settings > General screen and choose the Applicable workflow stage for required validation.

settings workflow stage

Once this step is done, it means that all required fields will not have null or undefined values for your API request. The steps below will ensure that the TypeScript in your front end will highlight code that does not process these fields accurately to ensure that they are not null or not undefined.

Step 3: Enable strict mode

To make sure to get accurate TypeScript types from your GraphQL schema, go to the Settings > Access Tokens screen. Choose the GraphQL access token with the same available workflow stages chosen above for the required validation and check the Enable strict mode box.

Enable strict mode

Now that the Prepr environment is ready, follow the steps below to update your front end.

Step 4: Add GraphQL queries to your front-end application

Add GraphQL queries to your front-end application like in the example below.

# ./queries/get-articles.graphql

query Home{
  Articles {
    items {
      _id
      _slug
      title
    }
    total
  }
}

Info

In the next steps, you will install and use a GraphQL code generator to generate the TypeScript types from your queries.

Step 5: Install a GraphQL code generator

Now that your queries are ready, install a GraphQL code generator like codegen with the following command:

npm i @graphql-codegen/cli @graphql-codegen/typed-document-node @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-typed-document-node/core

Once successfully installed, create a configuration file, graphql.config.yml in the root of your project like in the example below.

  • Set the schema value to the API URL from a Prepr access token.

  • Set the documents value to the location of your queries.

  • Set the generates path to the directory where the generated TypeScript file should be placed.

# ./graphql.config.yml

schema:
  - https://graphql.prepr.io/aab2129476732aa4e5e1a2b93fb4e5...
documents: './queries/*.graphql'
generates:
  generated/generated.ts:
    plugins:
      - typescript
      - typescript-operations:
          strictScalars: true
          scalars:
            _Date: string
            _DateTime: string
      - typed-document-node

Step 6: Generate TypeScript types

Generate the TypeScript types by adding the following line in the scripts object of the package.json file.

"generate-ts": "graphql-codegen --config graphql.config.yml",
and run the following command:
npm run generate-ts

The generated.ts file will be generated like in the partial example below.

// # ./generated/generated.ts

import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: { input: string; output: string; }
  String: { input: string; output: string; }
  Boolean: { input: boolean; output: boolean; }
  Int: { input: number; output: number; }
  Float: { input: number; output: number; }
  _Date: { input: any; output: any; }
  _DateTime: { input: any; output: any; }
};

/** This union type holds all content models. */
export type AllModels = Article | Blogpost | CallToAction | Category | Page | Person;

export type ApplePodcast = {
  __typename?: 'ApplePodcast';
  _id: Scalars['String']['output'];
  url: Scalars['String']['output'];
};

/** Single Article. */
export type Article = Model & {
  __typename?: 'Article';
  /** Count of bookmark events. */
  _bookmarks: Scalars['Int']['output'];
  /** The time the content item was changed. */
  _changed_on: Scalars['String']['output'];
  /** Count of clicktrough events. */
  _clicktroughs: Scalars['Int']['output'];
  /** Count of comment events. */
  _comments: Scalars['Int']['output'];
  _context?: Maybe<Context>;
  /** The time the content item was created. */
  _created_on: Scalars['String']['output'];
  /** Id of your Prepr Environment. */
  _environment_id: Scalars['String']['output'];
  /** Unique identifier for each content item. */
  _id: Scalars['String']['output'];
  _last_published_on?: Maybe<Scalars['String']['output']>;
  /** Count of like events. */
  _likes: Scalars['Int']['output'];
  _locale: Scalars['String']['output'];
  _locales: Array<Scalars['String']['output']>;
  /** This field returns all localizations for this content item. */
  _localizations: Array<Article>;
  /** The time for when the content item is or will be published. */
  _publish_on?: Maybe<Scalars['String']['output']>;
  /** Count of purchase events. */
  _purchases: Scalars['Int']['output'];
  /** Calculated time to read in minutes. */
  _read_time?: Maybe<Scalars['Int']['output']>;
  /** Count of share events. */
  _shares: Scalars['Int']['output'];
  /** Unique within Type, string identifier for each content item. */
  _slug?: Maybe<Scalars['String']['output']>;
  /** Count of subscribe events. */
  _subscribes: Scalars['Int']['output'];
  /** Count of view events. */
  _views: Scalars['Int']['output'];
  /** Count of vote events. */
  _votes: Scalars['Int']['output'];
  authors: Array<Person>;
  border_color?: Maybe<Scalars['String']['output']>;
  categories: Array<Category>;
  content?: Maybe<Array<Maybe<_Prepr_Types>>>;
  cooking_posts?: Maybe<TwitterPost>;
  cover: Array<Asset>;
  event_location?: Maybe<Coordinates>;
  excerpt?: Maybe<Scalars['String']['output']>;
  my_html_text?: Maybe<Scalars['String']['output']>;
  my_text_area?: Maybe<Scalars['String']['output']>;
  opening_times?: Maybe<BusinessHours>;
  prepr_shop: Array<PreprExampleShop>;
  search_keywords?: Maybe<Array<Maybe<Tag>>>;
  seo?: Maybe<Seo>;
  test_boolean?: Maybe<Scalars['Boolean']['output']>;
  title: Scalars['String']['output'];
};

/** ArticleCollection component. */
export type ArticleCollection = Component & {
  __typename?: 'ArticleCollection';
  _context?: Maybe<Context>;
  _id: Scalars['String']['output'];
  articles: Array<Article>;
  cta_label?: Maybe<Scalars['String']['output']>;
  cta_url?: Maybe<Scalars['String']['output']>;
  description?: Maybe<Scalars['String']['output']>;
  heading?: Maybe<Scalars['String']['output']>;
};

...

export type HomeQueryVariables = Exact<{ [key: string]: never; }>;


export type HomeQuery = { __typename?: 'Query', Articles?: { __typename?: 'Articles', total: number, items: Array<{ __typename?: 'Article', _id: string, _slug?: string | null, title: string }> } | null };


export const HomeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Home"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"Articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"_slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]} as unknown as DocumentNode<HomeQuery, HomeQueryVariables>;

Note that a HomeDocument is generated based on the query Home defined in the previous step. This document will be used to make a query request in the next step.

Step 7: Make the GraphQL request

Once the types are generated and available you can make the GraphQL request to fetch the content you need to render in the front end.

Fetch the content for your front-end pages like in the example page.tsx below.

// ./app.page.tsx


import { Article, HomeDocument } from "../generated/generated"
import client from '@/services/apollo-client';

async function getData() {

  // Make the query request using the generated HomeDocument
  const {data} = await client.query({
    query: HomeDocument,
  })

  // Return the list of articles from the query results
  return data.Articles?.items;
}


export default async function Home() {

  const articles = await getData();

  return (
    <ul>
      {articles && articles.map((article) => (
        <li key={article._id}>
          {article.title}
        </li>
      ))}
    </ul>
  )
}

The Strict Mode feature is currently a public beta release, so please let us know if you have any feedback when using it.