I’ve used Next.js 13 in many of my projects, but I was eager to explore the new features in version 14. In this article, we’ll delve into implementing server actions with a repository layer using Next.js 14 along with a MongoDB database.
Let’s kick things off by creating a new Next.js project:
npx create-next-app@latest
Make sure to enable typescript and Tailwind CSS during the project creation. Navigate to the root folder:
cd ./root
Now, let's install the necessary packages using pnpm to generate the pnpm-lock.yaml
file:
pnpm install
MongoDB Connections
To establish connections with MongoDB, we need to install the MongoDB package:
pnpm add mongodb
Create a new folder named /lib
under src/and
add a file named mongodb.ts
inside it. This file will contain our database connections class:
import { MongoClient } from 'mongodb';
// Replace with your MongoDB connection string
const uri = process.env.MONGODB_URI as string;
const options = {};
declare global {
var _mongoClientPromise: Promise<MongoClient>;
}
class Singleton {
private static _instance: Singleton;
private client: MongoClient;
private clientPromise: Promise<MongoClient>;
private constructor() {
this.client = new MongoClient(uri, options);
this.clientPromise = this.client.connect();
if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable to preserve the value
// across module reloads caused by HMR (Hot Module Replacement).
global._mongoClientPromise = this.clientPromise;
}
}
public static get instance() {
if (!this._instance) {
this._instance = new Singleton();
}
return this._instance.clientPromise;
}
}
const clientPromise = Singleton.instance;
// Export a module-scoped MongoClient promise.
// By doing this in a separate module,
// the client can be shared across functions.
export default clientPromise;
This code sets up a MongoDB connection class, providing a singleton instance for efficient connection management. Remember to replace the placeholder in process.env.MONGODB_URI
with your actual MongoDB connection string.
Please be aware that the clientPromise
will be invoked later in our repository class to establish the database connection.
Creating the Repository Layer
Following the establishment of the MongoDB connections class, let’s build a generic service layer that dynamically queries the MongoDB client instance using T
.
To achieve this, create a new folder at src\Server\RepositoryService
and add two files: IRepositoryService.ts
and RepositoryService.ts
.
Inside IRepositoryService.ts
, define a typescript interface for the service:
interface IRepository<T> {
find(
filter: Partial<T>,
page: number,
limit: number,
projection?: Partial<Record<keyof T, 1 | 0>>,
): Promise<{ data: T[], totalCount: number }>;
}
In RepositoryService.ts
, implement the find()
method:
import clientPromise from "@/lib/mongodb";
import { MongoClient } from "mongodb";
export class Repository<T> implements IRepository<T> {
private collection: string;
constructor(collection: string) {
this.collection = collection;
}
// Asynchronously find documents in the collection
async find(
filter: Partial<T>,
page: number = 1,
limit: number = 10,
projection?: Partial<Record<keyof T, 1 | 0>>,
): Promise<{ data: T[], totalCount: number }> {
try {
// Await the client promise to get an instance of MongoClient
const client: MongoClient = await clientPromise;
// Calculate how many documents to skip
const skip = (page - 1) * limit;
// Access the database and the collection
const collection = client.db().collection(this.collection);
// Get the total count of all items
const totalCount = await collection.countDocuments(filter);
// Access the database and the collection, then find documents matching the filter
// If a projection is provided, apply it to the query
// Convert the result to an array and return it
const data = await collection
.find(filter, { projection })
.skip(skip)
.limit(limit)
.toArray();
return { data: data as unknown as T[], totalCount };
} catch (error: unknown) {
// Catch and log any connection errors
if (error instanceof Error) {
if (error.message.includes("ECONNREFUSED")) {
console.error("Failed to connect to MongoDB. Connection refused.");
} else {
console.error("An error occurred:", error.message);
}
}
return { data: [], totalCount: 0 };
}
}
}
The find()
method supports filtering, projection, and pagination, returning a promise that resolves to an object with matching documents and the total count.
Creating the Book Entity
To utilize the repository layer, create an entity service. Let’s use BookService
for this. Create the src\Server\Service\BookService
folder with files IBookService.ts
and BookService.ts
.
In IBookService.ts
, define the interface:
export type IBook = {
// ... (book properties)
};
export interface IBookService {
searchBook(filter: Partial<IBook>): Promise<{ data: IBook[], totalCount: number }>;
}
Create BookService.ts
:
import { Repository } from "@/Server/RepositoryService/RepositoryService";
import { IBook, IBookService } from "./IBookService";
export class BookService implements IBookService {
private repository: Repository<IBook>;
constructor() {
this.repository = new Repository<IBook>("books");
}
async searchBook(
filter: Partial<IBook>,
page: number = 1,
limit: number = 10
): Promise<{ data: IBook[], totalCount: number }> {
return this.repository.find(filter, page, limit);
}
}
Server Actions
In Next.js 14, server actions can be used directly without creating APIs. Create src/Server/actions/BookActions.ts
:
'use server';
import { BookService } from "../Service/BookService/BookService";
export const fetchBooks = async (pageNumber: number) => {
const bookService = new BookService();
return await bookService.searchBook({}, pageNumber, 10);
};
Make sure to include 'use server'
for using the action function inside the React component.
Updating Home Component
Finally, in src/app/page.tsx
, integrate everything:
import React, { Suspense } from 'react';
import { BookCard } from '@/Client/Components/Home/BookCard';
import { fetchBooks } from '@/Server/actions/BookActions';
import Pagination from '@/Client/Components/Home/Pagination';
type ISearchQuery = {
page: string;
}
type HomeProps = {
searchParams?: { [key: string]: string | string[] | undefined };
}
export default async function Home({
searchParams
}: HomeProps) {
// get the current page number
const { page } = searchParams as ISearchQuery;
const pageNumber = page && !isNaN(Number(page)) ? Number(page) : 1;
/* begin:: feetch book list */
const { data: books, totalCount } = await fetchBooks(pageNumber);
/* end:: feetch book list */
return (
<div className="home-page w-full px-8 grow flex flex-col mt-2">
<h2>Books:</h2>
<hr className='w-full my-3 h-1 border-stone-400' />
<span className='text-sm'>{books.length} of {totalCount} Items </span>
<div className="mt-6 result-container">
<Suspense fallback={<h6 className='text-center ltr'>📡 Loading data please wait ... </h6>}>
{books?.length ?
<>
{books.map((book, index) => (<BookCard key={index} {...book} />))}
<Pagination
currentPage={pageNumber}
totalItems={totalCount}
itemsPerPage={10} // replace this with your actual items per page
/>
</>
:
<h4 className='text-center'>No books found</h4>}
</Suspense>
</div>
</div>
);
}
This component uses fetchBooks()
to get books on the server side and renders them using the <BookCard>
component.
Conclusion
In conclusion, this article has walked you through the implementation of a robust repository layer in Next.js 14, leveraging MongoDB for database interactions. By creating a generic service layer, you can seamlessly query the MongoDB client instance dynamically using typescript generics. The demonstrated example includes the creation of a Book entity service, server actions, and the integration of these components into a Next.js application.
The repository layer, as showcased, provides a flexible and reusable solution for handling database operations, supporting features such as filtering, projection, and pagination. By utilizing server actions in Next.js 14, you can directly invoke server-side functionality without the need for additional API endpoints, enhancing the efficiency of your application.
As you embark on implementing similar solutions in your projects, feel free to customize and extend the presented concepts to suit your specific use cases. I hope this article proves valuable in enhancing your understanding of Next.js 14 and its capabilities for building powerful and scalable applications.
Thank you for reading, and I sincerely hope you find this article insightful and beneficial for your development journey. Wish you the best of luck, and I hope you enjoy implementing these concepts in your own projects!