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
- Minimal footprint
Built with zero dependencies and a small API surface
- Pages router support
Pages or app router, it matters not. Both are supported
- IDE and runtime type safety
Linter errors when building and red squiggly lines in your editor
- Easy to integrate
Built to work seamlessly with your existing Next.js project
- Endpoint autocompletion
Make requests to typed endpoints with confidence
- Built on Zod
Leverage the power of Zod to define your input and output schemas
Getting started
INSTALLATION
npm install typed-handlers@latest
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