Fetch with async & await and TypeScript
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;
}
// example consuming code
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;
}
// example consuming code
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;
}
// example consuming code
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 {
// may error if there is no body
response.parsedBody = await response.json();
} catch (ex) {}
if (!response.ok) {
throw new Error(response.statusText);
}
return response;
}
// example consuming code
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));
};
...
// example consuming code
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
With some nice wrapper functions we can easily use fetch
with async
and await
and TypeScript. We’ve also chosen to raise errors when HTTP errors occur which is arguably a more common behaviour of a HTTP library. Having functions for each HTTP method makes it super easy to interact with a web service.
If you to learn more about TypeScript, you may find my free TypeScript course useful: