A Crash Course in Next.js

Thank U, Next

James Collerton
13 min readMay 13, 2023
Next.js: The framework for React’s library

Audience

This article is aimed at developers with a solid understanding of web development, JavaScript, TypeScript and React. If you don’t feel comfortable with the last two I recommend reading my TypeScript article here, and React article here.

Within the article we will conduct a whistle stop tour of Next.js, introducing some of the core features with coded examples.

Please note, we will be focussing on pre-13 versions. At the end of the piece we will chat through some of the changes introduced since then.

Argument

We begin with our motivation.

Why do we need Next.js?

As mentioned in my previous article, React is a library. You can import it into an existing project to provide a more intuitive, modular way of declaring user interfaces. However, it provides none of the tooling for building, rendering, routing, etc.

Previously when I set up React projects I had to do a lot of configuration. I used Express as a server, which meant I had to write a lot of code for routing, middleware, manually creating a root HTML div for React to attach to, etc.

Additionally if I wanted to use webpack or Babel I needed to set them up myself. As anyone will tell you, I’m no front-end dev, so this was a complete headache.

Next.js looks to lessen the pain by providing a framework around React. It makes it much easier to use and removes the need for a lot of manual set up, leaving more time for back-end devs like myself to write poorly constructed TypeScript.

Let’s solidify this notion with examples.

Setting up a Next.js project

To set up our Next.js project we’ll be using the npx create-next-app command.

From there we can go into the created directory, run npm run dev, visit http://localhost:3000, and get a nice welcome screen!

Let’s examine some of the options we selected.

  • We’ve decided to use TypeScript in this project. The benefits of which are detailed here.
  • We’re also using ESLint which automatically detects code problems and helps you fix them. TypeScript will let us catch typing errors at compile time, but ESLint will help us spot coding issues as we program.
  • We’re using Tailwind CSS. This is a CSS framework which gives us access to things like flex boxes, text centring, rotation and so on. Additionally it minifies the amount of CSS in our bundle, making our site smaller. For those of you familiar, it’s not dissimilar to Bootstrap.
  • src/ is a convenience directory to store the code we are writing.
  • The App Router is a new part of Next.js 13. There are a bunch of new features in this release, which we will look into later.
  • The import alias is convenience functionality to stop you having to refer to absolute paths all the time. If I have a file at src/button.tsx I can configure it to import using @components/button.

Let’s look at the generated code. Note, I have added a few more bits and pieces in so we can explore the new and old way of doing things, so I recommend downloading the example repo here.

An example file structure for a Next.js project.

The main points of interest are:

  • The public folder. This is for serving static assets like images or your robots.txt file.
  • The src folder. This contains all of our source code.
  • The src/app folder. This is where we will put all of our code for using the new, version 13 way of doing things.
  • The src/pages folder. Here is where we create web pages in the old way of doing things. We now have one page, pre-13. If we go to http://localhost:3000/pre-13 we will see a ‘Hello world!’ page. This is as the path reflects the directory name.
  • The src/pages/api folder. Here is where we define API routes we would like to sit on our server. Next.js controls both the pages that are served and the backend APIs they talk to.
  • The config files. We’ve included a number of dependencies (Tailwind, TypeScript, ESLint) that need to be told how to behave. We can do this at the top level directory.

Although this seems cursory, we will deep dive into each of them as we explore.

Data Fetching and Rendering

Previously we covered the src/pages folder, and the directory-based routing it entails (a folder with an index.js file corresponds to a web page, in the example this is pre-13). We will look into this in more detail shortly, but first let’s look at how pages are rendered.

The main areas of interest are:

  1. Server-side rendering.
  2. Static generation (including incremental static regeneration).
  3. Client-side rendering (although the docs don’t detail how this works).

Let’s examine them.

Pre-rendering is the process of creating the HTML file server-side, sending it to the browser, then providing the necessary JavaScript to make it interactive (hydration).

If we do this on a per-request basis this is server-side rendering (SSR). If we do this once at build time this is static site generation (SSG).

We can select which render type we use on a per-page basis by utilising either the getServerSideProps (SSR) or getStaticProps (SSG) methods.

We demonstrate this for SSR below

import Head from 'next/head'
import styles from '@/styles/Home.module.css'

// Here we declare an interface for the props
// we intend to pass to the functional component
interface Title {
title: string,
randomNumber: number
}

// This is the functional component acting as our
// web page. Notice how it expects props, which are
// in turn fed to it by the 'getServerSideProps'
// function
const Home = (title: Title) => {
return (
<>
{/* The head component is used for metadata
surrounding the page */}
<Head>
<title>Server-Side Rendering</title>
<meta name="description" content="Server-side rendering example" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
{/* Here we unpack our props and display
them on the page */}
<h1>{title.title}</h1>
<p>{title.randomNumber}</p>
</main>
</>
)
}

// The getServerSideProps function is ran every time
// we load the page. It can be responsible for fetching
// data or doing any per-request functionality.
export async function getServerSideProps() {

// We will see this printed on every page load.
console.log("Server side rendering called!")

// Here we pass on a static title and a random
// number. We will get a different number on every
// page load.
return {
props: {
title: "Server side rendering is great!",
randomNumber: Math.random()
}
};

}

export default Home

A similar idea holds for SSG. However, we would not get a different number for each page load, as it would only be generated once, at build time (this only applies in production).

Vercel (the developers of Next.js) also offer a React data fetching dependency, SWR, which can be employed as a React hook.

So what is incremental static regeneration? At a top level it allows us to rerun our getStaticProps function and refresh the data in the page. This can be done periodically or on-demand.

Finally, client-side rendering is where we return a basic HTML file to a request, along with accompanying JavaScript, allowing the JavaScript to compile the completed page. Allegedly Next.js supports this, although the docs disagree

Routing

Basic routing in Next.js is very simple. If you would like a page at /blog then you would create a file at either pages/blog/index.js or pages/blog.js.

There are two more involved routing methods. The first is dynamic routing. This is where you don’t know which pages you need before they’re requested.

Imagine we had a site where we wanted to flexibly create recipe pages. For example, when a user navigates to /recipes/cheesecake we would query a backend system for the cheesecake recipe, pull it back and turn it into a web page.

To do this we would create the below file at /recipes/[list].js (note the [] syntax).

import { useRouter } from 'next/router';

// Here we define how to dynamically render pages
export default function Page() {

// In this case we retrieve the router and
// then use it to display the path for the
// dynamic routing (e.g. for recipes/cheesecake
// we would want cheesecake).
const router = useRouter();

// Here we could do some work to retrieve
// data or dynamically render a page depending
// on the path. In our example we may take the
// recipe name and pull data for it

// We return a simple header including the
// path. Here it would be 'The path is: cheesecake'
return (
<h1>
The path is: {router.query.list}
</h1>
);
}

The second is client-side routing. In the rendering section we talked about how each request for a page goes to our server, which is responsible for creating the HTML file returned to the browser.

This differs to client-side routing, where the app acts as a single-page application. Rather than make another request, the necessary files are already loaded (with some caveats below). The URL in the browser bar changes and we load a new page without a network call.

We do this using the Link component.

{/* By putting the link component here we
can pre-emptively load page two, then
when we click the link the necessary
files will be in the browser and ready
to go. */}
<Link href="/pre-13/client-side-routing/page-two">
Click for page two!
</Link>

In the above we will pre-load the page at path pre-13/client-side-routing/page-two, and on clicking the link navigate directly to it without another request.

However, we only pre-load the page when the Link is in view, and only with statically generated pages.

But what about when it all goes wrong? How do we handle error routing? Next.js is clever enough to pick up 404 and 500 errors. We can define custom error screens at pages/404.js and pages/500.js.

The framework is also responsible for providing API endpoints. This is handled with API routes.

Similar to how we had a /pages directory, we have a nested /pages/api folder where we can define our APIs. Let’s demonstrate this, pulling together a few of the things we’ve learnt so far.

// Here we create an interface telling
// TypeScript what information it can
// expect from our API
export interface HelloWorldData {
text: string
}

// This is the function representing our
// backend API.
export default function handler(
req: NextApiRequest,
res: NextApiResponse<HelloWorldData>
) {
res.status(200).json({
text: 'Hello world from your API!'
})
}

The above is our API file which defines a route returning json at /api/hello-world. We define a new page at /pre-13/api-routing which retrieves this information and displays it on a web page.

import Head from 'next/head'
import styles from '@/styles/Home.module.css'
import { HelloWorldData } from '@/pages/api/hello-world'

// Here is our page functional component
// responsible for taking the retrieved
// data and displaying it in the browser
const Home = (helloWorldData: HelloWorldData) => {
return (
<>
<Head>
<title>API Routing</title>
</Head>
<main className={styles.main}>
{/* Here we unpack our props and display
them on the page */}
<h1>{helloWorldData.text}</h1>
</main>
</>
)
}

// This will be run every time we load the
// page.
export async function getServerSideProps() {

// Here we retrieve the results from our
// newly defined API.
const res = await fetch(`http://localhost:3000/api/hello-world`);
const helloWorldData = await res.json();

// Finally returning them to the component
// to display them
return {
props: helloWorldData
};

}

export default Home

This is quite a lot, so feel free to take a break before we get onto our last topic, custom apps.

Apps handle parts of your application which you would like to remain consistent between pages. This might include styles, data or layouts. A default is generated behind the scenes, but we can customise it to add our own flavour.

To do so we make a ./pages/_app.tsx file which is used to alter data or components as they pass through it. We demonstrate below.

import type { AppProps } from 'next/app'

// This is in our _app.tsx file and
// defines our custom app
export default function App({ Component, pageProps }: AppProps) {

// Here we log the incoming pageProps. These
// are the same properties we return from our
// getServerSideProps or getStaticProps on our
// page
console.log(pageProps)

// Here we demonstrate how we can add extra
// information in order to feed it onto the
// page to be rendered
pageProps.extraInfo = "Adding extra data!"

// Now we add some functionality that appears
// on every page. We will get the page itself
// as well as the components defined in the
// individual files
return (
<>
<h1>Title on every page!</h1>
<Component {...pageProps} />
</>
)
}

We can show how we use this by writing a page to display the extra information.

import Head from 'next/head'
import styles from '@/styles/Home.module.css'

// Here we declare an interface for the props
// we intend to pass to the functional component.
// Note how we have added another field corresponding
// to the extra data we expect from the custom
// app.
interface Title {
title: string,
extraInfo: string
}

// Here we render a page using the data from
// our props and the data added by the Custom
// App
const Home = (title: Title) => {
return (
<>
<Head>
<title>Custom App</title>
</Head>
<main className={styles.main}>
<h1>{title.title}</h1>
<p>{title.extraInfo}</p>
</main>
</>
)
}

// Basic function ran on the server after every
// request.
export async function getServerSideProps() {

// Here we return some static props. It is these
// props that are logged in the custom app each time
// we load a page.
return {
props: {
title: "Title of Custom App page",
}
};

}

export default Home

Static Assets

Up to now we’ve covered how serving up web pages works. However, our requirements are not always so complex. Sometimes all we need is to host static assets like images, a security.txt or a robot.txt.

This is the role of the /public folder at the top level directory. A file next.svg will be available at /next.svg. We can demonstrate accessing these images using the Next.js custom Image component.

<Image src="/next.svg" alt="Next Logo" width="64" height="64" />

As well as allowing us to serve static assets it also provides functionality for image optimization. This includes optimizing for size, flexibility and speed, and properties like only loading the image as it scrolls into the viewport.

Styling

Next.js offers a few different ways to ease styling in your web application. Initially there are CSS modules.

CSS modules create unique names for your classes, which helps avoid conflicts in large applications. Additionally it allows Next to minify and split your style files to improve performance.

To demonstrate, imagine a file ./styles/CssModule.module.css with the below content.

/* In this file we declare a class */
.green {
color: greenyellow;
}

We would employ it in our application with:


import styles from '@/styles/CssModule.module.css'

...

<h1 className={styles.green}>This should be green!</h1>

If you read through some of our other example code you’ll notice we’ve been using this quite a lot.

Combining them with our custom apps we can add styling to our whole site! Aside from this Next offers support for Tailwind and CSS in JS.

Deployment

Now we want to look into how to deploy our application. There is a handy list of pre-production checks here that look useful even for non-Next projects!

Up until this point we’ve been using the npm run dev command to run our application locally. However, for production we need to build our site in order to carry out all of the optimisations and clever tricks we’ve discussed thus far.

Where you run your application is then (to an extent) up to you. Vercel, the team behind Next, offer their own solution, which has some nice features like automatically hosting assets and middleware at global edge locations.

Alternatively you can deploy it to anywhere which will host a Node server or run a Docker image. After all, it’s just serving web functionality!

So what changed in version 13?

The majority of things we’ve talked about this far are for versions below 13. However, in version 13 some new ideas were introduced. The exhaustive list is contained in the link, however we give the world’s briefest overview of the core ones below.

The first is how routing works. In earlier sections we discussed the /pages and /pages/api directories, and how they related to serving up HTML files and endpoints.

In version 13 we dispense with /pages and instead replace it with /app. Inside, our directory tree corresponds to the path we will render our page at. For example, if we have folder /app/folder-one/folder-two this will render a page at path /app/folder-one/folder-two.

There are a number of files we can put inside the folder, each corresponding to a different functionality.

  • page.tsx: This contains the code for rendering the UI (equivalent to index.tsx in the old way of doing things).
  • route.ts: This replaces the file in the /api folder and provides API endpoints. Notice how it now sits with files for creating pages.
  • layout.tsx: This is responsible for wrapping a page, and any other pages in directories lower down the folder tree (its children).
  • template.tsx: Similar to the above, except as we navigate through the site they are created fresh with each navigation. This is as opposed to layouts which retain state.
  • loading.tsx: Creates loading UI for this page and its children.
  • error.tsx: Creates error UI for this page and its children.

We note how we move to a colocation model, where files are separated by where they are used, rather than what they do. There is a handy link here explaining how they interweave.

Another important step is to denote the difference between server components and client components. This isn’t necessarily a Next.js thing, it’s more a React thing that Next.js supports.

A helpful comparison of the two can be found here.

Additionally, data fetching has changed. Previously we had the getServerSideProps and getStaticProps methods, which in turn were used to define if the page was statically rendered.

In later versions of Next there is a new API based on fetch. One important difference is that we use this API within our component. It is not a separate function whose result we pass to our component as props.

The rough translation is:

  • getServerSideProps: Use await fetch(`https://...`, { cache: ‘no-store’ });. The fact that we don’t cache the data means it is retrieved on each request.
  • getStaticProps: Use await fetch(`https://...`); . This will cache the data until told otherwise.

Notice we no longer need to tell Next if this is a statically rendered page, we only tell it about the required freshness of the data. We now exercise an alternate control with our server and client components.

Conclusion

In conclusion, we have looked at some of the very top-level information surrounding Next.js. It is an expansive framework, we haven’t even began to discuss areas like analytics or developer experience. I’ll leave that to you!

--

--

James Collerton

Senior Software Engineer at Spotify, Ex-Principal Engineer at the BBC