TNS
VOXPOP
Will real-time data processing replace batch processing?
At Confluent's user conference, Kafka co-creator Jay Kreps argued that stream processing would eventually supplant traditional methods of batch processing altogether.
Absolutely: Businesses operate in real-time and are looking to move their IT systems to real-time capabilities.
0%
Eventually: Enterprises will adopt technology slowly, so batch processing will be around for several more years.
0%
No way: Stream processing is a niche, and there will always be cases where batch processing is the only option.
0%
DevOps / Frontend Development / Software Development

Integrating the SWR Library with a Type-Safe API Client

Once API responses in our app are loaded into the cache, we don’t need to wait to refetch them if another page needs them.
Jul 25th, 2023 9:07am by
Featued image for: Integrating the SWR Library with a Type-Safe API Client
Image via Shutterstock.

When taking on a new project earlier this year, we had the chance to build a new frontend product from scratch using the latest tools the frontend ecosystem has to offer. We chose Next.js and used the SWR library hook to manage data loading.

We ran a hackathon just a few weeks after launching that product, which gave me some time to think about what might be applicable to our main dashboard React app.

What’s SWR?

SWR is a great little library for fetching data from an API into your React app. The acronym stands for “stale while revalidate,” which is a caching strategy best described in the SWR docs:

“SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.”

This is pretty magic: If you visit a page, switch to another page, and then switch back, it will render almost instantly using the stale data from the cache.

There’s also lots of API responses in our app that are used across many different pages: for example, what your custom fields are. Once those are loaded into the cache, we don’t need to wait to refetch them if another page needs them.

Life Before SWR

We’d built our own data-loading hook called useFetchData, which worked something like this:


There were some things we really liked about this:

  1. It’s all type-checked: Because the API client is generated from our OpenAPI schema, if I forget or misspell a parameter, TypeScript will tell me right away.
  2. That includes the type of data: because fetcher returns a specific generated type, useFetchData can tell TypeScript “the type of data is whatever the return type of the function I’m passed is,” and the rest happens by magic.
  3. It reminds me to think about loading states and failed API requests — I could do nothing with those loading and error variables, but I’d have to do so explicitly.

…but it had some downsides:

  1. On complex pages, we needed to load all the data in a React context, to avoid fetching the same thing many times.
  2. It was pretty verbose: The fetcher had to be written out every time.
  3. Keeping a page up to date once it’s loaded was fiddly: We had another hook that handled “reload content every so often while the app is open,” and it was pretty complex to debug.

Drinking the SWR Kool-Aid

At first we started replacing uses of useFetchData with the direct equivalent:


This was pretty good, but still quite repetitive.

There also wasn’t a clear pattern for what the cache key should look like. If two people wrote different cache keys for the same data, we’d lose out on the benefits of the shared cache, which isn’t great, but if two people used the same key for different data, we’d have really weird bugs. Imagine writing a component thinking you are working with an array of custom fields but sometimes you’re getting something totally unrelated.

Back to Type Safety

OK, so we want to have a way of using SWR that:

  1. Generates a cache key consistently for the same API
  2. Understands our generated API client to type-check the request, and use the right type on the returned data

There were a couple of options:
1. Code-Generation: Our backend is written in Go, so we’re no strangers to code-gen. With this approach, we would generate a hook for each of our internal APIs, something like:


2. Generics: TypeScript has some pretty powerful generic types magic. This would look something like:


While we do lots of code-gen in Go, we don’t have a pattern for doing custom code-gen in TypeScript yet, so I decided to see if I could get generics to do the job.

Making Generics Work

Our internal API client is generated using the OpenAPI Generator for Typescript, which gives us a few types that are useful to us.

BaseAPI is the hard-coded part of the client. This has methods for building and executing requests, parsing errors, configuring authentication, etc. ClientType combines that with all the methods that are generated from our OpenAPI spec.

That means we can get an enum containing all the possible APIs that can be called like this:


That’s Step 1. We can now enforce that you can only use APIs that actually exist.

Now we need to get the request and response type for that request. That’s a little harder, but we can get there step by step. After a few hours of trial and error, this is what the hook ended up looking like:


Phew, that’s a lot of types! Let’s break it up and see what’s going on:


First, we’re creating a type variable called TApi, which must fit into the AvailableAPIs type. In other words, it must be one of the keys of our ClientType, but not one of the utilities on the BaseAPI class.


This is another type variable, this time holding the type of the function on our API client. So if api is of type TApi, then TApiFunc is the type of apiClient[api]. For example, if api = "customFieldsList", TApiFunc is the type of apiClient.customFieldsList.

In other words, this is the type of our API-calling function.


If you’ve not seen it before, this is a super useful pattern for generic types, but it is a little weird to read, because extends is being used in two slightly different ways in the same line.

First TFetcher extends means “I’m defining a new type variable called TFetcher, and it must fit into whatever comes after this.

Then we’re using a ternary statement. You can read it as “if TApiFunc looks like a function that takes one argument and returns a promise, then return TApiFunc, otherwise return never“. As a magic type in TypeScript, never tells the compiler to produce an error if it’s being forced to use it.

That means that if TApiFunc doesn’t match the expected “one argument and returns a promise” constraint, TypeScript will return a type error.


These are just here for convenience. They’re saying:

  • TRequest must be the first parameter of TApiFunc
  • TResponse must be whatever is inside the promise returned by TApiFunc

So with all our types defined, the actual hook is pretty simple:


We are:

  • grabbing an instance of our API client to perform the request
  • generating a fetcher function that will call the right method on that client
  • handing it off to SWR, using [api, request] as the cache key

The SWRResponse<TResponse, ErrorResponse> annotation tells TypeScript that the data will be of type TResponse and any error will be our ErrorResponse type, which comes from our generated client types.

Putting It All Together

That’s an awful lot of types and a little bit of code, but it works wonders when you’re using it:


With that one line, TypeScript will infer:

  1. customFieldsList is a valid API you can call.
  2. Its request type is undefined, so you’re not allowed to pass in any request parameters.
  3. Its response type is CustomFieldsListResponseBody and assigns that type to data.

We’re now back in the nice type-safe world of useFetchData, only we’ve added the magic of SWR to make fewer requests and render pages more quickly, and made our code simpler and harder to mess up.

What Did We Learn?

Firstly, there are some great tools out there in the React ecosystem. Frontend engineering moves infamously quickly, and there’s great stuff getting built all the time. incident.io has only been around for two years, but in that time SWR has gone from pre-1.0 to stable and widely used.

We also learned that creating space for experimentation can produce great results. Our new status page product was just right for that: It was both small and pre-release, which made trying new things safe, but because it was going to become a core part of our product, it was real enough that we could learn a lot about how SWR works in the wild.

It’s easy to experiment with new libraries and techniques when building something small and low-risk, but getting successful experiments adopted across your whole codebase is where the real value lies. Using something like SWR for a little side project can validate that it’s very cool and useful (It is!), but doesn’t help you find the tricky parts of rolling it out across a large codebase with lots of people working on it.

Group Created with Sketch.
TNS owner Insight Partners is an investor in: Pragma.
THE NEW STACK UPDATE A newsletter digest of the week’s most important stories & analyses.