next-minimal-routes
v1.0.0
Published
Next.js dynamic URLs for the minimalist.
Downloads
4
Maintainers
Readme
next-minimal-routes
Next.js dynamic URLs for the minimalist.
- ✔️ Routes are represented as plain objects.
- ✔️ path-to-regexp patterns.
- ✔️ Use the standard Next.js
<Link>
andRouter
. - ✔️ Optional: Build the abstraction you need on top.
- ✔️ Well tested.
Inspired by next-routes and nextjs-dynamic-routes.
Contents
Installation
npm install next-minimal-routes
import { makeRoute, makeUrls, matchRoute } from "next-minimal-routes";
const { getRequestHandler } = require("next-minimal-routes/server");
Example
Define your routes somewhere where both your server and your Next.js app can
access them. For example, in routes.js
next to pages/
. Routes are just plain
objects. makeRoute
helps you create them.
// routes.js
const { makeRoute } = require("next-minimal-routes");
// It’s nice storing your routes in an object (allowing you to easily refer to
// them by name), but you can store them however you want.
module.exports = {
home: makeRoute({ page: "/" }),
about: makeRoute({ page: "/about" }),
product: makeRoute({ page: "/product", pattern: "/product/:slug" }),
};
In your server, use getRequestHandler
to route requests to the correct page.
(This example uses Express, but you don’t have to.)
const express = require("express");
const next = require("next");
const slashes = require("connect-slashes");
// New imports:
const { getRequestHandler } = require("next-minimal-routes/server");
const routes = require("../routes");
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
// Use this instead of the usual `app.getRequestHandler()`.
const handle = getRequestHandler({ app, routes: Object.values(routes) });
app.prepare().then(() => {
const server = express();
// Optional: Redirect away trailing slashes.
server.use(slashes(false));
server.get("*", handle);
server.listen(port, err => {
if (err) {
throw err;
}
console.log(`> Ready on http://localhost:${port}`);
});
});
In your Next.js app, use makeUrls
whenever you use Next.js functions that need
href
and as
.
// pages/index.js
import Link from "next/link";
import Router from "next/router";
import { makeUrls } from "next-minimal-routes";
import routes from "../routes";
import CustomLink from "../components/Link";
export default () => (
<div>
<h1>Home</h1>
<Link {...makeUrls({ route: routes.about })}>
<a>About</a>
</Link>
<Link {...makeUrls({ route: routes.product, params: { slug: "hammer" } })}>
<a>Product: Hammer</a>
</Link>
{/* There’s nothing stopping you from making a convenience component if you want: */}
<CustomLink route="product" params={{ slug: "hammer" }}>
<a>Product: Hammer</a>
</CustomLink>
<button
type="button"
onClick={() => {
const { href, as } = makeUrls({
route: routes.product,
params: { slug: "nails" },
});
Router.replace(href, as);
}}
>
Product: Nails
</button>
</div>
);
Optionally, make your own <Link>
component, wrapping next/link
, for
convenience. I usually do that anyway (for example, I might want passHref
to
default to true
, or automatically add /en/
etc. on a multi-language site).
For inspiration, there are three examples in components/Link.js.
The URL parameters are available in the query
object given to
getInitialProps
:
export default class Product extends React.Component {
static propTypes = {
slug: PropTypes.string.isRequired,
};
static getInitialProps({ query: { slug } }) {
return {
slug,
};
}
render() {
const { slug } = this.props;
return (
<div>
<h1>Product: {slug}</h1>
</div>
);
}
}
Finally, don’t forget to turn off file system routing:
// next.config.js
module.exports = {
useFileSystemPublicRoutes: false,
};
For a full example, see routes.js
, server/
, pages/
, components/
in this
repo. To run that example, clone this repo and run:
npm ci
npm run next:dev
Summary
After the initial setup, there’s not much to remember:
Use makeRoute to create a route, and makeUrls to link to it. Implement
static getInitialProps({ query }) {}
to get your params (fromquery
).
// In routes.js (or wherever you store your routes):
const productRoute = makeRoute({
page: "/product",
pattern: "/product/:slug",
});
// When you need a link:
const { href, as } = makeUrls({
route: productRoute,
params: { slug: "hammer" },
});
// When you need a param:
class ProductPage extends React.Component {
static getInitialProps({ query: { slug } }) {
console.log(slug);
}
}
Note that if your project has a custom <Link>
component, you probably don’t
need to use makeUrls
at all most of the time.
Reference
First off, there are two important types of objects:
With those out of the way, this are the actual exported functions:
Route
A Route
is a plain object with a few required keys:
type Route = {
page: string,
pattern: string,
match: string => QueryObject | undefined,
reverse: QueryObject => string, // Can throw errors.
...rest,
};
| Key | Type | Description |
| --------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| page | string
| The path to the page in pages
that should be used for the route. Must start with a /
. |
| pattern | string
| The URL to the page (will be shown in the address bar). Can contain URL parameters. See path-to-regexp for pattern syntax. Must start with a /
. Just like standard Next.js, URLs are case sensitive and optional trailing slashes are not allowed: Neither /About
nor /about/
match /about
. |
| match | string => QueryObject | undefined | A function that matches the pathname part of a URL (starting with a /
) against pattern
. If it matches, the URL parameters (if any) of the URL are returned, otherwise undefined
. |
| reverse | QueryObject => string
| A function that replaces all URL parameters in pattern
(if any) with values from the given object. May throw an error if the given parameters are invalid for the pattern
. |
| ...rest
| any
| Apart from the above keys you may attach any information you like to your routes. For example, sitemap data. |
QueryObject
The query
object that Next.js gives you in getInitialProps
is a
QueryObject
. You can pass QueryObject
s to the href
and as
parameters of
Next.js’ <Link>
as well as to many Router methods
(next/router
). In other words, this is no new concept if you’re familiar with
Next.js, but next-minimal-routes puts a name on it and uses the same
QueryObject
structure for objects of URL parameters.
type QueryObject = {
[key: string]: string | Array<string> | undefined,
};
For query parameters
The keys can be anything, since the user can type any query parameters they like into the address bar, and you can create links with any query parameters you like.
The values vary:
| Type | Situation |
| --------------- | ----------------------------------------------------------- |
| string
| A query parameter is supplied once. ?foo=1
|
| Array<string>
| A query parameter is supplied several times. ?foo=1&foo=2
|
| undefined
| A query parameter is not supplied at all. |
For URL parameters
The keys can only be the names specified in a given Route
pattern, both when
matching and reversing URLs.
The values vary:
| Type | Situation |
| --------------- | ------------------------------------------ |
| string
| For regular, required parameters. :foo
|
| Array<string>
| For repeating parameters. :foo*
, :foo+
|
| undefined
| For optional parameters. :foo?
, :foo*
|
Mixing both URL parameters and query parameters
The way Next.js is designed when it comes to implementing dynamic routes (URL
parameters), is to merge the URL parameters into the query
object given to
getInitialProps
(which normally only contains query parameters).
Since URL parameters are merged into query
, you can’t have URL parameters and
query parameters with the same name. URL parameters always take precedence over
query parameters, overwriting any query parameters with the same names. Rename
your parameters if you run into conflicts.
makeRoute({ page, pattern = page, ...rest }): Route
Routes are just plain objects – see Route. You can make them yourself if you
like. makeRoute
helps you do so:
- It validates that
page
is present. - It defaults
pattern
topage
.makeRoute({ page: "/about" })
andmakeRoute({ page: "/about", pattern: "/about" })
are equivalent. - It makes the
match
andreverse
functions out ofpattern
.
makeRoute
takes one argument which is an object:
| Key | Type | Default | Description |
| --------- | -------- | ------------ | -------------------------------------------------- |
| page | string
| Required | Path to a page in pages/
. Must start with a /
. |
| pattern | string
| page
| path-to-regexp URL pattern for the page. |
| ...rest
| any | {}
| Any extra keys are passed along. |
Returns a Route.
makeUrls({ route, params = {}, query = {}, hash = "" }): { href, as }
Next.js’ <Link>
component (next/link
) as well as many Router functions
(next/router
) take href
and as
parameters. makeUrls
makes those for you.
Given a route (route
) and URL params for it (params
, if any), as well as
optional query parameters (query
) and an optional fragment identifier
(hash
), makeUrls
returns href
and as
objects to be used with <Link>
,
Router.push
and others.
makeUrls
takes a single argument which is an object:
| Key | Type | Default | Description |
| ------ | ------------- | ------------ | --------------------------- |
| route | Route | Required | The route to make URLs for. |
| params | QueryObject | {}
| URL parameters. |
| query | QueryObject | {}
| Query parameters. |
| hash | string
| ""
| Fragment identifier. |
The return value looks like this:
type Urls = {
href: {
pathname: string,
query: QueryObject,
},
as: {
pathname: string,
query: QueryObject,
hash: string,
},
};
The easiest way to see exactly what this function does is to look at its source code:
export function makeUrls({ route, params = {}, query = {}, hash = "" }) {
return {
href: {
pathname: route.page,
query: {
...query,
...params,
},
},
as: {
pathname: route.reverse(params),
query,
hash,
},
};
}
Optionally, make your own <Link>
component, wrapping next/link
, for
convenience. I usually do that anyway (for example, I might want passHref
to
default to true
, or automatically add /en/
etc. on a multi-language site).
For inspiration, there are three examples in components/Link.js. This allows
importing only your Link
instead of routes
, getUrls
and next/link
.
matchRoute(routes, pathname): { route, params } | undefined
Given an array of Route objects and pathname part of a URL (starting with a
/
), matchRoute
returns the first route (if any) that matches the pathname.
Note that in Node.js you typically need to run decodeURIComponent
on
pathname
before trying to match on it or extract URL parameters from it.
getRequestHandler uses matchRoute
behind the scenes, so usually you won’t
need this function. But if you need to manually match something in the browser
some time, matchRoute
can be handy.
| Parameter | Type | Description |
| --------- | -------------- | ---------------------- |
| routes | Array<Route>
| Routes to match. |
| pathname | string
| Path to match against. |
Not only the matching route is returned, but also any URL parameters:
type ReturnValue = {
route: Route,
params: QueryObject,
}
Note: It’s common to store routes in an object. This function takes an
array of routes. If so, simply use Object.values
:
matchRoute(Object.values(routes), pathname)
.
getRequestHandler({ app, routes, skip = skipStatic }): Function
When using a custom server in Next.js, these two lines are crucial:
const app = next({ dev });
const handle = app.getRequestHandler();
getRequestHandler
is a subsititute for the second line.
const handle = getRequestHandler({ app, routes });
getRequestHandler
calls const handle = getRequestHandler({ app, routes })
internally, and uses app.render
for matched routes, and handle
for
everything else.
Note that you need to import getRequestHandler
from
"next-minimal-routes/server"
(rather than just "next-minimal-routes"
).
getRequestHandler
takes a single argument which is an object:
| Key | Type | Default |
| ------ | ----------------------- | ------------------------------ |
| app | Next.js app | Required |
| routes | Array<Route>
| Required |
| skip | (req, res) => boolean
| Skip static files – see below. |
Returns a typical Node.js (req, res) => {}
request handler function.
The skip
function lets skip route matching for certain requests, and use the
standard Next.js handle
function directly for them. By default, skip
does so
for /_next/*
and /static/*
URLs (static files).
Note: It’s common to store routes in an object. This function takes an
array of routes. If so, simply use Object.values
:
matchRoute(Object.values(routes), pathname)
.
Development
You need Node.js 10 and npm 6.
npm scripts
npm run next
: Start the standard Next.js dev server, for comparison. It can also be used to run arbitrarynext
commands, such asnpm run next -- export
. Runnpm run build
first.npm run next:build
: Make a Next.js production build.npm run next:dev
: Start the Express/Next.js example custom server in development mode.npm run next:prod
: Start the Express/Next.js example custom server in production mode. Runnpm run build
andnpm run next:build
first.npm run watch
: Start Jest in watch mode.npm run eslint
: Run ESLint (including Prettier).npm run eslint:fix
: Autofix ESLint errors.npm run prettier
: Run Prettier for files other than JS.npm run doctoc
: Run doctoc on README.md.npm run jest
: Run unit tests.npm run coverage
: Run unit tests with code coverage.npm run build
: Compile with Babel.npm test
: Check that everything works.npm publish
: Publish to npm, but only ifnpm test
passes.
Servers are run at http://localhost:3000.
Directories
src/
: Source code.test/
: Tests.dist/
: Compiled code, built bynpm run build
. This is what is published in the npm package.pages/
,components/
,server/
androutes.js
: Express/Next.js example app.stub/
: An empty next-minimal-routes package to fool ESLint and Next.js packages check, and to requirenext-minimal-routes/server
inserver/index.js
.