@edward644/jsonapi
v2.0.1
Published
`@edward644/jsonapi` is a TypeScript library for building JSON:API-compliant responses for [express.js](https://expressjs.com/). It provides a flexible and extensible way to construct JSON:API documents, including support for nested relationships, error h
Downloads
46
Readme
JsonAPI
@edward644/jsonapi is a TypeScript library for building JSON:API-compliant responses for express.js. It provides a flexible and extensible way to construct JSON:API documents, including support for nested relationships, error handling, and metadata.
Features
- JSON:API Specification: Adheres to the JSON:API 1.1 specification.
- Nested Includes: Dynamically fetch and include related resources using dot-separated paths.
- Error Handling: Easily add and manage errors in the response.
- Customizable: Define custom getters for fetching related resources.
- TypeScript Support: Fully typed for better development experience.
Installation
Install the package via npm:
npm install @edward644/jsonapiUsage
Basic Example
import { Builder } from '@edward644/jsonapi';
import { Config, Item, ResourceIdentifier } from '@edward644/jsonapi/@types';
// Example data
const article = {
type: 'articles',
id: '1',
title: 'JSON:API paints my bikeshed!',
};
const author = {
id: '2',
type: 'people',
firstName: 'Dan',
lastName: 'Gebhardt',
};
// Example getter functions
async function getArticleAuthor(articleId: string): Promise<ResourceIdentifier> {
return { id: author.id, type: author.type };
}
async function getAuthorsById(relatedIds: string[]): Promise<Item[]> {
return [author];
}
async function getAuthorArticles(authorId: string): Promise<ResourceIdentifier[]> {
return [{ id: article.id, type: article.type }];
}
async function getArticlesById(relatedIds: string[]): Promise<Item[]> {
return [article];
}
// This configuration defines the relationships between data objects, enabling the serializer to construct JSON:API-compliant relationships in the response.
const config: Config = {
// This config will apply to data objects with a type of 'articles'.
articles: {
// This is the name of the relationship.
author: {
type: 'people',
related: (articleId: string) => getArticleAuthor(articleId),
include: (relatedIds: string[]) => getAuthorsById(relatedIds),
},
},
authors: {
articles: {
type: 'articles',
related: (authorId: string) => getAuthorArticles(authorId),
include: (relatedIds: string[]) => getArticlesById(relatedIds),
},
},
};
const builder = new Builder(config).create();
const document = await builder.with.data(article).build();
console.log(JSON.stringify(document, null, 2));This will output:
{
"jsonapi": {
"version": "1.1"
},
"data": {
"id": "1",
"type": "articles",
"attributes": {
"title": "JSON:API paints my bikeshed!"
},
"relationships": {
"author": {
"data": {
"id": "2",
"type": "people"
}
}
}
}
}Nested Includes
The library supports nested relationships using dot-separated paths in the include query parameter. For example:
const builder = new Builder(config).create({ include: ['comments.author'] });This will include the comments of the resource, and the author on those comments.
Error Handling
You can add errors to the response using the with.error method:
const document = await builder.with.error(new Error('Something went wrong')).build();Note: This will remove any data an meta that was previously added to the builder.
Metadata and Links
Add metadata and links to the response:
const document = await builder
.with.data(articles)
.with.meta({ rows: 100, totalRows: 5000, page: 4 })
.with.links({ related: 'http://example.com/articles', next: 'http://example.com/articles?continuation=eyJjdX...' })
.build()Important: When the
originalUrlparameter is supplied to theBuilder.createfunction, theselflink in the generated JSON:API document will consistently reflect this value.
Using with Express.js
Basic Example
import express from 'express';
import { middleware, serialiser, deserialiser, errors } from '@edward644/jsonapi/express';
import { Config, Item } from '@edward644/jsonapi/@types';
const { jsonapi, queries, errorHandler } = middleware;
type Article = {
title: string;
createdAt: string;
updatedAt: string;
authorId: string;
} & Item;
const config: Config = {
/* JSON:API configuration */
};
//#region Express API
const app = express();
app.use(jsonapi(config));
app.post('/', queries([]), async (req, res, next) => {
try {
const document = deserialiser(req);
const upsert = document.data<Article>();
if (Array.isArray(upsert)) throw new errors.ValidationError('Expected a single object, but received an array.');
const related = document.related();
upsert.authorId = related.author.at(0);
const article = await createArticle(upsert);
const body = await serialiser(req).with.data(article).build();
res.status(201).send(body);
} catch (err) {
next(err);
}
});
app.get('/', queries(['fields', 'include', 'sort']), async (req, res, next) => {
try {
const articles = await getArticles();
const document = await serialiser(req).with.data(articles).build();
res.send(document);
} catch (err) {
next(err);
}
});
app.use(errorHandler());
app.listen(9000, () => {
console.info('API started on port 9000');
});
//#endregion
//#region Example Data
const articles = [
{
id: '1',
type: 'articles',
title: 'Exploring the Cosmos: A Journey Through Space',
createdAt: '2025-07-15T10:00:00Z',
updatedAt: '2025-07-15T10:00:00Z',
},
{
id: '2',
type: 'articles',
title: 'The Art of Minimalism: Living with Less',
createdAt: '2025-08-20T14:30:00Z',
updatedAt: '2025-08-20T14:30:00Z',
},
{
id: '3',
type: 'articles',
title: 'Mastering TypeScript: Tips and Tricks for Developers',
createdAt: '2025-09-10T09:15:00Z',
updatedAt: '2025-09-10T09:15:00Z',
},
{
id: '4',
type: 'articles',
title: 'The Future of AI: Opportunities and Challenges',
createdAt: '2025-10-05T16:45:00Z',
updatedAt: '2025-10-05T16:45:00Z',
},
{
id: '5',
type: 'articles',
title: 'Sustainable Living: Eco-Friendly Practices for Everyday Life',
createdAt: '2025-11-25T08:00:00Z',
updatedAt: '2025-11-25T08:00:00Z',
},
];
async function getArticles() {
return articles;
}
async function createArticle(upsert: Partial<Article>) {
const errs: Error[] = [];
if (!upsert.type) errs.push(new errors.ValidationError('Missing property: type'));
if (!upsert.title) errs.push(new errors.ValidationError('Missing property: title'));
if (!upsert.authorId) errs.push(new errors.ValidationError('Missing property: authorId'));
if (errs.length) throw new errors.ErrorCollection(errs);
const article: Article = {
id: upsert.id ?? (articles.at(-1)!.id + 1).toString(),
type: 'articles',
title: upsert.title!,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
authorId: '',
};
articles.push(article);
return article;
}
//#endregionLicense
This project is licensed under the ISC License.
Next Steps
- Filter query improvements.
