Consume OpenAPI in TypeScript Without Code Generation

Over the last few years, TypeScript, have been widely used to provide a type-safe development experience and de facto became a standard. However, generating types from an API typically requires a build step using a command-line interface (CLI) tool. Otherwise, you can write these types manually, but that means more work and can easily get outdated.

We, The Guild, are a group of developers mainly working on the GraphQL ecosystem, but one of the biggest pain points we see in our users’ codebases is consuming REST APIs in a GraphQL BFF (backend-for-frontend).

Even when you provide a well-crafted schema for the consumer (frontend) in GraphQL, finding solutions to maintain the same type-safe experience behind the scenes when communicating with underlying REST-based services can be challenging. The individual teams developing these services often have varying levels of expertise in GraphQL and use different languages and frameworks, such as Java, Spring Boot, or .NET.

One common standard embraced by these teams is OpenAPI. Formerly known as Swagger, it become a standard within the REST community for describing APIs. It provides a comprehensive description of an API’s capabilities, allowing consumers to understand its functionality without needing to know the details of its implementation.

πŸ”₯Β Introducing feTS Client πŸ”₯

We’re thrilled to show you what we’ve been building recently β€” a feTS Client that allows you to create an SDK-like client that infers types from an OpenAPI specification document.

feTS Client is not just another code generator that requires an additional build step. Instead, it’s a tool designed to work exclusively during build time, specifically during TypeScript transpilation. This means that it doesn’t add the entire schema to your final code bundle, as it doesn’t perform any runtime operations. As a result, feTS Client offers a lightweight and efficient solution for maintaining type safety in your code without impacting the size of your compiled assets.

In summary, feTS Client means:

😌 Type-safety out of the box β€” no CLI, no build step

πŸš€Β No runtime overhead β€” it’s super lightweight and performant

πŸ’ͺΒ Support across different JavaScript environments, including Node.js, Deno, BUN, Cloudflare Workers, and AWS Lambda (it utilizes the web standard Fetch API)

πŸ”ŽΒ IDE features like Go To Definition to explore the API from TypeScript

Now, let’s see the code!

Quick Start

You can install feTS client with the following command:

pnpm add fets
# or yarn add fets
# or npm i fets

Using feTS Client is as simple as importing the createClient function and providing the URL to your API.

import { createClient, type NormalizeOAS } from 'fets'
import openAPIDoc from './openapi-doc'
 
const client = createClient<NormalizeOAS<typeof openAPIDoc>>({
  endpoint: 'http://my-api.com/api'
})

Note: we have to use an NormalizeOAS type here β€” it resolves all $refs in the OpenAPI document and normalizes the types for the createClient’s generic parameter.

Once you’ve done that, you can call the endpoints as defined in your OpenAPI Schema. The intuitive design allows you to take full advantage of the auto-completion features provided by your IDE, streamlining the development process and ensuring that you’re working with accurate, up-to-date types for your API calls.

const response = await client['/todo/:id'].get({
  params: {
    id: '1'
  }
})
 
if (response.status === 404) {
  console.error('Todo 1 not found')
  return
}
 
const post = await response.json()
console.log('Todo 1', post)

What about Middlewares, Plugins, or Auth?

Similar to other HTTP clients, feTS Client offers a plugin system that allows you to hook into various stages of the HTTP connection. This flexibility enables you to customize and extend the functionality of the feTS Client, tailoring it to your specific needs and optimizing your application’s network interactions. Here’s an example:

import { createClient, Plugin, type NormalizeOAS } from 'fets'
 
function useAuth(): Plugin {
  return {
    async onRequestInit({ requestInit }) {
      const token = await getMyToken()
      requestInit.headers.authorization = `Bearer ${token}`
    }
  }
}
 
const client = createClient<NormalizeOAS<typeof openAPIDoc>>({
  endpoint: 'http://my-api.com/api',
  plugins: [useAuth()]
})

What about the Server?

The motivation behind creating feTS Client stemmed from the desire to have an agnostic solution that could work seamlessly across various platforms and environments. The goal was to decouple client and server, enabling developers to maintain type safety and efficient communication between components without being constrained by specific technologies or frameworks.

That means you can use anything on the server side as long as it gives you an OpenAPI spec.

Usage with an Existing tRPC Router

You can use tRPC with an OpenAPI plugin as the provider and then create a client for it using feTS.

Here’s an example:

// Setting up a tRPC router with OpenAPI support
 
import { initTRPC } from '@trpc/server';
import { OpenApiMeta } from 'trpc-openapi';
 
const t = initTRPC.meta<OpenApiMeta>().create();
export const appRouter = t.router({
  sayHello: t.procedure
    .meta({ /* πŸ‘‰ */ openapi: { method: 'GET', path: '/say-hello' } })
    .input(z.object({ name: z.string() }))
    .output(z.object({ greeting: z.string() }))
    .query(({ input }) => {
      return { greeting: `Hello ${input.name}!` };
    });
});
 
// Starting the HTTP server
 
import { createOpenApiHttpHandler } from 'trpc-openapi';
 
const server = http.createServer(createOpenApiHttpHandler({ router: appRouter })); /* πŸ‘ˆ */
server.listen(3000);
 
// Getting the OpenAPI spec
 
import { generateOpenApiDocument } from 'trpc-openapi';
 
export const openApiDocument = generateOpenApiDocument(appRouter, {
  title: 'tRPC OpenAPI',
  version: '1.0.0',
  baseUrl: 'http://localhost:3000',
});
 
// save openApiDocument to a file, e.g. openapi.ts. Add `as const`:
 
export const oas = {...} as const;
 
// And finally – creating a FEST client
 
import { createClient, NormalizeOAS } from 'fets';
import { oas } from "./openapi";
 
const client = createClient<NormalizeOAS<typeof oas>>({
  endpoint: 'http://my-api.com/api',
});

You can check out the full example in the examples folder.

feTS Server

There’s also a server part of the feTS project that you can use to create the provider part of your API β€” it allows you to create a fully type-safe REST API. You can check out this file to see how it’s being used and how to generate the OpenAPI spec: feTS server example.

Alternatives and Comparison

Featurefetsgotnode-fetchkyaxiossuperagentundiciswagger-js
Type-safety out of the boxβœ…βŒβŒβŒβŒβŒβŒβŒ
Custom plugins supportβœ…βœ…βŒβŒβœ…βœ…βŒβŒ
JavaScript environment supportNode.js, Deno, Bun, Cloudfare Workers, AWS LambdaNode.jsNode.js, BrowserNode.js, Browser, DenoNode.js, BrowserNode.js, BrowserNode.jsNode.js, Browser
HTTP/2 supportβœ…βœ…giβŒβŒβŒβœ…βœ…βŒ
Promise APIβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
Stream APIβœ…βœ…Node.js onlyβŒβŒβœ…βœ…βŒ
Pagination APIβŒβœ…βŒβŒβŒβŒβŒβŒ
Request cancellationβœ…βœ…βœ…βœ…βœ…βœ…βœ…βŒ
RFC compliant cachingβœ…βœ…βŒβŒβŒβŒβŒβŒ
Cookies (out-of-box)βœ…βœ…βŒβŒβŒβŒβŒβŒ
Follows redirectsβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
Retries on failureβœ… (via plugins)βœ…βŒβœ…βŒβœ…βŒβŒ
Progress eventsβŒβœ…βŒβœ…Browser onlyβœ…βŒβŒ
Handles gzip/deflateβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
Advanced timeoutsβŒβœ…βŒβŒβŒβŒβœ…βŒ
Timingsβœ…βœ…βŒβŒβŒβŒβœ…βŒ
Hooksβœ… (via plugins)βœ…βŒβœ…βœ…βŒβŒβŒ

Summary

Key features of feTS Client include type safety out of the box, no runtime overhead, support for different JavaScript environments, and IDE TypeScript support. With a simple setup, you can call endpoints as defined in your OpenAPI schema with full confidence given by the type-safe client. And if you need more features, you can also extend and customize it with the plugin system.

We’re super excited for you to try the feTS Client. As always, let us know your thoughts, so we can make it even better!

Join our newsletter

Want to hear from us when there's something new?
Sign up and stay up to date!

*By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.