npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ideative/next-handler

v0.1.0

Published

Next Handler - Simple Next.js handler for API routes

Readme

@ideative/next-handler

Docs Coverage Statements Coverage Branches Coverage Functions Coverage Lines

Typed helpers for Next.js route handlers with:

  • request context access (getRequest()),
  • Zod payload validation (payload()),
  • serializable errors that round-trip from backend to frontend.

Install

pnpm add @ideative/next-handler

Peer dependencies:

  • next >= 15
  • zod >= 4.3.6
  • react >= 19.2.4 (for intl context utilities)
  • next-intl >= 4.8.2 (for intl utilities)

Backend usage

Wrap handlers

import { NextResponse } from "next/server";
import { withApiHandler, payload, getRequest } from "@ideative/next-handler";
import { z } from "zod";

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

export const POST = withApiHandler(async () => {
  const body = await payload(schema);
  const req = getRequest();
  return NextResponse.json({ from: req.url, ...body });
});

Throw typed API errors

import {
  withApiHandler,
  BadRequestError,
  NotFoundError,
  UnauthorizedError,
} from "@ideative/next-handler";

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

Add request-scoped async context

import { NextResponse } from "next/server";
import { withApiHandler, UnauthorizedError } from "@ideative/next-handler";

const authApiHandler = withApiHandler.enhance(async (req) => {
  const token = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
  if (!token) throw new UnauthorizedError("Missing bearer token");

  const user = { id: "u_123", role: "admin" } as const;
  return { user };
});

export const GET = authApiHandler(async (_req, context) => {
  const { user } = authApiHandler.context();
  return NextResponse.json({
    userId: user.id,
    role: user.role,
    sameUserFromContext: context?.user.id,
  });
});

Built-ins:

  • BadRequestError (400)
  • UnauthorizedError (401)
  • ForbiddenError (403)
  • NotFoundError (404)
  • ConflictError (409)
  • InternalServerError (500)

Wire format contract

Known API errors are returned as a serialized object:

type SerializedApiError = {
  name: string;
  uid: string;
  message: string;
  status?: number;
  details?: unknown;
  isSerializableError: true;
  [key: string]: unknown;
};

Unhandled non-library errors return:

{ "error": "An error occurred" }

Frontend integration with ky (afterResponse)

import ky from "ky";
import {
  scanResponseAndThrowErrors,
  BadRequestError,
  NotFoundError,
} from "@ideative/next-handler";

const api = ky.create({
  prefixUrl: "/api",
  hooks: {
    afterResponse: [
      async (_req, _opts, response) => {
        await scanResponseAndThrowErrors(response);
        if (!response.ok) throw new Error(response.statusText);
        return response;
      },
    ],
  },
});

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

If you already have a Response, you can also call:

import { scanResponseAndThrowErrors } from "@ideative/next-handler";

await scanResponseAndThrowErrors(response);

Frontend integration with axios (response interceptor)

import axios from "axios";
import { deserializeApiError } from "@ideative/next-handler";

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

api.interceptors.response.use(
  (res) => res,
  (err) => {
    if (!axios.isAxiosError(err) || !err.response) return Promise.reject(err);
    const data = err.response.data;
    const candidate =
      typeof data === "object" && data !== null && "error" in data
        ? (data as { error: unknown }).error
        : data;
    const apiError = deserializeApiError(candidate);
    return Promise.reject(apiError ?? err);
  }
);

Custom error types

EndpointError is abstract and expects (name, status, message, details?).

import {
  apiErrorFactory,
  EndpointError,
  type ErrorDeserializer,
} from "@ideative/next-handler";

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

const deserialize: ErrorDeserializer<PaymentRequiredError> = (d) =>
  new PaymentRequiredError(d.message);

apiErrorFactory.register(PaymentRequiredError, deserialize);

Register custom errors in both server and client runtime initialization so deserialization works everywhere.

Intl exports

Import intl helpers from:

  • @ideative/next-handler/intl
  • @ideative/next-handler/intl/intl-context

API summary

| Export | Description | | --------------------------------------------- | ------------------------------------------------------------------------------------- | | withApiHandler(handler) | Wraps route handlers and converts thrown EndpointError values to JSON responses. | | withApiHandler.enhance(enhancer) | Returns an enhanced handler with .context() for request-scoped async ALS context. | | payload(schema) | Reads and validates request JSON with Zod, throws BadRequestError on invalid input. | | getRequest() | Gets current NextRequest from AsyncLocalStorage context. | | serializeApiError(error) | Converts SerializableError to transport-safe payload. | | deserializeApiError(data) | Converts payload back to typed error, or null if payload is not recognized. | | isSerializedApiError(data) | Runtime type-guard for serialized payload shape. | | scanResponseAndThrowErrors(response) | Scans non-OK responses and rethrows serialized API errors if present. | | apiErrorFactory.register(ctor, deserialize) | Register custom error classes for round-trip behavior. |

Coverage

  • Latest measured coverage: statements 94.81%, branches 90.51%, functions 96.05%, lines 94.81%.
  • Generate coverage locally with:
pnpm dlx c8 --reporter=text-summary --reporter=text ava --node-arguments='--import=tsx'
  • Live docs: https://acominotto.github.io/next-handler/
  • Repository: https://github.com/acominotto/next-handler

v0.1.0 release checklist

  • pnpm run build emits all exported entry points.
  • pnpm test passes.
  • Coverage is checked (c8) and badges are up to date.
  • README examples match current runtime contracts.
  • package.json exports resolve to emitted dist/ files.