Ensuring Type Safety in Next.js Routing
Tags: nextjs, routing, webdev, javascript
Published: 2024-04-20 12:32:00

Last week, I explored an open-source project called declarative routing. This project facilitates the implementation of a type-safe routing system within Next.js applications. I'm interested in writing a short article about it and the insights I gained from building a small project that utilized declarative routing.

## What Is [Declarative-Routing](https://github.com/ProNextJS/declarative-routing)?
  
While type-safe routing offers many benefits, it can introduce challenges when working with URLs. Consider the following example:
```tsx 
<Link to={/post/${post.id}}>post</Link>
```
This approach might lead to issues if the URL structure or parameters change later.

Declarative routing offers a solution by providing components specifically typed into routes. These components handle URL construction automatically. For instance, with a component named `PostDetail.Link`, you could write the code as

```tsx
 <PostDetail.Link id={post.id}>post</PostDetail.Link>
```
This approach ensures that any updates to the route or its parameters are reflected consistently throughout your application, wherever the `PostDetail.Link` component is used.

## **Build a Small Next.js App with Declarative Routing**

The source code for this project is publicly available on [GitHub] (https://github.com/Kumneger0/typesafe-routing-next-js)
to get started with Declarative Routing: run the following command: 

```bash
npx declarative-routing init
```

This will create a directory called @/routes, which you can use to access websites and submit API queries. In the routes directory, it also creates a [README.md] (http://readme.md/) file with instructions on how to utilize the system.

**To keep the `@/routes` directory synchronized with your application's routes, be sure to run the following command whenever you modify existing files or introduce new routes:

```bash
npx declarative-routing build
```

This will generate info.ts files in the same directory as page.tsx or route.ts Subsequently, it will import these .info.ts files into @/routes/index.ts, allowing you to import all the routes from @/routes and utilize them within your application

The project appears to have three routes:

- **Home (/): l**eads to a list of posts.
- **PostID (/posts/<postID>):** Enables viewing individual posts.

Additionally, there is one API endpoint located at **`src/app/api/posts/route.ts`**, which is used to retrieve lists of posts. The posts being used are located at **`src/db/db.ts`**, which exports an array of dummy data. We are sending that data from our API route.

## Home

here is the code for the homepage

```tsx
"use client";
import EachPost from "@/components/posts";
import { getApiPosts } from "@/routes";

import React, { useEffect, useState } from "react";

function Post() {
  const [posts, setPosts] = useState<Awaited<ReturnType<typeof getApiPosts>>>(
    []
  );

  const [limit, setLimit] = useState<number | null>(null);

  useEffect(() => {
    getApiPosts({}, { limit }).then(setPosts);
  }, [limit]);

  return (
    <div>
      <div>
        <h1 className="font-bold text-2xl ">API Route Example</h1>
      </div>
      <div>
        <input
          type="number"
          placeholder="Add limit"
          className="p-3 rounded-xl border border-black"
          onChange={(e) => setLimit(Number(e.target.value))}
        />
      </div>
      <div>
        {posts?.map(({ id, title }) => (
          <EachPost id={Number(id)} title={title} key={id} />
        ))}
      </div>
    </div>
  );
}

export default Post;
```

Let me explain the code above. We import `getApiPosts` from `"@/routes,"` which is auto-generated by calling `declarative-routing build`. This function retrieves a list of posts from `/api/posts` and updates the state variable.

At this time, you may be wondering why not simply use a server component and invoke **`getApiPosts`** at the top level of the component? Well, you are right, but there is an issue. 

if you do that, you will get an error  `Failed to parse URL /api/posts`

When calling **`getApiPosts`**, we are simply making a fetch call to **`/api/posts`**.

In server components, you need to specify an absolute URL. In a web browser, when you say **`fetch('/api/posts')`**, the browser assumes that it's relative to the document base URL/origin.

For more information, check out this [issue](https://github.com/vercel/next.js/issues/48344).

## How Does getApiPosts function work?

if you see `@routes/index.ts` below

```tsx
// Automatically generated by declarative-routing; do NOT edit
import { z } from "zod";
import { makeGetRoute, makeRoute } from "./makeRoute";

const defaultInfo = {
  search: z.object({})
};

import * as HomeRoute from "@/app/page.info";
import * as ApiPostsRoute from "@/app/api/posts/route.info";
import * as PostsPostIDRoute from "@/app/posts/[postID]/page.info";

export const Home = makeRoute(
  "/",
  {
    ...defaultInfo,
    ...HomeRoute.Route
  }
);
export const PostsPostID = makeRoute(
  "/posts/[postID]",
  {
    ...defaultInfo,
    ...PostsPostIDRoute.Route
  }
);

export const getApiPosts = makeGetRoute(
  "/api/posts",
  {
    ...defaultInfo,
    ...ApiPostsRoute.Route
  },
  ApiPostsRoute.GET
);

```

The `makeGetRoute` function is a helper function that generates these type-safe get functions. It takes three arguments:

1. The path for which the function should be generated. In the case of `getApiPosts`, this would be "/api/posts.".
2. An object that contains the schema for the route's parameters and query parameters. This is typically generated from a`.info.ts` file that is associated with the route.
3. An optional object that represents the schema for the response data expected from the endpoint. This ensures that the data returned from the endpoint adheres to the expected structure, further enhancing the type safety of your application.

## Creation of page routes with Declarative Routing

- `makeRoute`: This function is used to define a page route. It takes the path of the route as a string and an info object that contains the name of the route, as well as the Zod schemas for the route parameters and search parameters. This function ensures that any access to the specified route adheres to the expected structure, enhancing the type safety of your application. Here is an example usage:

```tsx
export const Home = makeRoute(
  "/",
  {
    ...defaultInfo,
    ...HomeRoute.Route
  }
);

```

In the example above, `Home` is a type-safe function that constructs the URL for the home page. It ensures that any access to the home page route adheres to the expected structure, further enhancing the type safety of your application.

## Creation of API routes with Declarative Routing

In declarative routing, API routes are defined using `makeGetRoute`, `makePostRoute`, `makePutRoute`, and `makeDeleteRoute` functions. These functions facilitate the creation of type-safe functions for API endpoints in your Next.js application.

- `makePostRoute`: This function is used to create a type-safe function for POST requests to an API endpoint. It takes the path of the route as a string, an info object that contains the name of the route, and Zod schemas for the route parameters and search parameters. It also takes a secondary object containing the body schema. The function ensures that any POST request to the specified route adheres to the expected type structure, enhancing the type safety of your application.
- `makePutRoute`: Similar to `makePostRoute`, this function is used to create a type-safe function for PUT requests to an API endpoint. It takes the same arguments as `makePostRoute`, including the path, info object, and body schema. The use of this function ensures consistency and type safety when making PUT requests in your application.
- `makeDeleteRoute`: This function is used for DELETE requests. It takes the path of the route as a string, an info object containing the name of the route, and the Zod schemas for the route parameters and search parameters. It does not require a body schema, as DELETE requests typically do not include a body.

The getApiPosts function accepts three parameters based on the following object located at `src/app/api/route.info.ts`:

```tsx
export const Route = {
  name: "ApiPosts",
  params: z.object({}),
  search: z.object({ limit: z.number().nullable() }),
};
```

**Parameters:**

- **The first argument** corresponds to the `Route.params` property, which is an empty object in this case.
- **The second argument** aligns with the `Route.search` property. It accepts an optional `limit` parameter of type `number`. If you omit the `limit` parameter, an error will occur. `Argument of type '{}' is not assignable to parameter of type '{ limit: number | null; }'`.
- **The third argument (optional)** allows you to pass additional options like headers and methods, similar to the `options` argument in the Fetch API

## How Does getApiPosts function work?

if you see getApiPosts in `@/routes/index.ts` 

After retrieving the posts from the server, we update the state variable. The posts are then displayed on the page, and the `EachPost` component handles rendering each post's title

```tsx
import { PostsPostID } from "@/routes";
import React from "react";

function EachPost({ id, title }: { id: number; title: string }) {
  return (
    <div key={id}>
      <PostsPostID.Link postID={String(id)}>
        <div className="text-xl text-blue-700 hover:text-blue-500 hover:underline">
          title : {title}
        </div>
      </PostsPostID.Link>
    </div>
  );
}

export default EachPost;
```

The `EachPost` component accepts two properties: `postId` and `title`. It renders a link to `/posts/<postId>` using the `PostsPostID.Link` component. As previously mentioned, this component is auto-generated and functions similarly to the following code:

```tsx
<Link href={`/posts/${id}`}>{title}</Link>
```

However, the advantage of using `PostsPostID.Link` is that it offers type safety. This means that if you forget to pass the `postID` or pass it in the wrong type, your code will not compile, thus preventing potential runtime errors. This is a significant benefit of declarative routing in Next.js applications.

The open-source project, declarative routing, provides a type-safe routing system within Next.js applications. It offers components specifically typed into routes, handles URL construction automatically, and ensures consistent updates throughout the application.


Thanks for reading! If you enjoyed this post and want to see more content like this, head over to my [website](https://kumneger.vercel.app/blog) I'd love to see you there!