A single source of truth for API request/response shapes, including JSON schema, TypeScript types, and runtime validators?

Viewed 1303

I'm writing a typical Node.js REST service with endpoints that receive JSON input (as POST bodies) and return JSON responses.

I want these three things:

  1. JSON schemas defining the shapes of request bodies and response bodies for my endpoints. (Or Swagger files, or anything suitable for defining the contract for clients.)
  2. TypeScript types/interfaces reflecting the JSON schemas perfectly.
  3. Runtime validation functions that I can call from my handlers to ensure an object is in the correct shape. (And once an input object has been validated, my TypeScript code should know its interface.)

But I want a single source of truth, to avoid these three components going out of sync. So I only want to hand-maintain one of them (either the JSON Schemas or the TypeScript typings). The other two components should be generated from the hand-written one, and they should not be committed to version control.

I feel like this must be a fairly common set of requirements nowadays, but I can't find much online about it. Perhaps I'm using the wrong search terms to research it. Is there any framework or conventional pattern that addresses this goal, i.e. the goal of defining a JSON API with strict validation of input, standardised schema documentation, and all the convenience of fully typed objects in TypeScript?

3 Answers

I am assuming you are using TypeScript in the frontend and backend.

Custom Type Reuse

Let's say we have a project repository containing three main folders: client, server and shared.

In shared you have a Types.ts file which contains all your custom type defintions. For example:

export interface Person {
  name: string;
  age: number;
}

Now you can import these types from any sub-folder like .\client\src\components\SomeComponent.ts or server .\server\src\api\SomeRoute.ts with:

import { Person } from `..\..\..\..\shared\Types.ts`;

So in this way you have one place for type definitions that can be used project wide in server and client.

JSON Model and Documentation Generation

You could use ts-doc comments for documenting your api and ts-docs-gen to generate a markdown document.

An alternative would be typedoc. TypeDoc converts comments in TypeScript source code into rendered HTML documentation or a JSON model. You can configure it in a way that only the relevant api files are taken into account.

Runtime Validation

Regarding runtime validation of input and output you can either write your own validation function for every type which is is explained by stereobooster in this article: pragmatic-types-io-validation-or-how-to-handle-json-based-apis-in-statically-typed-language. Or you use a library like io-ts.

There Probably isn't one fits it all tool, because it heavily depends on the exact requirements what you want to accomplish. So composing your toolset of different tools should guide you into the right direction. Feel free to post an answer yourself, if you have found a setup that works well for you.

I like the idea of a website where JSON Schema Definitions (JSD) live for your project. This can be internal (intranet) or external (Internet) and the schemas are there to reference by URI. (see https://www.schemastore.org/json/ for examples of what this can looks like, but at your own website URL). You can also reasearch "schema stores" (like a database for your schemas, but that too hides it a bit and ties you to a pattern. This pattern makes sense as you can validate JSON anywhere (UI, middle-tier, backend, just before repository insert, etc). I see the issue with protobuf, Typecript models, contracts in NuGet packages, etc. is you force codebase adhesions, and get "too married to a specific stack".
Not agnostic enough. This problem of client/server contracts has been going on and re-hashed for ages (SOAP/WSDL, Corba/IDL, etc) and each stack, even Protobuf has you married to their stack.
So one could argue the same about the data definitions being in JSD's but I contend, like Javascript, JSON is the most common transport mechanism for business data (overtaking XML some time ago). So I like the idea of a pattern for projects that have all of the JSON data defined through JSD schemas. The JSD schema IS the contract. You can tie JSD's to Kafka topics (as opposed to their Avro format), you can tie JSD's to Mongo, or you can just use them in any JSON validator library your language uses (like Newtonsoft in the .NET world) The JSD is the holy grail of language-neutral contracts. I will concede here that binary data with gRPC and Protobuf has speed advantages, and should be considered. I will also state that I see 99 percent of data is JSON body business data and can be done using this pattern. Recap: Build your JSD, put it in a place (website location) that can be reference by your code for validation at any layer of your software system.

I think it can be good idea to take a look at Google RPC protocol.

https://grpc.io/docs/languages/node/basics/

You describe all protocol interactions (aka object structures, responses, requests, etc) in single .proto file, and provided utilities can generate both client and server codes for nodejs, client side javascript, other programming languages.

Related