REST API Construction for TypeScript Zealots

Stuart Schechter
5 min readJun 7, 2018

--

Using TypeScript for both client and server code should make APIs easier to write correctly. TypeScript can ensure that the API’s implementation, and the clients that call the API, agree on the types of the parameters that should be passed and the value that should be returned— but TypeScript can only do enforce the correct types if given correct types. We created rest-contracts to make it easy to specify REST APIs using TypeScript, to bind specification to implementation so that TypeScript can provide and enforce the correct types, and to automatically generate correctly-typed client functions to ensure the API is called with the correct types.

A rest-contract is an object that specifies a single REST API call’s method, path, parameter types, and return type. You can create contracts using function calls (Figure 1.a below), pass them to helpers for implementing the API on the server (Figure 1.b) and pass them to factories that automatically create client functions used to call the API (Figure 1.c).

(1.a) A contract object is created by calling a chain of function calls, each of which is documented to help you along the way.
(1.b) When implementing a contract, TypeScript infers the correct parameter and return types from the contract (the first parameter to the implement method above). TypeScript can then provide your implementation function with parameters of the types specified in the contract and ensure the result is of the type specified in the contract. For example, above we see by highlighting the first parameter of the implementation we can see that it should receive an object with an id field of type ExcuseId.
(1.c) The client function used to call this API, which can be generated automatically, ensures that the caller passes parameters of the correct type and receives a response of the correct type. VS Code and TypeScript remind the developer which parameters to pass.

Let’s step through the how rest-contracts helps you specify a REST API, implement the API, and generate a client to call the API — with illustrations to show how TypeScript and VS Code help you along the way.

Constructor functions help you write contracts

We built functions to create these contracts to provide a developer experience that provided documentation in the IDE, with no need to look up syntax. Each step should be as simple typing a dot, seeing specification options, and choosing from among those options (Figure 2.a-2.e, below).

(2.a) After you type API followed by a dot, IntelliSense shows the five possible REST method types to choose from.
(2.b) After choosing a method, IntelliSense prompts you for the API call’s path.
(2.c) After specifying a path string, you may specify any path and/or query parameters. (If this were a PATCH, POST, or PUT method, you would have the option to specify a body type instead of query parameters.)
(2.d) After specifying all parameters, you finish creating your API by specifying its return type. In this case, we do so without specifying query parameters because this API call takes no query parameters.
(2.e) When you have completed the contract, you can inspect its object type (top right).

In Figure 2.e, above, VS Code shows the type of the Get API object constructed via the function chain. The union type reveals that this API is (1) a GET method, (2) hosted at path “/excuses/:id/”, (3) with the sole path parameter “id” is of an ExcuseId, and which(4) returns an ExcuseDbRecord type (promise-based clients will wrap the return type in a Promise).

We present generic typing because the structure of the underlying object is an implementation detail that developers using rest-contracts should not need to know or understand. We use a union type to present the information a developer needs to know in a logical order, eschewing types with multiple generic parameters which require developers to interpret ordering.

Helper functions help you implement APIs correctly

We provide helpers for implementing APIs with express and lambda. You pass the rest-contract object as the first parameter to the implementation helper (Figure 3.a). TypeScript infers the API’s types from the contract object. It then helps you write the function that implements your API by assigning the correct types to your implementation function’s parameters (Figure 3.b).

(3.a) When you implement the API, you can look at the contract type...
(3.b) …but you may not need to, as your parameter’s types are provided by TypeScript/VS Code, which reads the types from the contract.

Factory functions create functions to call your APIs

We provide two libraries with factories to create the client functions you will need to call the API specified in a contract: a compact browser-only client library, which has no external dependencies beyond the base rest-contracts library, and an axios-based client, which can be run within the node.js environment. These libraries generate client functions with the parameter and return types specified in your contract (Figure 4, below).

(4) Client libraries automatically generate client functions with the types from the contract.

How to get started

The example contract, server implementation, and client are on GitHub and can be downloaded via an NPM package.

When you are ready to create your own APIs using rest-contracts, you will need to download the rest-contracts package, a helper package to implement your API (currently available for express and lambda), and a factory package to generate functions to call your API (currently browser or axios).

npm install --save-dev rest-contracts rest-contracts-[browser|axios]-client rest-contracts-[express-server|lambda]

We hope you’ll take the time to provide feedback, report issues, and send pull requests with improvements to our GitHub repository.

Alternatives and trade-offs

If you are writing server code written in languages other than TypeScript and want to build APIs from a single specification, you may consider an API specification language such as Swagger. If you are looking for richer semantics than REST you may also consider tools like GraphQL. The benefits of richer, more complex systems come with costs: more languages and syntax to learn, a larger tool set to maintain, a longer compilation chain, and an experience that may not be as well optimized for TypeScript and VS Code. One reason to adopt server-side TypeScript is to avoid these costs.

If you have already adopted TypeScript on the server side, there are alternatives to rest-contracts that add a step to your compile chain to derives API specifications from your APIs’ implementation, which you will need to augment with decorators to include information that cannot be derived from the code. Decorator-based approaches keep the specification and implementation in one place and can potentially reduce code size. However, if your team grows to include client developers or product architects who want to be able to specify APIs, or write code to call the API before the implementation is ready, they will have to modify the server code base with stub implementations. If you start by using rest-contracts, you can keep your compile chain simple and bypass the learning curve required to specify APIs using decorators.

--

--

Stuart Schechter

Associate@Harvard SEAS. Founder@DiceKeys. Formerly researching human factors of security at Microsoft Research, MIT, & Harvard. https://www.stuartschechter.org