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

mv-api-response

v0.1.0

Published

Standardized API response envelopes for Next.js and Fastify

Downloads

96

Readme

api-response

Libreria TypeScript per uniformare le risposte delle API nei progetti Next.js (App Router) e Fastify. Definisce una struttura di envelope condivisa e fornisce adapter pronti all'uso per entrambi i framework.


Sommario


Installazione

npm install api-response

next e fastify sono peer dependency opzionali: installa solo quello che usi.

# Solo Next.js
npm install next

# Solo Fastify
npm install fastify

# Entrambi
npm install next fastify

Struttura delle risposte

Tutte le risposte condividono un envelope comune. Ci sono tre varianti:

Risposta di successo

{
  "success": true,
  "status": 200,
  "data": { "id": 1, "name": "Mario Rossi" },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-03-20T12:00:00.000Z"
  }
}

Risposta di errore

{
  "success": false,
  "status": 404,
  "error": {
    "code": "NOT_FOUND",
    "message": "Utente non trovato."
  }
}

Risposta con errori di validazione

{
  "success": false,
  "status": 422,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "I dati inviati non sono validi.",
    "details": [
      { "field": "email", "message": "Deve essere un indirizzo email valido." },
      { "field": "password", "message": "Deve contenere almeno 8 caratteri." }
    ]
  }
}

Risposta paginata

{
  "success": true,
  "status": 200,
  "data": [{ "id": 1 }, { "id": 2 }],
  "pagination": {
    "page": 2,
    "perPage": 10,
    "total": 153,
    "totalPages": 16,
    "hasNextPage": true,
    "hasPrevPage": true
  }
}

Utilizzo con Next.js

Importa dall'entry point dedicato api-response/nextjs. Le funzioni restituiscono direttamente un NextResponse.

// app/api/users/[id]/route.ts
import { errorResponse, successResponse } from "api-response/nextjs";
import { ErrorCode } from "api-response";

export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const user = await db.users.findById(params.id);

  if (!user) {
    return errorResponse({
      status: 404,
      code: ErrorCode.NOT_FOUND,
      message: "Utente non trovato.",
    });
  }

  return successResponse(user);
}

Risorsa creata (201)

// app/api/users/route.ts
import { errorResponse, successResponse } from "api-response/nextjs";
import { ErrorCode } from "api-response";

export async function POST(req: Request) {
  const body = await req.json();
  const validation = validateUserBody(body);

  if (!validation.ok) {
    return errorResponse({
      status: 422,
      code: ErrorCode.VALIDATION_ERROR,
      message: "I dati inviati non sono validi.",
      details: validation.errors,
    });
  }

  const user = await db.users.create(body);

  return successResponse(user, { status: 201 });
}

Lista paginata

// app/api/users/route.ts
import { paginatedResponse } from "api-response/nextjs";

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const page = Number(searchParams.get("page") ?? "1");
  const perPage = Number(searchParams.get("perPage") ?? "20");

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

  return paginatedResponse(users, { page, perPage, total });
}

Errore interno con meta osservabilità

import { errorResponse } from "api-response/nextjs";
import { ErrorCode } from "api-response";

export async function GET(req: Request) {
  const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();

  try {
    const data = await riskyOperation();
    return successResponse(data, { meta: { requestId } });
  } catch (err) {
    return errorResponse({
      status: 500,
      code: ErrorCode.INTERNAL_ERROR,
      message: "Errore interno del server.",
      meta: { requestId },
    });
  }
}

Utilizzo con Fastify

Importa dall'entry point dedicato api-response/fastify. Le funzioni ricevono reply e si occupano di chiamare reply.status().send().

import Fastify from "fastify";
import { sendError, sendSuccess } from "api-response/fastify";
import { ErrorCode } from "api-response";

const app = Fastify();

app.get("/users/:id", async (request, reply) => {
  const { id } = request.params as { id: string };
  const user = await db.users.findById(id);

  if (!user) {
    return sendError(reply, {
      status: 404,
      code: ErrorCode.NOT_FOUND,
      message: "Utente non trovato.",
    });
  }

  return sendSuccess(reply, user);
});

Risorsa creata (201)

app.post("/users", async (request, reply) => {
  const validation = validateUserBody(request.body);

  if (!validation.ok) {
    return sendError(reply, {
      status: 422,
      code: ErrorCode.VALIDATION_ERROR,
      message: "I dati inviati non sono validi.",
      details: validation.errors,
    });
  }

  const user = await db.users.create(request.body);
  return sendSuccess(reply, user, { status: 201 });
});

Lista paginata

import { sendPaginated } from "api-response/fastify";

app.get("/users", async (request, reply) => {
  const { page = 1, perPage = 20 } = request.query as { page?: number; perPage?: number };

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

  return sendPaginated(reply, users, { page, perPage, total });
});

Errore di autenticazione

app.addHook("preHandler", async (request, reply) => {
  const token = request.headers.authorization?.replace("Bearer ", "");

  if (!token || !isValidToken(token)) {
    return sendError(reply, {
      status: 401,
      code: ErrorCode.UNAUTHORIZED,
      message: "Token non valido o mancante.",
    });
  }
});

Utilizzo diretto dei builder

I builder sono funzioni pure che restituiscono un plain object senza dipendere da alcun framework. Utili quando devi costruire la risposta senza inviarla immediatamente (es. logging, testing, transformazioni).

import { buildSuccess, buildError, buildPaginated } from "api-response";
import { ErrorCode } from "api-response";

const successEnvelope = buildSuccess({ id: 1, name: "Mario" });
// { success: true, status: 200, data: { id: 1, name: "Mario" } }

const errorEnvelope = buildError({
  status: 403,
  code: ErrorCode.FORBIDDEN,
  message: "Non hai i permessi necessari.",
});
// { success: false, status: 403, error: { code: "FORBIDDEN", message: "..." } }

const paginatedEnvelope = buildPaginated(
  [{ id: 1 }, { id: 2 }],
  { page: 1, perPage: 10, total: 42 },
);
// { success: true, status: 200, data: [...], pagination: { page: 1, perPage: 10, total: 42, totalPages: 5, hasNextPage: true, hasPrevPage: false } }

Codici di errore

ErrorCode è un oggetto const con i codici machine-readable predefiniti:

| Costante | Valore stringa | Uso tipico | |---|---|---| | ErrorCode.BAD_REQUEST | "BAD_REQUEST" | Request malformata (400) | | ErrorCode.VALIDATION_ERROR | "VALIDATION_ERROR" | Dati non validi (422) | | ErrorCode.UNAUTHORIZED | "UNAUTHORIZED" | Autenticazione assente o non valida (401) | | ErrorCode.FORBIDDEN | "FORBIDDEN" | Permessi insufficienti (403) | | ErrorCode.NOT_FOUND | "NOT_FOUND" | Risorsa non trovata (404) | | ErrorCode.CONFLICT | "CONFLICT" | Conflitto di stato (409) | | ErrorCode.UNPROCESSABLE | "UNPROCESSABLE" | Entità non processabile (422) | | ErrorCode.RATE_LIMITED | "RATE_LIMITED" | Troppo richieste (429) | | ErrorCode.INTERNAL_ERROR | "INTERNAL_ERROR" | Errore interno (500) |

Codici personalizzati

Puoi usare qualsiasi stringa come code senza dover modificare la libreria:

return errorResponse({
  status: 402,
  code: "SUBSCRIPTION_EXPIRED",
  message: "Il tuo piano è scaduto. Rinnova l'abbonamento per continuare.",
});

Per avere type-safety sui tuoi codici personalizzati, crea un oggetto const nel tuo progetto:

// lib/error-codes.ts
export const AppErrorCode = {
  SUBSCRIPTION_EXPIRED: "SUBSCRIPTION_EXPIRED",
  QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
  FEATURE_DISABLED: "FEATURE_DISABLED",
} as const;

Il campo meta

meta è opzionale ed è aperto a qualsiasi chiave. Non viene mai incluso nella risposta se non lo passi esplicitamente.

return successResponse(data, {
  meta: {
    requestId: "req_abc123",      // tracciabilità
    timestamp: new Date().toISOString(),
    version: "2",                 // versioning API
    duration: 42,                 // tempo di elaborazione in ms
    region: "eu-west-1",          // qualsiasi chiave custom
  },
});

Narrowing TypeScript lato client

AnyApiResponse<T> è una union discriminata su success. Puoi usarla per tipare le risposte sul client:

import type { AnyApiResponse } from "api-response";

async function fetchUser(id: string): Promise<AnyApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

const response = await fetchUser("123");

if (response.success) {
  // TypeScript sa che response.data è User
  console.log(response.data.name);
} else {
  // TypeScript sa che response.error esiste
  console.error(response.error.code, response.error.message);
}

Riferimento API

buildSuccess<T>(data, options?)

| Parametro | Tipo | Default | Descrizione | |---|---|---|---| | data | T | — | Payload della risposta | | options.status | number | 200 | HTTP status code | | options.meta | ResponseMeta | undefined | Metadati opzionali |

buildError(options)

| Parametro | Tipo | Default | Descrizione | |---|---|---|---| | options.code | string | — | Codice errore machine-readable | | options.message | string | — | Messaggio human-readable | | options.status | number | 500 | HTTP status code | | options.details | ErrorDetail[] | undefined | Dettagli campo per campo | | options.meta | ResponseMeta | undefined | Metadati opzionali |

buildPaginated<T>(data, pagination, options?)

| Parametro | Tipo | Default | Descrizione | |---|---|---|---| | data | T[] | — | Array di risultati | | pagination.page | number | — | Pagina corrente (1-indexed) | | pagination.perPage | number | — | Elementi per pagina | | pagination.total | number | — | Totale elementi | | options.status | number | 200 | HTTP status code | | options.meta | ResponseMeta | undefined | Metadati opzionali |

totalPages, hasNextPage e hasPrevPage vengono calcolati automaticamente.

Adapter Next.js (api-response/nextjs)

  • successResponse<T>(data, options?)NextResponse
  • errorResponse(options)NextResponse
  • paginatedResponse<T>(data, pagination, options?)NextResponse

Adapter Fastify (api-response/fastify)

  • sendSuccess<T>(reply, data, options?)void
  • sendError(reply, options)void
  • sendPaginated<T>(reply, data, pagination, options?)void