Building features for your Next.js Makerkit application

Learn the flow and patterns that you can use to build new features

As we've mentioned before, the Starter Kit uses various conventions around how the code is split.

While you're free to use your own conventions, in this post I want to describe how to build new features using the existing conventions of the Next.js and Firebase kit.

Let's take a high-level overview of how we can go about adding new features. In this example, we want to add "events" to our application:

1) Adding the business logic

First, we create a new folder within lib. For example, such as lib/events. This folder will contain all the code related to events.

In this folder, we will add types, hooks, utilities, and anything regarding the domain of this feature.

For example, let's assume we want to write a feature that allows users to fetch and list events. We will create a lib/events/hooks/use-fetch-events.ts file that will contain the hook for fetching a list of events from Firestore.

lib/events/hooks/use-fetch-events.ts
export default function useFetchEvents() {
const firestore = useFirestore();
const eventsCollection = 'events';
const collectionRef = collection(
firestore,
eventsCollection
) as CollectionReference<WithId<Task>>;
const path = `organizationId`;
const operator = '==';
const constraint = where(path, operator, organizationId);
const organizationsQuery = query(collectionRef, constraint);
return useFirestoreCollectionData(organizationsQuery, {
idField: 'id',
});
}

We will now use the hook above within our React components to fetch the events.

2) Adding the "events" components

We will add a new folder within src/components called events. This folder will contain all the components related to events.

For example, EventsContainer.tsx, EventDetail.tsx and so on. These components will be imported within our Next.js pages components.

components/events/EventsContainer.tsx
export default function EventsContainer() {
const { data, status } = useFetchEvents();
if (status === `loading`) {
return <Loading />;
}
if (status === `error`) {
return <Error />;
}
return (
<div>
{data.map((event) => (
<EventDetail key={event.id} event={event} />
))}
</div>
);
}

3) Finally, we add the events Next.js Pages

Finally, we need to add the pages to our Next.js application. We can do this by adding the events pages to pages/events/**.tsx.

The page components will then import the components as building blocks from components/events.

pages/events/page.tsx
export default function EventsPage() {
return (
<RouteShell>
<EventsContainer />
</RouteShell>
);
}
export function getServerSideProps(ctx: GetServerSidePropsContext) {
return withAppProps(ctx);
}

4) Using API Requests

Let's now assume you need to use an API request to fetch your data; since this can be due to various factors:

  1. You need to fetch data from an external API
  2. You need to fetch data from a different database
  3. You need to fetch from Firestore that requires elevated permissions
  4. You need to fetch from particularly complex Firestore queries

These are all good reasons! The flow explained above still makes a lot of sense, but the convention used by the kit is slightly different.

In fact, we store the server queries within ~lib/server/events. Of course, you can also add these below your domain ~/lib/events/server. The choice is yours.

lib/server/events/fetch-events.ts
export default function fetchEvents() {
return makeExternalDatabaseRequest();
}

Now that we have a function to retrieve data from the external service, we can finally create an API handler to call this function.

pages/api/events.ts
export default async function eventsHandler(
req: NextApiRequest,
res: NextApiResponse
) {
const events = await fetchEvents();
res.status(200).json(events);
}

From the client side, we can use the hook useApiRequest to call the API handler.

lib/events/hooks/use-fetch-events.ts
import useQuery from 'swr';
export default function useFetchEventsFromApi() {
const fetcher = useApiRequest();
return useQuery([`events`], () => fetcher(`/api/events`));
}

Finally, we can use the hook within our React components.

components/events/EventsContainer.tsx
export default function EventsContainer() {
const { data, loading, error } = useFetchEventsFromApi();
if (loading) {
return <Loading />;
}
if (error) {
return <Error />;
}
return (
<div>
{data.map((event) => (
<EventDetail key={event.id} event={event} />
))}
</div>
);
}