In this article, I will share a simple yet functional approach to generating typescript code from OpenAPI definitions that you can use in your application. By the end of it, you’ll be able to provide an OpenAPI definition URL and get out of it a fully-typed function for each defined action.
This article assumes you’re familiar with OpenAPI and how to create an OpenAPI definition file.
I’ll be using this OpenAPI example definition as a reference during the article, feel free to use it or use your own definitions
Fetching the OpenAPI definitions
First of all, we need to fetch the definitions (of course if you have them locally you can skip this step). There’s really not a lot to it in this step, we just need to fetch the data and save it in a file
const fs = require("fs");
const url =
"https://gist.githubusercontent.com/asainz/1fa1e3df21513263d6a0cb146d7b192b/raw/01968060378e39add59115debd77f1d88249e815/openapi-example.json";
const response = await fetch(url);
const apiDefinition = await response.json();
fs.writeFileSync("./api-definitions.json", JSON.stringify(apiDefinition));
There you go, easy. We have now the definition saved in the file ./api-definitions.json
Generating the TypeScript interfaces
From that definition, we need to create typescript interfaces based on the types described there. We can leverage this popular library to do this work. The simplest way I found to do this in a script is with the following code:
At this point, you’ll have a set of TypeScript interfaces like this that you can import into your code and use as you’d use any TypeScript interface.
const types = execSync(`npx openapi-typescript ./api-definitions.json`);
fs.writeFileSync("./types.ts", types.toString());
Using the generated types
At this point you can use the beautiful library to generate a fetcher method per each action in the definition at runtime: you feed it the generated types and you’ll get in return a fetcher
object that you can use to fetch the resources from the definition. From their documentation and applying it to our example:
import { Fetcher } from "openapi-typescript-fetch";
import { paths } from "./types"; // the file we generated in the previous step
const fetcher = Fetcher.for<paths>()
fetcher.configure({
baseUrl: 'https://yourapi.com',
init: {
headers: {
},
}
})
// create the fetchers
const getProfileById = fetcher.path('/profile/{userId}').method('get').create()
// use them
const { status, data: profile } = await getProfileById({
userId: '123456'
})
This is already quite useful in my opinion because you’ll have always up-to-date type-safe fetchers — but it you have many different services with many different actions each it can become annoying to manually write the code to create all the fetchers. We will take it up a notch and remove that problem by generating the code that creates the fetchers, so you’ll get a file with all the possible actions that you can use ready to be imported into your code!
Generating the fetchers code
This is a very rudimentary but pragmatic way of doing it, I am sure there are more elegant ways of doing it but in my use case I have only a bunch of OpenAPI definitions to process and it’s not an issue at all.
The code is very simple: it just needs to spit out some text content and iterate over the definitions!
const fs = require("fs");
function getActions(defs) {
const paths = Object.keys(defs.paths);
return paths.map((path) => ({
path,
actions: Object.keys(defs.paths[path]).map((method) => ({
method,
name: defs.paths[path][method]["operationId"],
})),
}));
}
function generateFetchers(actions) {
return actions
.map((action) => {
const { path, actions } = action;
return actions.map((action) => {
return `export const ${action.name} = fetcher.path('${path}').method('${action.method}').create()`;
});
})
.flat();
}
const actions = getActions(apiDefinition);
const fetchers = generateFetchers(actions);
const content = [
`
fetcher.configure({
baseUrl: 'https://yourapi.com',
init: {
headers: {
},
}
})`,
fetchers,
];
fs.writeFileSync("./generated-fetchers.ts", content.join("\n"));
Let’s go through it step by step.
All the information we need is in the definitions file we fetched in the very first step of this article. The first thing we need is the actions, and we know that actions are defined for each path so all we’re doing in getActions()
is to go over each path and return ay action defined for it.
We know that we want a fetcher method for each action, so our next step is to iterate over each path and each action again and simply return the code to generate the fetcher according to openapi-typescript-fetch
.
Now we need to stick that together with the code that configures the fetcher
object and feeds the definitions and saves them in a file.
You application can import any exported method from generated-fetchers.ts
and you’ll be able to use it without worrying of what interfaces you need for each operation and its results, it’s already there :)
Integrating it into a Next.js application
You can write a simple script and run it every time you need it and it’ll save you tons of time already. But we can take it a step further and integrate this in your Next.js build process (assuming, of course, you’re using next!).
Next.js uses Webpack behind the scenes (at the time of writing at least) so all we need to do is to extend its configuration with our code.
In your next.config.js
you’ll need a code similar to this.
const config = {
webpack: (config, { buildId, dir: webpackRootDir }) => {
config.plugins.push(
new PrebuildPlugin({
build: async (compiler, compilation, matchedFiles) => {
if (buildId === "development") {
generateFetchers() // this is where we can run our code
}
},
compilationNameFilter: "client", // only run on client build
})
);
return config;
},
};
module.exports = config;
In this code, we’re running our code to generate the fetchers (under the name generateFetchers()
) after each development build.
Recap
Let’s recap what we achieved! Every time you run the development build in your Next.js application, we will fetch the latest OpenAPI definitions that you need to use and generate fetchers methods for each action defined so all you need to do to consume that API is to include one of those methods and rely on TypeScript to ensure you sending and receiving the right data.
Conclusion
This is, of course, a simplified version of the set-up I have currently in the project I am working on: in there I have the flexibility to use a different baseUrl
for each definition, it handles authentication to fetch the definitions, it has tests and a few more things that were added along the way. But, regardless, the code shared in this article is a pretty good starting point and you can simply put this code in a file, set it up with your definitions and you’ll be good to go to start using it.