Type Safety with Code Generation for Third Party APIs

May 27, 2020

Client-side JavaScript appli­ca­tions often need to query third party APIs for pop­u­lat­ing strings, such as for string trans­la­tions or for dis­play­ing con­tent from a CMS.

If you’re devel­op­ing with Type­Script, these APIs rep­re­sent a sig­nif­i­cant omis­sion in type safety.

This post will explain a tech­nique to intro­duce type safety when using such APIs in your own code, using code gen­er­a­tion and a bit of inge­nu­ity. I will be using Pris­mic as an exam­ple, but as you will see, this tech­nique can also be used with other APIs.


Pris­mic has a nice UI where anyone can edit con­tent, which will be queried and dis­played during run­time with­out need­ing to rebuild the app. Here, and else­where in this post, I will be focus­ing on a single type called my_page, con­tain­ing three fields: header, body, and footer.

my page prismic

Let’s start with a simple exam­ple (using npx create-react-app --typescript) to show how Pris­mic’s JavaScript API is inte­grat­ed, and what poten­tial prob­lem this par­tic­u­lar approach might have:

import React, { useState, useEffect } from "react";
import Prismic from "prismic-javascript";

const client = Prismic.client("https://my-project.cdn.prismic.io/api/v2");

interface MyPageStrings {
  header: string;
  body: string;
  footer: string;
}

function App() {
  const [strings, setStrings] = useState<MyPageStrings>({
    header: "",
    body: "",
    footer: "",
  });
  useEffect(() => {
    const fetchData = async (): Promise<void> => {
      const resp = await client.getSingle("my_page", {});
      if (resp) {
        setStrings(resp.data);
      }
    };
    fetchData();
  }, []);
  return (
    <div>
      <div>{strings.header}</div>
      <div>{strings.body}</div>
      <div>{strings.footer}</div>
    </div>
  );
}

export default App;

Looks good enough, but there’s already a bit of boil­er­plate code creep­ing in. For larger com­po­nents, this can add up, caus­ing visual clut­ter.

What if we could have these inter­faces gen­er­at­ed for us auto­mat­i­cal­ly, instead of writ­ing them by hand, and in this case, colo­cat­ing it with the com­po­nent?

The gist of the pro­posed improve­ment looks like this:

  1. Query the API as you would oth­er­wise
  2. See what the API shape looks like
  3. Gen­er­ate inter­faces based off Step 2
  4. ???
  5. Get VC fund­ing

Pre­sent­ed in full below is a script that does this for Pris­mic (generate-prismic-types.js):

const fetch = require("node-fetch");
const util = require("util");
const fs = require("fs");

// Prismic APIs
const topLevelQuery = "https://my-project.cdn.prismic.io/api/v2";
const typeQuery = (ref, type) =>
  `https://my-project.cdn.prismic.io/api/v2/documents/search?ref=${ref}&q=[[at(document.type, "${type}")]]`;
const fetchType = (ref, type) =>
  fetch(typeQuery(ref, type)).then((e) => e.json());

// Utils
const toCamel = (s) =>
  s.replace(/([-_][a-z])/gi, ($1) => $1.toUpperCase()).replace(/_/g, "");
const toPascal = (s) => s.charAt(0).toUpperCase() + toCamel(s.slice(1));

// Code for generating the interfaces
const generateInterface = (name, data) => {
  const returnType = toPascal(name) + "Strings";
  if (data && typeof data === "object") {
    return `
    export interface ${returnType} {
      ${Object.keys(data)
        .map((key) => `${key}: string`)
        .join("\n      ")}
    }

    const empty${returnType}: ${returnType} = {
      ${Object.keys(data)
        .map((key) => `${key}: \"\"`)
        .join(",\n      ")}
    }

    export const use${returnType} = (): ${returnType} => usePrismicDocByType<${returnType}>("${name}", empty${returnType})`.replace(
      /^ {4}/gm,
      ""
    );
  }
  return "";
};

const main = async () => {
  const { refs, types } = await (await fetch(topLevelQuery)).json();
  const ref = refs.filter((e) => e.id === "master")[0].ref;
  const typeNames = Object.keys(types);

  const results = await Promise.all(
    typeNames.map(async (type) => {
      const typeInfo = await fetchType(ref, type);
      if ("results" in typeInfo && typeInfo.results.length && "data" in typeInfo.results[0]) {
        return typeInfo.results[0].data || null;
      }
      return null;
    })
  );

  const header = `/*
 * This file is generated by generate-prismic-types.js. Do not edit by hand.
 *
 * This file contains types generated by querying the Prismic API.
 */

import { useEffect, useState } from "react"
import Prismic from "prismic-javascript"

const prismic = Prismic.client("https://my-project.cdn.prismic.io/api/v2")

const usePrismicDocByType = <T>(documentType: string, defaultValue: T): T => {
  const [strings, setStrings] = useState<T>(defaultValue)
  useEffect((): void => {
    const fetchData = async () => {
      const resp = await prismic.getSingle(documentType, {})
      if (resp) setStrings(resp.data)
    }
    fetchData()
  }, [documentType])
  return strings
}
`;

  // Generate TS types
  const typings = Object.values(results)
    .map((typing, i) => generateInterface(typeNames[i], typing))
    .filter((e) => e)
    .join("\n\n");
  fs.writeFileSync("PrismicApi.ts", header + typings);
};

main();

Run­ning this with node generate-prismic-types.js will gen­er­ate the fol­low­ing file (PrismicApi.ts):

/*
 * This file is generated by generate-prismic-types.js. Do not edit by hand.
 *
 * This file contains types generated by querying the Prismic API.
 */

import { useEffect, useState } from "react"
import Prismic from "prismic-javascript"

export const prismic = Prismic.client("https://my-project.cdn.prismic.io/api/v2")

export const usePrismicDocByType = <T>(documentType: string, defaultValue: T): T => {
  const [strings, setStrings] = useState<T>(defaultValue)
  useEffect((): void => {
    const fetchData = async () => {
      const resp = await prismic.getSingle(documentType, {})
      if (resp) { setStrings(resp.data) }
    }
    fetchData()
  }, [documentType])
  return strings
}

export interface MyPageStrings {
  header: string
  body: string
  footer: string
}

const emptyMyPageStrings: MyPageStrings = {
  header: "",
  body: "",
  footer: ""
}

export const useMyPageStrings = (): MyPageStrings => usePrismicDocByType<MyPageStrings>("my_page", emptyMyPageStrings)

The gen­er­at­ed file can be regen­er­at­ed as part of your dev build and checked in, or you can let Github Actions do it for you.

We can then get rid of all the boil­er­plate code and use the custom hook like this:

import React from "react";
import { useMyPageStrings } from "./PrismicApi";

function App() {
  const strings = useMyPageStrings();
  return (
    <div>
      <div>{strings.header}</div>
      <div>{strings.body}</div>
      <div>{strings.footer}</div>
    </div>
  );
}

export default App;

With all the benefits of type safety, as you'd expect.
With all the benefits of type safety, as you'd expect.


Now, we’ll take a closer look at the gen­er­a­tion code, so you can see how it works and check if any ana­logues exist in your API of choice.

We first query the top level Pris­mic API (topLevelQuery), to get the ID of the ref, and a list of all the types that we have in Pris­mic.

// console.log of the refs variable in the generation script
[ { id: 'master',
    ref: 'abcd1234',
    label: 'Master',
    isMasterRef: true } ]
// console.log of the types variable in the generation script
{ my_page: 'My Page',
  my_other_page: 'My Other Page' }

Then, for each type, we query Pris­mic (typeQuery) to see what fields it has:

// console.log of the typeInfo variable in the generation script
{ page: 1,
  results_per_page: 20,
  results_size: 1,
  total_results_size: 1,
  total_pages: 1,
  next_page: null,
  prev_page: null,
  results:
   [ { id: '12345',
       uid: null,
       type: 'my_page',
       href:
        'https://my-project.cdn.prismic.io/api/v2/documents/search?ref=abcd1234q=...',
       tags: [],
       first_publication_date: '2020-05-23T08:43:34+0000',
       last_publication_date: '2020-05-23T08:43:34+0000',
       slugs: [ 'header' ],
       linked_documents: [],
       lang: 'en-us',
       alternate_languages: [],
       data:
        { header: 'Header', 
          body: 'This is a body.', 
          footer: 'Footer' } } ],
  version: '8991d00',
  license: 'All Rights Reserved' }

The data we want is in .results[0].data. We only care for the keys of the data object, since the values are free to change at run­time. Once we’ve gotten the fields for each type (header, body, and footer in the case of the my_page type), we can gen­er­ate the inter­faces, includ­ing an object rep­re­sent­ing the default/​empty value of the inter­face.


Remov­ing fields from Pris­mic works as you’d expect. Deployed apps will simply fall back to empty strings for those fields during run­time, while any other code­bas­es in devel­op­ment will regen­er­ate the updat­ed types and fix the code accord­ing­ly.

Cred­its to Philippe for enlight­en­ing me on this tech­nique!