Fetch Wrapper in TypeScript
Alexander Eckert
July 12, 2020
The Fetch API is a
native browser function that provides an interface for fetching resources
asynchronous across the network. How can we use fetch
to simplify the code for
making HTTP requests? How can we take full advantage of TypeScript's type
system? Let's find out …

Photo by Kameron Kincade
How to use fetch
It is simple to get started with fetch
:
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
All we had to do to fetch data from a REST API is provide the URL. We are using
the fantastic JSONPlaceholder fake REST
API for our example code. The endpoint /todos
will respond with a list of
to-dos. All we need to do to get the response body is to call the json
method
as follows:
const body = await response.json()
const body = await response.json()
Note, that we need to use the await
keyword again because it is asynchronous.
We need to await the response before calling json
.
Creating a fetch wrapper with typed response data
Let's create a wrapper for fetch
combining these two lines of code as well as
adding types:
async function http<T>(request: RequestInfo): Promise<T> {
const response = await fetch(request)
return await response.json()
}
// example consuming code
type Todo = {
userId: number
id: string
title: string
completed: boolean
}
const data = await http<Todo[]>('https://jsonplaceholder.typicode.com/todos')
async function http<T>(request: RequestInfo): Promise<T> {
const response = await fetch(request)
return await response.json()
}
// example consuming code
type Todo = {
userId: number
id: string
title: string
completed: boolean
}
const data = await http<Todo[]>('https://jsonplaceholder.typicode.com/todos')
Our fetch
wrapper function takes in a generic parameter T
for the type of
the response body, hence data
is strongly typed as Todo[]
in our consuming
code.
Catching errors for HTTP error codes
So far our fetch wrapper does not handle errors very graciously, so let's improve that.
async function http<T>(request: RequestInfo): Promise<T> {
const response = await fetch(request)
if (!response.ok) {
throw new Error({ name: response.status, message: response.statusText })
}
// may error if there is no body, return empty array
return response.json().catch(() => ({}))
}
// example consuming code
type Todo = {
userId: number
id: string
title: string
completed: boolean
}
try {
const data = await http<Todo[]>('https://jsonplaceholder.typicode.com/todos')
} catch (error) {
console.log('Error', error)
}
async function http<T>(request: RequestInfo): Promise<T> {
const response = await fetch(request)
if (!response.ok) {
throw new Error({ name: response.status, message: response.statusText })
}
// may error if there is no body, return empty array
return response.json().catch(() => ({}))
}
// example consuming code
type Todo = {
userId: number
id: string
title: string
completed: boolean
}
try {
const data = await http<Todo[]>('https://jsonplaceholder.typicode.com/todos')
} catch (error) {
console.log('Error', error)
}
HTTP methods
We can also use other HTTP methods than GET
with our fetch wrapper.
type Post = {
id: string
userId: number
title: string
body: string
}
const request = new Request('https://jsonplaceholder.typicode.com/posts', {
method: 'post',
body: JSON.stringify({
title: 'my post',
body: 'some content',
userId: 1,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const response = await http<Post>(request)
type Post = {
id: string
userId: number
title: string
body: string
}
const request = new Request('https://jsonplaceholder.typicode.com/posts', {
method: 'post',
body: JSON.stringify({
title: 'my post',
body: 'some content',
userId: 1,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const response = await http<Post>(request)
This will create a new todo and return to use the created post. Note, that the
body needs to be stringified by using JSON.stringify()
. However, I don't feel
like typing so much for every post request. So let's refactor our http wrapper
to expose a helper function for each HTTP method.
async function http<T>(path: string, config: RequestInit): Promise<T> {
const request = new Request(path, config)
const response = await fetch(request)
if (!response.ok) {
throw new Error({ name: response.status, message: response.statusText })
}
// may error if there is no body, return empty array
return response.json().catch(() => ({}))
}
export async function get<T>(path: string, config?: RequestInit): Promise<T> {
const init = { method: 'get', ...config }
return await http<T>(path, init)
}
export async function post<T, U>(
path: string,
body: T,
config?: RequestInit
): Promise<U> {
const init = { method: 'post', body: JSON.stringify(body), ...config }
return await http<U>(path, init)
}
export async function put<T, U>(
path: string,
body: T,
config?: RequestInit
): Promise<U> {
const init = { method: 'put', body: JSON.stringify(body), ...config }
return await http<U>(path, init)
}
async function http<T>(path: string, config: RequestInit): Promise<T> {
const request = new Request(path, config)
const response = await fetch(request)
if (!response.ok) {
throw new Error({ name: response.status, message: response.statusText })
}
// may error if there is no body, return empty array
return response.json().catch(() => ({}))
}
export async function get<T>(path: string, config?: RequestInit): Promise<T> {
const init = { method: 'get', ...config }
return await http<T>(path, init)
}
export async function post<T, U>(
path: string,
body: T,
config?: RequestInit
): Promise<U> {
const init = { method: 'post', body: JSON.stringify(body), ...config }
return await http<U>(path, init)
}
export async function put<T, U>(
path: string,
body: T,
config?: RequestInit
): Promise<U> {
const init = { method: 'put', body: JSON.stringify(body), ...config }
return await http<U>(path, init)
}
Note, that the get
method only has one generic argument for the response body.
However, post
and put
each have two generic arguments. The first is for the
request body and the second one for the response body. Let's see an example to
clarify this.
import * as fetch from './fetch.ts'
type RequestBody = {
userId: number
title: string
body: string
}
type ResponseBody = RequestBody & {
id: string
}
const newPost = {
userId: 1,
title: 'my post',
body: 'some content',
}
const response = await fetch.post<RequestBody, ResponseBody>(
'https://jsonplaceholder.typicode.com/posts',
newPost
)
import * as fetch from './fetch.ts'
type RequestBody = {
userId: number
title: string
body: string
}
type ResponseBody = RequestBody & {
id: string
}
const newPost = {
userId: 1,
title: 'my post',
body: 'some content',
}
const response = await fetch.post<RequestBody, ResponseBody>(
'https://jsonplaceholder.typicode.com/posts',
newPost
)
This is much better! We are still calling our basic fetch wrapper, but we set the correct HTTP method and serialize the request body. This is much simpler to use! TypeScript helps us to strictly type the response as well as the request body.