DEV Community

Cover image for Consume OpenAPI in TypeScript Without Code Generation
TheGuildBot for The Guild

Posted on • Updated on • Originally published at the-guild.dev

Consume OpenAPI in TypeScript Without Code Generation

This article was published on Monday, April 24, 2023 by Aleksandra Sikora @ The Guild Blog

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
Enter fullscreen mode Exit fullscreen mode

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'
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()]
})
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

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

Feature fets got node-fetch ky axios superagent undici swagger-js
Type-safety out of the box βœ… ❌ ❌ ❌ ❌ ❌ ❌ ❌
Custom plugins support βœ… βœ… ❌ ❌ βœ… βœ… ❌ ❌
JavaScript environment support Node.js, Deno, Bun, Cloudfare Workers, AWS Lambda Node.js Node.js, Browser Node.js, Browser, Deno Node.js, Browser Node.js, Browser Node.js Node.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!

Top comments (0)