nexios-core
v1.0.1
Published
Axios-compatible HTTP client built on fetch — with first-class Next.js App Router support, a plugin ecosystem, and an experimental Chain API.
Maintainers
Readme
nexios-core
A fetch-based HTTP client with a full Axios-style API
First-class support for Next.js App Router · 10 plugins · Experimental Chain API
✨ Why nexios-core?
| | nexios-core | axios | native fetch | |---|---|---|---| | Familiar API | ✅ Axios-identical | ✅ | ❌ | | Built on fetch | ✅ | ❌ XHR/http | ✅ | | Next.js ISR / Tags | ✅ built-in | ❌ | manual | | Interceptors | ✅ | ✅ | ❌ | | Auto-retry | ✅ built-in | ❌ | ❌ | | Token Refresh | ✅ plugin | ❌ | ❌ | | Circuit Breaker | ✅ plugin | ❌ | ❌ | | Rate Limiter | ✅ plugin | ❌ | ❌ | | Mock Adapter | ✅ plugin | ✅ | ❌ | | GraphQL helper | ✅ plugin | ❌ | ❌ | | SSE streaming | ✅ plugin | ❌ | manual | | Pagination | ✅ plugin | ❌ | ❌ | | Zod validation | ✅ plugin | ❌ | ❌ | | Chain API | ✅ beta | ❌ | ❌ | | TypeScript | ✅ full | ⚠️ partial | ⚠️ | | Zero dependencies | ✅ | ❌ | ✅ |
📦 Installation
# npm
npm install nexios-core
# pnpm
pnpm add nexios-core
# yarn
yarn add nexios-core
# bun
bun add nexios-coreRequirements: Node.js ≥ 18, or any environment with native
fetchsupport (Deno, Bun, Browser, Edge Runtime)
🚀 Quick Start
import nexios from 'nexios-core'
// GET
const { data } = await nexios.get('/api/users')
// POST
const { data: user } = await nexios.post('/api/users', {
name: 'Ahmed',
email: '[email protected]',
})
// TypeScript — fully typed
interface User { id: number; name: string; email: string }
const { data } = await nexios.get('/api/users')
// ^? User[]📖 API 1 — Axios Style (Stable ✓)
HTTP Methods
nexios.get(url, config?)
nexios.post(url, data?, config?)
nexios.put(url, data?, config?)
nexios.patch(url, data?, config?)
nexios.delete(url, config?)
nexios.head(url, config?)
nexios.options(url, config?)
// Form Data — automatically sets Content-Type: multipart/form-data
nexios.postForm(url, data?, config?)
nexios.putForm(url, data?, config?)
nexios.patchForm(url, data?, config?)
// URL builder — without making a request
nexios.getUri({ url: '/users', params: { page: 2 } })
// → "https://api.example.com/users?page=2"Request Config
const config: NexiosConfig = {
// ── Core ─────────────────────────────────────────
url: '/api/users',
method: 'GET',
baseURL: 'https://api.example.com',
headers: { 'X-Custom': 'value' },
params: { page: 1, tags: ['a', 'b'] }, // → ?page=1&tags=a&tags=b
data: { name: 'Ahmed' }, // auto JSON.stringify
// ── Params serializer (Axios-compatible) ──────────
paramsSerializer: {
indexes: null, // → tags=a&tags=b (default)
indexes: false, // → tags[]=a&tags[]=b
indexes: true, // → tags[0]=a&tags[1]=b
serialize: (params) => customSerialize(params),
},
// ── Timeout & Cancel ──────────────────────────────
timeout: 10_000, // 10 seconds
signal: controller.signal, // AbortController
cancelToken: token, // Axios-compatible
// ── Auth ──────────────────────────────────────────
auth: { username: 'user', password: 'pass' }, // → Basic Auth header
withCredentials: true, // send cookies cross-origin
// ── Response ──────────────────────────────────────
responseType: 'json', // json | text | blob | arrayBuffer | formData | stream
validateStatus: (s) => s < 500, // custom — default: 200–299
// ── Next.js ───────────────────────────────────────
cache: 'no-store', // fetch cache directive
next: {
revalidate: 60, // ISR
tags: ['posts', 'featured'], // on-demand revalidation
},
// ── Retry ─────────────────────────────────────────
retry: {
attempts: 3,
delay: 500, // fixed ms
delay: (attempt) => attempt * 1000, // dynamic
retryOn: [429, 500, 502, 503, 504],
retryOnNetworkError: true,
},
// ── Transforms ────────────────────────────────────
transformRequest: [(data, headers) => data],
transformResponse: [(data) => data],
// ── Size limits ───────────────────────────────────
maxContentLength: 5_000_000, // 5MB response limit
maxBodyLength: 2_000_000, // 2MB request body limit
// ── XSRF ──────────────────────────────────────────
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
}Custom Instance
import { Nexios } from 'nexios-core'
// lib/api.ts
const api = new Nexios({
baseURL: 'https://api.example.com/v1',
timeout: 10_000,
headers: { 'X-App-Version': '1.0.0' },
retry: { attempts: 3, delay: 500 },
})
// Add auth token to every request
api.interceptors.request.use((config) => {
const token = getToken()
if (token) config.headers['Authorization'] = `Bearer ${token}`
return config
})
// Centralized error handling
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.status === 401) router.push('/login')
return Promise.reject(error)
}
)
export default apiHierarchical Defaults (Axios-identical)
import nexios from 'nexios-core'
// Applied to all requests
nexios.defaults.headers.common['Authorization'] = `Bearer ${token}`
nexios.defaults.headers.common['X-App-ID'] = 'my-app'
// Applied to POST only
nexios.defaults.headers.post['Content-Type'] = 'application/json'
// Applied to GET only
nexios.defaults.headers.get['Accept'] = 'application/json'
// Global baseURL & timeout
nexios.defaults.baseURL = 'https://api.example.com'
nexios.defaults.timeout = 10_000Error Handling
import { isNexiosError, NexiosError, ErrorCodes } from 'nexios-core'
try {
const { data } = await nexios.get('/api/data')
} catch (err) {
if (!isNexiosError(err)) throw err // re-throw non-nexios errors
// Properties
err.status // 404 | 500 | undefined
err.code // "ERR_BAD_REQUEST" | "ERR_NETWORK" | ...
err.response?.data // server error body
err.config.url // failed URL
err.isTimeout // true if request timed out
err.isCancelled // true if request was cancelled
err.isNetworkError // true if no connection
// Error codes
switch (err.code) {
case ErrorCodes.ERR_BAD_REQUEST: /* 400–499 */ break
case ErrorCodes.ERR_BAD_RESPONSE: /* 500–599 */ break
case ErrorCodes.ERR_NETWORK: /* offline */ break
case ErrorCodes.ECONNABORTED: /* timeout */ break
case ErrorCodes.ERR_CANCELED: /* canceled */ break
}
// JSON-safe — for logging
logger.error(err.toJSON())
}Timeout & Cancellation
import { CancelToken } from 'nexios-core'
// Timeout
await nexios.get('/api/slow', { timeout: 5_000 })
// CancelToken — Axios-compatible
const { token, cancel } = CancelToken.source()
nexios.get('/api/data', { cancelToken: token })
setTimeout(() => cancel('User left'), 2000)
// AbortController — Web standard
const controller = new AbortController()
nexios.get('/api/data', { signal: controller.signal })
controller.abort()
// React: cancel on unmount
useEffect(() => {
const { token, cancel } = CancelToken.source()
api.get('/users', { cancelToken: token }).then(r => setUsers(r.data))
return () => cancel()
}, [])
// throwIfRequested — for polling loops
token.throwIfRequested() // throws if already cancelledParallel Requests
const [users, posts] = await nexios.all([
nexios.get('/api/users'),
nexios.get('/api/posts'),
])
// with spread
nexios
.all([nexios.get('/api/a'), nexios.get('/api/b')])
.then(nexios.spread((a, b) => {
console.log(a.data, b.data)
}))⚡ Next.js App Router
import { withRevalidate, withTags, withNext, noCache, forceCache, isrConfig } from 'nexios-core'
// ISR — revalidate every 60 seconds
const { data: posts } = await nexios.get('/api/posts', withRevalidate(60))
// On-demand revalidation with tags
const { data } = await nexios.get('/api/posts', withTags(['posts', 'featured']))
// No cache (force-dynamic)
const { data } = await nexios.get('/api/live', noCache())
// Always from cache (force-static)
const { data } = await nexios.get('/api/config', forceCache())
// ISR + tags combined
const { data } = await nexios.get('/api/products', isrConfig(3600, ['products']))
// Manual — full control
const { data } = await nexios.get('/api/dashboard', {
next: { revalidate: 30, tags: ['dashboard'] },
headers: { Authorization: `Bearer ${token}` },
})
// On-demand revalidation — Route Handler
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
const { tag } = await req.json()
revalidateTag(tag)
return Response.json({ revalidated: true })
}🔌 Plugins
1. Token Auto-Refresh
import { applyTokenRefresh } from 'nexios-core/plugins'
applyTokenRefresh(api, {
// Called when a 401 is received
refreshToken: async () => {
const res = await authApi.post('/auth/refresh')
return res.data.accessToken
},
// Optional: customize how the token is applied
applyToken: (config, token) => ({
...config,
headers: { ...config.headers, Authorization: `Bearer ${token}` },
}),
onRefreshFailure: () => router.push('/login'),
})
// Now every 401 → auto refresh → retry2. Request Deduplication
import { applyDedupe } from 'nexios-core/plugins'
applyDedupe(api)
// 10 components requesting the same endpoint simultaneously
// → only one real request is sent; all callers share the same response
await Promise.all([
api.get('/users'),
api.get('/users'),
api.get('/users'),
])
// fetchCount === 1 ✓3. In-Memory Cache (TTL + LRU)
import { applyCache } from 'nexios-core/plugins'
const store = applyCache(api, {
ttl: 60_000, // 1 minute
maxSize: 500, // max entries (LRU eviction)
})
await api.get('/users') // → real fetch
await api.get('/users') // → from cache
await api.get('/users') // → from cache
store.clear() // clear all cache entries
store.prune() // remove expired entries only
store.size() // current entry count4. Circuit Breaker
import { applyCircuitBreaker } from 'nexios-core/plugins'
const { breaker } = applyCircuitBreaker(api, {
failureThreshold: 5, // open circuit after 5 failures
successThreshold: 2, // close after 2 successes in HALF_OPEN
resetTimeout: 30_000, // retry after 30 seconds
onStateChange: (from, to) => {
logger.warn(`Circuit: ${from} → ${to}`)
},
})
breaker.getState() // "CLOSED" | "OPEN" | "HALF_OPEN"
breaker.getFailures() // consecutive failure count
breaker.reset() // manual reset5. Rate Limiter (Token Bucket)
import { applyRateLimit } from 'nexios-core/plugins'
const { limiter } = applyRateLimit(api, {
max: 10, // 10 requests
per: 1_000, // per second
onExceeded: 'queue', // 'queue' | 'throw'
maxQueue: 50, // max pending requests in queue
})
limiter.getTokens() // current token balance
limiter.getQueueLength() // pending requests in queue6. Pagination
import { paginate, paginateAll, offsetStrategy, cursorStrategy } from 'nexios-core/plugins'
// Fetch all pages at once
const allPosts = await paginateAll(api, '/posts', {
getNextConfig: offsetStrategy({ pageSize: 20 }),
})
// Async generator — page by page with full control
for await (const page of paginate(api, '/posts', {
getNextConfig: offsetStrategy({ pageSize: 20 }),
})) {
renderPage(page.items) // page.items | page.page | page.response
if (page.page === 5) break // stop whenever you want
}
// Cursor-based
const allFeed = await paginateAll(api, '/feed', {
getItems: (res) => res.data.items,
getNextConfig: cursorStrategy({
cursorPath: 'meta.nextCursor', // path to cursor in response
cursorParam: 'cursor', // query param name
}),
})7. Mock Adapter
import { createMockAdapter } from 'nexios-core/plugins'
const mock = createMockAdapter(api)
// Static response
mock.onGet('/users').reply(200, [{ id: 1, name: 'Ahmed' }])
// Dynamic — handler function
mock.onPost('/users').reply((config) => ({
status: 201,
data: { id: 99, ...config.data },
}))
// Regex pattern
mock.onGet(/\/users\/\d+/).reply(200, { id: 1 })
// Simulate network error
mock.onGet('/broken').reply({ networkError: true })
// Simulate delay
mock.onGet('/slow').reply({ status: 200, data: {}, delay: 2000 })
mock.reset() // clear all registered routes
mock.restore() // restore real fetch8. GraphQL Client
import { createGQLClient } from 'nexios-core/plugins'
const gql = createGQLClient(api, {
endpoint: '/graphql',
throwOnError: true, // throws GraphQLError if errors are present
})
// Query
const { data } = await gql.query(`
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
`, { id: '1' })
// Mutation
const { data } = await gql.mutate(`
mutation CreatePost($input: PostInput!) {
createPost(input: $input) { id title }
}
`, { input: { title: 'Hello' } })9. Server-Sent Events (SSE)
import { subscribeSSE } from 'nexios-core/plugins'
const sub = subscribeSSE(api, '/api/events', {
onMessage(event) {
console.log(event.type, event.data)
},
onClose() { console.log('stream closed') },
onError(e) { console.error(e) },
// Auto-reconnect
reconnect: 3000, // reconnect after 3 seconds
maxReconnects: 5,
})
// Close the subscription
sub.close()
console.log(sub.open) // false10. Zod Schema Validation
import { z } from 'zod'
import { validatedGet, applyZodValidation } from 'nexios-core/plugins'
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
})
// Standalone
const { data } = await validatedGet(api, '/users/1', UserSchema)
// ^? { id: number; name: string; email: string }
// Extension on the instance
const validatedApi = applyZodValidation(api)
const { data } = await validatedApi.getValidated('/users/1', UserSchema)🔗 API 2 — Chain (@beta)
⚠️ Experimental — may change without notice. Logs a warning to the console on first use. Not recommended for production yet.
import nexios, { createAPI } from 'nexios-core'
const api = createAPI(nexios, { baseURL: 'https://api.example.com' })
// ── URL building ─────────────────────────────────────────
api.users.url() // "/users"
api.users[1].posts.url() // "/users/1/posts"
// ── GET / POST / PUT / PATCH / DELETE ────────────────────
await api.users.get()
await api.users.get({ page: 2, limit: 20 })
await api.users.post({ name: 'Ahmed' })
await api.users[1].put({ name: 'Updated' })
await api.users[1].patch({ active: false })
await api.users[1].delete()
// ── Modifier chain (each modifier returns a new chain — immutable) ─
const { data: posts } = await api.users[1].posts
.cache(60) // ISR revalidate
.tag('posts', 'user-1') // cache tags
.cacheStrategy('force-cache') // fetch cache directive
.timeout(5_000) // 5 seconds
.retry(3) // 3 attempts
.retry({ attempts: 3, delay: 500 }) // or full config object
.headers({ Authorization: `Bearer ${token}` })
.params({ sort: 'desc' })
.get({ page: 2 }) // extra params at execute time
// ── Schema validation (Zod) ───────────────────────────────
import { z } from 'zod'
const PostSchema = z.object({ id: z.number(), title: z.string() })
const { data: post } = await api.posts[1].schema(PostSchema).get()
// ^? { id: number; title: string }
// ── Reuse partial chains ──────────────────────────────────
const authed = api.users.headers({ Authorization: `Bearer ${token}` })
await authed.get() // GET /users
await authed.post({ name: 'x' }) // POST /users
await authed[1].delete() // DELETE /users/1
// ── Debug ─────────────────────────────────────────────────
api.users[1].posts.inspect()
// { url: "/users/1/posts", resolvedURL: "https://api.example.com/users/1/posts", timeout: undefined, ... }License
MIT © 2025 nexios-core contributors
