TypeScript Safe API Requests

November 13, 2019 0 Comments

TypeScript Safe API Requests

 

 

How to add TypeScript types to API requests made on the client, allowing type safety to spread to the entire web app.

Dom DiCicco
TypeScript logo altered to look like a lock, locking down axios.
TypeScript logo altered to look like a lock, locking down axios.

There are many quick wins you get by introducing TypeScript to your project. As they start to stack up, the confidence level alone that your engineering team gains is worth it.

One of the quickest wins you can get in increasing type safety of your app is by making your API calls type-safe. By doing this you’ll be setting up your code that orchestrates those calls to be type-safe, and soon enough type safety seeps through the rest of your codebase.

The example laid out here is specific to axios, but could be extrapolated to any other http clients that have TypeScript bindings.

While axios is currently written in plain JS, the TS definitions are stored directly in the repo. The published axios npm package includes those definitions, thus you’ll get TS support wherever you use axios in a .ts file.

Now for defining your first type-safe API function. You’ll set up the axios client.

const apiClient = axios.create({ baseURL: 'https://yoursite.com/api', responseType: 'json', headers: { 'Content-Type': 'application/json' }

});

Then create a method for a single API call that uses apiClient.

const createUser = async (newUser: NewUser) => { try { const response = await apiClient.post<User>('/users', newUser); const user = response.data; return user; } catch (err) { if (err && err.response) { const axiosError = err as AxiosError<ServerError> return axiosError.response.data; } throw err; }

};

Let’s dissect what’s going on in the createUser function. First off, we’ve made the decision to not directly expose the axiosClient to any module in our codebase. All usages of it will be encapsulated in functions written for each remote call that our app will make. This allows us to only concern ourselves with Axios specific types in this one location, whereas the rest of the app will use the domain-specific types that are ultimately being returned from these functions. It also has the added benefit of defining all remote calls in one location.

const createUser = async (newUser: NewUser) => {

Our function accepts a newUser parameter that has a type of NewUser. That provides type safety to the parameters the body of our API call will take. It would most likely define an object with a few fields on it (e.g. type NewUser = { firstName: string, lastName: string } ).

const response = await apiClient.post<User>('/users', newUser);

Here’s our first interaction with Axios types. Axios makes it’s post method reusable and type-friendly by utilizing generics. The generic parameter we’re supplying as <User> is what Axios will use to type the successful response body.

const user = response.data;
return user;

Since we used generics to provide the response body type to Axios, the next line is statically guaranteed to recognize const user as type User. That’s great because now any other code that calls the createUser function will know that the return type must be a User .

} catch (err) { if (err && err.response) { ... } throw err;

}

Ok, so what about the catch block? The happy path where the response succeeds was captured in the try block. When the response fails, we want to make sure we have that path properly typed.

Axios treats any non-200 level response as an exception, which is why we’re using try/catch. The data that it throws is of type AxiosError<TResponseBody>. Some basic validation is done by checking that it is indeed an AxiosError and not some other JS error being thrown by verifying err.response exists. In the case that it is not an AxiosError, then the error is thrown again and will need to be caught by some other application code.

const axiosError = err as AxiosError<ServerError>
return axiosError.response.data;

As for what happens when it’s determined we are working with a non-200 level response, we can and should handle that under normal application flow. To do that, we cast the err from the catch block over to the provided AxiosError type. It also accepts a generic parameter for the response body, same asAxiosResponse. ServerError is a custom type that you would define based on what you know your own API returns (e.g. type ServerError = { code: string, description: string } ). At this point, we can now return the response body just like we did in the 200 level happy path.

The createUser function now has an implicit return value of Promise<User | ServerError>.


Tag cloud