Jordan Nelson

A Pattern for Type-Safe REST API Requests in TypeScript

Working with REST APIs can sometimes be challenging, especially when you want to ensure type safety and schema validation in your TypeScript projects. While GraphQL offers built-in type safety, REST APIs require a bit more effort to achieve the same level of confidence in your code. In this guide, I'll show you how to create a type-safe REST API helper using JSON Schema and TypeScript.

The result is a JSON Schema-validated, TypeScript type-safe API helper that provides a clean interface for making API requests. You can find the complete code in my GitHub repository.

We'll use a simple messaging system as an example, where you can send a message or get a list of messages from a contact. Let's add an API for retrieving an individual contact's details.

1. Define Endpoint

Start by creating a new file in api/endpoints named 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(GetContactEndpointSpecification);
  

This code defines the endpoint for getting contact details. The endpoint specification includes the URL, HTTP method, and schema names for request parameters, request body, and responses.

Next, add this endpoint specification to your endpoint index in api/endpoints/index.ts:

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

Then, include it in your API module api/index.ts to simplify imports:

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

2. Define JSON Schema

Now, create three new files to define the JSON Schema for this endpoint. These files will specify the structure of the request and response data.

Create a file in api/schema/contact named 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
}
  

This schema defines the expected structure of a successful response, including required fields like firstName and lastName.

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

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

Since this is a GET request, there are no body parameters.

Finally, create a file in api/schema/contact named get-contact-request-body-params.json:

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

The contactId parameter is part of the URL and is required for the request.

3. Run JSON-TypeScript Type Generator

Run the following command to generate TypeScript types from your JSON Schemas:

$ yarn generate-types
  

This command 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

To call the new API, add the following code to sample.ts:

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

Your editor should provide auto-completion for the parameter object properties. The contactResponse constant will be typed according to the expected return type. The response object is discriminated by the ok parameter, allowing you to easily check if the request was successful and access the returned data in contactResponse.response.

At runtime, both the request and response are validated against their respective JSON Schemas, ensuring that the data conforms to the expected structure.


This type of API helper is particularly useful when the server and front end are developed simultaneously. It helps catch discrepancies between expected and actual server responses early in the development process.

Updated and derived from the original version of the article authored by me while working at Atomic Object.