This series is my Next.js study resume, and despite it’s keen to a vanilla Next.js, all the features are applicable with Sitecore SDK. It is similar to the guide I recently wrote about GraphQL and aims to reduce the learning curve for those switching to it from other tech stacks.
- In part 1 we covered some fundamentals of Next.js – rendering strategies along with the nuances of getStaticProps, getStaticPaths, getServerSideProps as well as data fetching.
- In part 2 we spoke about UI-related things coming OOB with Next.js – layouts, styles and fonts powerful features, Image and Script components, and of course – TypeScript.
In this post we are going to talk about routing with Next.js – pages, API Routes, layouts, and Middleware.
Routing
Next.js routing is based on the concept of pages. A file located within the pages
directory automatically becomes a route. Index.js
files refer to the root directory:
pages/index.js
->/
pages/blog/index.js
->/blog
The router supports nested files:
pages/blog/first-post.js
->/blog/first-post
pages/dashboard/settings/username.js
->/dashboard/settings/username
You can also define dynamic route segments using square brackets:
pages/blog/[slug].js
->/blog/:slug
(for example:blog/first-post
)pages/[username]/settings.js
->/:username/settings
(for example:/johnsmith/settings
)pages/post/[...all].js
->/post/*
(for example:/post/2021/id/title
)
Navigation between pages
You should use the Link
component for client-side routing:
import Link from 'next/link' export default function Home() { return ( <ul> <li> <Link href="/"> Home </Link> </li> <li> <Link href="/about"> About </Link> </li> <li> <Link href="/blog/first-post"> First post </Link> </li> </ul> ) }
So we have:
/
→pages/index.js
/about
→pages/about.js
/blog/first-post
→pages/blog/[slug].js
For dynamic segments feel free to use interpolation:
import Link from 'next/link' export default function Post({ posts }) { return ( <ul> {posts.map((post) => ( <li key={post.id}> <Link href={`/blog/${encodeURIComponent(post.slug)}`}> {post.title} </Link> </li> ))} </ul> ) }
Or leverage URL
object:
import Link from 'next/link' export default function Post({ posts }) { return ( <ul> {posts.map((post) => ( <li key={post.id}> <Link href={{ pathname: '/blog/[slug]', query: { slug: post.slug }, }} > <a>{post.title}</a> </Link> </li> ))} </ul> ) }
Here we pass:
pathname
is the page name under thepages
directory (/blog/[slug]
in this case)query
is an object having a dynamic segment (slug
in this case)
To access the router
object within a component, you can use the useRouter
hook or the withRouter
utility, and it is recommended practice to use useRouter
.
Dynamic routes
If you want to create a dynamic route, you need to add [param]
to the page path.
Let’s consider a page pages/post/[pid].js
having the following code:
import { useRouter } from 'next/router' export default function Post() { const router = useRouter() const { id } = router.query return <p>Post: {id}</p> }
In this scenario, routes /post/1
, /post/abc
, etc. will match pages/post/[id].js
. The matched parameter is passed to a page as a query string parameter, along with other parameters.
For example, for the route /post/abc
the query
object will look as: { "id": "abc" }
And for the route /post/abc?foo=bar
like this: { "id": "abc", "foo": "bar" }
Route parameters overwrite query string parameters, so the query
object for the /post/abc?id=123
route will look like this: { "id": "abc" }
For routes with several dynamic segments, the query is formed in exactly the same way. For example, the page pages/post/[id]/[cid].js
will match the route /post/123/456
, and the query will look like this: { "id": "123", "cid": "456" }
Navigation between dynamic routes on the client side is handled using next/link
:
import Link from 'next/link' export default function Home() { return ( <ul> <li> <Link href="/post/abc"> Leads to `pages/post/[id].js` </Link> </li> <li> <Link href="/post/abc?foo=bar"> Also leads to `pages/post/[id].js` </Link> </li> <li> <Link href="/post/123/456"> <a>Leads to `pages/post/[id]/[cid].js`</a> </Link> </li> </ul> ) }
Catch All routes
Dynamic routes can be extended to catch all paths by adding an ellipsis (...
) in square brackets. For example, pages/post/[...slug].js
will match /post/a
, /post/a/b
, /post/a/b/c
, etc.
Please note: slug
is not hard-defined, so you can use any name of choice, for example, [...param]
.
The matched parameters are passed to the page as query string parameters (slug
in this case) with an array value. For example, a query for /post/a
will have the following form: {"slug": ["a"]}
and for /post/a/b
this one: {"slug": ["a", "b"]}
Routes for intercepting all the paths can be optional – for this, the parameter must be wrapped in one more square bracket ([[...slug]]
). For example, pages/post/[[...slug]].js
will match /post
, /post/a
, /post/a/b
, etc.
Catch-all routes are what Sitecore uses by default, and can be found at src\[your_nextjs_all_name]\src\pages\[[...path]].tsx
.
The main difference between the regular and optional “catchers” is that the optional ones match a route without parameters (/post
in our case).
Examples of query
object:
{ } // GET `/post` (empty object) { "slug": ["a"] } // `GET /post/a` (single element array) { "slug": ["a", "b"] } // `GET /post/a/b` (array with multiple elements)
Please note the following features:
- static routes take precedence over dynamic ones, and dynamic routes take precedence over catch-all routes, for example:
pages/post/create.js
– will match/post/create
pages/post/[id].js
– will match/post/1
,/post/abc
, etc., but not/post/create
pages/post/[...slug].js
– will match/post/1/2
,/post/a/b/c
, etc., but not/post/create
and/post/abc
- pages processed using automatic static optimization will be hydrated without route parameters, i.e.
query
will be an empty object ({}
). After hydration, the application update will fill out thequery
.
Imperative approach to client-side navigation
As I mentioned above, in most cases, Link
component from next/link
would be sufficient to implement client-side navigation. However, you can also leverage the router from next/router
for this:
import { useRouter } from 'next/router' export default function ReadMore() { const router = useRouter() return ( <button onClick={() => router.push('/about')}> Read about </button> ) }
Shallow Routing
Shallow routing allows you to change URLs without restarting methods to get data, including the getServerSideProps
and getStaticProps
functions. We receive the updated pathname
and query
through the router
object (obtained from using useRouter()
or withRouter()
) without losing the component’s state.
To enable shallow routing, set { shallow: true }
:
import { useEffect } from 'react' import { useRouter } from 'next/router' // current `URL` is `/` export default function Page() { const router = useRouter() useEffect(() => { // perform navigation after first rendering router.push('?counter=1', undefined, { shallow: true }) }, []) useEffect(() => { // value of `counter` has changed! }, [router.query.counter]) }
When updating the URL, only the state of the route will change.
Please note: shallow routing only works within a single page. Let’s say we have a pages/about.js
page and we do the following:
router.push('?counter=1', '/about?counter=1', { shallow: true })
In this case, the current page is unloaded, a new one is loaded, and the data fetching methods are rerun (regardless of the presence of { shallow: true }
).
API Routes
Any file located under the pages/api
folder maps to /api/*
and is considered to be an API endpoint, not a page. Because of its non-UI nature, the routing code remains server-side and does not increase the client bundle size. The below example pages/api/user.js
returns a status code of 200
and data in JSON format:
export default function handler(req, res) { res.status(200).json({ name: 'Martin Miles' }) }
The handler
function receives two parameters:
req
– an instance ofhttp.IncomingMessage
+ several built-in middlewares (explained below)res
– an instance ofhttp.ServerResponse
+ some helper functions (explained below)
You can use req.method
for handling various methods:
export default function handler(req, res) { if (req.method === 'POST') { // handle POST request } else { // handle other request types } }
Use Cases
The entire API can be built using a routing interface so that the existing API remains untouched. Other cases could be:
- hiding the URL of an external service
- using environment variables (stored on the server) for accessing external services safely and securely
Nuances
- The routing interface does not process CORS headers by default. This is done with the help of middleware (see below)
- routing interface cannot be used with
next export
As for dynamic routing segments, they are subject to the same rules as the dynamic parts of page routes I explained above.
Middlewares
The routing interface includes the following middlewares that transform the incoming request (req
):
req.cookies
– an object containing the cookies included in the request (default value is{}
)req.query
– an object containing the query string (default value is{}
)req.body
– an object containing the request body asContent-Type
header, ornull
Middleware Customizations
Each route can export a config
object with Middleware settings:
export const config = { api: { bodyParser: { sizeLimit: '2mb' } } }
bodyParser: false
– disables response parsing (returns raw data stream asStream
)bodyParser.sizeLimit
– the maximum request body size in any format supported by bytesexternalResolver: true
– tells the server that this route is being processed by an external resolver, such as express or connect
Adding Middlewares
Let’s consider adding cors middleware. Install the module using npm install cors
and add cors
to the route:
import Cors from 'cors' // initialize middleware const cors = Cors({ methods: ['GET', 'HEAD'] }) // helper function waiting for a successful middleware resolve // before executing some other code // or to throw an exception if middleware fails const runMiddleware = (req, res, next) => new Promise((resolve, reject) => { fn(req, res, (result) => result instanceof Error ? reject(result) : resolve(result) ) }) export default async function handler(req, res) { // this actually runs middleware await runMiddleware(req, res, cors) // the rest `API` logic res.json({ message: 'Hello world!' }) }
Helper functions
The response object (res
) includes a set of methods to improve the development experience and speed up the creation of new endpoints.
This includes the following:
res.status(code)
– function for setting the status code of the responseres.json(body)
– to send a response in JSON format,body
should be any serializable objectres.send(body)
– to send a response,body
could be astring
,object
orBuffer
res.redirect([status,] path)
– to redirect to the specified page,status
defaults to307
(temporary redirect)
This concludes part 3. In part 4 we’ll talk about caching, authentication, and considerations for going live.