next-type-safe-routes
parses the /pages
folder in your Next.js app and generates types for all the pages and API routes in the application. These types can then be used to ensure that you only link to pages (and only use API routes) that actually exists in your application.
With the types generated, you can use the getRoute
utility to retrieve links that are guaranteed to exist in your the application:
- Automatic route listing. Avoid having to maintain a list of existing pages for your application
- Compile time route validation. Avoid having to run your application to verify if links are correct, just use types
- Unopinionated. Use our simple and composable utils or create your own abstraction
Install using yarn:
yarn add @axelraag/next-type-safe-routes
Or using npm:
npm install @axelraag/next-type-safe-routes --save
For an example setup, see the
/example
folder
The easiest way to use next-type-safe-routes
, is with next-compose-plugins
. With next-compose-plugins
installed, you can add a next.config.js
file with the following content:
const withPlugins = require("next-compose-plugins");
const nextTypeSafePages = require("next-type-safe-routes/plugin");
module.exports = withPlugins([nextTypeSafePages]);
When you start up your application, it will generate types for all of your pages and API routes and save them to the file next-type-safe-routes/dist/utils.d.ts
in the package. The file will be updated whenever you add or remove pages and API routes.
To generate the types without starting the application, execute the command next-type-safe-routes
. This may be necessary to use it in CI/CD that all test pass.
{
"scripts": {
"postinstall": "next-type-safe-routes"
}
}
You can now import the getRoute
util from next-type-safe-routes
and use it to retrieve a route that's is guaranteed to exist in your application.
import { getRoute } from "next-type-safe-routes";
// for simple routes (e.g. the file `/pages/users.tsx`)
getRoute("/users");
// for dynamic routes (e.g. the file `/pages/users/[userId]/page.tsx`)
getRoute({ route: "/users/[userId]", params: { userId: "1" } });
// for catch all routes (e.g. the file `/pages/catch-all/[[...slug]].tsx`)
getRoute({ route: "/catch-all", path: "/a/b/c" });
Now you just need to decide how you want to integrate next-type-safe-routes
in your project. If you want inspiration, we demonstrate how to create a simple abstraction for the Next.js Link
and router
in the example project.
Since the Next.js router is based (strictly) on the file-system, we can determine which pages and API routes exists in an application simply by parsing the /pages
folder. And due to the strictness, we can also determine which parameters are needed for dynamic routes.
As mentioned in the usage section, we generate a module declaration specific to your project when running your project. The output looks like this:
declare module "next-type-safe-routes" {
export type TypeSafePage = ... // all your pages
export type TypeSafeApiRoute = ... // all your routes
export const getPathname = ... // typed based on your routes
export const getRoute = ... // typed based on your routes
}
See
/example/src/@types/next-type-safe-routes/index.d.ts
for a real example
The trick here is, that we override the types for next-type-safe-routes
. And we (re)define the args accepted by the getRoute
and getPathname
to match the types for your project.
The declaration will be written to @types/next-type-safe-routes/index.d.ts
in the root (determined by Next.js) of your project.
How you ensure that only links to existing pages is essentially up to you, but we do expose a few tiny util methods to help you do this.
A simple method that converts a type-safe route to an "actual" route.
Examples:
import { getRoute } from "next-type-safe-routes";
// For simple (non-dynamic) routes
const route = getRoute("/users"); // => "/users"
// With query params
const route = getRoute({
route: "/users",
query: { "not-typed": "whatevs" },
}); // => "/users?not-typed=whatevs"
// For dynamic routes
const route = getRoute({
route: "/users/[userId]",
params: { userId: 1234 },
}); // => "/users/1234"
// For catch all routes
const route = getRoute({
route: "/catch-all",
path: "/can/be/anything",
}); // => "/catch-all/can/be/anything"
Optional catch all routes are also supported.
The getPathname
works similarly to the getRoute
. It just returs a Next.js pathname. For instance:
import { getPathname } from "next-type-safe-routes";
const path = getPathname({
route: "/users/[userId]",
params: { userId: 1234 },
}); // => `/users/[userId]`
These can be useful for making your own abstraction. For instance, if you want to make a tiny abstraction ontop of the next/router
:
import { TypeSafePage, getRoute } from "next-type-safe-routes";
import { useRouter as useNextRouter } from "next/router";
const useRouter = () => {
const router = useNextRouter();
// Say you only want to allow links to pages (and not API routes)
const push = (typeSafeUrl: TypeSafePage) => {
router.push(getRoute(typeSafeUrl));
};
return { ...router, push };
};
export default useRouter;
For basic routes, the type can be of the type string
or:
{
route: string,
query?: { ... } // any key value pairs (not type-safe)
}
And for dynamic routes, the type is always:
{
route: string,
params: { ... }, // based on the file name
query?: { ... } // any key value pairs (not type-safe)
}
And for catch all routes, a (non-typed) path
will also be required (or optional for optional catch all routes):
{
route: string,
path: string,
params: { ... }, // based on the file name
query?: { ... } // any key value pairs (not type-safe)
}
Examples:
type Query = { [key: string]: any };
export type TypeSafePage =
| "/users"
| { route: "/users"; query?: Query }
| {
route: "/users/[userId]";
params: { userId: string | number };
query?: Query;
}
| {
route: "/users/[userId]/catch-all-route";
params: { userId: string | number };
path="/catch/all/path"
query?: Query;
};
Note, the
TypeSafePage
andTypeSafeApiRoute
are kept separate even though they are essentially the same type. We do this, as you may potentially want to distinguish between them in your application.
At my company, Proper, we like pages. Like...a lot! Our platform is a fairly large Next.js application consisting of ~70 pages. And we link between pages ~200 places in the application.
We find that having pages make features easily discoverable by end-users and developers alike. And having pages (urls) for each of our features help us maintain a sane information architecture throughout our platform.
The Next.js file-system based router help us stay consistent and organised around our pages. But we've had some incidents where our application was released with dead links.
At one point, a file in the /pages
folder was renamed and we simply overlooked (forgot to change) some of the links to that page. Another time, a bit of "clever" string concatenation caused an issue. In this case, we had moved a page, and failed to update all links to the page correctly due to the concatenated links.
With the next-type-safe-routes
, we're trying to mitigate this issue. The plugin gives us confidence when refactoring as well as a top notch developer experience.
We considered something like the
next-routes
approach, but we don't want to manually have to maintain a list of routes in the application. We prefer conventions to be enforced when possible.