Made by @vndaba_

Simple, type-safe REST APIs in Next.js

A simple approach to achieving type safety in your Next.js project without sacrificing developer experience or the simplicity of RESTful APIs

Getting started

INSTALLATION

npm install typed-handlers@latest
CONFIGURATION

The package exposes a next.js plugin that is used to generate the route definitions. You will need to add the following to your next.config.js file. If you are using this with the pages router, you will need to set the legacy option to true

// next.config.js

import { withTypedHandlers } from 'typed-handlers/next';

export default withTypedHandlers({
	//... next config 
},{
 legacy: true // pages router
});

Once you've added this to your config, you'll notice that a new file routes-env.d.ts is created add added to the include options in your tsconfig.json file. If this doesn't happen automatically for whatever reason, ensure your setup resembles the following

// routes-env.d.ts

/// <reference types='typed-handlers/routes' />
// tsconfig.json

{
  // ...
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "routes-env.d.ts"
  ],
}

And that's it! In dev mode, the package will watch for changes/new api routes and generate the necessary types for you to use in your app. In production, the package will generate the types at build time.

Example usage

// app/api/projects/[id]/route.ts (Route handler)

import { NextResponse } from "next/server";
import type { Params } from "typed-handlers";

export const GET = async (
  req: Request,
  { params }: { params: Params<"/api/projects/[id]"> }
) => {
  return NextResponse.json({
    id: params.id,
    name: "My Project",
    title: "My Project",
    slug: "my-project",
    description: "This is my project",
  });
};
// app/projects/[id]/page.tsx (Client component)

"use client";

import { useEffect, useState } from "react";
import { handle } from "typed-handlers";

export default function ProjectsPage({ params }: { params: { id: string } }) {
  useEffect(() => {
    const cfg = handle("/api/projects/[id]", {
      params,
    });

    async function load() {
      fetch(cfg.url)
        .then((res) => res.json())
        .then((data) => console.log(data));
    }

    load();
  }, [params]);

  return <div>{/* ... */}</div>;
}

The simplified example above demonstrates how to use the package. As you can see, it exposes a very minimal API surface with the handle method being the only available import. This method is used to make requests to typed endpoints with confidence. You pass it the type-safe route path, in the same format as the Next.js API route, and an options object that optionally contains a zod request body schema, query string params, and route parameters. A full-fleged example would look like this:

import { handle } from "typed-handlers";
import { z } from "zod";

const updateProjectSchema = z.object({
  name: z.string(),
  description: z.string(),
  completed: z.boolean(),
});

const cfg = handle("/api/projects/[id]", {
  params: {
    id: "123456",
  },
  query: {
    include: "some_value",
  },
  bodySchema: updateProjectSchema,
});

console.log(cfg.url); // /api/projects/123456?include=some_value

The handle method returns an object with a url property containing the actual url that you can use to make requests. You can then pass this to your request library of choice. (fetch/axios etc)

Other than that, it also returns a body method that you can use to type check the request body. This method will throw a type check error if the body does not match the zod schema you provided. Please note that this method does not actually parse the body, it only helps with type checking.

const typeSafeBody = cfg.body({
  name: "My Project",
  description: "This is my project",
  completed: false,
});

In summary

File-system based routers are great for DX, but they lack the type safety that is expected in a modern typescript project. And while this is by no means a fully type-safe solution, I think it's a start. The API is intentionally minimal as to avoid injecting too much opinions into your project. It's built to work seamlessly with your existing Next.js project and is designed to be as unobtrusive as possible.

If you like what you just read, please consider giving it a star on Github