REST Easy

Photo by Toa Heftiba on Unsplash

REST Easy

Generic client-side REST API with imaginary typeness for JS/TS.

·

3 min read

Some time ago, I got so frustrated with an "API library" I had intended to use for a spare-time project that I just said "screw this" and spent 2 extra weeks writing my own. It was an abstraction layer for a REST API + a WebSockets API. Here, I'll focus on the REST part.

Because as it turns out, I got frustrated writing dozens of functions of boilerplate that all really just do the same few actions over and over again. So I wrote a piece of code that literally just pretends it knows what you're talking about.

Introducing @kiruse/restful, a micro-library with <300 lines of code that can adapt to any RESTful API. And here's how you use it:

import restful, { RestMethods } from '@kiruse/restful';

interface User {
  uid: string;
  name: string;
  age: number;
}

type MyApi = {
  v1: {
    users: RestMethods<{
      get(): User[];
      post(body: Omit<User, 'uid'>): User;
    }> & {
      [uid: string]: RestMethods<{
        get(): User;
        delete(): boolean;
      }>
    };
  };
};

const api = restful.default<MyApi>({
  baseUrl: 'http://localhost:8008/',
});

await api.v1.users('GET'); // User[]
const user = await api.v1.users('POST', {
  name: 'John Doe',
  age: 51,
}); // User (John Doe)
await api.v1.users[user.uid]('GET'); // User (John Doe)
await api.v1.users[user.uid]('DELETE'); // true

As you may have guessed from the above usage, the library has no idea what your API actually looks like. It simply uses the property path to build the endpoint. Instead of the usual .get, .post, .delete etc. methods, every property along the way is a function. This frees up the respective properties for use in your path.

The library achieves its generic isomorphism through JavaScript's Proxy objects. It wraps a single function definition in a Proxy with a get trap which simply creates another instance of this function with the new path wrapped in yet another Proxy. On top of these proxies, it simply layers your API definition and pretends everything is alright.

restful is a library that literally just pretends. Consuming developers may not even be aware that the api object they're using is actually so generic they could use it to GET /foo/bar/baz by calling api.foo.bar.baz('GET') even though that's not a real endpoint. But that doesn't matter. What matters is that the API type definition you provided helps them navigate your API and use it correctly.

This of course also means that restful does no validation. It has no idea what your API actually looks like, so it can't make sure that what your users feed it, or what it gets back from your servers, is actually what you're expecting. If your API definition becomes outdated, you will get weird unexpected behavior. But this is generally the case with any sort of I/O in TypeScript. It does, however, provide some remedies in the shape of marshalling and so-called morphing - but I won't go over that here.

A huge advantage is that restful makes it trivial to simply generate the API from a Swagger aka OpenAPI definition. You no longer need to write down and wrap every single endpoint in a function - that has already happened. Even if you use OpenAPI to generate code, you now only need half the code - the part that literally does not produce any additional JavaScript in a transpiled final build.

The biggest disadvantage is probably that it creates a LOT of functions & objects, heheh.