REST API Construction for TypeScript Zealots
--
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).
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).
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).
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).
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.