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.
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.
When the API functions are "standalone" and not tied to a specific page, it makes sense to add them to routes/resources
.
For example, the following code calls the action
function owner
from the
file routes/resources/organizations/members/transfer-ownership.ts
.
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 routes/resources/organizations.ts
.
export async function action(args: ActionArgs) {
const req = args.request;
// logic to transfer ownership
}
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;
It's important to notice that in this case the payload is sent as FormData
, not as 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
.
- The CSRF token is generated when the page is server-rendered
- The CSRF token is stored in a
meta
tag - The CSRF token is sent to an HTTP POST request automatically when using
the
useFetch
hook - 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);
}
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.