Writing a web app in Typescript provides type safety in both the client and the server but, what about the communication between the two sides? The payload of network requests remains untyped by default, boycotting our attempts to keep a consistent code base. With that idea in mind I wrote @typed-web-api, a minimalist approach to adding type safety to fetch requests. Here is how to use it.

Telecommunications antenna

typed-web-api comes in three parts. First we must create a type definition for the web API via @typed-web-api/common. The type definition is a description of all the endpoints, including their path, method and response type. Optionally we can specify their request payload type too.

This is what the type declaration would look like for a sample web API with three endpoints: POST /users/login, GET /users, GET /users/:userId.

  • Each endpoint must be listed using the /path_method format. This opinionated format is a simple convention that allows the library for validating the payload request type depending on the method.
  • Splitting the type declarations into subtypes is not necessary, but doing so will make it easier to implement the server endpoints in different controllers (a common practice on the server side).

Next we can start adding type safety to the client side network requests using @typed-web-api/client. For that we will need to obtain a typed instance of the browser-native fetch function, by passing the previously created type definition to the getTypedFetch method. Using the typed fetch function the return type of the network requests will be inferred by Typescript automatically. Using @typed-web-api/client requires a few minor changes:

  • We must obtain the typed fetch function by calling getTypedFetch, passing the API type declaration as a type parameter. The returned function can be used everywhere in the client app.
  • All calls to typed fetch must provide the method along with the path, following the /path_method format used in the API type definition.
  • Additional parameters of fetch calls must be passed to the typed fetch function via the init parameter.
  • When using query string parameters (i.e. /users?limit=25) or URL parameters (i.e. /users/xyz) the values must be passed via the queryString or urlParams properties:
  • When using typed request payloads, the typed fetch function will expect the request payload to be provided via the corresponding properties.
    Typescript validation of the request payload in VSCode

    Typescript validation of the request payload in VSCode

Finally, the server side. Here we need to make sure that the web API implementation satisfies the type definition we have generated earlier. This will vary depending on the framework the web API is built with. @typed-web-api supports two popular frameworks: express and NestJS.

Consider the following implementation of the sample endpoints described above (I'm using NestJS here but there are examples for both platforms in the official repository). In this code the return type of each endpoint is determined by the type the returned payload happens to have.

Using @typed-web-api/nestjs we can guarantee the return types are consistent with the web API type definition just by applying a couple tweaks:

  • Make the controller implement the type definition. By using the generic ServerEndpoints interface we enforce the controller to implement the endpoints passed in the type parameter, with the appropriate return type. Note that ServerEndpoints requires the methods' name to contain the full pathname; controller prefixes must be removed (e.g. '/users').
  • Replace the NestJS method decorators with the almighty HttpMethod decorator. This decorator is a simple wrapper that calls the corresponding NestJS method decorators based on the method included in function name, passing the pathname as parameter.

And that's about it. This library is my second attempt at brining type safety to network requests and it benefits from my previous learnings (i.e. express-web-api). It is meant to be generic, framework agnostic on the client side, and impose as little changes as possible in both sides of the code. I'm pretty confident with the result but, hey, one always thinks to have done an outstanding job in their limited understanding of the world. Let me know what you think about it if you have a go.

Posts timeline