SolidJS: Consume REST API in an elegant way

Damien Le Dantec - Jun 5 '23 - - Dev Community

In this brief article, I will show you an elegant way (for me) to consume REST API.

Context

When we consume REST API endpoints, it is often the same.
REST API are strict on HTTP verbs and naming of endpoints.

It is often a GET request to fetch array of items and POST / DELETE / PATCH to respectively create / delete / edit an item.

This article only deals with the following HTTP verbs: GET / POST / DELETE / PATCH. You can easly adapt the code for your case.

What do we want

The goal of this article is to create a hook to easly consume REST API.

We want something like this (example of Quotes REST API management):

const { items, add, remove, edit } = useQuotes();
Enter fullscreen mode Exit fullscreen mode

remove and not delete because this is a reserved keywork in JavaScript

This hook handles the REST API calls AND updates local data!
The fetch is automatically done.

To be able to do it, we use createResource API and Context API.

To facilate the creation of this hook, we create a "Hook contructor" to easily create many types of endpoints.

How is our "Hook constructor" used?

Simply:

useCitations.tsx

import { createRESTApiHook } from "./createRESTApiHook";

export type Quote = {
  id: string;
  text: string;
  title: string;
  author?: string;
  tags: string[];
  numberOfVotes: number;
};

const { Provider, useRESTApi } = createRESTApiHook<Quote>();

export {
  Provider as QuotesProvider,
  useRESTApi as useQuotes
};
Enter fullscreen mode Exit fullscreen mode

Usage of useQuotes:

  const { items, add, remove, edit, refetch } = useQuotes();
Enter fullscreen mode Exit fullscreen mode

Because we use Context:

App.tsx

<QuotesProvider baseURL="http://localhost:8080/quotes">
  // Components using API
</QuotesProvider>
Enter fullscreen mode Exit fullscreen mode

And that's all!

How is our "Hook contructor" created?

Simply:

createRESTApiHook.tsx

import {
  createContext,
  createResource,
  Resource,
  JSXElement,
  useContext,
} from "solid-js";

interface ProviderProps {
  baseURL: string;
  children: JSXElement;
}


export function createRESTApiHook<T>() {
  interface ContextValue {
    items: Resource<T[]>;
    add: (item: Omit<T, "id">) => Promise<boolean>;
    edit: (itemId: string, item: Partial<T>) => Promise<boolean>;
    remove: (itemId: string) => Promise<boolean>;
    refetch: () => void;
  }

  const context = createContext<ContextValue>();


  function Provider(props: ProviderProps) {
    const [items, { mutate, refetch }] =
      createResource<T[]>(fetchItems);

    async function fetchItems() {
      const res = await fetch(props.baseURL);
      return res.json();
    }

    async function add(item: Omit<T, "id">) {
      try {
        const res = await fetch(props.baseURL, {
          method: "POST",
          body: JSON.stringify(item),
          headers: {
            "Content-Type": "application/json",
          },
        });
        const addedItem = await res.json();
        mutate((prev) => (prev ? [...prev, addedItem] : [addedItem]));
        return true;
      } catch (err) {
        console.error(err);
        return false;
      }
    }

    async function edit(itemId: string, item: Partial<T>) {
      try {
        const res = await fetch(`${props.baseURL}/${itemId}`, {
          method: "PATCH",
          body: JSON.stringify(item),
          headers: {
            "Content-Type": "application/json",
          },
        });
        const updatedItem = await res.json();
        mutate((prev) =>
          prev?.map((elt: any) => (elt.id === itemId ? updatedItem : elt))
        );
        return true;
      } catch (err) {
        console.error(err);
        return false;
      }
    }

    async function remove(itemId: string) {
      try {
        await fetch(`${props.baseURL}/${itemId}`, {
          method: "DELETE",
        });
        mutate((prev) => prev?.filter((elt: any) => elt.id !== itemId));
        return true;
      } catch (err) {
        console.error(err);
        return false;
      }
    }

    const value: ContextValue = {
      items,
      add,
      edit,
      remove,
      refetch,
    };

    return <context.Provider value={value}>{props.children}</context.Provider>;
  }

  function useRESTApi() {
    const ctx = useContext(context);

    if (!ctx) {
      throw new Error("useRESTApi must be used within a RestAPIProvider");
    }

    return ctx;
  }

  return {
    Provider,
    useRESTApi
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

You can already put this "Hook contructor" in your application.

Of course, this function can be improved or adapted.
For example, if the ID field of your items is not id, you can put something else or be adaptable via a props.

Thanks for reading!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .