Dynamic routing in Next.js using REST and GraphQL APIs

Next.js is a React framework for building fast and user-friendly web applications, building API endpoints, and accessing databases. Overall, Next.js simplifies full-stack development.

Next.js is lightweight and built for speed, performance, and search engine optimization (SEO). By default, Next.js pre-renders your pages and also does code splitting, which is only loading the JavaScript necessary for a particular page you are on. This makes your website load very fast and ensures it’s optimized for search engines.

The framework brought some modifications to building web applications with React, including dynamic routing, server-side rendering, static generation, and scalability.

This article aims to teach you what dynamic routing in Next.js is about and how to use it for page routing with REST and GraphQL APIs.

Keep reading to learn:

  • Pre-rendering in Next.js
  • How routing is done in Next.js
  • What is dynamic routing?
  • A brief overview of REST and GraphQL
  • How to use dynamic routes to render REST and GraphQL APIs.

Pre-rendering in Next.js

To understand dynamic routing in Next.js, you have to know what pre-rendering is and which form of pre-rendering you should use for your application. Next.js generates HTML for each page of your application in advance and hydrates them when they are loaded.

Static generation (SG) and server-side rendering (SSR) are the two forms of pre-rendering in Next.js. For static generation, the HTML is generated at build time; in server-side rendering, the HTML is generated at request time. If your application doesn’t deal with data that changes frequently, it’s highly recommended that you make use of static generation for better performance. More often than not, you’ll need to use external data in your application. If you don’t need to get the data at request time, it’s good practice to pre-render with static generation using the function getStaticProps

A blog post is an example of a page you should generate statically. However, if you need to get this data at request time, maybe due to the nature of the data, you should use the getServerSideProps function for the data request since it gets called by the server on every request.Later in this article, you’ll learn more about using getStaticProps and getStaticPaths for external data fetching. This is what we’ll use to demonstrate dynamic routing using REST and GraphQL.

How routing is done in Next.js

Unlike in React where you have to install the react-router dependency and then wrap BrowserRouter around the parent component of your application, Next.js has a built-in routing system. Therefore, you don’t need to install any external dependencies before you can carry out routing.

Next.js uses a file-based routing system in which each page automatically becomes a route, adopting the file name as the path. Every file inside the pages directory becomes a route.

├── pages
│   ├── index.js
│   ├── blog.js

For example, in the above directory structure, /blog is rendered as a route (in a development environment, localhost:3000/blog will route into the blog.js page) while index.js is the default route /. However, there’s an exemption to the “every page becomes a route” rule. Pages prefixed with an underscore are referred to as custom or special pages and therefore do not participate in routing. An example is the _app.js page inside your Next.js project.

Next provides a Link component for performing client-side routing between different pages of your application. This Link accepts a required href prop, which is essentially the page you want to navigate to.

import Link from 'next/link'

function Blog() {
  return (
    <div>
        <Link href="/">
           <p>Go to home page</p>
        </Link>

        <Link href="/team">
          <p>team page </p>
        </Link>

        <Link href="/careers">
          <p>Careers page</p>
        </Link>

    </div>
  )
}

export default Blog

The first Link with the href prop / navigates to the index page. /team takes you to the team’s page and /careers takes you to the careers page.

Link takes other optional props — such as passHref, pre-fetch, replace, and scroll — but href is the only required prop.

Depending on its pattern, your route is either an index route, nested route, or a dynamic route.

Index route

In a development environment, localhost:3000 exposes you to the index route. The index route is usually the starting point of your application. The pages/index.js file will route as /. You can also use index route to avoid route repetition inside your application.

├── node_modules
├── pages
│      ├── blog
│             └── blog.js
├── public
├── styles
├── .gitignore
├── package.json
└── README.md

/blog/blog routes to the blog.js file. It’s redundant having a route like that. Since the directory name is the same as the route name, you should do index.js instead of the blog.js file. With this change, you can navigate to the blog route with /blog, and Next would know to render index.js as the index route.

Nested route

Nested routes are deeper than index routes.

├── pages
     ├── post
           └── firstPost.js
           └── secondPost.js

We have two nested routes here: /post/firstPost and /post/secondPost. Nested routes can become complex and redundant if you need to create a route for every page you need to render. For example, if you have 1,000 posts on your blog, it would be ridiculous to need to create 1,000 routes, one for each post. How can you solve this? With dynamic routes!

What is dynamic routing?

Imagine you want to render a plethora of posts from an API. It would be tedious and unscalable to create a route for each post returned from your API request. Dynamic routes can help you access individual data with efficiency by allowing you to add custom parameters to your URLs. Bracket syntax is used to denote a dynamic segment. For example, with [id].js, Next will automatically know that this page is a dynamic route.

├── pages
│   ├── blog
│      └── [post].js

Dynamic routes are not limited to files alone; you can create dynamic routes for folders, too. In other words, you can create multiple dynamic route segments.

The useRouter hook has a query object property that exposes dynamic route parameters. The query object is usually empty during pre-rendering if the page is not using server-side rendering.

pages/post/[id].js:

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  const { id } = router.query

  return <p>Id of post: {id}</p>
}

export default Post

Routing to /post/123 will give the query object {"id": "123"}.

Let’s see what a multiple routes segment would look like.

pages/post/[author]/[id].js:

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  const {author, id } = router.query

  console.log(author, id)

  return <div></div>
}

export default Post

If you route to /post/emmanuel/1, the query object returned will be this:

{"author": "emmanuel", "id": "1"}

Dynamic routes can be extended to catch all paths. Prefixing to a dynamic route will tell Next that this is a catch-all route.

For example, pages/blog/[...id].js matches /blog/1, blog/1/2, /blog/1/2/3 and so on.The optional catch-all route works similarly, but it will fall back to the parent route if the route intended is not available. For example, pages/blog/[...id].js matches /blog/1/, /blog/1/2 and so on and will fall back to /blog if the route is not available. It’s good practice to not have more than one dynamic route inside a folder level.

A brief overview of REST and GraphQL

Before we try out dynamic routing with the REST API and GraphQL API, let’s understand what these APIs are and how they work.

What is REST?

Unlike GraphQL, which is an application layer, REST (representational state transfer) is an architectural style that conforms to a set of constraints for building web services. Although JSON is the most popular data format used today, REST can also present data in XML and YAML formats.

A REST API request contains an endpoint to fetch the resources from, an HTTP method which describes to the server the type of operation a client wants to carry out (e.g., GET, POST, PUT, PATCH, and DELETE), a header, and a request body.

HTTP methods are used to specify the type of CRUD operation you want to carry out. For example, GET is used to read a resource, the POST method creates a new resource, PUT updates a resource, and the DELETE method deletes a resource.

What is GraphQL?

GraphQL was built by Facebook in 2012. Initially, it was used internally for mobile applications. But in 2015, it was open sourced and is now governed by the GraphQL Foundation

GraphQL is a query language as well as a server-side runtime for building application programming interfaces (APIs). One of the reasons it gained popularity quickly was because it solved the problems of over-fetching and under-fetching, which was a challenge with REST. GraphQL was designed to give clients the amount of data they requested — nothing more, nothing less — which makes it flexible and super fast. Query and Mutation are the most common GraphQL operations and match the CRUD model. Queries are comparable to the Read operation and Mutations to Update, Delete, and Create operations.

How to use dynamic routes to render REST and GraphQL APIs

It’s time to experiment with dynamic routes using APIs. You can clone my completed source code or follow along with the instructions below.

First, create a Next.js project using your preferred package manager.

npx create-next-app
# or
yarn create next-app
# or
pnpm create next-app

The name of our project is dynamic-routes-example.

Let’s install graphql-request, a dependency for making GraphQL requests. If you prefer using the Apollo Client, go ahead to install that.

yarn add graphql-request graphql

After the dependency has been installed successfully, start your application.

cd dynamic-routes-example && yarn dev

Let’s create two directories inside the pages folder. These directories will hold two files each

  • pages/graphql, a directory that holds the files for the GraphQL API routes
  • pages/rest, which holds the files for the REST API routes
  • pages/graphql/index.js, the index route for the GraphQL directory
  • pages/graphql/[detail].js, the dynamic route for the GraphQL API
  • pages/rest/index.js, the index route for the REST directory
  • pages/rest/[detail].js, the dynamic route for the REST API

Let’s fetch our REST endpoint inside the pages/rest/index.js file using getStaticProps. We are going to use a mock API, JSON placeholder, as our endpoint.

pages/rest/index.js:

import Link from 'next/link'

export async function getStaticProps(){
    const apiCall = await fetch('<https://jsonplaceholder.typicode.com/users/>')

    const data = await apiCall.json()

    return{
        props:{
            data
        },

    }
}

export default function Home({data}) {
  return (
    <div>
        <h1>REST API data</h1>
        {data.map(user =>(
            <Link href={'/rest/'+ user.id} key={user.id}>
                <div className='container'>

                        <p>{user.name}</p>

                <hr />
                </div>
            </Link>
        ))}

        <Link href={'/'}>
        <button>
            Home
        </button>

        </Link>
    </div>
  )
}

getStaticProps is used to fetch the REST API data since our page uses a static generator. With getStaticProps, Next would know to prerender the page at build time using the props returned. You need to export getStaticProps as a standalone function to have excess props inside the page component.

Here, getStaticProps returns the REST API data as props; data was mapped inside <Home />.

The Link component uses the id from the REST API to route dynamically to /rest/[detail].js when you click on the div container.

Now, let’s go over to pages/rest/[detail].js and make dynamic route work for our REST API.

The API returns an array of 10 data. For Next to statically generate each of them as a standalone page, we need to get the paths.

export async function getStaticPaths(){
    const res = await fetch('')
    const data = await res.json()

    const paths = data.map(user =>({
             params: {detail: user.id.toString()},

    }))
}

A fetch request was made to the data again, this time with getStaticPaths. This is because we want to get the path to each data. Next.js will statically fetch all the paths using id. Since the id is an integer, it was converted to a string using the toString() method. We returned paths and set fallback: false so that a 404 page is returned if the route is not found

Now that we have access to the paths, we need to make another call to the REST API inside getStaticProps and append the paths to the endpoint to generate each page. In doing so, data was returned as props. We now have access to it inside our page component.

pages/rest/[detail].js

import Link from 'next/link'

export async function getStaticPaths(){
    const res = await fetch('<https://jsonplaceholder.typicode.com/users/>')
    const data = await res.json()

    const paths = data.map(user =>({
             params: {detail: user.id.toString()},

    }))

    return {
        paths,
        fallback: false
    }
}

export async function getStaticProps({params}){

    const id = params.detail;
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    const data = await res.json()

    return{
        props:{
            data
        }
    }
}

export default function Details({data}) {
    const {id, name, username, website} = data
  return (
    <div>

        <div className='detail-container'>
            <h3>Welcome to REST details page</h3>
            <p>id: {id}</p>
            <p>name: {name}</p>
            <p>username: {username}</p>
            <p>contact website: {website}</p>
        </div>
        <Link href={'/rest'}>
            <button>
                Back
            </button>
        </Link>
    </div>

  )
}

Now, the Dynamic route for our REST API works fine. We should do the same for our GraphQL API.

Navigate to the graphql directory and go inside index.js. We will use an open source mock API by GraphQL Zero to experiment with dynamic routes using GraphQL.

pages/graphql/index.js:

import Link from 'next/link'

export const query = gql`
{
    users{
        data{
          id
          email

        }
      }
}

`
export async function getStaticProps(){
    const data = await request({
        url: '<https://graphqlzero.almansi.me/api>',
        document: query
    })

    return{
        props:{
            data
        }
    }
}

export default function Home({data}) {
    const userData = data.users.data
  return (
    <div>
        <h1>Graph API data</h1>
        {userData.map(user =>(
            <Link href={'/graphql/'+ user.id} key={user.id}>
                <div className='container'>

                        <p>{user.email}</p>

                <hr />
                </div>
            </Link>
        ))}

        <Link href={'/'}>
        <button>
            Home
        </button>

        </Link>
    </div>
  )
}

In this instance, request and gql were imported from graphql-request to send requests and write queries respectively.

The getStaticProps function makes the request to the endpoint on the server-side at build time and sends back a static result (like we experimented with the REST API). We have access to the data returned as props inside <Home />.

Let’s build a dynamic route for it. Unlike with the REST API where you can append the paths to the endpoint, you can’t do that with a GraphQL API because it has just a single endpoint. To get the paths, you first query the id alone. That’s what the queryIdOnly query to GraphQL does, then getStaticPaths returns the id of each result data. Once we’ll have access to each id as a path, we can make an API request to the endpoint inside getStaticProps for Next to generate each path as a standalone page.

pages/graphql/[detail].js:

import {request, gql} from 'graphql-request'
import Link from 'next/link'

const queryIdOnly = gql`

    {
        users{
        data{
            id
        }

        }
  }

`

export async function getStaticPaths(){
    const data = await request({
        url: '<https://graphqlzero.almansi.me/api>',
        document: queryIdOnly
    })

    const paths = data.users.data.map(user =>{
        return{
            params: {detail: user.id.toString()}
        }
    })

    return{
        paths,
        fallback: false
    }
}

const queryUserById = gql`
  query ($id: ID!)
  {
    user(id: $id) {
        id
        email
        name
        phone

      }
  }

`

export async function getStaticProps({params}){

    const variables = {
        id: params.detail
    }

    const data = await request({
        url: '<https://graphqlzero.almansi.me/api>',
        document: queryUserById,
        variables: variables

    })

    return{
        props: {
            data
        }
    }
}

export default function Detail({data}) {
    const {name, email, id, phone} = data.user

  return (
    <div>

        <div className='detail-container'>
            <h3>Welcome to GraphQL detail page</h3>
            <p>id: {id}</p>
            <p>name: {name}</p>
            <p>email: {email}</p>
            <p>contact: {phone}</p>
        </div>
        <Link href={'/graphql'}>
            <button>
                Back
            </button>
        </Link>
    </div>

  )
}

queryUserById accepts an id variable which will be the data returned from getStaticPaths. Each page is generated by Next.js at build time using the props returned by getStaticProps. Since we have access to the data as a prop inside <Detail />, the data was then mapped.

Summary

Dynamic routes allow you to add custom params to your URLs, enabling you to eliminate route redundancy and simplify page navigation. With dynamic routes, you can render a lot of data without creating a custom route for each of them.

To learn more about dynamic routes in Next.js, check out the live demo and source code.Interested in learning more about Next.js? Find out how to add Google and GitHub logins to your next Next.js app using NextAuth and Typescript.

Read more about:

GraphQL Next.js

Emmanuel is a software engineer and Technical Writer with over 5 years of experience building high end and scalable application in cross functional teams. He has experience in catering to developer needs through documentation. He has vast experience with JavaScript, Typescript, React, NextJS, Node.js and Redux. In his spare time, Emmanuel writes for software startups including Webiny Headless CMS.