Skip to content

acominotto/next-handler

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

next-handler

Next.js API route helpers: typed handlers, Zod-validated bodies, and serializable errors that round-trip between server and client.

Install

pnpm add next-handler next zod

Peer dependencies: next (≥15), zod (≥4.3.6).


Backend (API routes)

Wrap your handler

Use withApiHandler so the request is in context and thrown errors become JSON responses.

// app/api/users/route.ts
import { NextResponse } from "next/server";
import { withApiHandler, payload, getRequest } from "next-handler";
import { z } from "zod";

const createUserSchema = z.object({ name: z.string(), email: z.string().email() });

export const POST = withApiHandler(async () => {
  const body = await payload(createUserSchema);
  // body is { name: string; email: string }
  const request = getRequest();
  // ... create user, then:
  return NextResponse.json({ id: "1", ...body });
});

Throwing errors

Throw BadRequestError, NotFoundError, etc. They are serialized and sent as JSON with the right status.

import {
  withApiHandler,
  payload,
  BadRequestError,
  NotFoundError,
  UnauthorizedError,
} from "next-handler";

export const GET = withApiHandler(async (req) => {
  const auth = req.headers.get("authorization");
  if (!auth) throw new UnauthorizedError("Missing token");
  // ...
});

export const POST = withApiHandler(async () => {
  const body = await payload(someSchema); // throws BadRequestError if invalid
  const user = await findUser(body.id);
  if (!user) throw new NotFoundError("User");
  return NextResponse.json(user);
});

Built-in error classes: BadRequestError (400), UnauthorizedError (401), ForbiddenError (403), NotFoundError (404), ConflictError (409), InternalServerError (500). All extend EndpointError and are serialized with name, message, status, and optional details.


Frontend: using the errors with ky

When the API returns an error, the response body is the serialized error object (or { error: "An error occurred" } for unhandled errors). Use afterResponse to deserialize and rethrow so callers get BadRequestError, NotFoundError, etc.

import ky from "ky";
import {
  deserializeApiError,
  BadRequestError,
  NotFoundError,
} from "next-handler";

const api = ky.create({
  prefixUrl: "/api",
  hooks: {
    afterResponse: [
      async (_request, _options, response) => {
        if (response.ok) return response;
        let data: unknown;
        try {
          data = await response.json();
        } catch {
          return response;
        }
        const error = deserializeApiError(data);
        if (error) throw error;
        const fallback = new Error(
          typeof data === "object" && data !== null && "error" in data
            ? String((data as { error: unknown }).error)
            : response.statusText
        );
        (fallback as Error & { status: number }).status = response.status;
        throw fallback;
      },
    ],
  },
});

// Errors are proper classes
try {
  await api.get("users/123").json();
} catch (e) {
  if (e instanceof NotFoundError) {
    console.log(e.message);   // "User not found"
    console.log(e.resource);  // "User"
    console.log(e.status);    // 404
  }
  if (e instanceof BadRequestError) {
    console.log(e.details);  // e.g. Zod issues
  }
}

Minimal hook that only rethrows when the body is a serialized API error:

import { deserializeApiError } from "next-handler";

const api = ky.create({
  prefixUrl: "/api",
  hooks: {
    afterResponse: [
      async (_request, _options, response) => {
        if (response.ok) return response;
        try {
          const data = await response.json();
          const error = deserializeApiError(data);
          if (error) throw error;
        } catch (e) {
          if (e instanceof Error && "status" in e) throw e;
          throw e;
        }
        return response;
      },
    ],
  },
});

Frontend: using the errors with axios

Use a response interceptor to turn error responses into thrown BadRequestError / NotFoundError / etc. when the body is a serialized API error.

import axios from "axios";
import { deserializeApiError, BadRequestError, NotFoundError } from "next-handler";

const api = axios.create({ baseURL: "/api" });

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (!axios.isAxiosError(error) || !error.response) return Promise.reject(error);
    const data = error.response.data;
    const apiError = deserializeApiError(data);
    if (apiError) return Promise.reject(apiError);
    return Promise.reject(error);
  }
);

try {
  await api.get("/users/123");
} catch (e) {
  if (e instanceof NotFoundError) {
    console.log(e.message, e.resource, e.status);
  }
  if (e instanceof BadRequestError) {
    console.log(e.details);
  }
}

With TypeScript, ensure your catch clause can narrow to your error types (e.g. import NotFoundError, BadRequestError from next-handler).


Custom error types

Register a custom error so it serializes and deserializes across the wire:

import {
  apiErrorFactory,
  EndpointError,
  type ErrorDeserializer,
  type SerializableErrorCtor,
} from "next-handler";

class PaymentRequiredError extends EndpointError {
  static ErrorName() {
    return "PaymentRequiredError";
  }
  constructor(message = "Payment required") {
    super(402, message);
    this.name = PaymentRequiredError.ErrorName();
  }
}

const deserialize: ErrorDeserializer = (d) => new PaymentRequiredError(d.message);
apiErrorFactory.register(PaymentRequiredError as SerializableErrorCtor, deserialize);

Do this once (e.g. in a shared module or app setup) on both server and client so responses using PaymentRequiredError round-trip correctly.


API summary

Export Description
withApiHandler(handler) Wraps a route handler; provides request context and turns thrown EndpointError into JSON responses.
payload(schema) Parses and validates request body with Zod; throws BadRequestError on invalid input.
getRequest() Returns the current NextRequest inside a wrapped handler.
serializeApiError(error) Turns an error into a plain object for JSON (used by the handler).
deserializeApiError(data) Turns a parsed JSON object into an error instance, or null if not a serialized API error.
apiErrorFactory Registry: register(ctor, deserialize) for custom error types.
BadRequestError, NotFoundError, … Built-in error classes.

Response body for API errors is the serialized error object (e.g. { name, message, status, details?, isSerializableError: true }). Use deserializeApiError(responseBody) in ky/axios interceptors to rethrow the correct class on the client.

About

Next Handler, to simplify error management and payload management in Nextjs

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors