Skip to content

Let’s build an Events website with Next.js: Intro to Next.js

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Let’s build an Events website with Next.js: Intro to Next.js

This is the first article of a series of posts where I will write about Next.js, the most complete framework for writing React.js applications. Through this guide, you will learn the basics of Next.js, as well as how to integrate it with GraphCMS, the GraphQL native headless CMS.

What is Next.js?

Next.js is a web framework for React.js, built and maintained by the Vercel team.

With Next.js, you can create both server-rendered and statically-generated websites, with out-of-the-box support for internationalization, image optimization, routing, TypeScript support, CSS and Sass support, code splitting, Serverless API Routes, and more. All of the above is offered out-of-the-box, with no further config needed.

Core concepts

File-system Routing

Unlike other React applications, where you may need to import a routing library, and write a routes config file associating each component with a route path, in Next.js it is a simple as adding files to the pages directory and defaultly exporting a React component. There, the route path will be the filename. (i.e. pages/index.js, pages/about.js pages/blog/index.js)

Matching dynamic parameterized routes is also possible by wrapping the filename with a bracket syntax (i.e. pages/blog/[slug].js) and catch-all routes, which allows it to have as many subpaths as possible (i.e. pages/parametrized/[...paths].js).

The following file structure:

.
|- pages/
|- |- about.js
|- |- blog/index.js
|- |- blog/[slug].js
|- |- index.js
|- |- parametrized/[...paths].js

Will match the following paths:

  • pages/about.js => https://my-website.com/about
  • pages/blog/index.js => https://my-website.com/blog
  • pages/blog/[slug].js => https://my-website.com/blog/hello-world, https://my-website.com/blog/my-first-post, https://my-website.com/blog/hello-world
  • pages/index.js => https://my-website.com
  • pages/parametrized/[...paths].js => https://my-website.com/parametrized/a, https://my-website.com/parametrized/a/b, https://my-website.com/parametrized/a/b/c ... and so on.

Data fetching

There is two ways to fetch data from a Next.js application. You can either generate static pages at build time, or fetch data from the server at run time.

1. Static pages generation

It is the recommended way to fetch public data coming from any data source. This can be a CMS, a database, or even a local or remote JSON file. One of the benefits of generating pages at build time is that you can put a CDN in front of it, and serve the already generated assets to the user.

To set up a page to be statically generated, export an asynchronous getStaticProps function from it, returning an object with a nested props object, which will be passed to the route component.

pages/blog/index.js

import cms from 'lib/cms'

export async function getStaticProps() {
  const posts = await cms.getPosts()
  /**
  [
    {
      slug: 'hello-world',
      title: 'Hello World',
      body: 'This is the hello world blog post'
    },
    {
      slug: 'my-first-post',
      title: 'My first post',
      body: 'This is my first post'
    },
    {
      slug: 'my-second-post',
      title: 'My second post',
      body: 'This is my second post'
    }
  ]
  */

  return {
    props: {
      posts
    }
  }
}

export default function BlogHomePage(props) {
  return (
    <ul>
      {props.posts.map(post => (
        <li key={post.slug}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  )
}

When working with dynamic routes, an extra asynchronous function getStaticPaths needs to be exported, which needs to return an object with a fallback property (this will be explained later), and a nested paths array. Each item of the array needs to be an object with a nested params object, which will be received by getStaticProps to know which object to fetch.

pages/blog/[slug].js

import cms from 'lib/cms'

export async function getStaticPaths() {
  const posts = await cms.getPosts()
  /**
  [
    {
      slug: 'hello-world',
      title: 'Hello World',
      body: 'This is the hello world blog post'
    },
    {
      slug: 'my-first-post',
      title: 'My first post',
      body: 'This is my first post'
    },
    {
      slug: 'my-second-post',
      title: 'My second post',
      body: 'This is my second post'
    }
  ]
  */

  const paths = posts.map(post => ({ 
    params: { slug: post.slug }
  }))
  /**
  [
    { slug: 'hello-world' },
    { slug: 'my-first-post' },
    { slug: 'my-second-post' }
  ]
  */

  return {
    paths,
    fallback: false
  }
}

export async function getStaticProps(context) {
  const post = await cms.getPostBySlug(context.params.slug)
  /**
  {
    slug: 'hello-world',
    title: 'Hello World',
    body: 'This is the hello world blog post'
  }

  (the above can change depending on the requested slug)
  */

  return {
    props: {
      post
    }
  }
}

export default function BlogPostPage(props) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  )
}

Note: the above will generate all blog posts at build time. If you don't want to generate all blog post static pages at build time, you can opt-in to Incremental Static Regeneration, which will be explained later on this article.

2. Server-side rendering

The other technique to hydrate a page with dynamic data is to render it from the server. This can be useful when working with private pages that can't be put on a CDN, and the data will change according to the request context. For that, an asynchronous function called getServerSideProps will need to be exported from the route component, returning an object with a nested props object.

pages/profile.js

import db from 'lib/db'

export async function getServerSideProps(context) {
  const currentUserId = getCurrentUserIdFromCookies(context)

  const user = await db.getUserById(currentUserId)
  /* 
  {
    name: 'John Doe',
    somePrivateData: 'foo'
  }
  */

  return {
    props: {
      user
    }
  }
}

export default function ProfilePage(props) {
  return (
    <div>
      <h1>{props.user.name}</h1>
      <h2>Private data:</h2>
      <p>{props.user.somePrivateData}</p>
    </div>
  )
}

Note: one of the caveats of server-side rendering is a page using it can't be cached by a CDN without additional configuration, and your page relies on a long-life server to be running. A workaround for this is to just export a component from the page route file, and do the fetching client-side using fetch either on a useEffect hook, or using a data fetching library such as SWR.

API Routes

If you want to create serverless API endpoints for external consumers, or for client-side fetching, you can add API routes files to an api directory within the pages directory exporting a handler that receives a req and a res argument. It can also receive dynamic and catch-all parameters, just like a regular app route.

pages/api/profile.js

import db from 'lib/db'

export default async function profileApiRoute(req, res) {
  const currentUserId = getCurrentUserIdFromCookies(req)

  const user = await db.getUserById(currentUserId)
  /* 
  {
    name: 'John Doe',
    somePrivateData: 'foo'
  }
  */

  res.json({
    user
  })
}

The above will create an API endpoint at https:my-website.com/api/profile that can be consumed from anywhere.

import { useState, useEffect } from 'react'

export default function ProfilePage() {
  const [user, setUser] = useState(undefined)

  useEffect(() => {
    const fetchUser = async () => {
      const res = await fetch('/api/profile')
      const json = await res.json()
      setUser(json.user)
    }
  }, [])

  if (user === undefined) {
    return <p>Fetching user...</p>
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <h2>Private data:</h2>
      <p>{user.somePrivateData}</p>
    </div>
  )
}

Moving forward

The above core concepts are just a small taste of what Next.js can offer. In the upcoming articles in this series, you will continue learning how to use more techniques like image optimization, Incremental Static Regeneration, and Internationalization. Stay tuned!

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co