Heavy, Engine, Ear
HeavyEngineer
Published on

How to use Openapi-typescript module to generate typescript types for Strapi integration

I spend most of my time with strongly typed languages like Go and Dart. I've learned to stop worrying and love the compiler. Honestly, i wish someone had said something earlier in my career. My initial experiences with Java in the late 90's put me right off compiled languages. This error led to wandering the PHP wilderness for ten years.

More recently I've been working on Video Content Management at scale using Kubernetes on GCP and AWS, and trying to find something as useful as Drupal was back in the day. I hear it's still useful but it's still on PHP... and the community is still... the community. So i'm not touching that with anyone's barge pole.

I have however invested a lot of time into learning Strapi - an extensible headless content management system written in Typescript. Ok, so still not compiled to distributable binaries, but at least it's a fully fledged type system. Strapi is pretty awesome. It does a lot of stuff really well, and implements the Dependency Injection,Strategy and Middleware patterns atop an MVC architecture and an effective Plugin API, making adding or altering functionality a breeze. It's a real pleasure to work with. An important extension is the Documentation - this will walk your routes and types and produce valid OpenAPI 3.0 documentation which you can browse and test using the HTML frontend. All good stuff.

The special sauce though is that this is a standardised representation of your entire application. From what i've read you can use it to run fully typed fetch methods and the like. Sounds fantastic.

For now though i wanted to integrate a NextJS 13 app with Strapi and I wanted it as strongly typed as possible. This next section discusses how I've used 'OpenAPI-typescript' to generate the types and then integrate them with NextJS getStaticProps and getStaticPaths to speed development and fully utilise all the great stuff that's availble in an IDE when you have types available.

Here's how I've gone about it. I'm sure it can be improved though.

Generate Strapi OpenAPI documentation

Install the Documentation plugin in Strapi.

npx strapi install documentation

When this is running correctly you will have a json file that fully describes the API. This will be located here: myStrapiApp/src/extensions/documentation/documentation/1.0.0/full_documentation.json

Generate typscript types

Install openapi-typescript from npmjs.com or Github.

npm i -g openapi-typescript

Then run the tool against the OpenAPI documentation you just created:

npx openapi-typescript myStrapiApp/src/extensions/documentation/documentation/1.0.0/full_documentation.json --output src/types/schema.ts```

In this instance i am using the generated types as part of a Nextjs application. So i have set the output flag to dump the schema.ts file into the src/types directory inside the Nextjs app.

Now i can import the types from Strapi and use them in my Nextjs app. In this example i'm using the types from the schema.ts that was generated earlier. Another function has called the Strapi endpoint, and now this component will deal with the objects that were returned from Strapi. In this case the components["schemas"]["ArticleListResponseDataItem"] is a News article and i've built the Nextjs app (where this is running) to specifically implement the Strapi API so i can write less code.

import React from "react";
import ArticleCard from "./ArticleCard";
import { components } from "../types/schema";

type Article = components["schemas"]["ArticleListResponseDataItem"];

interface NewsProps {
  articles?: Article[];
  children?: React.ReactNode;
}

Now i can destructure the data, using the types and i can then use them in the component:

const News: React.FC<NewsProps> = ({ articles = [], children }) => {
  return (
    <div className="container mx-auto px-6 pb-10">
      <div className="mt-8 grid grid-cols-1 gap-16 md:mt-8 md:grid-cols-3">
        {articles.map((article, index) => {
          const id = article.id;
          // there might be a better way to do this, but it was the only way i could stop the
          // compiler complaining about nullable values
          const { Title, updatedAt } = article.attributes ?? {};
          const heroMedia = article.attributes?.heroMedia ?? {};
          const heroMediaAttributes = heroMedia.data?.attributes ?? {};

          const { url, height, width, alternativeText } = heroMediaAttributes;

          if (Title && url) {
            return (
              <ArticleCard
                key={index}
                id={id}
                attributes={{
                  Title,
                  updatedAt,
                  heroMedia: {
                    data: {
                      attributes: {
                        url,
                        height,
                        width,
                        alternativeText,
                      },
                    },
                  },
                }}
              />
            );
          }
        })}
        {children}
      </div>
    </div>
  );
};

The best thing though, is once you make a change to the content types in Strapi, you can trigger this to run again, refresh the schema and then you have the new field available. e.g. if i add a 'Slug' to the Article content type in Strapi, re run the openapi-typescript types generator and then make this change in the code:

const News: React.FC<NewsProps> = ({ articles = [], children }) => {
          // ....snip...
          // notice i've added Slug to the destructure from article.attributes
          const { Title, updatedAt,Slug } = article.attributes ?? {};
          const heroMedia = article.attributes?.heroMedia ?? {};
          const heroMediaAttributes = heroMedia.data?.attributes ?? {};

          const { url, height, width, alternativeText } = heroMediaAttributes;

          if (Title && url) {
            // And Slug is now passed to the ArticleCard as a prop
            // Obviously you will need to alter the ArticleCard to work with the new prop
            return (
              <ArticleCard
                key={index}
                id={id}
                attributes={{
                  Title,
                  Slug,
                  updatedAt,
                  heroMedia: {
          // ....snip...

Then the Slug field i just added in Strapi is now available in the app as a fully typed variable.

When hovering over types in vscode, you now also see the hints from the schema.ts