A Pattern for Type-Safe REST API Requests in TypeScript

Earlier this year, I worked on a project for a large client. Our task was entirely focused on the front end, while the back end was specified and developed by another team. These days, many new projects opt for GraphQL, but we weren’t so lucky; the other team decided to go ahead with a REST API. To alleviate some of the pain of using a REST API, I wanted to bring some niceties like schema checking and auto-complete into this type of environment.

The results of my effort are a JSON Schema-validated, TypeScript type-safe API helper that provides a clean interface. A complete set of code is available in my GitHub repo.

This example is set up to work as a simple messaging system where you can send a message or get a list of messages from a contact. Let’s add an API for getting an individual contact’s details.

1. Define Endpoint

First, we’ll create a new file in api/endpoints called get-contact.ts:

import * as ApiRequest from '../api-request';
import { HttpMethod } from '../http-request';
import * as Types from '../types';
import { ApiEndpointSpecification } from './specification';

export const GetContactEndpointSpecification: ApiEndpointSpecification = {
  url: '/contact/:contactId',
  method: HttpMethod.GET,
  requestParamsSchemaName: 'GetContactRequestParams',
  requestBodySchemaName: 'GetContactRequestBody',
  okResponseSchemaName: 'GetContactOkResponse',
  notOkResponseSchemaName: 'NotOkResponse',
};

export const getContact = ApiRequest.makeApiRequestFn<Types.GetContactRequestParams, Types.GetContactRequestBody, Types.GetContactOkResponse>(GetContactEndpointSpecification);

Next, we’ll add this endpoint specification to our endpoint index api/endpoints/index.ts file:

export * from './get-messages';
export * from './send-message';
export * from './get-contact';

Then, we’ll add this to our API module api/index.ts file to make it easy to import and use later:

export { getMessages, sendMessage, getContact } from './endpoints';

2. Define JSON Schema

Next, we’ll create three new files to define our JSON Schema for this endpoint. We’ll have a file for the “OK” response from the server, a file for the request body, and a file for the request parameters.

Create a new file in api/schema/contact called get-contact-ok-response-schema.json:

{
  "title": "GetContactOkResponse",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string"
    },
    "lastName": {
      "type": "string"
    },
    "email": {
      "type": "string"
    },
    "phoneNumber": {
      "type": "string"
    },
  },
  "required": ["firstName", "lastName"],
  "additionalProperties": false
}

Then, create a new file in api/schema/contact called get-contact-request-body-schema.json:

{
  "title": "GetContactRequestBody",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

In this example, we won’t have any body params as this is a GET request.

Next, create a new file in api/schema/contact called get-contact-request-body-params.json:

{
  "title": "GetContactRequestParams",
  "type": "object",
  "properties": {
    "contactId": {
      "type": "string"
    }
  },
  "required": ["contactId"],
  "additionalProperties": false
}

This contactId parameter is part of the URL.

3. Run JSON-TypeScript Type Generator

$ yarn generate-types

This will generate TypeScript files in api/types, a schema index in api/schema/index.ts, and a type index in api/types/index.ts.

4. Make API Calls

Finally, add the following to sample.ts to call our new API:

const contactResponse = await getContact({
  contactId: 'my-contact'
});

Your editor should auto-complete in the properties for the parameter object. In addition, the contactResponse constant will be populated according to the proper return type. This response object is discriminated on the ok parameter, so you can always know if the request was successful; if it was, you’ll have any data returned in the contactResponse.response object.

At run-time, the request will be JSON Schema validated prior to being made, and the response coming back from the server will also be JSON Schema validated.


For our particular project, this type of API helper worked especially well since the server was under development at the same time as our front end. The server didn’t always return back what we expected, and we could use these helpers to validate them easily.