Building a REST client
When consuming REST APIs you can use the built-in Fetch API or opt for a third-party library.
Recommended library: Wretch
An implementation based on the Fetch API is preferred, due its lightweight nature and lack of external dependencies, making it ideal for minimal and maintainable applications. However, using the Fetch API alone, without any library, has its limitations. It easily leads to verbose syntax, awkward error handling and there is no built-in support for middleware.
Wretch addresses these issues by providing a more expressive API, enabling fluent method chaining, automatic response parsing, and cleaner error handling, reducing boilerplate:
import wretch from "wretch";
const bookStoreClient = wretch()
.url("https://my-book-store.com")
.headers({ "X-Some-Api-Key": "..." });
const books = await bookStoreClient
.get("/api/v1/books")
.json();
await bookStoreClient
.body({ isbn: "1234", title: "My book" })
.put("/api/v1/books");
What about Axios?
In the past, Axios was the go-to solution for making HTTP requests in JavaScript and TypeScript. However, its implementation is based on XMLHttpRequest
, requiring more code and increasing your bundle size. It also has no automatic JSON handling, and requires more boilerplate code. Therefore, we recommend a more modern library instead.
Pre-configured REST client from Backpack
You can use the pre-configured REST client from @backpack/rest-client
, which is based on Wretch and ships with a few add-ons and middlewares enabled by default:
import { createRestClient } from "@backpack/rest-client";
const bookStoreClient = createRestClient({
baseUrl: "https://my-book-store.com",
apiManagementApiKey: '...',
timeout: 5_000,
// ...
});
const books = await bookStoreClient
.get("/api/v1/books")
.json();
Check out the API reference of @backpack/rest-client
to learn more.
Response validation
One way to make your response data type-safe, is by passing a generic type to the json()
method:
import { createRestClient } from '@backpack/rest-client';
const books = await createRestClient()
.get("/api/v1/books")
.json<BooksResponse>();
books
// ^ inferred as `BooksResponse`, but unsafe! ⚠️
However, since you are now basically type-casting your JSON result, it might be better to perform actual runtime validation to the response instead.
To achieve this, we recommend you to parse the result against a schema, using Zod:
import { z } from "zod";
import { createRestClient } from '@backpack/rest-client';
// define a Zod schema for your response
const BooksResponse = z.object({
isbn: z.string()
// ...
});
const books = await createRestClient()
.get("/api/v1/books")
.json()
.then(json => BooksResponse.parse(json));
// ^ `json` is `unknown` but can be parsed by Zod
books;
// ^ still inferred as `BooksResponse`, but now it's safe! 🚀
With the pre-configured REST client from Backpack, you can even use the shorthand .validatedJson()
instead:
const books = await createRestClient()
.get("/api/v1/books")
.validatedJson(BooksResponse);
For more information on Zod and validation, see Schema validation.
Configuring a timeout
To prevent hanging requests and handle failures gracefully, it is important to configure a timeout for your REST client.
With Wretch, you can achieve this by using the AbortAddon:
import wretch from 'wretch';
import AbortAddon from 'wretch/addons/abort';
const restClient = await wretch()
.addon(AbortAddon())
.setTimeout(3_000) // 3 seconds
const books = await restClient
.get("/api/v1/books")
// additional helper method to recover from an aborted request:
.onAbort(() => console.log("Aborted!"))
If you use the pre-configured REST client from @backpack/rest-client
, you don't have to worry about this:
import { createRestClient } from '@backpack/rest-client';
const restClient = createRestClient({
timeout: 3_000
});
const books = await restClient.get("/api/v1/books")
Error handling
How errors should be handled in your REST client, depends on the type of error and your use case.
To handle expected errors such as HTTP 404 responses gracefully, you can use the built-in helper methods of Wretch:
import { createRestClient } from '@backpack/rest-client';
const result = await createRestClient()
.get("/api/v1/books")
.notFound(() => {
// recover from a HTTP 404
return "some fallback";
})
.json()
Another way to handle errors is to propagate them through a chain of Promises:
const result = createRestClient()
.get("/api/v1/books")
.json()
.then(json => BooksResponse.parse(json))
.then(data => /* do other stuff */)
.catch(error => {
logger.warn('Got unexpected error', error);
return "some fallback";
})
With the AsyncResult
utility from @backpack/error-handling
you can even handle every kind of error differently:
import { createRestClient } from '@backpack/rest-client';
import { AsyncResult } from "@backpack/error-handling/async-result";
const result = await AsyncResult
.try(() =>
createRestClient()
.get("/api/books")
.json()
)
.map((json) => BooksResponse.parse(json))
.map(data => /* do other stuff */)
.onFailureIfInstanceOf(ZodError, (error) => /* ... */)
.onFailureIfInstanceOf(WretchError, (error) => /* ... */)
.recover(() => "some fallback");
Logging
Given how easily logging becomes opinionated, the default REST client in Backpack avoids it entirely.
However, if you do want to add some logging, you can easily do so using a middleware function. If you are using the Powertools Logger, you can use the existing logging middleware from Backpack:
import {
createRestClient,
powertoolsLoggingMiddleware
} from "@backpack/rest-client";
const myRestClient = createRestClient()
.middlewares([
powertoolsLoggingMiddleware({
logger: myLogger, // pass your logger instance to the middleware
logRequests: true,
// to see all options, check the API reference
})
]);
const result = myRestClient
.get("/api/books")
.json()
Alternatively, you can easily add your own middleware:
import type { FetchLike, WretchOptions } from "wretch";
export const myMiddleware = (
// ... additional options can be passed here
) => {
return (next: FetchLike): FetchLike =>
async (url: string, options: WretchOptions) => {
// do something with the request...
const response = await next(url, options);
// do something with the response...
return response;
};
};