Contract first, strictly typed endpoints in TypeScript with runtime validation

Willem Veelenturf
Flock. Community
Published in
5 min readMay 8, 2021

This blog is about endpoints and why typing them might be a good idea. This blog discusses the concept behind Zod-endpoints. A pet project built by the author of this blogpost.

Endpoints are often implemented as magic strings. Server-side, these strings are matched against regular expressions, and client-side, they are dynamically concatenated.

Wouldn’t it be great if all endpoints were safe at compile-time and validated at runtime? This can be achieved by typing them and making the endpoints part of your model.

Below you’ll find an example of a simple Express server implementation. Endpoints are defined as strings. The library can match the endpoints against the requests and execute the function in that context.

Server example in express

The native way to interact with endpoints in the browser is the fetch API. The first argument of the fetch function is a string. In both examples, endpoints are defined as strings.

Client example

Make endpoints part of the model

With TypeScript it is actually possible to make your endpoints part of your model and type them strictly. Along with an example of the browser fetch API is shown how to do this step by step.

A naive way to fetch the response of an endpoint could be like in the example below. The problem here is that you can request any endpoint, even when that endpoints does not exist on the server.

simple client

By using a union type it is possible to limit the input of the httpRequest function. Now it is only possible to request endpoints that are in the union of string literals. However, this will only work for simple, not for dynamic endpoints with parameters in the path eg: /todos/1.

union of string client

The issue can be solved by representing the path as a tuple type. Every section of the path is defined as one element of the tuple. This can be either a literal or a basic type (boolean, number, boolean). In the request, the tuple is joined back together with a slash to make the request.

union of tuple client

In reality, however, endpoints are much more complex. They consist of a method, path, query parameters, header parameters, and body. This can be typed as a union of request objects.

union object client

We can take it even one step further and make the response also part of the union type. With some TypeScript magic, this can be converted into a typed function from request to response. This function can be used to type-check the communication between client and server.

client with responses

The above approach will make it possible to validate requests and responses at build time, even though there is no validation of the input and output at run time. We have to bear in mind that when a response comes in at the client the input is not validated. The same applies to requests when using this approach server-side. They are not checked and can cause invalid states.

Type validation at runtime

TypeScript is great as it will guard the application against painful bugs at compile-time. But when you are running your code on node.js or in the browser all of that safety is gone. And that is because the in- and outputs of the application are not validated at run-time.

Zod is a TypeScript-first schema declaration and a validation library. Defining your schema with Zod will give you the best of both worlds. Types can be inferred from the schema and used by the compiler. Runtime input and output can be validated against the Zod schema.

example of zod

The above shows a basic example of the working of Zod. With Zod a schema is defined in TypeScript, from which TypeScript types can be inferred. At the same time, you can validate the input against the same schema. For more detail, please consult the GitHub of Collinhacks.

Defining your endpoints with Zod

Zod-endpoints is an experimental library created by the author of this blog. It combines the idea of typed endpoints with schema validation of the Zod library.

This approach yields reliable requests and responses allowing the focus to shift to defining business logic instead of input validation and error handling.

The library consists of:

  • Zod HTTP schema definition;
  • DSL to easily compose Zod schema;
  • Types to transform the schema into an API or Client;
  • A function to transform the schema into open API documentation.

Getting started

npm install zod-endpoints

Used in Deno

import * as z from "https://unpkg.com/zod-endpoints/lib/deno/mod.ts"

With Zod-endpoints, you define your endpoints with the Zod-Endpoint DSL in TypeScript. In the example below two endpoints are defined. Both endpoints return their own success response and share the same error response.

Example application

Once the schema is defined with Zod-endpoints DSL this declaration can be used as the basis for the server or client implementation and generation of documentation. Zod-endpoint has types that transform your schema into a client or server type.

type Client = z.Client<typeof schema>

These transformation types conserve all type information and produce a collection of functions from request to response. A full working example of a client and server can be found in the Zod-endpoints example project.

The movie below shows how all type information is conserved from the schema to the client implementation. When a type in the schema is changed it will break in the client as well in the server implementation.

Youtube zod-endpoints type safety

Besides the compile safety in the example project is demonstrated how runtime validation can be leverage to guarantee that only valid requests and responses are processed by client and server.

Contracts and documentation

The last feature of the Zod endpoints is creating API documentation. With one function the schema can be transformed open API specification.

const docs = JSON.stringify(z.openApi(schema), null, 4);

This JSON can be used to render Swagger documentation as shown in the example below. Or can be used to generate other types of clients with open API generator for example.

Swagger documentation example

Conclusion

Making endpoints part of your model gives you compile safety. In combination with Zod also runtime validation can be achieved. Zod-endpoints combines these two concepts into an experimental lib which is demonstrated in an example application.

--

--