GraphQL types

When I first joined Frec one of my big questions was why they'd chosen a GraphQL API. At Twitter we'd migrated to GraphQL and while it was a huge improvement over the existing JSON API, I felt that was probably because the existing JSON API was completely undocumented, and the code was scattered over a vast scala repo that took serious archaeological skills to navigate. Is there a need for that at a small startup, building from scratch?

But I've learned to love it. We're using Apollo, which gives us a headstart with so much and has some great docs, tools, and open code. It's self-documenting of course, and the types are exported to TypeScript, which is invaluable. If anything in your app is going to be typed, make it the API. And the great thing about GraphQL is that it's a well-known standard. When we want to integrate with Retool, we just connect to our GraphQL API, it uses schema introspection in developments environments, and it's good to go.

But we did learn some stuff that might've been easier to know first.

Initially, I planned to follow Relay's style, without using Relay. They keep the GraphQL schema for a component with the rest of the component files, and this is obviously an attractive proposition. However, we weren't using Relay, and it just got confused, so we pulled all the GraphQL definitions out into a top-level folder, split by queries and mutations.

I then found that I wanted Fragments. GraphQL lets you define fragments for your queries, which is obviously useful for querying common objects, but also has the side effect of producing another type. That type can be used in your TypeScript to describe only the fields you've queried, rather than all the fields available, which is what you get when using the standard GraphQL type. The fragment types are super-useful, except when sharing code between the server and the client. We have a common library that has numerous functions intended to work on both sides, but the fragments don't really make sense on the server-side. So we end up using the server's GraphQL types for most things anyway.

The hardest part has probably been Custom Scalars. A scalar is a primitive type in GraphQL, and has a very limited set - it doesn't even include Date! We wanted dates, obviously, but also bigints, and localdates (a date without a time, like a date-of-birth), and decimal.js for numbers. If you search the documents, you'll learn that Apollo has excellent support for custom scalars, allowing you to define serializers and deserializers with ease. After some time, you'll learn that this only applies to the server-side, and for the client you'll find this GitHub issue, which is an extension of this original GitHub issue, which was opened in 2016! Eeek.

There are a few solutions to this problem of custom scalars in the client:

You can just deal with it. Serialize your scalar to a string and deserialize for each use. We did this for a while. Again, the shared code (and types) eventually caused friction.

You can define an Apollo Link function for the client, but the difficult part is knowing which fields to deserialize. If you've serialized to a string, for example, then the JSON response over the wire no longer contains the type hint. You don't know which fields to deserialize. So instead you serialize to an object with a type hint, eg { __typename: 'Decimal', value: '1.00001' } and then writing ApolloLink is easy. The downside of this approach is that your responses are less friendly to other systems. In Retool, for example, you can't define a global transformer, so you constantly need to unpack these values. It's also a few more bytes over the wire.

Since the GraphQL code generator (which provides your types and hooks) is aware of the types, you can also use a plugin to make those types available. I wrote a plugin for this last week before finding an existing one doing the exact same thing, which I readily adopted. This produces a list of fields using your scalar, and connects them to deserializing functions, which can be easily plugged in to the Apollo cache. I hope something like this becomes a standard part of the project.

The final gotcha worth mentioning was an oversight on my part. I'd create a table in our db with a primary key that wasn't named "id". There was a reasonable reason for this, but I'm a little out of date with modern database design (never saw a server-side db at Twitter!) and so I wasn't familiar with the practice of generating all ids as uuids. Anyway, it didn't make any difference on the server at all, but on the client it had surprising effects. The Apollo cache automatically normalizes your responses based on objects with ids - so if you don't have a field named "id" you end up with multiple cached versions of the object, but without realizing it. I should've learned more about the cache from the outset. It was easy to fix this with a config, but better to add a new "id" field. The cache is effectively a replacement for the redux layer, and should not be underestimated.

Thanks for reading! I guess you could now share this post on TikTok or something. That'd be cool.
Or if you had any comments, you could find me on Threads.

Published