Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Fetch with async & await and TypeScript

January 29, 2019
typescriptjavascript

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 …

image by Jozef Fehér on www.pexels.com

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:

Learn TypeScript

Learn TypeScript
Take a look

Want more content like this?

Subscribe to receive notifications on new blog posts and courses

Required
© Carl Rippon
Privacy Policy