The fetch API is a native JavaScript function that we can use to interact with web services. How can we use fetch with async and await ? and how can we use this with TypeScript to get a strongly-typed response? Let’s find out …

Making a simple request

fetch supports async and await out of the box:

const response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ) ;

So, we simply put the await keyword before the call to the fetch function.

We’re using the fantastic JSONPlaceholder fake REST API in the example consuming code.

To get the response body, we call the responses json method:

const body = await response . json ( ) ;

Notice that we use the await keyword before the method call because it is asynchronous.

Creating a utility function

Let’s create a function that we can call that combines these two lines of code and returns the response body:

export async function http ( request : RequestInfo ) : Promise < any > { const response = await fetch ( request ) ; const body = await response . json ( ) ; return body ; } const data = await http ( "https://jsonplaceholder.typicode.com/todos" ) ;

So, we can use our new function to make a request and get the response body in a single line of code. Neat!

Typed response data

Notice that we typed the response body to any in the above example. Let’s make this a little more strongly-typed:

export async function http < T > ( request : RequestInfo ) : Promise < T > { const response = await fetch ( request ) ; const body = await response . json ( ) ; return body ; } interface Todo { userId : number ; id : number ; title : string ; completed : boolean ; } const data = await http < Todo [ ] > ( "https://jsonplaceholder.typicode.com/todos" ) ;

So, our http function now takes in a generic parameter for the type of the response body. In the consuming code, our data variable is strongly typed to Todo[] .

Full response

We are only getting the response body returned at the moment. We may need other information from the response such as the headers. Let’s refine our function again:

interface HttpResponse < T > extends Response { parsedBody ? : T ; } export async function http < T > ( request : RequestInfo ) : Promise < HttpResponse < T >> { const response : HttpResponse < T > = await fetch ( request ) ; response . parsedBody = await response . json ( ) ; return response ; } const response = await http < Todo [ ] > ( "https://jsonplaceholder.typicode.com/todos" ) ;

So, we have extended the standard Response type to include the parsed response body. We set this parsedBody property on the response before returning the whole response. We now get the full response in consuming code.

Raising errors for HTTP error codes

Let’s now enhance the http function to handle HTTP error codes. We can use the ok property in the response object to raise an error if the request is unsuccessful:

export async function http < T > ( request : RequestInfo ) : Promise < HttpResponse < T >> { const response : HttpResponse < T > = await fetch ( request ) ; try { response . parsedBody = await response . json ( ) ; } catch ( ex ) { } if ( ! response . ok ) { throw new Error ( response . statusText ) ; } return response ; } let response : HttpResponse < Todo [ ] > ; try { response = await http < Todo [ ] > ( "https://jsonplaceholder.typicode.com/todosX" ) ; console . log ( "response" , response ) ; } catch ( response ) { console . log ( "Error" , response ) ; }

We can use try ... catch in the consuming code to catch any errors.

HTTP specific functions

We can use HTTP methods other than GET by calling our http function as follows:

const response = await http < { id : number ; } > ( new Request ( "https://jsonplaceholder.typicode.com/posts" , { method : "post" , body : JSON . stringify ( { title : "my post" , body : "some content" } ) } ) ) ;

We’ve passed an inline type, {id: number} for the type of the response body we expect - i.e. we expect the id of the new post to be returned to us.

Notice also that we had to turn the post object into a string with JSON.stringify .

This is not the end of the world, but we can make things a little easier for consumers by having specific functions for the different HTTP methods:

export async function get < T > ( path : string , args : RequestInit = { method : "get" } ) : Promise < HttpResponse < T >> { return await http < T > ( new Request ( path , args ) ) ; } ; export async function post < T > ( path : string , body : any , args : RequestInit = { method : "post" , body : JSON . stringify ( body ) } ) : Promise < HttpResponse < T >> { return await http < T > ( new Request ( path , args ) ) ; } ; export async function put < T > ( path : string , body : any , args : RequestInit = { method : "put" , body : JSON . stringify ( body ) } ) : Promise < HttpResponse < T >> { return await http < T > ( new Request ( path , args ) ) ; } ; ... const response = await post < { id : number } > ( "https://jsonplaceholder.typicode.com/posts" , { title : "my post" , body : "some content" } ) ;

So, these functions call the base http function but set the correct HTTP method and serialize the body for us.

The consuming code is now a little simpler!

Wrap up