BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Pathpida Brings Types to Next.js and Nuxt.js Dynamic Routing with Zero Configuration

Pathpida Brings Types to Next.js and Nuxt.js Dynamic Routing with Zero Configuration

Bookmarks

Key Takeaways

  • Pathpida solves the challenge of validating the existence of dynamic routes in Next.js and Nuxt.js projects.
  • Pathpida automatically collects routes in one place.
  • Pathpida generates a TypeScript file to support static checking of routes.
  • Pathpida strives for zero-configuration.
  • Pathpida is easily added to existing Next.js and Nuxt.js projects.

Pathpida collects dynamic routing into a single TypeScript file

Pathpida is a library for TypeScript projects to collect dynamic routes in one place. It is a tedious task to do manually. This helps us check the existence of routes, which is often overseen as a project grows.

Pathpida is optimized for Next.js (React) and Nuxt.js (Vue). Pathpida can be added to existing Next.js and Nuxt.js projects without configuration.

What problems does Pathpida solve?

Managing routes across complex applications is challenging and tedious. Consider a simple example with Next.js, there is a Link to URL /post/1:

import Link from 'next/link'

export default () => {
  const url = `/post/${1}`
  return <Link href={url} />
}

Here, we cannot check statically the existence of  /post/{pid}. If pages/post/[pid].tsx is absent, route transition would unexpectedly fail at runtime.

Even with Template Literal Types shipped by TypeScript 4.1, it is a messy work to capture all routes manually. If we write all routes in one place manually, we need to do point-and-shoot check, also every time we change the routes.

Pathpida watches and walks the pages directory to achieve checking existence of routes we use in components statically. This means, we can check whether all links are valid in CI with validating through TypeScript.

The core concept of watching, analyzing AST, and writing a TypeScript file is derived from aspida.

Consider the situation that some paths include dynamic route parameters such as article slugs or product ids in e-commerce:

pages/[pid].tsx
pages/blog/[...slug].tsx
pages/index.tsx

In this example, pathpida produces the following lib/$path.ts:

export const pagesPath = {
  _pid: (pid: number | string) => ({
    $url: () => ({ pathname: '/[pid]', query: { pid }})
  }),
  blog: {
    _slug: (slug: string[]) => ({
      $url: () => ({ pathname: '/blog/[...slug]', query: { slug }})
    })
  },
  $url: () => ({ pathname: '/' })
}

The properties of the generated client corresponds to routes, one by one, including dynamic routes. The value returned by .$url() gets passed to next/link and next/router. All routes are typed by inference and can get statically checked for existence.

Now, we can write the links using the Pathpida generated routing client. For example, within components:

// pages/index.tsx
import Link from 'next/link'
import { pagesPath } from '../lib/$path'
 
export default () => {
  return <Link href={pagesPath.blog._slug(['a', 'b', 'c']).$url()} />
}

Introducing pathpida to a Next.js project

Consider an environment with Next.js and TypeScript. It is easy to use npm-run-all for convenience with npm or yarn.
Run yarn add pathpida npm-run-all --dev and add the following npm scripts to package.json:

{
  "scripts": {
    "dev": "run-p dev:*",
    "dev:next": "next dev",
    "dev:path": "pathpida --watch",
    "build": "pathpida && next build"
  }
}

Now, yarn dev starts pathpida alongside the Next.js dev server. In either the utils or lib directory, $path.ts gets generated.

Each time we update, add or delete files under the pages/ (also supports configurations like using src/pages/), lib/$path.ts would get replaced automatically. Files under the directory pages/api in Next.js projects are ignored because they are reserved as an API, not pages.

Then we can use the pathpida client by importing pagesPath from $path.ts. An example with the Next.js link and router:

lib/
  $path.ts
pages/
  articles/
    [id].tsx
  users/
    [...userInfo].tsx
  _app.tsx
  index.tsx
 
// components/ActiveLink.tsx
import Link from 'next/link'
import { useRouter } from 'next/router'
import { pagesPath } from '../lib/$path'
 
function ActiveLink() {
  const router = useRouter()
 
  const handleClick = () => {
    router.push(pagesPath.users._userInfo(['mario', 'hello', 'world!']).$url())
  }
 
  return <>
    <div onClick={handleClick}>Hello</div>
    <Link href={pagesPath.articles._id(1).$url()}>
      World!
    </Link>
  </>
}
 
export default ActiveLink

Supplying the required query string parameter

Pathpida can also add types for query string by exporting Query type from the pages component. To make the route /user?userId={number}, edit pages/user.tsx as follows:

export type Query = {
  userId: number
}
 
export default () => <div />

With this small change we can specify the query string as pagesPath.user.$url({ query: { userId: 1 }}).

Using Query, we should provide one argument to .$url if all properties are optional. To make the argument itself optional, use OptionalQuery instead of Query:

export type OptionalQuery = {
  userId: number
}
 
export default () => <div />

This change allows us to call .$url without any arguments.

import { pagesPath } from '../lib/$path'
 
pagesPath.user.$url({ query: { userId: 1 }})
pagesPath.user.$url()

Hash can also be specified using the hash property.

import { pagesPath } from '../lib/$path'
 
pagesPath.user.$url({ query: { userId: 1 }, hash: 'hoge' })
pagesPath.user.$url({ hash: 'fuga' })

Get static file paths under the public directory with type-safety

By supplying the flag --enableStatic, pathpida generates the staticPath client by watching the public directory:

{
  "scripts": {
    "dev": "run-p dev:*",
    "dev:next": "next dev",
    "dev:path": "pathpida --enableStatic --watch",
    "build": "pathpida --enableStatic && next build"
  }
}

Consider an example where the public directory consists of one JSON and one png file:

public/aa.json
public/bb/cc.png
 
lib/$path.ts or utils/$path.ts

Then we can see staticPath generated in $path.ts. This has all static paths as string in properties, with periods converted to underscores as follows:

// pages/index.tsx
import Link from 'next/link'
import { staticPath } from '../lib/$path'
 
console.log(staticPath.aa_json) // /aa.json
 
export default () => <img src={staticPath.bb.cc_png} />

Introducing pathpida to Nuxt.js projects

For projects set up with Nuxt.js and TypeScript, add pathpida client as a plugin to nuxt.config.js:
 

{
  plugins: ['~/plugins/$path']
}

We can access the pathpida client from Vue/Vuex instances via $pagesPath. Using --enableStatic, pathpida also provides the $staticPath. For example:
 

<!-- pages/index.vue -->
<template>
  <div>
    <nuxt-link :to="$pagesPath.post._pid(1).$url()" />
    <div @click="onclick" />
  </dijv>
</template>
 
<script lang="ts">
import Vue from 'vue'
 
export default Vue.extend({
  methods: {
    onclick() {
      this.$router.push(this.$pagesPath.post._pid(1).$url())
    }
  }
})
</script>

Pathpida treats the project as Nuxt.js when detecting nuxt.config.js or nuxt.config.ts in project root, otherwise falling back to Next.js. $path.ts could be different for Next.js and Nuxt.js. When Nuxt.js gets used, files with names starting with hyphen get ignored. With Vue files, we cannot use exported Query consisting of non-global types. For details, refer to the generated $path.ts.

Pathpida is open source software available under the MIT license. Contributions and feedback are encouraged via the Pathpida GitHub project.

About the Author

Teppei Kawaguchi is a web developer, especially backend and TypeScript. He is contributing to open source projects whatever he likes. He is an enthusiastic competitive programmer, qualified in more than 8 contests. He experiences constructing a cloud architecture to achieve delivering efficiently.

 

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT