This post describes how to make API calls in Typescript, and how we can support types and interfaces while calling REST APIs.

If you just want to see the example code, you can view it on Github

banner

Typescript helps developers by providing static analysis and autocomplete features for their Javascript code. When calling REST APIs, the response is normally in a serialized JSON format.

When working with REST API calls in Typescript into two parts:

  1. Assigning a type to function that calls the API
  2. Assigning a type to the API response

As an example, let’s consider a REST API endpoint to get a list of users with their name and age. To do so, we will make a GET request to the /users.json endpoint.

The API response will be in JSON format:

[
  {
    "name": "Lorem",
    "age": 20
  },
  {
    "name": "Ipsum",
    "age": 18
  },
  {
    "name": "Dolor",
    "age": 35
  }
]

Let’s illustrate how we can support Typescript while making a REST API call to get the above list of users.

Creating an Interface for the API Response

Since we want a list of users, let’s create an interface for them:

interface User {
  name: string
  age: string
}

We’re using an interface here (instead of a class) because we want to cast each user object to the User type, and not create a new instance of a User class, as we’ll see in a later section.

For now, we’ll make a simple interface with the name and age fields corresponding to the data.

Assigning a Type to the API Call

In order to make the API call, we will use the browsers fetch API, which returns a Promise type.

In most cases, API calls are wrapped in functions to encapsulate the API call itself.

Since API calls are async, this function will return a Promise that resolves to an array of User types:

function getUsers(): Promise<User[]> {
  // Code for fetching users goes here
}

Assigning a Type to the API Response

Let’s fill in the getUsers function with the code for the API call:

function getUsers(): Promise<User[]> {
  // For now, consider the data is stored on a static `users.json` file
  return fetch('/users.json')
    // the JSON body is taken from the response
    .then(res => res.json())
    .then(res => {
      // The response has an `any` type, so we need to cast
      // it to the `User` type, and return it from the promise
      return res as User[]
    })
}

The important thing to remember is to cast the response type to the type you are expecting, and return it from the Promise callback.

Without these two steps, the Typescript compiler will give you an error, since the getUsers function is supposed to return a Promise<User[]> type.

Usage and Examples

Making a GET Request

Let’s take a look at the complete code for the above example, to fetch the list of users, and assign their names to an element in our HTML document:

interface User {
  name: string
  age: string
}

function getUsers(): Promise<User[]> {

  // We can use the `Headers` constructor to create headers
  // and assign it as the type of the `headers` variable
  const headers: Headers = new Headers()
  // Add a few headers
  headers.set('Content-Type', 'application/json')
  headers.set('Accept', 'application/json')
  // Add a custom header, which we can use to check
  headers.set('X-Custom-Header', 'CustomValue')

  // Create the request object, which will be a RequestInfo type. 
  // Here, we will pass in the URL as well as the options object as parameters.
  const request: RequestInfo = new Request('./users.json', {
    method: 'GET',
    headers: headers
  })

  // For our example, the data is stored on a static `users.json` file
  return fetch(request)
    // the JSON body is taken from the response
    .then(res => res.json())
    .then(res => {
      // The response has an `any` type, so we need to cast
      // it to the `User` type, and return it from the promise
      return res as User[]
    })
}

const result = document.getElementById('result')
if (!result) {
  throw new Error('No element with ID `result`')
}

getUsers()
  .then(users => {
    result.innerHTML = users.map(u => u.name).toString()
  })

When we call the getUsers function, Typescript knows that we’re supposed to get back a list of User types, and so we can make use of type annotations:

as well as autocomplete:

I use VSCode as my editor, but Typescript annotations and autocompletion features are present in most popular text editors.

Adding Request Headers

In the previous section, we saw how to make a simple GET request. Let’s now see how we can add request headers to our API call.

In this case, instead of passing in the URL to the fetch function, we will pass in a Request object, which contains the URL, as well as some additional options, such as the request headers:

function getUsers(): Promise<User[]> {

  // We can use the `Headers` constructor to create headers
  // and assign it as the type of the `headers` variable
  const headers: Headers = new Headers()
  // Add a few headers
  headers.set('Content-Type', 'application/json')
  headers.set('Accept', 'application/json')
  // Add a custom header, which we can use to check
  headers.set('X-Custom-Header', 'CustomValue')

  // Create the request object, which will be a RequestInfo type. 
  // Here, we will pass in the URL as well as the options object as parameters.
  const request: RequestInfo = new Request('./users.json', {
    method: 'GET',
    headers: headers
  })

  // Pass in the request object to the `fetch` API
  return fetch(request)
    .then(res => res.json())
    .then(res => {
      return res as User[]
    })
}

Let’s look at the types we used here:

  1. The Headers interface gives us methods to add and remove headers from the request.
  2. The RequestInfo type is a union type that can be either a string or a Request object. In our case, we are passing in a Request object, with the URL and the options object.

Making a POST Request

Let’s now see how we can make a POST request, and send some data along with it.

For this example, let’s assume that the server has a POST /users endpoint, which accepts a User object in the request body, and creates a new user.

The code for this is similar to the previous example, except that we will pass in the data as the body parameter in the options object:

// Send a POST request to create a new user, which takes a `User` object
// as the parameter and returns a void promise (since we won't be using the result)
function createUser(user: User): Promise<void> {
  // Since this request will send JSON data in the body,
  // we need to set the `Content-Type` header to `application/json`
  const headers: Headers = new Headers()
  headers.set('Content-Type', 'application/json')
  // We also need to set the `Accept` header to `application/json`
  // to tell the server that we expect JSON in response
  headers.set('Accept', 'application/json')

  const request: RequestInfo = new Request('/users', {
    // We need to set the `method` to `POST` and assign the headers
    method: 'POST',
    headers: headers,
    // Convert the user object to JSON and pass it as the body
    body: JSON.stringify(user)
  })

  // Send the request and print the response
  return fetch(request)
    .then(res => {
      console.log("got response:", res)
    })
}

// Call the function and log a confirmation message
createUser({ name: 'New User', age: '30' })
  .then(() => {
    console.log("User created!")
  })

When creating the request, the body parameter is of type BodyInit, which is a union type that can also be a string. Since we are sending JSON data, we need to convert the User object to a JSON string using the JSON.stringify function.

Making PUT, DELETE and PATCH Requests

The code for making PUT, DELETE and PATCH requests is almost the same as the POST request example above. The only difference is that we need to change the method parameter in the options object:

const request: RequestInfo = new Request('/users', {
  method: 'PUT' // or DELETE or PATCH
  headers: headers,
  body: JSON.stringify(user)
})

The rest of the code remains the same.

If you want to see the complete working example, you can find the code on Github