Skip to content
Albert edited this page Jun 26, 2024 · 49 revisions

Project Overview

  • Create a NextJS project
  • Connect server NextJS to mongoDB
  • server NextJS publish API movies
  • client NextJS consumes API movies
  • client/server side rendering and data fetching

Tech stack

image

References

NextJS:

Tree-folder

Initial state (api):

image

Server/Client approach

image

Final state (data fetching from client):

image

A basic Next.js project structure for a tree folder project with MongoDB integration would include the following directories:

  1. .next: This is a generated folder that contains the build output of your Next.js application.
  2. lib: This folder is used to store utility functions and classes that can be reused across your application.
  3. 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.
  4. 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.
  5. node_modules: This folder contains all the project dependencies installed via npm.
  6. public: This directory contains static files that you want to be served directly by the server.
  7. 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.

image

NextJS

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.

Features NextJS

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.

Pages

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.

Create a project

//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

MongoDB

Mongo connection

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

MongoDB client

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;



Document movie

{"_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"}}}}

image

Data Fetching strategies/tools

  1. API call function: API Route
  2. Code Execution at Server and Client: getServerSideProps()
  3. Next.js static generation (Server Side Rendering): getStaticProps()
  4. 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

API call function

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:

  1. The clientPromise retrieves the MongoDB client.
  2. The client connects to the "sample_mflix" database.
  3. A query is made to the movies collection, sorting by the metacritic score and limiting the results to 10.
  4. 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);
    }
}

Code Execution at Server and Client

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 the Movies 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.

Server side static generation

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: [] },
       };
   }
};

Client Side Fetching

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:

  1. It defines a Movie interface to type the movie data.
  2. It uses the useState hook to manage the state of the movies, loading status, and any potential errors.
  3. It uses the useEffect hook to fetch the data from the /api/movies endpoint when the component loads.
  4. It handles loading and error states.
  5. 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>
  );
}

Output

http://localhost:3000/

image

http://localhost:3000/api/movies

image

http://localhost:3000/

image

http://localhost:3000/movies

image

http://localhost:3000/movies-static

image

http://localhost:3000/movies-client

image

http://localhost:3000/

image