Next.js Tutorial for Beginners

This is a complete Next.js tutorial for beginners and anyone looking to get started with the framework for the first time.

Next.js is an open-source React-based front-end framework. Next.js comes with features such as File System Based Routing, Server Side Rendering (SSR), Static Site Generator (SSG), Incremental Static Regeneration (ISR), Image Optimization, Code Splitting, Pre-fetching, Serverless Functions related to performance, SEO and efficiency of application development. Features like Fast Refresh are pre-built. 

Many of these features have to be implemented manually in other frameworks. Still, with Next.js, you can spend less time implementing cutting-edge features and instead focus on writing your application code.

Next.js is still evolving, and its functions are expanded with every version upgrade. In fact, Next is arguably the most frequently updated framework in this – Node.js – category.

This tutorial explains the basic functions of Next.js for those who have never touched Next.js but are interested in it or want to use it in the future. 

💡The current (stable) version of Next.js is 13.0

Here is a summary of what this tutorial will teach and cover:

  • How to create a Next.js project
  • How to create Static files
  • How to create Dynamic files
  • How to set links between pages
  • Layout settings in _app.js
  • Using an API to fetch and render data (getStaticProps, getServerSideProps)
  • How to use global.css and CSS modules
  • How to apply CSS with Tailwind CSS

In order to install Next.js, it is necessary to download and install Node.js on your machine.

Node.js download page
Node.js home page

Supported OS are MacOS, Windows (including WSL), and Linux are supported.

Creating a Next.js Project

Create a new Next.js project using the npx command, or you can use yarn, pnpm, etc.

% npx create-next-app@latest
Need to install the following packages:
  create-next-app
Ok to proceed? (y) y
✔ What is your project named? … new-app

// this will install all the required dependencies automatically

You will be asked for a project name, so enter any project name. Press Enter to proceed with the default name. An arbitrary name or my-app directory will be created in the directory where the command was executed .

This tutorial does not use TypeScript, but if you want to use TypeScript, run npx create-next-app and append --typescript; this will automatically change files from JS to JSX.

Error during installation (macOS)

The following error is for OSX users only. It is directly related to Command Line Tools, so you may have to reinstall them entirely.

Even if the installation process goes to the end and then the npm run dev command is executed, the initial screen of Next.js is displayed, but the following error is displayed in the installation log.

gyp: No Xcode or CLT version detected!
gyp ERR! configure error 
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onCpExit (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16)
gyp ERR! stack     at ChildProcess.emit (events.js:314:20)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)
gyp ERR! System Darwin 19.6.0
gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /Users/NextJS/Desktop/new-app/node_modules/fsevents
gyp ERR! node -v v14.7.0
gyp ERR! node-gyp -v v5.1.0
gyp ERR! not ok 

It means that there is a problem with the Command Line Tools, so check the path and delete it.

% xcode-select --print-path
/Library/Developer/CommandLineTools
% sudo rm -rf /Library/Developer/CommandLineTools

After deleting, you can execute the following command to reinstall the Command Line Tools:

% xcode-select --install

If you get the following error, “This software cannot be installed because it is not currently available from the software update server.”, you need to go to the Apple Developer site and grab the copy yourself.

You can then delete your new-app directory, and rerun npx create-next-app – the installation of your project should be complete without hiccups.

Starting the Next.js development server

When the installation is completed, the directory set by the project name will be created in the folder where the command was executed.

└─ yocto-queue@0.1.0
✨  Done in 7.18s.

Initialized a git repository.

Success! Created new-app-next at /Users/NextJS/Desktop/new-app-next

This is the directory we’ll be doing all the development work, so move to the project directory new-app and execute the npm run dev command. If you check the logs after running the command, you can see that the server is running at localhost:3000.

% cd new-app
% npm run dev

> new-app-next@0.1.0 dev
> next dev

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 2.2s (165 modules)

Start your browser and access http://localhost:3000 to display the welcome screen.

Welcome to Next.js
Next.js Welcome Page on First Launch

Next.js installation is now up and running in your dev environment.

By checking the package.json file in the project, you can check the commands that can be used during development (dev, build, etc.) and the installed Next.js version.

{
  "name": "new-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "12.3.1",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "eslint": "8.24.0",
    "eslint-config-next": "12.3.1"
  }
}

Welcome to Next.js

This section introduces directory structure, page creation, and dynamic naming.

Directory Structure

Once your project is up and running, you can check your app’s folder to glance at the directory structure that Next.js is using.

Four directories, pages, styles, node_modules, and public, are available immediately after the project is created. There are also three other files: README.md, package.json, and package-lock.json

There is also a .next directory and a .gitignore file with a .(dot) at the beginning.

The page content displayed in the browser is described in the index.js file in the /pages directory. You will write the core code of your application in this directory.

NextJS default directory structure
NextJS default directory structure

You can also access the files stored in the /public directory, directly from the browser.

In other words, it is possible to serve static files (such as index.html) and render them without touching the .js files.

CSS files are stored under the /styles directory. You can apply styling using CSS files placed under the public directory and specifying it with a link tag instead of a JavaScript bundle.

Updating index.js (Fast Refresh)

Any changes you make to the index.js file will be automatically rendered in the browser as long as your server is running.

You can try it yourself by changing any of the lines in the default file.

/* <h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1> */

<h1 className={styles.title}>
Fast Refresh <a href="https://nextjs.org">Next.js!</a>
</h1>

Because React is bundled into Next.js by default, it’s unnecessary to import React.

If you run npm run dev, updating index.js will automatically update the page in your browser. This is a feature Next.js calls Fast Refresh

This very convenient function improves developers’ efficiency because updates are reflected immediately without reloading the page.

Hello Next.js

The next step is to remove everything inside the index.js file and replace it with the following:

export default function PreRender() {
  return <h2>Next.js Tutorial for Beginners</h2>;
}

Once you have done this, go back to localhost:3000 and view the page source.

page source for index.js

You can see that the string “Next.js Tutorial for Beginners” is included in the page source. Since index.js is a JavaScript file, normally, the browser receives the JavaScript file, processes the JavaScript file, and displays its contents. If you look at the browser page source, you can see that the browser directly receives HTML information (h2 tag). 

This is because Next.js does pre-rendering on the server side before sending it to the browser. Since the HTML information is received as is, there is no need to process JavaScript on the browser side to display “Next.js Tutorial for Beginners”. By default, Next.js does pre-rendering on every page. Pre-Rendering is a function that creates a page in advance on the Next.js side (Server-Side Rendering) and sends the created page to the client before processing with JavaScript on the client side.


To further illustrate this concept, we can create a new React project and compare the difference. Take the “Next.js tutorial” function above and place it in your React project’s index.js file. Then access the URL to render the function. It will work as it does in Next, but if you inspect the page source and then search for the string you entered, it will not be shown in the page source. This is because React uses Client-Side Rendering.

Next.js provides three different ways to pre-render files, including Static Generation (SSG), Incremental Static Regeneration (ISR), and Server Side Rendering (SSR).

Creating a Page (Static Routing)

With Next.js, page routing is set automatically (File System Based Routing), so if you go ahead and create a new file called contact.js in your /pages directory, you can then access that file by going to http://localhost:3000/contact directly.

export default function Contact() {
  return <h2>My contact address is: root@localhost</h2>;
}

And we can confirm that it works:

contact.js rendered without manual routes

Next.js doesn’t use React Router.

Functions can be written in numerous ways, so it’s up to you decide which one is you like the best.

function Contact() {
  return (
    <h2>Contact Us</h2>
  )
}

export default Contact

// Arrow function
const Contact = () => {
  return (
    <h2>Contact Us</h2>
  )
}

export default Contact

What about 404 pages? With the current configuration, anything that isn’t the root directory (/) or the pages we’ve created – /contact – will return a 404 error.

404 not found

Let’s create a new folder inside our /pages directory called blog, and then create a file inside that directory called post.js.

export default function BlogPost() {
  return <h2>The title for my #1 blog post</h2>;
}

Once you do, you can go to http://localhost:3000/blog/post and the page will be rendered, showcasing how Next.js uses Nested Routes for page/directory hierrarchy.

post.js

Creating Dynamic Files (Dynamic Routing)

Let’s learn about Dynamic Routing in Next.

For example, if you have an eCommerce store, you’re going to have a products page, and within the scope of that page you will also have the actual products themselves.

Should you be creating a phone.js, computer.js file every time you add a new product?

Thanks to Dynamic Routing, we can create a single file and then serve request content based on the URL itself.

First, let’s create a new folder called /products in our /pages folder, and then create a file called [name].js in the folder we just created.

export default function ProductName() {
  return <h2>Viewing Product: </h2>;
}

You can now go to http://localhost:3000/products/computer and replace computer with any string, and the page will be rendered using the functions defined in the [name].js file.

Dynamic Routing in Next.js
Dynamic Routing in Next.js

Even if you change the URL to phone or tablet, the same “Viewing Product: ” is displayed, so the next step is to use the useRouter Hook so we can dynamically display the product string inside the page content. By using useRouter Hook, you can dynamically change the content of the page according to the accessed URL. Using the useRouter Hook, you can access the router object that has information about routing from the function component.

The useRouter Hook imports from next/router:

import { useRouter } from "next/router";

export default function ProductName() {
  const router = useRouter();
  return <h2>Viewing Product: {router.query.name}</h2>;
}

The character string included in the URL can be obtained from router.query.name.

You can now refresh the page to see the new result:

useRouter Hook
NextJS useRouter Hook

We can also use console.log to check the data stored in the router.query.

import { useRouter } from "next/router";

export default function ProductName() {
  const router = useRouter();
  console.log(router.query);
  return <h2>Viewing Product: {router.query.name}</h2>;
}

If you check the console of the browser’s developer tools, you can see that the name is included in the object, as shown below:

name: "computer"
[[Prototype]]: Object

Also, even if you add a parameter to the URL, you can get the value of the added parameter from router.query.

You can try it with: http://localhost:3000/products/computer?gpu=amd

query string parameters

In addition, Dynamic Routing can be used even when the page hierarchy is deep.

Create a [name] directory under the products directory. 

Then, create a [gpu].js file under the [name] directory.

import { useRouter } from "next/router";

export default function ProductGPU() {
  const router = useRouter();
  console.log(router.query)
  return <h2>Viewing Product: {router.query.name} with {router.query.gpu} GPU</h2>;
}

You can now access http://localhost:3000/products/computer/amd to see how it works.

query string deep hierarchy
query string deep hierarchy

The [gpu].js file can also be written using the destructuring assignment.

import { useRouter } from "next/router";

export default function ProductGPU() {
  const router = useRouter();
  const { name, gpu } = router.query
  return <h2>Viewing Product: { name } with { gpu } GPU</h2>;
}

Linking to Pages

In Next, managing links is done using the Link component.

We use it as you would imagine – to move from one page to the next. And seeing how we have so many pages already, we might as well create links to them since there is no other way to access them unless you share them directly.

Here is a simple function to let us navigate to the contact.js page.

import Link from "next/link";

export default function Home() {
  return (
    <div>
      <ul>
        <li>
          <Link href="/contact">
            <a>Contact</a>
          </Link>
        </li>
      </ul>
      <h2>Welcome to my homepage!</h2>
    </div>
  );
}

The Contact string has an a tag, but if you want to set a CSS class, use the className attribute in the a tag instead of the Link tag. 

If you save that and open it in the browser, you can click on the Contact link and see that Next preloaded the page, and you were able to access it instantly without a refresh.

Creating links using the Link component

You can also take out the <Link /> component entirely and then use a traditional HTML link; you should immediately be able to see that without the Link component, the page refreshes rather than loads instantly.

Since we talked about Dynamic Routing already, it is possible to set the href value to use Objects as well as paths.

import Link from 'next/link';

export default function Home() {
  return (
    <div>
      <ul>
        <li>
          <Link
            href={{
              pathname: '/contact',
              query: { name: 'admin' },
            }}
          >
            About
          </Link>
        </li>
      </ul>
      <h2>Welcome to my homepage!</h2>
    </div>
  );
}

This same principle can be applied by using an array to define the final destination for each page, and the same concept of loading pages without refreshing them is applied.

import Link from "next/link";

const products = [{ name: "phone" }, { name: "computer" }, { name: "tablet" }];
export default function Home() {
  return (
    <div>
      <ul>
        {products.map((product) => {
          return (
            <li key={product.name}>
              <Link href={`/products/${product.name}`} >
                <a>{product.name}</a>
              </Link>
            </li>
          );
        })}
        <li>
          <Link href="/contact">
            <a>Contact</a>
          </Link>
        </li>
      </ul>
      <h2>Welcome to my homepage!</h2>
    </div>
  );
}

You now have a functional way to link to your pages dynamically:

Linking to multiple pages

Understanding prefetch

In addition to href, the Link component props include prefetch, etc.

By default, prefetch is set to true. The link automatically downloads the linked JavaScript file when it enters the browser’s viewport. Since it is impossible to check the operation in the development environment (npm run dev), we will run it in the production environment (npm run build && npm run start). 

npm run build builds the created project. npm run start will do the build, and the production server will start.

Make sure you save the file below and then do the build and start commands.

After, open localhost:3000, but don’t scroll down before you open Developer Tools -> Network. Once you do, you can scroll down.

import Link from "next/link";

const products = [{ name: "phone" }, { name: "computer" }, { name: "tablet" }];
export default function Home() {
  return (
    <div>
      <ul>
        {products.map((product) => {
          return (
            <li key={product.name}>
              <Link href={`/products/${product.name}`} >
                <a>{product.name}</a>
              </Link>
            </li>
          );
        })}
        <li style={{ marginTop: '100em' }}>
          <Link href="/contact">
            <a>Contact</a>
          </Link>
        </li>
      </ul>
      <h2>Welcome to my homepage!</h2>
    </div>
  );
}

Because our margin from the top is set to 100em, you need to scroll down the page, and as you reach the Contact link – you should see that the page prefetched the contact.js document.

It’s also possible to flag prefetch as false, in this case, the file will only be requested whenever you hover over the link.

<Link href="/contact" prefetch={false}>

Layout Structure

When developing an application with multiple pages, some page elements, such as Headers and Footers, will (almost) always remain the same. Furthermore, designing certain content areas separately means you won’t need to update pages individually when you make big changes. So let’s create a basic layout structure.

First, create a components directory (in your app directory) and create a Layout.js file to set the default layout for the entire application.

import Header from './header';
import Footer from './footer';

export default function Layout({ children }) {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  );
}

In the same directory, create header.js and footer.js files. You can also add a preliminary header style by linking to some of the pages we’ve already created.

import Link from 'next/link';

export default function Header() {
  return (
    <ul>
      <li>
        <Link href="/">
          <a>Home</a>
        </Link>
      </li>
      <li>
        <Link href="/contact">
          <a>Contact</a>
        </Link>
      </li>
    </ul>
  );
}

And also a basic footer:

export default function Footer() {
  return (
    <div>
      <p>&copy; 2022</p>
    </div>
  );
}

After creating the Layout.js file and related files, open the _app.js file (inside /pages folder), import the Layout component, and apply the settings as shown below. 

The _app.js file is the application’s entry point, and this file wraps all pages. It can be used when applying common components to all pages.

import '../styles/globals.css';
import Layout from '../components/layout';

export default function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

You can now go to localhost:3000 to see the changes.

Header and Footer components applied

All the links are working as intended.

Preserving the State

Do you understand the difference is between setting Layout in index.js and contact.js instead of setting it in _app.js? Let’s look at why it’s important.

Go ahead and change your index.js and contact.js files as shown below:

import Layout from '../components/layout';

export default function Home() {
  return (
    <Layout>
      <h2>Welcome to my homepage</h2>
    </Layout>
  );
}
import Layout from '../components/layout';

export default function Contact() {
  return (
    <Layout>
      <h2>My contact address is: root@localhost</h2>
    </Layout>
  );
}

Then restore the _app.js file to its original state.

import '../styles/globals.css';

export default function MyApp({ Component, pageProps }) {
  return (
    <Component {...pageProps} />
  );
}

If you check the browser’s response, the site’s appearance will not change, and the initial functionality is still there. Even if you do not set Layout in _app.js, the above settings simply work. However, the difference in behavior becomes clear when you have variables such as useState in your Layout files.

Let’s apply the useState Hook in the footer.js file and create a simple button counter:

import { useState } from 'react';

export default function Footer() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <div>
        <button onClick={() => setCount(count + 1)}>Click me!</button>
      </div>
      <p>&copy; 2022</p>
    </div>
  );
}

Click the button to increase the count number.

NextJS button counter
Counter in the footer.

Make sure you clicked it a few times and now navigate to the Contact page.

nextjs counter reset
Moving to a different page, the counter reset back to 0

Everything works fine, except the counter has been reset back to zero.

Let’s go ahead and unset the Layout from the index.js and contact.js files, and reassign it to the _app.js file.

import '../styles/globals.css';
import Layout from '../components/layout';

export default function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

Click the button a few times, and then change the page by clicking on Contact or whatever else page you’re working on.

As you’ll see, the State of the counter is retained even if you change pages.

next.js preserving state

As such, defining Layout in _app.js helps us wrap the entire layout without losing state, such as input values.

Different Layout for Each Page

If you’d like to use a different layout for various pages, the way to do so is through the getLayout property.

Import the Layout component in contact.js and set the getLayout property to a function.

import Layout from '../components/layout';

export default function Contact() {
  return <h2>My contact address is: root@localhost</h2>;
}

Contact.getLayout = function getLayout(page) {
  return <Layout>{page}</Layout>
};

Update _app.js as shown below. If the Component has the getLayout property, the getLayout function set in contact.js will be executed, and if there is no getLayout, the component will be returned as is.

export default function MyApp({ Component, pageProps }) {
  const getLayout = Component.getLayout || ((page) => page);
  return getLayout(<Component {...pageProps} />);
}

If you go to localhost:3000/contact you’ll see that the Header and Footer are displayed, however, because getLayout has not been set elsewhere, other pages will not inherit either.

As such, if you’d like to use a different layout for each page, you have to import Layout and then set getLayout explicitly.

Image Display

Next has a built-in Image component for displaying images. For this demo, you can take any image you like, and then save it in the public directory.

To use the Image component, you need to import it from next/image, and set the width and height of props.

import Image from "next/image";

export default function Home() {
  return (
    <div>
      <h2>Welcome to my homepage</h2>
      <Image src="/demo.jpeg" width={500} height={300} />
    </div>
  );
}

/* keep in mind we're working with Layout being defined in _app.js */

If you save this and check the homepage, the image should show up:

next/image component
next/image component

An alternative is to use an image from an external site, such as Pexels or Unsplash:

import Image from "next/image";

export default function Home() {
  return (
    <div>
      <h2>Welcome to my homepage</h2>
      <Image
        src="/https://images.pexels.com/photos/9832430/pexels-photo-9832430.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1demo.jpeg"
        width={500}
        height={300}
      />
    </div>
  );
}

If you check the homepage, you should see the following error:

Unhandled Runtime Error

Error: Invalid src prop (https://images.pexels.com/photos/9832430/pexels-photo-9832430.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1demo.jpeg) on next/image, hostname “images.pexels.com” is not configured under images in your next.config.js

This error comes up because the external domain has not been registered in the next.config.js file.

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  images: {
    domains: ["images.pexels.com"],
  },
};

module.exports = nextConfig;

Once you update next.config.js you need to rerun npm run dev to reload the config. Your external images will appear on the pages where you have placed them.

SEO: Setting Up Meta Tags

It is necessary to set meta tags for search engines to understand the structure of your site. In Next, meta tags are handled using the Head component.

Here is an example of setting a page title:

import Link from 'next/link';
import Head from 'next/head';

export default function Home() {
  return (
    <div>
      <Head>
        <title>My page title</title>
      </Head>
      <ul>
        <li>
          <Link href="/about">
            <a>About</a>
          </Link>
        </li>
      </ul>
      <h1>Hello Next.js</h1>
    </div>
  );
}

You can reload localhost:3000 to confirm, and the title tag is also shown in the page source. Now, the Head component offers many more ways to specify SEO tags, like so:

<Head>
  <title>My page title</title>
  <meta name="description" content="Description about my site." />
  <meta property="og:title" content="Social Media Title" />
  <meta property="og:description" content="Social Media Description" />
</Head>

In the above example, the value was set directly, but the title must be shown dynamically on individual pages (blog, products, etc.). 

Normally, settings are made using the meta information of individual pages included in props, but we can also use arrays that we defined in an earlier example of product pages.

import Link from 'next/link';
import Head from 'next/head';

const products = [{ name: "phone" }, { name: "computer" }, { name: "tablet" }];

export default function Home() {

  return (
    <div>
      <Head>
        <title>{products[0].name}</title>
        <meta name="description" content={products[0].name} />
        <meta property="og:title" content={products[0].name} />
        <meta
          property="og:description"
          content={products[0].name}
        />
      </Head>
     </div>
);
}

Importing Data from an API (SSG, SSR)

So far, we’ve looked at many basic Next.js concepts, but one of the main draws behind Next and many similar frameworks is the ability to import external data from an API. And, on top of that, be able to serve that data through methods such as Server-Side Rendering, Static Site Generation, and Incremental Static Regeneration.

For this segment, we are going to use the JSONPlaceholder API.

JSON Placeholder API

We will check how to display it on the browser using the data obtained from the outside. I will use the JSONPlaceholder service to get the data.

The goal is to use this API to fetch the placeholder data and display it in our Next.js application.

As mentioned earlier, Next.js has 3 methods (SSG, SSR, ISR) for fetching data and Pre-Rendering on the server side. For this tutorial, we will focus on 2 different methods:

  • getStaticProps (Static Site Generation)
  • getServerSideProps (Server-Side Rendering)

getStaticProps fetches data only once at build time with Static Site Generation (SSG) to pre-render the page. getServerSideProps does Server-Side Rendering (SSR), which acquires and pre-renders data on the server side when accessed from the client. getStaticProps and getServerSideProps cannot be executed in any file, only in the page file and not in the component file.

SSG is used when pages such as blog articles are not frequently added or updated. SSR can be used when the content displayed changes depending on the request, such as a search page. SSG creates an HTML page at build time and caches it on the CDN (Content Delivery Network), so the page is displayed immediately. 

Is it possible to get data from external sources only on the server side? 

It is possible to get data on the client side instead of the server side. Next.js provides a React Hook library called SWR (stale-while-revalidate) that can be used to acquire data on the client side, so in this segment, we will also look at how to acquire data using SWR.

Ultimately, the goal of SSR, SSG, ISR, and SWR is to speed up your application and the pace at which you develop and implement new features.

Method #1 – getServerSideProps

Create a new posts directory and create an index.js file.

export default function index({ posts }) {
  return (
    <div>
      <h2>Posts API</h2>
    </div>
  );
}

export async function getServerSideProps() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  console.log(posts);
  return { props: { posts } };
}

If you open localhost:3000/posts – you will see that nothing happened.

In fact, despite having specified console.log(posts) – the data fetched from the API is not shown in the Developer Console (Browser).

This is because getServerSideProps renders on the server side.

But if you go to your Terminal where your dev environment is running, you will see that the API data is shown there.

nextjs server side rendering terminal

Now that we have confirmed that the data is being pulled, we can use the map function to expand and present the data on our page.

export default function index({ posts }) {
  return (
    <div>
      <h2>Posts API</h2>
      <ul>
        {posts.map((post) => {
          return <li key={post.id}>{post.title}</li>;
        })}
      </ul>
    </div>
  );
}

export async function getServerSideProps() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  console.log(posts);
  return { props: { posts } };
}

If you check your browser, the full list of posts is now rendered on the page.

POST list is displayed

Important to note that getServerSideProps can be executed in page components, but not in regular components. You can instead, acquire the API data in the page component and pass the acquired data to other components in props.

Creating and Displaying Individual Pages

The next step is to retrieve individual posts and display them using Dynamic routing.

Create a [post].js file under /posts.

export default function post() {
  return <h2>Posts API</h2>;
}

Next, we will import the Link component and update the display structure so that individual links (posts) can be clicked on.

import Link from "next/link";

export default function index({ posts }) {
  return (
    <div>
      <h2>Posts API</h2>
      <ul>
        {posts.map((post) => {
          return (
            <li key={post.id}>
              <Link href={`/posts/${post.id}`}>
                <a>{post.title}</a>
              </Link>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

At this point, we’ve created a route to the individual posts, but if you go to localhost:3000/posts/1 – the page will be blank outside of the default Layout.

Obtaining the Individual IDs

You will need to write a separate getServerSideProps function to get the ID of the individual post in the API. Once again, this renders on the server side, and every time you access a unique ID, you’ll get a { post: '1' } response in your Terminal.

export async function getServerSideProps({ params }) {
  console.log(params);
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { props: { posts } };
}

We got the ID value in params because we used dynamic routing, but you can also get values ​​such as req, res, and query in addition to params. If you want to check those values, put context in the argument of getServerSideProps and check with console.log(context)

req is an abbreviation for request, and you can also check the client information and header information that has been accessed.

export async function getServerSideProps(context) {
  console.log(context);
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { props: { posts } };
}

For beginners, even if you look at the context, there is too much information to process, so don’t worry too much about it. 

Just keep in mind the various types of information you can get like this.

Get the Data for Individual IDs

Now we’ll grab the data inside the ID saved in params.

export default function post({ post }) {
  return (
    <div>
      <h1>Post ID: {post.id}</h1>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </div>
  );
}

export async function getServerSideProps({ params }) {
  const id = params.post;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post = await res.json();
  return { props: { post } };
}

Now, if you go to localhost:3000/posts/42 , you’ll see the data being fetched and rendered.

Obtaining Data for Individual IDs
Obtaining Data for Individual IDs

ID Not Found (404)

What happens when a request is made to an ID that doesn’t exist in the API?

In this case, JSONPlaceholder has a limit of 100 posts, and if you go to localhost:3000/posts/101 it will return a blank page.

Let’s use console.log to check what kind of data is obtained when an ID that does not exist is accessed.

export async function getServerSideProps({ params }) {
  const id = params.post;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post = await res.json();
  console.log(post);
  return { props: { post } };
}

If you look at your Terminal, you can see that it is an empty object {}.

When executing getServerSideProps, an object with props was returned in return, but you can also return an object with notFound in addition to props. 

notFound can have true and false values, but setting it to false will result in an error.

return {
  notFound: true,
};

Since it is confirmed that it will be an empty object accessed with an ID that doesn’t exist, set it so that an object with notFound is returned when judging an empty object.

export async function getServerSideProps({ params }) {
  const id = params.post;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post = await res.json();
  if (!Object.keys(post).length) {
    return {
      notFound: true,
    };
  }
  return { props: { post } };
}

If you visit localhost:3000/posts/101 again, you will see a 404 error.

404 Page not Found

Method #2 – getStaticProps

getServerSideProps gets data on the server side for each request, but getStaticProps gets data on the server side at build time. Since the code executed by getStaticProps is not included in the JavaScript bundle code sent to the client side, it is also not possible to check the contents of the code on the client side.

Changing to getStaticProps

Change getServerSideProps to getStaticProps in the index.js file in the posts directory.

export async function getStaticProps() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { props: { posts } };
}

Even while running npm run dev – the change of method was applied to the /posts index page.

Now change from getServerSideProps to getStaticProps in the [post].js file as well.

export async function getStaticProps({ params }) {
  const id = params.post;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post = await res.json();
  return { props: { post } };
}

In the case of index.js, the data was displayed in the same way just by changing from getServerSideProps to getStaticProps, but an error is returned in the [post].js file:

getStaticPaths is required for dynamic SSG pages
getStaticPaths is required for dynamic SSG pages

The error states that getStaticPaths is required for dynamic SSG pages.

Getting the Path Information with getStaticPaths

Let’s add the getStaticPaths method as shown in the error message. 

In the getStaticPaths method, you need to create a path list of pages to be created at build time and return with paths. Like getStaticProps, getStaticPaths is also executed on the server side, so it is not included in the JavaScript bundle code sent to the client side, so it cannot be executed on the client side and the contents of the code cannot be seen.

export async function getStaticPaths() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  const paths = posts.map((post) => `/posts/${post.id}`);
  return {
    paths,
    fallback: false,
  };
}

Here is a summary of what is happening in the above function:

  1. Access JSONPlaceholer to get a list of posts,
  2. expand the acquired posts with the map function,
  3. extract the id of individual pages,
  4. and create a path /posts/${post.id}.

If you can get the path information of the individual page with getStaticPaths, the page will be displayed without error even if you access the individual page.

The function can also be written as follows:

export async function getStaticPaths() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  const paths = posts.map((post) => ({
    params: { post: post.id.toString() },
  }))
  return {
    paths,
    fallback: false,
  };
}

getStaticPaths has the role of passing the path information of the page to be created, so if there is no getStaticPaths, there is no path information, so an error like the one above will occur.

fallback Settings

The fallback can be set to true or false; if set to false, a 404 error will be displayed when accessing a page other than an existing one.

If fallback is set to true, a server error screen will be displayed.

TypeError: Cannot read properties of undefined (reading 'id')
fallback: true;

If a page that did not exist at the time of build is added, by setting fallback to true, when the page is accessed, the page is created, and the created page can be returned to the user.

Using the router’s isFallback property, “Loading…” can be displayed on the screen before the page is created. When the page is created, “Loading…” will disappear from the screen and display the created page. “Loading…” is displayed for a moment. isFallback is true at first, but when the page is created, it becomes false, and “Loading…” disappears.

if (router.isFallback) {
    return <div>Loading...</div>
}

If you access a page that has not been added, the error “Unhandled Runtime Error: Failed to load static props” will be displayed on the screen after “Loading…” is displayed.

You can also set “blocking” for fallback, and if you set blocking, it will wait until the page is created, so errors will not be displayed. The value of isFallback remains false when blocking, so there is no need to set router.isFallback, and when blocking, “Loading…” will not be displayed on the screen.

Executing npm run build

GetStaticProps create static pages when building, so let’s run npm run build

You can check from the build log that the static pages are generated in real-time.

% npm run build

info  - Generating static pages (108/108)
info  - Finalizing page optimization  

Route (pages)                              Size     First Load JS
┌ ○ /                                      4.27 kB        84.2 kB
├ ● /posts (309 ms)                        372 B          80.3 kB
└ ● /posts/[post] (6364 ms)                329 B          80.3 kB
    ├ /posts/1
    ├ /posts/2
    ├ /posts/3
    └ [+97 more paths]

In our scenario, because the API we’re using has 100 posts inside of it, the build command will create 100 .HTML (static) files and 100 .JSON files.

You can verify this by going to .next/server/pages/posts.

Lastly, if you execute npm run dev, all the files will be deleted. This is why it’s important to separate your dev environment from the production environment.

Retrieving Data with SWR (Client)

SWR is a state-while-revalidate React Hooks library for data fetching on the client side. It is developed and maintained by Vercel. Let’s install it first and look at some examples of how to use it.

% npm install swr

added 1 package, and audited 272 packages in 1s

To use it, import useSWR from swr and set the key to the first argument of useSWR Hook and the fetcher function to the second argument. Set the URL for the key. Worth noting that since not only data but also errors are returned, error handling can be done easily.

import useSWR from 'swr'

const { data, error } = useSWR(
  'https://jsonplaceholder.typicode.com/posts',
  fetcher
);

It is also possible to set options in the third argument.

The fetcher function describes the data acquisition process using the URL of the first argument.

const fetcher = (url) => fetch(url).then((res) => res.json());

Using these processes, the index.js file in the /posts directory can be specified as shown below.

An error message will be displayed on the browser if an error is returned. Since the value of data is undefined while data is being acquired, the string “Loading…” is displayed in the browser.

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function index() {
  const { data, error } = useSWR(
    "https://jsonplaceholder.typicode.com/posts",
    fetcher
  );

  if (error) return <div>Failed to load.</div>;
  if (!data) return <div>Loading...</div>;

  return (
    <div>
      <h2>Posts API using SWR</h2>
      <ul>
        {data.map((post) => {
          return <li key={post.id}>{post.title}</li>;
        })}
      </ul>
    </div>
  );
}

The information obtained from JSONPlaceholder will be displayed if you check your browser. 

In this way, you can use SWR to retrieve data on the client side.

Fetching API using SWR
Fetching API using SWR

In the case of data acquisition processing on the client side, SWR is not required, and it is possible to acquire data using useState, useEffect, etc. In addition to this, SWR has various functions, such as caching data based on the key of the first argument of useSWR, and automatically re-acquiring data when you remove the cursor from the browser and click the browser again.

By checking the network tab of the browser’s developer tools, you can confirm that data reacquisition is automatically performed.

Setting environment variables

.env.local file

You can use the .env.local file if you want to use environment variables instead of writing values directly in the code, such as API_KEY and database information.

You can create a .env.local file in your project directory and set the environment variables as follows.

API_KEY=apikey
DB_HOST=localhost
DB_USER=user
DB_PASS=password

If you want to use it in your code, you can get the values ​​with process.env.API_KEY and process.env.DB_HOST.

If you want to use it with getServerSideProps, you can do it like so:

export async function getServerSideProps() {
  const api_key = process.env.API_KEY;
  const result = await fetch(
    `https://api.url/?api_key=${api_key}`
  );

Getting values ​​from the .env.local file is possible when executed on the server side, such as when you’re using getServerSideProps, but if you want to use it on client-side, you need to add NEXT_PUBLIC_.

NEXT_PUBLIC_API_KEY=apikey
API_KEY=apikey
DB_HOST=localhost
DB_USER=user
DB_PASS=password

And it can be called using process_env.NEXT_PUBLIC_API_KEY from your app code.

useEffect(() => {
 const fetchData = async () => {
 const api_key = process.env.NEXT_PUBLIC_API_KEY;

If the same environment variable is used but can be obtained in one process but not another, check whether the process is performed on the server or the client side. If you can’t tell the difference, try setting NEXT_PUBLIC_ in the app code and test it.

If you add an environment variable and the value is undefined, re-run the npm run dev command.

Other environment variables

In addition to .env.local, you can create files named .env.development (for dev environment) and env.production (for the production environment) to save environment variables. 

If you add environment variables with the same name to .env.local and .env.development in your development environment and set different values, they will be overwritten by .env.local

The goes for .env.production which is overridden by .env.local.

The .env.local file is specified in the .gitignore file, so it will not be uploaded even if you push it with git.

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

In the .gitignore file, you can see the files .env.development.local and .env.production.local are different from the files explained earlier. This can be used in development and production environments respectively, and the value you set will take precedence over .env.local.

You can also use .env.test and .env.test.local files for testing.

In the dev environment, .env.development.local has the highest priority.

Applying CSS with the global.css file

CSS in Next.js can be applied in several ways. It can also be applied using globals.css in the styles directory. Any name other than global.css can be used.

The globals.css file is imported in the _app.js file under the pages directory in any Next.js project.

Go ahead and open the global.css file and apply the following style:

.home-heading {
  color: orange;
}

Then apply the .home-heading class to your h2 using className.

export default function Home() {
  return (
    <div>
      <h2 className="home-heading">Welcome to my homepage</h2>
    </div>
  );
}

If you check your browser, you can see that the heading parameters specified in globals.css have been applied.

Styling with global.css

An error will occur if you try to Import global.css anywhere other than _app.js.

So, what to do if you want to apply custom CSS to a specific component?

Applying CSS with module.css files

Whenever you create a new Next.js project – in your /styles directory, you will find two files: global.css and Home.module.css. This is also known as CSS Modules for styling. The way it works is that a Module will apply its syntax at the beginning of the class name, so even if you use the same class name elsewhere – it will have no effect other than on the custom component that you’re using the CSS Module on.

global.css can be imported from _app.js and applied to the entire application. 

The {name}.module.css is used to apply CSS to specific components. 

Here is how you import custom modules:

import styles from "../styles/Home.module.css";

Imported styles become objects and set classes are registered as object properties. 

If you want to apply a class using the styles imported from the Home.module.css file, you can do the following:

import styles from "../styles/Home.module.css";

export default function Home() {
  return (
    <div>
      <h2 className={styles.heading}>Welcome to my homepage</h2>
    </div>
  );
}

You can place a custom style for your .heading in Home.module.css.

.heading {
  background-color: indigo;
  color: #fff;
  padding: 1.5rem;
  border: 1px dotted yellow;
}
CSS Modules in Next.js
CSS Modules in Next.js

You can verify the custom class names by placing console.log(styles) before the Home() function, then open Console from your Browser’s developer tools.

{
    "heading": "Home_heading__BTwrO",
    "container": "Home_container__bCOhY",
    "main": "Home_main__nLjiQ",
    "footer": "Home_footer____T7K",
    "title": "Home_title__T09hD",
    "description": "Home_description__41Owk",
    "code": "Home_code__suPER",
    "grid": "Home_grid__GxQ85",
    "card": "Home_card___LpL1",
    "logo": "Home_logo__27_tb"
}

Using Tailwind CSS with Next.js

You can also follow along the official Tailwind CSS docs on how to install Tailwind for Next.js projects.

Let’s install the package:

% npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

added 41 packages, changed 1 package, and audited 271 packages in 5s

After installing the package, create the Tailwind CSS configuration file:

% npx tailwindcss init -p
  
Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

Open the tailwind.config.js file to confirm.

module.exports = {
  content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  darkMode: media,
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Write the @tailwind directive for Tailwind CSS in the global.css file in the /styles directory. 

By default, global.css is imported into the _app.js file.

@tailwind base;
@tailwind components;
@tailwind utilities;

The setup for using Tailwind CSS is now complete. If you like, you can take the following function below to see that it works in practice.

export default function Home() {
  return (
    <div>
      <h2 className="text-2xl font-bold text-gray-700 dark:text-white hover:text-gray-600 dark:hover:text-gray-200 hover:underline">
        Tailwind CSS in Next.js
      </h2>
    </div>
  );
}

Customization with _document.js

In Next, tags such as Body, Head, and Script are automatically set in your index.js file. And with _document.js you can customize Global settings for all your pages/application.

Speaking of common settings for all pages, there is _app.js which used for layout settings. In _app.js, settings are reflected only in the body tag, but in _document.js, changes can be made to the HTML and Head tags. It is also possible to add components inside the Body tag in _document.js.


Let’s create a _document.js file in the /pages directory. 

Then, you can take the function below and place it in that file.

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <div id="portal"></div>
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

/* this function is a snippet from the official Next.js docs */

We know that the _document.js file customizes the HTML tag, but you may be wondering what would happen if we removed the Main tag in the _document.js file. 

Let’s delete it and check.

If you delete the Main tag, the content written in the index.js file will not be displayed. From this, we can see that the content of the index.js file is displayed in the Main tag. The Head tag and NextScript tag written in _document.js, not the Main tag, are required, so if you delete them, the page will not be displayed due to an error.

Since the _document.js file is processed on the server side, it cannot be executed even if a click event is set. Instead, if you want to use React’s Portal feature for things like modal windows, you can add <div id=”portal”></div> before the closing body tag.

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html lang="ja">
        <Head />
        <body>
          <Main />
          <div id="portal"></div>
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Looking at the source, the script is registered by the NextSciprt tag before the closing tag of the body tag, but the added div id=”portal” element is below the <div id=”_next”></div> tag.

<body>
    <div id="__next"></div>
    <div id="portal"></div>
</body>

Font Settings

You can also use the _document.js file if you want to use a custom font from sites like Google Fonts.

First, search through Google Fonts to find the font you want. My preference is Outfit, but it doesn’t matter. Click on the font you want, select the style and grab the details.

Google Fonts – Outfit

Copy the <link> tag and place it directly in your _document.js file. Like so:

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <link rel="preconnect" href="https://fonts.googleapis.com" />
          <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
          <link
            href="https://fonts.googleapis.com/css2?family=Outfit&display=swap"
            rel="stylesheet"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

You can then define Outfit in the global.css file.

html,
body {
  padding: 0;
  margin: 0;
  font-family: Outfit, -apple-system, BlinkMacSystemFont, Segoe UI,
    Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

If global.css is not imported in _app.js, the font will not be applied, so you need to ensure that global.css is imported.

Summary

If you made it this far, congrats – you should be well on your way to having a general understanding of how Next.js works, and now it’s time for you to take all that knowledge and work on a project of your own.

And lastly, this tutorial is actively being updated and monitored to ensure that the instructions provided are in line with the latest version of Next and cover any new features that may be introduced to the framework in future releases.

If you enjoyed this tutorial, share it with your friends!