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
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
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)