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

next-response-kit

v2.0.0

Published

Drop-in NextResponse replacement for Next.js App Router — consistent API shape, full TypeScript, zero config.

Readme

next-response-kit

A drop-in replacement for NextResponse — same API you already know, with a consistent response shape on every route, automatically.

npm version license Next.js TypeScript


The problem in every Next.js project

Every developer on the team returns responses differently:

// route A — raw data
return NextResponse.json({ user }, { status: 200 });

// route B — wrapped with success
return NextResponse.json({ success: true, data: user });

// route C — plain error string
return NextResponse.json({ error: "Not found" }, { status: 404 });

// route D — forgot the status code entirely
return NextResponse.json({ message: "Something broke" });

The frontend ends up doing if (res.user || res.data?.user || res.error) in every component. It's a mess that gets worse as the team grows.

next-response-kit fixes this with zero learning curve — it works exactly like NextResponse but every response, success or error, always returns the same consistent shape.


Install

npm install next-response-kit

Peer dependency: next >= 13.0.0 (App Router)


Two ways to use it — pick one, or mix both

Option 1 — Drop-in replacement (zero change to your mental model)

Just replace next/server with next-response-kit in your import. Everything works exactly the same — .json(), .redirect(), .next(), all of it. Plus you get new typed helpers on the same object.

// Before
import { NextResponse } from 'next/server';

// After — one character change, that's it
import NextResponse from 'next-response-kit';

Your existing code keeps working:

// This still works exactly as before
return NextResponse.json({ user }, { status: 200 });
return NextResponse.redirect(new URL('/login', req.url));
return NextResponse.next();

And now you also have typed helpers:

return NextResponse.ok({ data: user });
return NextResponse.notFound({ message: 'User not found' });
return NextResponse.serverError(error);

Option 2 — Named imports (one line, tree-shakeable)

import { ok, created, notFound, unprocessable, serverError } from 'next-response-kit';

That's the only import line you'll ever need. Use whichever helpers apply:

export async function GET(_req, { params }) {
  const user = await findUser(params.id);
  if (!user) return notFound({ message: 'User not found' });
  return ok({ data: user });
}

Every response returns the same shape

No matter which helper or style you use, the response is always:

{
  "success": true,
  "message": "User fetched",
  "data": { "id": "u-001", "name": "Shrikant Yadav" },
  "errors": null,
  "meta": null,
  "timestamp": "2025-04-23T10:30:00.000Z"
}

On error:

{
  "success": false,
  "message": "Validation Failed",
  "data": null,
  "errors": [
    { "field": "email", "message": "Must be a valid email" },
    { "field": "name",  "message": "Name is required" }
  ],
  "meta": null,
  "timestamp": "2025-04-23T10:30:01.000Z"
}

The frontend now always reads res.data for success and res.errors for failures. One pattern. Every route. Every developer.


Complete API reference

Success helpers

| Helper | Status | When to use | |---|---|---| | ok({ data, message }) | 200 | Any successful GET or action | | created({ data, message }) | 201 | POST that creates a new resource | | noContent() | 204 | DELETE with no body | | paginated({ data, total, page, limit }) | 200 | Lists with pagination | | respond({ success, data, status }) | any | Escape hatch — full control |

Error helpers

| Helper | Status | When to use | |---|---|---| | badRequest({ message, errors }) | 400 | Malformed input, missing fields | | unauthorized({ message }) | 401 | Not authenticated, token invalid | | forbidden({ message }) | 403 | Authenticated but no permission | | notFound({ message }) | 404 | Resource doesn't exist | | methodNotAllowed() | 405 | Wrong HTTP method | | conflict({ message }) | 409 | Duplicate resource, state conflict | | unprocessable(errors) | 422 | Validation errors (works with Zod) | | tooManyRequests({ message }) | 429 | Rate limit exceeded | | serverError(error) | 500 | Unexpected server failures |


Real route handler examples

Basic CRUD

// app/api/users/[id]/route.ts
import { ok, notFound, noContent, unprocessable, serverError } from 'next-response-kit';
import { db } from '@/lib/db';
import { updateUserSchema } from '@/lib/schemas';

export async function GET(_req, { params }) {
  try {
    const user = await db.user.findUnique({ where: { id: params.id } });
    if (!user) return notFound({ message: 'User not found' });
    return ok({ data: user });
  } catch (error) {
    return serverError(error);
  }
}

export async function PATCH(req, { params }) {
  try {
    const body = await req.json();
    const parsed = updateUserSchema.safeParse(body);

    if (!parsed.success) {
      return unprocessable(parsed.error.flatten());
      // Automatically maps Zod errors to:
      // { errors: [{ field: 'email', message: 'Invalid email' }] }
    }

    const user = await db.user.update({ where: { id: params.id }, data: parsed.data });
    return ok({ data: user, message: 'User updated' });
  } catch (error) {
    return serverError(error);
  }
}

export async function DELETE(_req, { params }) {
  try {
    await db.user.delete({ where: { id: params.id } });
    return noContent(); // 204 — no body
  } catch (error) {
    return serverError(error);
  }
}

Paginated list

// app/api/products/route.ts
import { paginated, serverError } from 'next-response-kit';

export async function GET(req) {
  try {
    const { searchParams } = new URL(req.url);
    const page  = Number(searchParams.get('page')  ?? 1);
    const limit = Number(searchParams.get('limit') ?? 20);

    const [items, total] = await Promise.all([
      db.product.findMany({ skip: (page - 1) * limit, take: limit }),
      db.product.count(),
    ]);

    return paginated({ data: items, total, page, limit });
    // meta is auto-calculated: { page, limit, total, totalPages }
  } catch (error) {
    return serverError(error);
  }
}

Zod validation — zero extra work

// app/api/products/route.ts
import { created, unprocessable, conflict, serverError } from 'next-response-kit';
import { z } from 'zod';

const schema = z.object({
  name:  z.string().min(2),
  price: z.number().positive(),
  sku:   z.string().regex(/^[A-Z0-9-]+$/),
});

export async function POST(req) {
  try {
    const body   = await req.json();
    const parsed = schema.safeParse(body);

    if (!parsed.success) {
      // Pass zod's .flatten() directly — no extra mapping needed
      return unprocessable(parsed.error.flatten());
    }

    const existing = await db.product.findUnique({ where: { sku: parsed.data.sku } });
    if (existing) return conflict({ message: `SKU "${parsed.data.sku}" already exists` });

    const product = await db.product.create({ data: parsed.data });
    return created({ data: product, message: 'Product created' });
  } catch (error) {
    return serverError(error);
  }
}

Using the drop-in style (migration path)

If you have an existing project and want to adopt the package gradually — just change the import. Your old code keeps working, and you start using helpers on new routes.

// Old code in the same file — still works
import NextResponse from 'next-response-kit';

export async function GET() {
  // Old style — still works, unchanged
  return NextResponse.json({ products }, { status: 200 });
}

export async function POST(req) {
  // New style on new routes — consistent shape
  const body = await req.json();
  const parsed = schema.safeParse(body);
  if (!parsed.success) return NextResponse.unprocessable(parsed.error.flatten());
  const product = await createProduct(parsed.data);
  return NextResponse.created({ data: product });
}

Reading the response on the client

// lib/api.ts
import type { ApiResponse } from 'next-response-kit';

export async function apiGet<T>(url: string): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return res.json();
}

// In your component or hook:
const result = await apiGet<User>('/api/users/u-001');

if (!result.success) {
  // result.errors is ApiError[] | null
  result.errors?.forEach(err => {
    if (err.field) form.setError(err.field, { message: err.message });
  });
  return;
}

// result.data is typed as User | null
console.log(result.data?.name);

serverError() — smart in dev, safe in production

No config needed. In development it exposes the error for debugging. In production it hides it automatically.

try {
  const result = await riskyDatabaseCall();
  return ok({ data: result });
} catch (error) {
  return serverError(error);
  // Development → errors: [{ message: 'Connection refused', code: 'Error' }]
  // Production  → errors: null  (never leaks stack traces)
}

respond() — full control when you need it

For non-standard status codes or special flows:

return respond({
  success: true,
  data: { jobId: 'batch-xyz' },
  message: 'Job queued for processing',
  status: 202,
  headers: { 'X-Job-Id': 'batch-xyz' },
});

TypeScript

All types are exported. Use them to type your client-side fetch calls, wrapper functions, and hooks:

import type {
  ApiResponse,   // The full response envelope
  ApiError,      // One error item { field?, message, code? }
  ResponseMeta,  // Pagination meta
  HttpStatusCode // Union of valid HTTP codes
} from 'next-response-kit';

unprocessable() accepts all error formats

You never need to transform errors before passing them in:

// Zod .flatten()
unprocessable(parsed.error.flatten())

// String array
unprocessable(['Email is required', 'Password too short'])

// ApiError array
unprocessable([{ field: 'email', message: 'Invalid', code: 'EMAIL_INVALID' }])

// Record<string, string[]>
unprocessable({ email: ['Invalid format'], age: ['Must be 18+'] })

Migration from plain NextResponse

| Before | After | |---|---| | NextResponse.json({ data }, { status: 200 }) | ok({ data }) or unchanged | | NextResponse.json({ error }, { status: 400 }) | badRequest({ message: error }) | | NextResponse.json({ error }, { status: 404 }) | notFound({ message: error }) | | NextResponse.json({ error }, { status: 500 }) | serverError(caughtError) | | NextResponse.json(null, { status: 204 }) | noContent() |

Or just swap the import to the default export and keep your existing .json() calls working while adopting helpers gradually.


Author

Built by Shrikant Yadav. MIT License.

If this saved you time, ⭐ the repo.

npm · GitHub · Issues