@andreyvalenko/next-fetch
v2.0.0
Published
Lightweight, typed fetch client for Next.js with interceptors, query params and timeouts
Downloads
297
Maintainers
Readme
next-fetch
A lightweight, fully typed wrapper around the native fetch, built for Next.js.
- ✅
nextFetch()factory with a sharedbaseURL, headers and cache config - ✅ Nested query params (objects, arrays,
Date) with correct encoding - ✅ Automatic JSON encoding — and raw pass-through for
FormData,Blob, streams, typed arrays - ✅ Request/response interceptors
- ✅ Per-request and per-instance timeouts (
TimeoutError) - ✅ Rich errors (
HttpErrorwithstatus,body,headers) - ✅ Next.js
fetchoptions:next.revalidate,next.tags,cache - ✅ Ships both ESM and CommonJS builds with type declarations
📦 Installation
npm install @andreyvalenko/next-fetchRequires Node 18+ (or any runtime with a global fetch).
Usage
1. Create an instance
import { nextFetch } from "@andreyvalenko/next-fetch";
export const api = nextFetch({
baseURL: "https://api.example.com",
headers: { "X-App": "web" },
timeout: 10_000, // ms, optional
});nextFetch.create({ ... }) is an alias, and new NextFetchClient({ ... }) works too.
baseURL is optional — without it, pass absolute urls.
2. GET
type User = { id: number; name: string };
const users = await api.get<User[]>("/users", {
params: { role: "admin", filter: { tags: ["new", "active"] } },
next: { revalidate: 60, tags: ["users"] }, // Next.js options
});
// GET /users?role=admin&filter[tags][0]=new&filter[tags][1]=active3. POST / PUT / PATCH
The body is the second argument; config is the third.
type LoginResponse = { token: string };
type LoginPayload = { email: string; password: string };
const { token } = await api.post<LoginResponse, LoginPayload>("/auth/login", {
email: "[email protected]",
password: "123456",
});FormData, URLSearchParams, Blob, ArrayBuffer, typed arrays and
ReadableStream are sent as-is and content-typed by the runtime; everything
else is JSON.stringify-ed with Content-Type: application/json.
4. DELETE / HEAD / OPTIONS
await api.delete<void>("/users/1");
await api.delete<void>("/users", { body: { ids: [1, 2] } }); // body allowed5. Errors
import { HttpError, TimeoutError } from "@andreyvalenko/next-fetch";
try {
await api.get("/users");
} catch (error) {
if (HttpError.isHttpError(error)) {
error.status; // 422
error.statusText; // "Unprocessable Entity"
error.body; // parsed JSON (or text) payload
error.headers; // response headers
} else if (TimeoutError.isTimeoutError(error)) {
// request exceeded `timeout`
}
}6. Interceptors
const id = api.interceptors.request.use((config) => {
const headers = new Headers(config.headers);
headers.set("Authorization", `Bearer ${getToken()}`);
return { ...config, headers };
});
api.interceptors.response.use(async (response) => {
if (response.status !== 401) return response;
await refreshSession();
return response;
});
api.interceptors.request.eject(id);
api.interceptors.response.clear();A response interceptor must not read the body (
.json(),.text()) and then return the same response — returnresponse.clone()or a newResponseinstead. Doing otherwise throws a clear error.
7. Timeouts and cancellation
await api.get("/slow", { timeout: 2_000 }); // per request
await api.get("/slow", { signal: controller.signal }); // your own AbortControllerBoth work together: whichever fires first aborts the request.
8. Escape hatch
api.raw(method, url, config) returns the untouched Response (interceptors
still run) when you need streaming or custom parsing.
Response parsing
| Response | Resolves to |
| ------------------------------------- | ------------------ |
| 204 / 205 / 304, empty body | null |
| */*json* | parsed JSON |
| text/*, xml, javascript, form | string |
| anything else | Blob |
Development
npm run typecheck
npm test # builds, then runs the suite against a local http server
npm run build # emits dist/cjs + dist/esmLicense
MIT © Andrii Valenko
