I made an API toolkit
Tags: speedapi, api, typescript, showdev
Published: 2022-06-04 21:14:50


![Cover image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tfaqsbap4lo0w38dz0ol.png)

In this article I would like to introduce [SpeedAPI](https://github.com/speedapi/info) - a project of my own. It's essentially an API toolkit that supports a _lot_ of features that, admittedly, GraphQL also supports, but does that with an extremely small overhead thanks to a custom binary encoding. This project used to be called AMOGUS before the 1.3 release which came out today (not too proud of that name.) Anyways, let's go!

---

## The Basics

SpeedAPI uses a separate language called SUS for its schema. Take a look:
```sus
globalmethod say_hi(0) {
    name: Str;
    returns {
        greeting: Str;
    }
}
```

This description of the `say_hi` should be quite self-explanatory and almost even feel natural... apart from one thing: what is that zero doing in those parentheses? That's the **value** - a number that distinguishes this method from other methods in this scope. SpeedAPI doesn't use its name to identify it when exchanging data over the network because that'd be wasteful - remember, our main goal here (along with developer satisfaction) is absolute maximum possible encoding efficiency.

Okay, now that we have this schema we need to **compile** it. In this case I'm going to be using TypeScript because that's the only language supported by the platform at the moment, though it is very extensible. To do that, I'm going to first install and then invoke the compiler like this:
```bash
$ pip3 install susc
$ susc -l ts api.sus
```

Oh cool! I now have an `api_output` folder next to my source file. Inside it I see a `ts` folder and `index.ts` inside that folder. This file is my API definition, but translated to TypeScript in a format that the `@speedapi/driver` npm library understands.

To make use of this data, let's first import that library and the compiled definition:
```typescript
import * as speedapi from "@speedapi/driver";
import * as api from "./api_output/ts/index";
```

And point the library to our definition by creating a dummy client and server that exchange data by passing binary data to each other in memory:
```typescript
type ApiType = ReturnType<typeof api.$specSpace>;
const { client, server } = createDummyPair<ApiType>(api.$specSpace);
```

Let's also **bind** our `client` and `server` (plain sessions) to the API:
```typescript
const clientSession = api.$bind(client);
const serverSession = api.$bind(server);
const serverHandler = new Server(server, {});
```

Great! Those two things can communicate now. What's most  important here is that with a static schema not only do we get extremely efficient data representation, but because we're using TypeScript we also get typing guarantees, meaning those two things can only strictly communicate in predefined ways. No unexpected `null`s and type mismatches!

Let's write the server-side handler:
```typescript
serverHandler.onInvocation("say_hi", async (method, _state) => {
    await method.return({
        greeting: `Hello, ${method.params.name}!`
    });
});
```

As an exercise, try changing the value of `greeting` from that template literal to some number or object literal and observe how your IDE puts a red squiggle below this line.

Now let's write the client:
```typescript
// not using 'await' because this is the top level
clientSession.sayHi({ name: "reader" }).then(({ greeting }) =>
    console.log(greeting));
```
(I once again encourage you to change the string literal to something else and note the typing)

If you then run this file (with `node` after first compiling it with `tsc` or by simply using `ts-node`), you should see `Hello, reader!` printed to console. What a fancy way to print a static string, huh?

## Under the hood
A lot of things were done to print that innocent string. First, the client initiated a **transaction** by sending the server a **MethodInvocation** segment. That segment contained the method number and its scope (global), along with its only argument - `name`, a string. This is exactly the bytes that it sent in hex:
```
00 00 00 00 06 72 65 61 64 65 72
|  |  |  \---/ \---------------/
|  |  |    |      string data
|  |  |    |
|  |  |    +- string length
|  |  |
|  |  +- method number and scope (global)
|  |
|  +- prefix byte:
|     t=0: MethodInvocation segment
|     O=0: no optional fields
|
+- transaction ID
```
Note the low overhead - we have 5 bytes describing the request and 6 bytes of actual data. Compare this to equivalent JSON data: `{"method":"say_hi","name":"reader"}`, where the metadata is 5x larger than the payload.

The server read that segment, ran our callback and ended the transaction by responding with a `MethodReturn` segment:
```
00 00 00 0E 48 65 6c 6c 6f 2c 20 72 ...
|  |  \---/ \-------------------------/
|  |    |          string data
|  |    |
|  |    +- string length (14)
|  |
|  +- prefix byte:
|     t=0: MethodReturn segment
|     O=0: no optional fields
|
+- transaction ID
```

The data stream is split into **segments** that can be interlaced, meaning the client could've sent another `MethodInvocation` with another transaction ID without waiting for the return of this one.

SpeedAPI also supports **confirmations** - a special request the server makes to a client while the latter is in the process of invoking a method.
```
-------+    do_thing()    +-------
       | ---------------> |
client |                  | server
       |   confirmation   |
       | <--------------- |
       |                  |
       |     response     |
       | ---------------> |
       |                  |
       |  method return   |
       | <--------------- |
-------+                  +-------
```

The server can provide data in its request, and the client can return some data in its response. For example, if you need CAPTCHAs, you can send an image URL in the request and expect a solution back. All of that conforming to the schema, of course :)

## Server-initiated transactions
For this we need to talk about entities. They're like objects in OOP languages: a piece of data with some actions associated with it.
```sus
entity User(0) {
    id: Int(8);   # 8-byte / 64-bit integer
    name: Str;
    bio: Str;
    # element type, index size (1 byte / 8 bit)
    loved_topics: List(Str, 1);  

    # this method is dynamic, meaning it's attached
    # to a particular instance of an entity, identified
    # by the id field
    method report(0) {
        reason: Str;
        returns { }
    }

    # this method is static, meaning it's not attached
    # to any particular instance of the entity, but it
    # _is_ within the scope of this entity
    staticmethod create(0) {
        name: Str;
        returns {
            id: Int(8);
        }
    }
}
```

The compiler has also sneaked in two methods for us: a dynamic `update` and a static `get`, which update and get the server-stored entity respectively.

SpeedAPI allows the server to unconditionally (outside of a method call context) send entities or entity updates to its clients. Internally this is implemented with a `EntityUpdate` segment and a transaction that is initiated and then immediately closed by the segment.

## New features in 1.3

### Partial List Updates
As the name implies, a party (the server or the client) can update parts of a list belonging to an entity instead of re-sending the full list again. You can either `append`, `prepend`, `insert` or `remove` a certain number of elements to/into/from the list.

Plain list representations are compatible with previous protocol versions as long as PLUs are not used and list lengths are kept to 15/16ths (approx. 93%) of their max capacity.

### Cache
The client can now use the `Cache` class that caches Entity `get` requests, listens to entity updates (automagically merging PLUs with already cached data too) and notifies its subscribers of updates.

### Non-`Int(8)` entity ids
Before 1.3, the only valid type for the `id` field of an entity was `Int(8)`. In 1.3, this field can now be of any type. 

Compatibility with previous protocol versions is not broken as long as `Int(8)` is used for the id field, or any other type that serializes to 8 bytes, e.g. a 6-char string.

## Further reading
This article has only scratched the surface of what's possible with this tool. There's an extremely detailed tutorial [here](https://github.com/speedapi/info/tree/master/speedapi-tutorial#readme).

Thank you for reading this article and cheers!