No runtime dependencies
Fetchja uses the platform fetch API and stays around 5 KB minified. There is no adapter stack to keep patched.
Say goodbye to complex serializers. Fetchja turns plain objects
into JSON:API requests and auto-resolves relationships, all while
staying tiny and built on native fetch.
import Fetchja from 'fetchja'
const api = new Fetchja({
baseURL: 'https://api.example.com'
})
const { data, meta } = await api.get('articles')
const { data: article } = await api.get('articles/1', {
params: { include: 'author' }
})
console.log(article.author.name) included data,
query strings, and relationship hydration so your app can stay close to
plain objects.
Fetchja uses the platform fetch API and stays around 5 KB minified. There is no adapter stack to keep patched.
The package is written in TypeScript and ships its own definitions, so autocomplete works without an extra @types package.
Send and receive plain objects. Fetchja handles resources, relationships, included data, and query strings.
Responses are deserialized with prototype-pollution guards before your app touches the payload.
Bring your own headers, query formatter, casing strategy, pluralizer, fetch implementation, and retry hook.
Nested relationship objects serialize correctly on write and are merged from included data on read.
fetch
exists: Node 18+, Deno, Bun, or any modern browser.
npm install fetchja get for reads. Responses expose
data, meta, status, and
headers as plain values.
import Fetchja from 'fetchja'
const api = new Fetchja({
baseURL: 'https://api.example.com'
})
const { data, meta } = await api.get('articles')
const { data: article } = await api.get('articles/1')
console.log(article)
// { type: 'articles', id: '1', title: 'Hello world', ... } Fetchja. JSON:API
headers and the built-in query serializer are applied by default.
const api = new Fetchja({
baseURL: 'https://api.example.com',
headers: {},
fetch: globalThis.fetch,
queryFormatter: undefined,
resourceCase: 'none',
typeCase: 'camel',
pluralize: true,
onResponseError: undefined
}) | Option | Type | Default | Behavior |
|---|---|---|---|
| baseURL | string | required | The base URL used to build every request. |
| headers | Record<string, string> | JSON:API headers | Headers merged into every request. |
| fetch | typeof fetch | global fetch | A custom fetch function for tokens, logging, or timeouts. |
| queryFormatter | (params) => string | URLSearchParams | built-in | Turns params into a JSON:API query string. |
| resourceCase | camel | kebab | snake | none | 'none' | Controls resource names in URL paths. |
| typeCase | camel | kebab | snake | none | 'camel' | Controls type names when request bodies are serialized. |
| pluralize | boolean | ((word) => string) | true | Pluralizes resource names, or accepts your own pluralizer. |
| onResponseError | (response) => Response | void | optional | Runs before Fetchja throws and can replay failed requests. |
get, post,
patch, and delete, or
the aliases fetch, create,
update, and remove.
| Method | Alias | Signature | HTTP |
|---|---|---|---|
| get | fetch | get(model, options?) | GET |
| post | create | post(model, body, options?) | POST |
| patch | update | patch(model, body, options?) | PATCH |
| delete | remove | delete(model, id, options?) | DELETE |
{
"data": [
{ "id": "1", "type": "articles", "title": "Hello world" },
{ "id": "2", "type": "articles", "title": "Second post" }
],
"meta": { "total": 42 },
"status": 200
} {
"data": {
"id": "1",
"type": "articles",
"title": "Hello world",
"author": { "id": "9", "name": "Ada Lovelace" }
},
"status": 200
} {
"data": {
"id": "3",
"type": "articles",
"title": "Hello"
},
"status": 201
} {
"data": {
"id": "1",
"type": "articles",
"title": "New title"
},
"status": 200
} {
"data": null,
"status": 204
} type and id inside your
data. Fetchja moves it into the JSON:API relationship shape and keeps the
included resource payload aligned.
await api.create('article', {
title: 'Hello world',
author: { type: 'people', id: '9' },
tags: [
{ type: 'tags', id: '1' },
{ type: 'tags', id: '2' }
]
})
When data comes back, resources from included are
merged into data. You can access relationships
like normal nested objects.
const { data } = await api.get(
'articles/1',
{ params: { include: 'author' } }
)
console.log(data.author.name)
// comes from included params object. Arrays become
comma-separated values, nested objects become bracket keys, and the final
URL follows JSON:API 1.1 conventions.
await api.get('articles', {
params: {
include: ['author', 'comments'],
fields: { articles: ['title', 'body'] },
filter: { published: true },
sort: ['-createdAt'],
page: { number: 1, size: 10 }
}
})
Some servers prefer repeated keys like
include[]=author&include[]=comments. Replace
the formatter once at construction and keep call sites unchanged.
import Fetchja from 'fetchja'
function bracketQueryFormatter (params) {
const search = new URLSearchParams()
for (const key in params) {
const value = params[key]
if (Array.isArray(value)) {
value.forEach(item => search.append(`${key}[]`, String(item)))
} else {
search.append(key, String(value))
}
}
return search
}
const api = new Fetchja({
baseURL: 'https://api.example.com',
queryFormatter: bracketQueryFormatter
}) fetch
signature. That keeps auth tokens, request tracing, timeouts, and tests in
the same place as the rest of your app.
const api = new Fetchja({
baseURL: 'https://api.example.com',
fetch: (url, init) => myCustomFetch(url, init)
}) FetchjaError. The error keeps
the HTTP status, status text, raw Response, and
JSON:API errors array.
import Fetchja, { FetchjaError } from 'fetchja'
try {
await api.get('articles/999')
} catch (error) {
if (error instanceof FetchjaError) {
console.log(error.status)
console.log(error.statusText)
console.log(error.errors)
console.log(error.response)
}
} onResponseError runs on a failing response. The
response includes replayRequest(), so token refresh
logic can retry the original request once and return the new response.
const api = new Fetchja({
baseURL: 'https://api.example.com',
headers: { Authorization: `Bearer ${getToken()}` },
onResponseError: async response => {
if (response.status === 401) {
api.headers.Authorization = `Bearer ${await refreshToken()}`
return response.replayRequest()
}
}
}) import {
useQuery
} from '@tanstack/react-query'
import Fetchja from 'fetchja'
const api = new Fetchja({
baseURL: 'https://api.example.com'
})
function useArticles () {
return useQuery({
queryKey: ['articles'],
queryFn: () => api.get('articles')
})
} import {
useMutation,
useQueryClient
} from '@tanstack/react-query'
function useCreateArticle () {
const queryClient = useQueryClient()
return useMutation({
mutationFn: article => api.create('article', article),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['articles']
})
}
})
} @types package.
import Fetchja, {
type FetchjaOptions,
FetchjaError
} from 'fetchja'
const options: FetchjaOptions = {
baseURL: 'https://api.example.com',
resourceCase: 'kebab'
}
const api = new Fetchja(options) pluralize: false and use exact resource names.
For irregular words like person to
people, inject a library such as
pluralize
.
import pluralize from 'pluralize'
const api = new Fetchja({
baseURL: 'https://api.example.com',
pluralize
}) const api = new Fetchja({
baseURL: 'https://api.example.com',
pluralize: false
}) Fetchja is a small JSON:API client built on native fetch. You work with plain objects while it serializes requests and resolves relationships from responses.
JSON:API is a specification for building JSON APIs with consistent resources, relationships, included data, filtering, sorting, fields, and pagination.
Axios is a general HTTP client. Fetchja is focused on JSON:API. It was inspired by kitsu, but uses native fetch, has no runtime dependencies, and ships its own TypeScript types.
Anywhere fetch exists: Node 18+, Deno, Bun, and modern browsers. The package is ESM-only.
Yes. The default formatter follows JSON:API 1.1, and you can pass queryFormatter when a server expects a different shape.
On write, nested objects with type and id are moved into JSON:API relationships and included data. On read, included resources are merged back into data so they behave like normal nested objects.