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.