Routex.js - Yet another router for Next.js

Index

Routex is a javascript library that tries to make easier dynamic routing in Next.js giving you all the control about how to define your routes.

It's inspired in fridays/next-routes and lydell/next-minimal-routes.

You can check the routex' source code here:

Why another router?

I've started using Next.js since version 5 in a project that due to its context Next.js wasn't probably the best solution. And one of the parts that suffered the most was the routing feature. That helped me to understand a little better how Next.js worked and to appreciate more this awesome React framework. So these are the reasons:

1. Learning!

First, and probably the most important for me was learn how Next.js deals with routing in server and client.

2. The current Next.js routing

To give some context, in Next.js' earlier versions you have a way to create links with parametrized urls that look good on the browser:

<Link href={`/post?slug=${slug}`} as={`/post/${slug}`}>
  <a>First Post</a>
</Link>
  • href refers to the path inside the pages directory with the query params.
  • as refers to the path that will be rendered in the browser url.

This is pretty fine, but with this approach you still need to create a custom server if you want beautiful routes in server side. Check this example on how to create parametrized routes with a custom server.

Now since with Next v9, you have a way to manage dynamic routing without creating a custom server structuring your pages architecture like this:

pages/
  |- index.js
  |- post/
     |- [slug].js # dynamic page

ℹ️ Note that the "slug" file must be named with some special notation ([slug].js) to tell Next.js that needs to resolve a dynamic param that points to that page.

And then, Link components should look like this: (check Next.js link docs for more info)

<Link href="/post/[slug]" as={`/post/${slug}`}>
  <a>First Post</a>
</Link>

This routing system is completelly fine. But I don't feel confortable using this in some close term projects by several reasons:

  • It forces the file system structure to be the same as the browser routes. If route paths need to change for some bussiness or SEO reasons, the code structure will need to change too.
  • The routing strategy does't follow any standard convention using brackets: pages/[dynamic-part]/[comment].js.
  • The route delimiter is forced to be an slash "/", because is the filesystem path delimiter itself. But what if I want a route like this? /post-[slug].
  • You can't have a regex pattern to match the route before being handled by your component or pass a route with optional parameter like this /comments/:id?.

ℹ️ Some of those things may be solved on the future by Next.js

3. The library I was using for this is no longer maintained

There are other libraries that solve dynamic routing like fridays/next-routes. But for unknown reasons seem to be deprecated and dependency updates are not merged next-routes/pull/275. I'm not blaming the author.

4. I have a side project that needs route localization

The final reason is because I have a side project where I need translated routes for SEO reasons (it is a multi country website). And this is a feature that I didn't find yet well resolved (I'm not saying routex gives a perfect solution about this, but it gives a solution).

There is a more detailed post about this topic here: alexhoma.com/localized-routes-for-nextjs-using-routex. And also you can check a code example on how to create localized routes with routex.

How it works?

To start, we need to define our application routes, let's call this "the routes manifest" from now on. So we'll need to create a file called routes.js (or whatever we want to call it) in our project's root:

// routes.js

module.exports = [
  {
    name: 'index',
    pattern: '/', // resolves to /pages/index.js
  },
  {
    name: 'post',
    pattern: '/post/:slug',
    page: 'post', // resolves to /pages/post.js
  },
];

This file will be the routing reference for both server and client. On server side, we need these routes to know which pattern match what page. So:

  • pattern will be an express-like route that matches regular expressions. It uses pillarjs/path-to-regexp library to manage this.
  • page refers to any file located inside Next.js pages/ directory.

On client side, we only need the name, to reference our routes from the application links.

Handle routes on the server

We need to handle the requested urls on the app server. Next.js currently has a way to handle server routes, but since we need that routes to be dynamic, you'll need to create a custom server. Let's create a server.js in our project's root:

// server.js

const express = require('express');
const next = require('next');
const nextApp = next({ dev: process.env.NODE_ENV !== 'production' });
const routes = require('./routes');
const { getRequestHandler } = require('routex.js');

// Wrap Next.js getRequestHandler with:
// - The current Next instance
// - The routes manifest
const routexHandlerMiddleware = getRequestHandler(nextApp, routes);

nextApp.prepare().then(() => {
  express()
    .use(routexHandlerMiddleware);
    .listen(3000);
});

ℹ️ The example uses Express for simplicity sake

Routex's getRequestHandler function is nothing but a wrapper of the current Next.js' getRequestHandler middleware that takes the browser url and finds any route in our manifest that matches with it. Once the route is found, it loads the corresponding page.

For example, a request like /post/my-next-js-article will be mapped to the post route and it will execute our component in /pages/post.js file, that will be similar to this:

// pages/post.js

function PostPage({ title, content }) {
  return (
    <section>
      <h1>{title}</h1>
      <p>{content}</p>
    </section>
  );
}

PostPage.getInitialProps = async function ({ query }) {
  // slug will be "my-next-js-article"
  const { slug } = query;

  const result = await getPostBySlugApiCall(slug);

  return {
    title: result.title,
    content: result.content,
  };
};

Handling routes in client

You can avoid the client side link if you want, since links can be composed using the current Next <Link /> component:

import NextLink from 'next/link';

<NextLink href={`/post/${slug}`}>
  <a>My post</a>
</NextLink>;

And this is totally fine since the server handles dynamic routes. But If you have a more complex situations or you need a simpler interface, it's easier to use the link() function that routex provides to you:

import NextLink from 'next/link';
import { createRouteLinks } from 'routex.js';
import routes from './routes';

// Creates a link function aware of our app routes
const { link } = createRouteLinks(routes);

const { as, href } = link({
  route: 'post', // route name
  params: { slug: 'my-post-slug' },
});

<NextLink href={href} as={as}>
  <a>My post</a>
</NextLink>;

Now we have link() that is aware of all the app routes. With that, its easier to create our links. link() will find the matching route by name (the one defined for every route in the manifest) and will replace all the dynamic params. All those params that don't match with the route pattern will be passed as query parameters: /post/my-post-slug?query=hello

Of course, the example avobe might seem a little verbose to use on every component that have links on it. So we can create our custom <Link /> component with a nicer interface to reuse it!

// CustomLink.js

import NextLink from 'next/link';
import { createRouteLinks } from 'routex.js';
import routes from './routes';

// create link() only once
const { link } = createRouteLinks(routes);

function CustomLink({ children, route, params }) {
  const { as, href } = link({ route, params: { ...params } });

  return (
    <NextLink as={as} href={href}>
      <a>{children}</a>
    </NextLink>
  );
}

export default CustomLink;

Then we can use our <CustomLink /> like this:

import CustomLink from './components/CustomLink';

// somewhere in our code...
<CustomLink route="post" params={{ slug: 'my-post-slug' }}>
  My post
</CustomLink>;

Finally, you will end with this basic project structure:

my-blog/
  |- pages/            # the Next.js pages dir
  |  |- index.js
  |  |- post.js
  |- components/
  |  |- CustomLink.js  # your custom Link component
  |- routes.js         # your routes manifest
  |- server.js         # your custom server

Here you can check some running examples: alexhoma/routex.js/examples

Known trade offs

Of course, using routex.js has its trade-offs. Let's have a look:

The good

  • The routes are not coupled to the file system pages architecture. That gives us more flexibility when changing routes for bussiness purposes without touching our file locations, or to have a more flat pages architecture to avoid complexity.
  • We can create routing website with localization without too much effort and without having to duplicate all our pages structure for every locale: /es/sobre-nosotros.js, /en/about-us.js, etc.
  • Standarized system to manage dynamic params in routes based in the commonly used express like regular expressions.

The bad

  • Performance. Routes manifest is present in client side, which means, your client will have to load all the routes payload even if the current page only have two links (this can be an improvement point for the library by now). For websites with a lot of routes it can impact to performance.
  • Going against the tie. With routex you need to implement a custom server. And although using a custom server is a common way to handle routes in Next, since its dynamic routing release in version 9, the community will stop using custom servers.

Extra cool things

  • I've used a mutant testing library called Stryker to refine my tests.
  • To distribute the library bundle I found developit/microbundle, a javascript bundler written on top of Rollup to generate the library ready for different module loaders (AMD, CommonJS, es6, etc).