API Routes
Learn how to create and manage API routes in your Next.js application.
Makerkit provides some utilities to reduce the boilerplate needed to write Next.js API functions. This section will teach you everything you need to know to write your API functions.
As you may know, we write Next.js' API functions at pages/api
. Therefore, every file listed in this folder becomes a callable HTTP function.
For example, we can write the file pages/api/hello-world.ts
:
export default function helloWorldHandler( req: NextApiRequest, res: NextApiResponse,) { res.send(`Hello World`);}
This API endpoint will simply return Hello World
.
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 useSWRMutation from "swr/mutation";import { useApiRequest } from '~/core/hooks/use-api';interface Body { idToken: string;}export function useCreateSession() { const endpoint = '/api/session/sign-in'; const fetcher = useApiRequest<void, Body>(); return useSWRMutation(endpoint, (path, { arg }: { arg: Body }) => { return fetcher({ path, body: arg, }); });}
As you can see above, we're using SWR, a popular library for data fetching. We
You can use this hook in your components:
import { useCreateSession } from '~/core/hooks/use-create-session';function Component() { const { trigger, error, isMutating, data } = useCreateSession(); return ( <> { isMutating ? `Loading...` : null } { error ? `Error :(` : null } { data ? `Yay, success!` : null } <SignInForm onSignIn={(idToken) => trigger({ idToken })} /> </> );}
Similarly, you can also use it to fetch data. Below, is an example for fetching data using SWR:
import type { User } from 'firebase/auth';import useSWR from 'swr';import { useApiRequest } from '~/core/hooks/use-api';/** * @name useFetchOrganizationMembersMetadata * @param organizationId */export function useFetchOrganizationMembersMetadata(organizationId: string) { const endpoint = getFetchMembersPath(organizationId); const fetcher = useApiRequest<User[]>(); return useSWR(endpoint, (path) => { return fetcher({ path, method: 'GET' }); });}function getFetchMembersPath(organizationId: string) { return `/api/organizations/${organizationId}/members`;}
And below, we're using the hook in a component:
import { Trans } from "next-i18next";import Alert from "~/core/ui/Alert";import LoadingMembersSpinner from "~/core/ui/LoadingMembersSpinner";function MembersListComponent() { const { data: members, isLoading: loading, error, } = useFetchOrganizationMembersMetadata(organizationId); if (loading) { return ( <LoadingMembersSpinner> <Trans i18nKey={'organization:loadingMembers'} /> </LoadingMembersSpinner> ); } if (error) { return ( <Alert type={'error'}> <Trans i18nKey={'organization:loadMembersError'} /> </Alert> ); } // display members using "members" return <>...</>;}
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()
.
Piping utilities
Makerkit uses a dead-simple utility to pipe functions named withPipe
. We can pass any function
to this utility, which will iterate over its parameters and execute each.
Each function must be a Next.js handler that accepts the req
and res
objects. Let's take a look at the below:
import {NextApiRequest, NextApiResponse } from "next";import { withPipe } from '~/core/middleware/with-pipe';import { withAuthedUser } from '~/core/middleware/with-authed-user';import { withMethodsGuard } from '~/core/middleware/with-methods-guard';import { withExceptionFilter } from '~/core/middleware/with-exception-filter';import { withAppCheck } from '~/core/middleware/with-app-check';export default function members( req: NextApiRequest, res: NextApiResponse) { const handler = withPipe( withMethodsGuard(['POST']), withAuthedUser, withAppCheck, membersHandler ); return withExceptionFilter(req, res)(handler);}function membersHandler( req: NextApiRequest, res: NextApiResponse) { // API logic}
What happens? We've passed 4 functions to the withPipe
utility. The utility iterates over each of them. So it will execute withMethodsGuard
, withAuthedUser
, withAppCheck
, and if everything went well during the checks, it will run the API logic we define in membersHandler
.
withMethodsGuard
checks if the method passed is supportedwithAuthedUser
checks if the user is authenticatedwithAppCheck
executes a Firebase App Check validation to prevent abusemembersHandler
is the actual logic of the API endpoint
API Authentication
To validate that API functions are called by users authenticated with Firebase Auth, we use withAuthedUser
.
This function will:
- Initialize the Firebase Admin - this is always needed when using Firebase!
- Get the user using the
session
cookie - Throw an error when not authenticated
By using this function, we ensure all the following functions will not be executed unless the user is authenticated.
import {NextApiRequest, NextApiResponse } from "next";import { withPipe } from "~/core/middleware/with-pipe";import { withAuthedUser } from "~/core/middleware/with-authed-user";import { withExceptionFilter } from '~/core/middleware/with-exception-filter';export default function members( req: NextApiRequest, res: NextApiResponse) { const handler = withPipe( withAuthedUser, membersHandler ); return withExceptionFilter(req, res)(handler);}function membersHandler( req: NextApiRequest, res: NextApiResponse) { // API logic}
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.
import { NextApiRequest, NextApiResponse } from "next";import { withPipe } from "~/core/middleware/with-pipe";import { withAuthedUser } from "~/core/middleware/with-authed-user";import { withExceptionFilter } from '~/core/middleware/with-exception-filter';import { withCsrf } from '~/core/middleware/with-csrf';export default function members( req: NextApiRequest, res: NextApiResponse) { const handler = withPipe( withAuthedUser, withCsrf(), membersHandler ); return withExceptionFilter(req, res)(handler);}function membersHandler( req: NextApiRequest, res: NextApiResponse) { // API logic}
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.
import {NextApiRequest, NextApiResponse } from "next";import { withPipe } from "~/core/middleware/with-pipe";import { withAuthedUser } from "~/core/middleware/with-authed-user";import { withExceptionFilter } from '~/core/middleware/with-exception-filter';import { withCsrf } from '~/core/middleware/with-csrf';export default function members( req: NextApiRequest, res: NextApiResponse) { const handler = withPipe( withAuthedUser, withCsrf(), membersHandler ); return withExceptionFilter(req, res)(handler);}function membersHandler( req: NextApiRequest, res: NextApiResponse) { // API logic}
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:
import logger from '~/core/logger';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` );}async function performAction() { // do something complex here}
It's always important to add context to your logs: as you can see, we use the first parameter to add important information.