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.