SolidJS: Consume REST API in an elegant way
Tags: solidjs, typescript, api, javascript
Published: 2023-06-05 14:17:31

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): 

```typescript
const { items, add, remove, edit } = useQuotes();
```

> `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](https://www.solidjs.com/docs/latest/api#createresource) and [Context API](https://www.solidjs.com/docs/latest/api#createcontext).

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**
```typescript
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
};
```

Usage of `useQuotes`:

```typescript
  const { items, add, remove, edit, refetch } = useQuotes();
```

Because we use *Context*:

**App.tsx**

```typescript
<QuotesProvider baseURL="http://localhost:8080/quotes">
  // Components using API
</QuotesProvider>
```

And that's all!

## How is our "Hook contructor" created?

Simply:

**createRESTApiHook.tsx**
```typescript
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
  }
}
```

## 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!