API Routes

The best practices to write API routes in your Remix Supabase application

Makerkit provides some utilities to reduce the boilerplate needed to write Remix API functions. This section will teach you everything you need to know to write your API functions.

There are many ways to call an API route in Remix:

  1. Using the Remix useSubmit or useFetcher hooks
  2. Using the fetch API wrapper in Makerkit useFetcher
  3. Using the fetch API directly

As a rule of thumb - it's best to use the useSubmit or useFetcher hooks whenever possible. It's the Remix way ad closer to the Remix philosophy.

With that said, there are times where the useFetch hook can be useful - where useFetcher and useSubmit may not be as convenient - for example - when you need to await the request using a Promise.

Calling API functions from the client

API functions in Remix are defined using the special functions loader and action.

These functions are defined in the routes.

These functions can be called in various ways, for example using Remix's useFetcher, useSubmit, <Form>, or can be called using simple fetch calls.

Defining global API functions

When the API functions are "standalone" and not tied to a specific page, it makes sense to prefix them with the resources path.

For example, the following code calls the action function owner from the file resources.organizations.members.transfer-ownership.ts.

Using Fetch + React Query

First, we define a function using the utility useFetch and React Query's useMutation hook.

import useFetch from '~/core/hooks/use-fetch'; import { useMutation } from '@tanstack/react-query'; function useTransferOrganizationOwnership() { const transferOrganizationFetch = useFetch<{ membershipId: number }>( `/resources/organizations/members/transfer-ownership`, 'PUT' ); return useMutation((membershipId: number) => { return transferOrganizationFetch({ membershipId }); }); } export default useTransferOrganizationOwnership;

Then, we can define an action function in the file resources.organizations.ts.

export async function action(args: ActionArgs) { const req = args.request; // logic to transfer ownership }

Using useFetcher and useSubmit

Alternatively, we can use the Remix functions useSubmit or useFetcher to call the same endpoint.

This is more conventional to Remix, so you may prefer this latter approach.

function useTransferOrganizationOwnership() { const submit = useSubmit(); return useCallback((membershipId: number) => { return submit({ data: { membershipId: membershipId.toString() }, action: `/resources/organizations/members/transfer-ownership`, method: `put` }); }); } export default useTransferOrganizationOwnership;

Submitting Actions using JSON

It's important to notice that in this case the payload is sent as FormData, not as JSON - but you can specify the encType parameter to change this behavior.

const fetcher = useFetcher(); <button onClick={() => { fetcher.submit(data, { method: 'POST', encType: 'application/json' }) }}>Submit</button>
export async function action(args: ActionArgs) { const req = args.request; const body = await req.json(); // ... }

Sending the CSRF Token

When you use the hook useFetch, you don't need to worry about sending the CSRF token. The hook will automatically send the token.

Instead, if you use fetch directly, you need to send the CSRF token. To retrieve the page's CSRF token, you can use the useGetCsrfToken hook:

const getCsrfToken = useGetCsrfToken(); const csrfToken = getCsrfToken(); console.log(csrfToken) // token

You will need to send a header x-csrf-token with the value returned by getCsrfToken().

CSRF Token check

We must pass a CSRF token when using POST requests to prevent malicious attacks. To do so, we use the pipe withCsrfToken.

  1. The CSRF token is generated when the page is server-rendered
  2. The CSRF token is stored in a meta tag
  3. The CSRF token is sent to an HTTP POST request automatically when using the useFetch hook
  4. This function will throw an error when the token is invalid

By using this function, we ensure all the following functions will not be executed unless the token is valid.

export const action: ActionFunction = async ({ request }) => { await withCsrf(request); }

Sending CSRF Token using useFetcher and useSubmit

When using useSubmit and useFetcher - you must pass the CSRF token manually:

const submit = useSubmit(); const csrfToken = useCsrfToken(); submit({ csrfToken }, { method: 'POST', encType: 'application/json' })

We can then validate it using the withCsrf function:

export const action: ActionFunction = async ({ request }) => { const body = await request.json(); await withCsrf(request, () => body.csrfToken); }

API Logging

The boilerplate uses Pino for API logging, a lightweight logging utility for Node.js.

Logging is necessary to debug your applications and understand what happens when things don't behave as expected. You will find various instances of logging throughout the API, but we encourage you to log more if necessary.

We import the logging utility from ~/core/logger. Typically, you can log every time you perform an action, both before and after. For example:

async function myFunction(params: { organizationId: string; userId: string; }) { logger.info( { organizationId: params.organizationId, userId: params.userId, }, `Performing action...` ); await performAction(); logger.info( { organizationId: params.organizationId, userId: params.userId, }, `Action successful` ); }

It's always important to add context to your logs: as you can see, we use the first parameter to add important information.


Subscribe to our Newsletter
Get the latest updates about React, Remix, Next.js, Firebase, Supabase and Tailwind CSS