-
Notifications
You must be signed in to change notification settings - Fork 0
Home
- Create a NextJS project
- Connect server NextJS to mongoDB
-
server NextJS
publishAPI movies
-
client NextJS
consumesAPI movies
- client/server side rendering and data fetching
- MongoDB Atlas (sign up).
- A Vercel account (sign up).
- NodeJS 18+ (npm and npx)
- NexJS 14 (Pages Router vs. App Router, upgrading Pages to Router)
"version": "14.2.3"
- Sample Mflix Dataset
pages
routing (not app/route)
References
NextJS:
- NextJS docs
- Start building with Next.js
- Integrate MongoDB & Next.js App
- Build an Inventory Management System Using MongoDB Atlas
- Create a Twitter Clone using Next.js, MongoDB, Auth0, & Deploy to Vercel
Initial state (api):
Server/Client approach
Final state (data fetching from client):
A basic Next.js project structure for a tree folder project with MongoDB integration would include the following directories:
-
.next
: This is a generated folder that contains the build output of your Next.js application. -
lib
: This folder is used to store utility functions and classes that can be reused across your application. -
pages
: This is the core of Next.js. Each file inside this directory is treated as a route. For example,pages/index.js
maps to the/
route. -
pages/api
: This directory allows you to create API routes. Each file inside this directory exports a default function that will be treated as a serverless function. -
node_modules
: This folder contains all the project dependencies installed via npm. -
public
: This directory contains static files that you want to be served directly by the server. -
types
: This folder is used to store TypeScript definition files (with MongoDB &mongodb.d.ts
file): here to define the types for your MongoDB models.
Note
Next.js is a React-based framework for building modern web applications. The framework adds a lot of powerful features — such as server-side rendering, automatic code splitting, and incremental static regeneration — that make it easy to build, scalable, and production-ready apps.
Hot Code Reloading: Next.js reloads the page when it detects any change saved to disk.
Automatic Routing: Any URL is mapped to the filesystem, to files put in the pages folder, and you don't need any configuration (you have customization options).
Single File Components: Using styled-jsx, completely integrated as built by the same team, it's trivial to add styles scoped to the component.
Server Rendering: You can render React components on the server side, before sending the HTML to the client.
Automatic Code Splitting: Pages are rendered with just the libraries and JavaScript that they need, no more. Instead of generating one single JavaScript file containing all the app code, the app is broken up automatically by Next.js in several different resources.
Prefetching: The Link component, used to link together different pages, supports a prefetch prop which automatically prefetches page resources (including code missing due to code splitting) in the background.
Before Next.js 13, the Pages Router was the main way to create routes in Next.js.
It used an intuitive file-system router to map each file to a route. The Pages Router is still supported in newer versions of Next.js, but we recommend migrating to the new App Router to leverage React's latest features.
//npx create-next-app --example with-mongodb mflix
//cd mflix
npx create-next-app --example with-mongodb next-flix
cd next-flix
npm install
npm run dev
Remember to provide app access to mongoDB by DatabaseAcces and Network Access
we'll find an env.local.example
file. Let's rename this file to env.local and open it.
This file contains one property that we'll need to fill out: MONGODB_URI=<<<<<<<<
.
Important
credentials are placeholders
user: albert
password: 1111
appId: 0b8fdd
db: sample_mflix
appName: ClusterTest
MONGODB_URI=mongodb+srv://albert:1111@clustertest.0b8fdd.mongodb.net/sample_mflix?retryWrites=true&w=majority&appName=ClusterTest
lib/mongodb.ts MongoDB client
import { MongoClient, ServerApiVersion } from "mongodb";
if (!process.env.MONGODB_URI) {
throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
}
const uri = process.env.MONGODB_URI;
const options = {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
deprecationErrors: true,
},
};
let client;
let clientPromise: Promise<MongoClient>;
if (process.env.NODE_ENV === "development") {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
let globalWithMongo = global as typeof globalThis & {
_mongoClientPromise?: Promise<MongoClient>;
};
if (!globalWithMongo._mongoClientPromise) {
client = new MongoClient(uri, options);
globalWithMongo._mongoClientPromise = client.connect();
}
clientPromise = globalWithMongo._mongoClientPromise;
} else {
// In production mode, it's best to not use a global variable.
client = new MongoClient(uri, options);
clientPromise = client.connect();
}
// Export a module-scoped MongoClient promise. By doing this in a
// separate module, the client can be shared across functions.
export default clientPromise;
{"_id":{"$oid":"573a1390f29313caabcd5293"},"plot":"Young Pauline is left a lot of money when her wealthy uncle dies. However, her uncle's secretary has been named as her guardian until she marries, at which time she will officially take ...","genres":["Action"],"runtime":{"$numberInt":"199"},"cast":["Pearl White","Crane Wilbur","Paul Panzer","Edward Josè"],"num_mflix_comments":{"$numberInt":"0"},"poster":"https://m.media-amazon.com/images/M/MV5BMzgxODk1Mzk2Ml5BMl5BanBnXkFtZTgwMDg0NzkwMjE@._V1_SY1000_SX677_AL_.jpg","title":"The Perils of Pauline","fullplot":"Young Pauline is left a lot of money when her wealthy uncle dies. However, her uncle's secretary has been named as her guardian until she marries, at which time she will officially take possession of her inheritance. Meanwhile, her \"guardian\" and his confederates constantly come up with schemes to get rid of Pauline so that he can get his hands on the money himself.","languages":["English"],"released":{"$date":{"$numberLong":"-1760227200000"}},"directors":["Louis J. Gasnier","Donald MacKenzie"],"writers":["Charles W. Goddard (screenplay)","Basil Dickey (screenplay)","Charles W. Goddard (novel)","George B. Seitz","Bertram Millhauser"],"awards":{"wins":{"$numberInt":"1"},"nominations":{"$numberInt":"0"},"text":"1 win."},"lastupdated":"2015-09-12 00:01:18.647000000","year":{"$numberInt":"1914"},"imdb":{"rating":{"$numberDouble":"7.6"},"votes":{"$numberInt":"744"},"id":{"$numberInt":"4465"}},"countries":["USA"],"type":"movie","tomatoes":{"viewer":{"rating":{"$numberDouble":"2.8"},"numReviews":{"$numberInt":"9"}},"production":"Pathè Frères","lastUpdated":{"$date":{"$numberLong":"1441993579000"}}}}
- API call function: API Route
- Code Execution at Server and Client: getServerSideProps()
- Next.js static generation (Server Side Rendering): getStaticProps()
- Client Side Fetching
-
4 commits, each one for approach. The fifth commit is just for update
index.tsx.
Date | Commit Message | Approach | Commit Hash |
---|---|---|---|
Jun 16, 2024 | first commit | API call function: API Route | c8111bc |
Jun 18, 2024 | getServerSideProps strategy | Code Execution at Server and Client: getServerSideProps() | 9169535 |
Jun 21, 2024 | static movies | Next.js static generation (Server Side Rendering): getStaticProps() | 58dd5a6 |
Jun 23, 2024 | Client Side Data Fetching | Client Side Fetching | 3644947x |
Next.js
uses the server API to handle backend logic, such as database queries, within the framework.
The server API handles a GET request to fetch the top 10 movies from a MongoDB
collection
. Here's how it works:
- The
clientPromise
retrieves theMongoDB
client. - The client connects to the "sample_mflix" database.
- A query is made to the
movies
collection, sorting by themetacritic
score and limiting the results to 10. - The results are sent as a
JSON
response to the client.
This server-side API provides secure and efficient data fetching.
within pages/api/ create movies.tsx
pages/api/movies.tsx
pages/api/Movies.tsx api
import clientPromise from "../../lib/mongodb";
import { NextApiRequest, NextApiResponse } from 'next';
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const client = await clientPromise;
const db = client.db("sample_mflix");
const movies = await db
.collection("movies")
.find({})
.sort({ metacritic: -1 })
.limit(10)
.toArray();
res.json(movies);
} catch (e) {
console.error(e);
}
}
within pages/api/ create comments.tsx
pages/api/comments.tsx
pages/api/Comments.tsx api
import clientPromise from "../../lib/mongodb";
import { NextApiRequest, NextApiResponse } from 'next';
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const client = await clientPromise;
const db = client.db("sample_mflix");
const comments = await db
.collection("comments")
.find({})
.sort({ date: -1})
.limit(10)
.toArray();
res.json(comments);
} catch (e) {
console.error(e);
}
}
Note
In Next.js with TypeScript (TSX), interfaces
define the structure of data objects, ensuring type safety.
For example, interface
Movie
describes a movie object, while interface
MoviesProps
specifies a component prop
holding an array of movies
. This prevents type errors and enhances code readability and maintainability.
pages/Movies.tsx server & render
import clientPromise from "../lib/mongodb";
import { GetServerSideProps } from 'next';
interface Movie {
_id: string;
title: string;
metacritic: number;
plot: string;
}
interface MoviesProps {
movies: Movie[];
}
const Movies: React.FC<MoviesProps> = ({ movies }) => {
return (
<div>
<h1>Top 20 Movies of All Time</h1>
<p>
<small>(According to Metacritic)</small>
</p>
<ul>
{movies.map((movie) => (
<li key={movie._id}>
<h2>{movie.title}</h2>
<h3>{movie.metacritic}</h3>
<p>{movie.plot}</p>
</li>
))}
</ul>
</div>
);
};
export default Movies;
export const getServerSideProps: GetServerSideProps = async () => {
try {
const client = await clientPromise;
const db = client.db("sample_mflix");
const movies = await db
.collection("movies")
.find({})
.sort({ metacritic: -1 })
.limit(20)
.toArray();
return {
props: { movies: JSON.parse(JSON.stringify(movies)) },
};
} catch (e) {
console.error(e);
return { props: { movies: [] } };
}
};
The code can be divided into two parts based on where they are executed:
Server-side Execution:
- The
getServerSideProps()
function is executed on the server side. - This function is a special Next.js function that runs** on the server for every request** and is used to fetch data before rendering the page.
- It fetches the necessary data from the MongoDB database and passes it to the React component as props.
- The server-side code is responsible for connecting to the
MongoDB
database, fetching the top 10 movies based on their metacritic score, and passing the fetched movies as props to theMovies
component.
Client-side Execution:
- The
Movies
component, which renders the list of movies, is executed on the client side. - This component receives the movie data as props from the server and renders it in the browser.
The
getServerSideProps()
function is executed on the server side because it needs to fetch data from the MongoDB database before the page is rendered. This ensures that the page is pre-populated with the necessary data when it is served to the client. On the other hand, the Movies component is executed on the client side because it is responsible for rendering the fetched movie data in the user's browser.
The clientPromise
is used to establish a connection to the MongoDB database. It is imported from the lib/mongodb module and is awaited to get the client instance, which is then used to interact with the database.
This approach fetches and renders the top 1000 movies on the server side using Next.js.
Server-side rendering (SSR) happens at the server level, sending fully-formed HTML to the client.
This approach:
- improves SEO, as search engines can index the content more easily.
- It also enhances performance by reducing the initial load time, as the client doesn't need to process JavaScript to render the page.
The getStaticProps
function connects to MongoDB, retrieves movie data, and passes it to the Top component for rendering. The component then displays the movies as a list with their titles, Metacritic scores, and plots.
pages/movies-static.tsx server & render
import { ObjectId } from "mongodb";
import clientPromise from "../lib/mongodb";
import { GetStaticProps } from "next";
interface Movie {
_id: ObjectId;
title: string;
metacritic: number;
plot: string;
}
interface TopProps {
movies: Movie[];
}
export default function Top({ movies }: TopProps) {
return (
<div>
<h1>Top 1000 Movies of All Time</h1>
<p>
<small>(According to Metacritic)</small>
</p>
<ul>
{movies.map((movie) => (
<li key={movie._id.toString()}>
<h2>{movie.title}</h2>
<h3>{movie.metacritic}</h3>
<p>{movie.plot}</p>
</li>
))}
</ul>
</div>
);
}
export const getStaticProps: GetStaticProps<TopProps> = async () => {
try {
const client = await clientPromise;
const db = client.db("sample_mflix");
const movies = await db
.collection("movies")
.find({})
.sort({ metacritic: -1 })
.limit(1000)
.toArray();
return {
props: { movies: JSON.parse(JSON.stringify(movies)) },
};
} catch (e) {
console.error(e);
return {
props: { movies: [] },
};
}
};
The Client Side Fetching approach is the React classical fetching data technique for the client-side seen in other labs.
This client component does the following:
- It defines a
Movie
interface to type the movie data. - It uses the
useState
hook to manage the state of the movies, loading status, and any potential errors. - It uses the
useEffect
hook to fetch the data from the/api/movies
endpoint when the component loads. - It handles loading and error states.
- Once the data is loaded, it renders the list of movies according to the specified format.
pages/movies-client.tsx server & render
import { useState, useEffect } from 'react';
interface Movie {
_id: string;
title: string;
metacritic: number;
plot: string;
}
export default function MoviesClient() {
const [movies, setMovies] = useState<Movie[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/movies')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch data');
}
return response.json();
})
.then(data => {
setMovies(data);
setIsLoading(false);
})
.catch(error => {
setError(error.message);
setIsLoading(false);
});
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Top 10 Movies of All Time</h1>
<p>
<small>(According to Metacritic)</small>
</p>
<ul>
{movies.map((movie) => (
<li key={movie._id}>
<h2>{movie.title}</h2>
<h3>Metacritic Score: {movie.metacritic}</h3>
<p>{movie.plot}</p>
</li>
))}
</ul>
</div>
);
}
http://localhost:3000/
http://localhost:3000/api/movies
http://localhost:3000/
http://localhost:3000/movies
http://localhost:3000/movies-static
http://localhost:3000/movies-client
http://localhost:3000/