API Routes
Learn how to create and manage API routes in your Remix Firebase 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.
Calling API functions from the client
To make requests to the API functions in the api
folder, we provide a custom hook useApiRequest
, a wrapper around fetch
.
It has the following functionality:
- Uses an internal
state
to keep track of the state of the request, likeloading
,error
andsuccess
. This helps you write fetch requests the "hooks way" - Automatically adds a
Firebase AppCheck Token
to the request headers if you enabled Firebase AppCheck - Automatically adds a
CSRF token
to the request headers
Similarly to making Firestore Requests, it's a convention to create a custom hook for each request for readability/reusability reasons.
import useApiRequest from '~/core/hooks/use-api';interface Body { idToken: string;}export function useCreateSession() { return useApiRequest<void, Body>('/api/session/sign-in');}
The hook returns an array:
- Callback: the first element is the callback to make the request
- State: the second element is the state object of the request
const [createSession, createSessionState] = useCreateSession();
The state object internally uses useApiRequest
, and has the following interface:
{ success: boolean; loading: boolean; error: string; data: T | undefined;}
When success
is true
, the property data
is inferred with its correct type.
You can use this hook in your components:
import { useCreateSession } from '~/core/hooks/use-create-session';function Component() { const [createSession, createSessionState] = useCreateSession(); return ( <> { createSessionState.loading ? `Loading...` : null } { createSessionState.error ? `Error :(` : null } { createSessionState.success ? `Yay, success!` : null } <SignInForm onSignIn={(idToken) => createSession({ idToken })} /> </> );}
Similarly, you can also use it to fetch data:
import { useCreateSession } from '~/core/hooks/use-create-session';function Component() { const [fetchMembers, { loading, error, data }] = useFetchMembers(); // fetch data when the component mounts useEffect(() => { fetchMembers(); }, [fetchMembers]); if (loading) { return <div>Fetching members...</div>; } if (error) { return <div>{error}</div>; } return ( <div> {data.map(member => <MemberItem member={member} />)} </div> );}
Writing your own Fetch Hook
You are free to use your own implementation for sending HTTP requests to your API or use a powerful third-party library.
With that said, you need to ensure that you're sending the required headers:
- The AppCheck token (if you use Firebase AppCheck)
- The CSRF Token (for POST requests)
Generating an App Check Token
To generate an App Check token, you can use the useGetAppCheckToken
hook:
const getAppCheckToken = useGetAppCheckToken();const appCheckToken = await getAppCheckToken();console.log(appCheckToken) // token
You will need to send a header X-Firebase-AppCheck
with the resolved value returned by the promise getAppCheckToken()
.
Sending 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
useApiRequest
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);}
Firebase App Check
App Check is a Firebase service that helps you to protect your web app from bots, spammy users, and general abuse detected by Google's Recaptcha.
Check out the documentation to setup Firebase App Check.
Using the withAppCheck
pipe, we ensure all the following functions will not be executed unless the App Check token is valid.
export const action: ActionFunction = async ({ request }) => { await withAppCheck(request);}
Catching and Handling Exceptions
To catch and gracefully handle API exceptions, we use the function withExceptionFilter
.
As seen from its usage above, we wrap the API function within the utility. When errors are caught, this function will:
- Log and debug the error to the console
- Report the error to Sentry (if configured)
- Return an object stripping the error's data to avoid leaking information
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.