How to create an API layer with React Hooks and TypeScript…and why
Scott Schwartz
Posted on April 3, 2023
One of the first things you should do when starting a new front end is to create an API layer. If you’ve already got a front end without an API layer then it should probably be the next thing on your ticket board. I am of course assuming though that you already have a back end that you’re going to be integrating with.
We’re going to be working with the following libraries/packages/technologies/functions but the ideas here are applicable with any tech stack.
- React (primarily hooks)
- TypeScript
- fetch
Umm… what is an API layer?
On the front end an API layer encapsulates all the logic necessary to call, receive, and transmit data to and from your back end. An example is probably the best place to start, so let’s take a look at some code from my company Jeeny.
export const SuppliersTableView: React.FC = () => {
const {
getSuppliers: {
query,
data,
loading
}
} = useSupplierApi()
useEffect(() => {
query()
}, [query])
const suppliers = data.items;
return loading ? <Loader /> : <SuppliersTable suppliers={suppliers} />
}
The example above uses one part of the Jeeny API layer — specifically the interactions with the Supplier record type (for us a supplier is a vendor or merchant, not a special programming language word). You can see that the useSupplierApi
hook returns a way to call the function getSuppliers
, a way to access the data that getSuppliers
returns, and the current loading state of the getSuppliers
query.
I didn’t have to worry about what endpoint to call, headers, setting up authorization, etc, (or misspelling any of those things). I imported the useSupplierApi
hook because I knew I was creating the UI for a table of suppliers and got what I needed. What I’m doing in the component is very clear and an example of declarative programming. I have abstracted away the how (how it talks to the server, how it transforms the data, etc.) and am focused on the what (I want to get the data and display it in a table).
Creating structure and consistency
An API layer helps to ensure that your codebase remains easy to read, especially as the number of developers working on it grows. Sticking with the same interface for each part of your API helps to make it predictable and easy to use. It also decreases the time spent onboarding new employees.
In effect, you are letting your code self-document your actual API. This is especially helpful if you’re using TypeScript (highly recommended!) and are able to share your types and interfaces between the front end and back end.
Let’s take a look at a few more hooks that we have in order to really understand the structure. It was super helpful for us to create a hook for each type of record in our database but a different approach might make sense for you and your team.
useSupplierApi
useItemApi
useUserApi
useCompanyApi
useFacilityApi
...etc
Separation of concerns
You’ve probably heard this a lot, but one of the fundamentals of being a great programmer is understanding and implementing separation of concerns. Not only does it make your code easier to read, but it makes it easier to replace. And I don’t mean replace your 2 a.m. spaghetti code. I’m talking about when structural changes happen to your API, infrastructure, technologies, etc. it becomes easier to adjust the rest of your codebase to work with those changes.
Let’s go with the simplest example first. We have an endpoint on our server at https://myserver.com/api/suppliers/retrieve
that will return a single supplier. In hindsight, this wasn’t the best naming convention but it worked at the time. Eventually though, we get around to changing it to the standard https://myserver.com/api/suppliers/get
.
Right now we don’t have an API layer in our application. Every time that we need the data for a single supplier we have a fetch call in our React component that uses the data.
What this means is that we have to find every instance in our code that fetched from https://myserver.com/api/suppliers/retrieve
and change it to https://myserver.com/api/suppliers/get
. Alright smart alec, I know we can “find and replace all” but this example is simple in order to prove a point.
Instead, if we had an API layer, that would mean there was only a single function in our application that had the code to make the network request to https://myserver.com/api/suppliers/retrieve
and therefore we only needed to replace that one line of code. Not only have we saved time, but we’ve also gained peace of mind that we didn’t miss anything that needed to be changed!
A more meaningful example
Ok now it’s time to really drive the point home. Let’s imagine that we’ve used fetch
as our resource for accessing the back end. We hear about this fancy new library called axios
that has over 35m downloads a week and is supposed to be even better than fetch! We decide to make the switch. (this is not a recommendation for/against axios
. We use Apollo Client at Jeeny — which I do recommend for GraphQL APIs).
Sounds pretty dreadful right? Axios
is going to be sooo great but first we have to rewrite hundreds if not thousands of fetch
calls that are in our components. Let’s actually push this to the backlog…
If you had originally created an API layer that used fetch
all you would need to do is rewrite parts of the layer to take advantage of axios
. Your UI components don’t care what library you use because the interface of your hooks don’t change. They send the same query function, data object, and loading boolean. We didn’t have to touch a single React component to make this switch.
Reducing code repetition
It should be clear by now but this will also help to remove the same code from appearing over and over and over again.
Enough already! Show me more code!
We use GraphQL so our API layer looks a bit different than this but the concepts should remain the same. I’m going to forego GraphQL examples and instead use fetch with a normal rest API.
Sidenote: If you’re thinking that an API layer defeats the purpose of GraphQL then our next article is for you! For now, I’ll calm any nerves by letting you know that our hooks accept custom document nodes.
First thing we need is our own fetch hook. We’ll use this to store the loading state and returned data whenever we make a call.
import { useState } from "react";
const DEFAULT_FETCH_OPTIONS = {};
type UseFetchProps = {
url: string;
method: "GET" | "POST" | "PUT" | "DELETE"
};
type CommonFetch = {
/** the variables that the endpoint expects to receive */
input?: { [index: string]: any };
/** this allows you to override any default fetch options on a
case by case basis. think of it like an escape hatch. */
fetchOptions?: RequestInit;
}
// <T> turns this into a generic component. We will take advantage of this
// by assigning the `data` variable the type T. If this doesn't make sense,
// it will when we get to the next file.
export function useFetch<T> ({ url, method }: UseFetchProps) {
const [isLoading, setIsLoading] = useState(false);
// we are assigning the generic type T to our data value here
const [data, setData] = useState<T | null>(null);
const commonFetch = async ({
input,
fetchOptions = {},
}: CommonFetch) => {
setIsLoading(true);
const response = await fetch(url, {
method,
...DEFAULT_FETCH_OPTIONS, // this should be defined as a const in a separate file
...fetchOptions, // this allows you to override any default fetch options on a case by case basis
body: JSON.stringify(input),
});
const data = await response.json();
setIsLoading(false);
setData(data);
};
return { isLoading, commonFetch, data };
};
Now that we’ve got that hook out of the way let’s create our useSupplierApi
hook. Under an api folder let’s create another folder called supplier
. It will have three files in it.
- api.ts
- requests.ts
- types.ts
I’ll start with the types.ts file. If you’re not using TypeScript then this is irrelevant for you. Ideally, this file is truly unnecessary because you have a way to share types between your front end and back end. That’s a topic for a different article though.
// types.ts
export type GetSupplierInput = { id: string }
export type CreateSupplierInput = {
name: string,
phoneNumber: string;
emailAddress: string;
}
export type Supplier = {
id: string;
name: string;
phoneNumber: string;
emailAddress: string;
createdOn: string;
createdBy: string
}
Now let’s write our requests.ts file. It will be a series of hooks that use the fetch hook from up above. It should be one function for each supplier related endpoint.
// requests.ts
import { CreateSupplierInput, GetSupplierInput, Supplier } from "./types"
export const useGetSupplier = () => {
// adding <Supplier> after useFetch will give the "data" value the type Supplier.
// This really helps to flesh out the quality of life for the API and is part
// of creating something that is self documenting. We put Supplier because we know
// that is what this endpoint will always return.
const { commonFetch, isLoading, data } = useFetch<Supplier>({
url: "http://myserver.com/api/suppliers/get",
});
// using typescript to define the input here means no mistakes can be
// made downstream when actually using our API layer
const getSupplier = (input: GetSupplierInput) => commonFetch({ input, method: "GET" });
return { getSupplier, isLoading, data };
};
export const useCreateSupplier = () => {
const { commonFetch, isLoading, data } = useFetch<Supplier>({
url: "http://myserver.com/api/suppliers/create",
});
const createSupplier = (input: CreateSupplierInput ) => commonFetch({ input, method: "POST" });
return { createSupplier, isLoading, data };
};
Now let’s move on to the api.ts file. It simply functions as a structured collection of the hooks from request.ts.
import { useGetSupplier, useCreateSupplier } from "./requests";
export const useSupplierApi = () => {
const {
getSupplier,
isLoading: getSupplierLoading,
data: getSupplierData,
} = useGetSupplier();
const {
createSupplier,
isLoading: createSupplierLoading,
data: createSupplierData,
} = useCreateSupplier();
return {
getSupplier: {
query: getSupplier,
isLoading: getSupplierLoading,
data: getSupplierData,
},
createSupplier: {
mutation: createSupplier,
isLoading: createSupplierLoading,
data: createSupplierData,
},
};
};
Let’s use it!
// CreateSupplier.tsx
export const CreateSupplier: React.FC = () => {
const {
createSupplier: { mutation: createSupplier, data, isLoading },
} = useSupplierApi();
const createForm = useForm();
const handleSubmit = async () => {
const values = createForm.getValues();
// TypeScript will tell us if our input object is wrong!
await createSupplier({
name: values.name,
phoneNumber: values.phoneNumber,
emailAddress: values.emailAddress,
});
};
return data === null ? (
<form>
<input {...createForm.register("name")} />
<input {...createForm.register("phoneNumber")} />
<input {...createForm.register("emailAddress")} />
<button onClick={handleSubmit}>Submit</button>
</form>
) : (
// TypeScript will tell us what attributes are on the data object!
<div>Successfully created supplier {data.name}</div>
);
};
Recap
Hopefully you understand what an API layer is and have seen enough code to build your own! If you have any questions please comment them below and I’d be happy to do my best to answer them.
- We walked through what an API layer is
- We saw a few reasons why you should write one
- We created our own hook around fetch
- We typed out the interface for our API
- We created hooks for each endpoint in our API
- We condensed this all into a single hook
- We used it :)
Thanks for getting this far and I hope it was helpful!
— Scott (founder @Jeeny)
If you have a minute to spare I’d love to share what Jeeny does. We built a composable set of APIs for inventory, procurement, fulfillment, and manufacturing. You can use our APIs to extend and enhance your current ERP, WMS, and fulfillment software. You can also use them to build your own applications.
We have a free tier that resets every month if you’d like to give it a go!
Posted on April 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.