Type-safe TypeScript/JavaScript client for consuming JSON:API endpoints built with @klerick/json-api-nestjs.
- 🎯 Full Type Safety - Complete TypeScript support with type inference from your entities
- 🔍 Advanced Filtering - Rich query builder with operators (eq, ne, in, like, gt, lt, etc.)
- 📦 Relationship Handling - Easy include, sparse fieldsets, and relationship management
- ⚡ Atomic Operations - Batch multiple operations in a single request with rollback support
- 📤 Meta Support - Pass additional business logic data with requests (JSON:API spec compliant)
- 🌐 Multiple HTTP Clients - Works with Axios, Fetch API, and Angular HttpClient
- 📄 Pagination & Sorting - Built-in support for pagination and multi-field sorting
- 🔄 Observable or Promise - Choose your async style (RxJS Observable or native Promise)
- 🔗 Relationship Operations - Post, patch, and delete relationships independently
- Installation
- Quick Start
- Configuration
- API Methods
- Working with Plain Objects
- Nullifying Relationships
- Clearing To-Many Relationships
- Query Options
- Atomic Operations
- Meta Support
- Examples
npm install @klerick/json-api-nestjs-sdkimport { JsonApiJs, adapterForAxios, FilterOperand } from '@klerick/json-api-nestjs-sdk';
import axios from 'axios';
import { Users } from './entities'; // Your entity classes
// 1. Create adapter
const axiosAdapter = adapterForAxios(axios);
// 2. Configure SDK
const jsonSdk = JsonApiJs(
{
adapter: axiosAdapter,
apiHost: 'http://localhost:3000',
apiPrefix: 'api',
dateFields: ['createdAt', 'updatedAt'],
operationUrl: 'operation',
},
true // true = return Promises, false = return Observables
);
// 3. Use SDK
// Fetch all users
const users = await jsonSdk.jsonApiSdkService.getAll(Users);
// Fetch with filtering and relationships
const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
isActive: { [FilterOperand.eq]: 'true' }
}
},
include: ['addresses', 'roles']
});
// Get one user
const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
include: ['addresses', 'comments', 'roles', 'manager']
});
// Create a user
const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';
newUser.isActive = true;
const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser);
// Update a user
createdUser.firstName = 'Jane';
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(createdUser);
// Delete a user
await jsonSdk.jsonApiSdkService.deleteOne(createdUser);import {
provideJsonApi,
AtomicFactory,
JsonApiSdkService
} from '@klerick/json-api-nestjs-sdk/ngModule';
import {
provideHttpClient,
withFetch,
} from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
// 1. Configure in your main.ts or app.config.ts
// Option A: Direct configuration object
const angularConfig = {
apiHost: 'http://localhost:3000',
idKey: 'id',
apiPrefix: 'api',
operationUrl: 'operation',
dateFields: ['createdAt', 'updatedAt']
};
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withFetch()),
provideJsonApi(angularConfig)
],
}).catch((err) => console.error(err));
// Option B: Factory function (useful for dynamic configuration)
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withFetch()),
provideJsonApi(() => {
const env = inject(EnvironmentService);
return {
apiHost: env.apiUrl,
idKey: 'id',
apiPrefix: 'api',
operationUrl: 'operation',
dateFields: ['createdAt', 'updatedAt']
};
})
],
}).catch((err) => console.error(err));
// 2. Use in your components
@Component({
standalone: true,
selector: 'app-users',
templateUrl: './users.component.html',
})
export class UsersComponent {
private jsonApiService = inject(JsonApiSdkService);
private atomicFactory = inject(AtomicFactory);
async loadUsers() {
const users = await this.jsonApiService.getAll(Users, {
include: ['addresses']
});
return users;
}
async createMultipleResources() {
const result = await this.atomicFactory()
.postOne(newUser)
.postOne(newAddress)
.run();
}
}type JsonSdkConfig = {
apiHost: string; // Base URL of your API (e.g., 'http://localhost:3000')
apiPrefix?: string; // API prefix (e.g., 'api' -> '/api/users')
idKey?: string; // Name of ID field (default: 'id')
idIsNumber?: boolean; // Parse IDs as numbers (default: false)
operationUrl?: string; // URL path for atomic operations (default: 'operation')
dateFields?: string[]; // Fields to convert to Date objects (e.g., ['createdAt', 'updatedAt'])
}
type JsonConfig = JsonSdkConfig & {
adapter?: HttpInnerClient; // HTTP client adapter (default: fetch)
}
// Angular: provideJsonApi accepts config or factory function
type JsonSdkConfigFactory = () => JsonSdkConfig;
type JsonSdkConfigOrFactory = JsonSdkConfig | JsonSdkConfigFactory;Axios Adapter:
import { adapterForAxios } from '@klerick/json-api-nestjs-sdk';
import axios from 'axios';
const adapter = adapterForAxios(axios);Fetch API (default):
// No adapter needed, fetch is used by default
const jsonSdk = JsonApiJs({
apiHost: 'http://localhost:3000',
apiPrefix: 'api',
}, true);Custom Adapter:
See HttpInnerClient interface for implementation details.
Fetch all resources with optional filtering, sorting, and relationships.
import { FilterOperand } from '@klerick/json-api-nestjs-sdk';
// Fetch all users
const users = await jsonSdk.jsonApiSdkService.getAll(Users);
// With filtering
const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
isActive: { [FilterOperand.eq]: 'true' },
id: { [FilterOperand.in]: ['1', '2', '3'] }
}
},
include: ['addresses', 'roles']
});
// Filter by relationship
const usersWithRoles = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
id: { [FilterOperand.in]: ['1', '2'] }
},
roles: {
name: { [FilterOperand.eq]: 'admin' }
}
},
include: ['roles']
});Fetch resources with pagination (returns paginated results).
const firstPage = await jsonSdk.jsonApiSdkService.getList(Users, {
page: {
number: 1,
size: 10
},
sort: {
target: {
id: 'ASC'
}
}
});
const secondPage = await jsonSdk.jsonApiSdkService.getList(Users, {
page: {
number: 2,
size: 10
},
sort: {
target: {
createdAt: 'DESC'
}
}
});Fetch a single resource by ID.
// Simple fetch
const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1');
// With relationships
const userWithRelations = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
include: ['addresses', 'comments', 'roles', 'manager']
});
// With sparse fieldsets
const userPartial = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
fields: {
users: ['firstName', 'lastName', 'email']
}
});Create a new resource.
// Simple create
const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';
newUser.isActive = true;
const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser);
// Create with client-generated ID
// Note: Server must have `allowSetId: true` option enabled
const userWithId = new Users();
userWithId.id = 'my-custom-uuid';
userWithId.firstName = 'Jane';
userWithId.lastName = 'Doe';
userWithId.login = 'janedoe';
const createdUserWithId = await jsonSdk.jsonApiSdkService.postOne(userWithId);
// Create with relationships
const newAddress = new Addresses();
newAddress.city = 'New York';
newAddress.state = 'NY';
newAddress.country = 'USA';
const savedAddress = await jsonSdk.jsonApiSdkService.postOne(newAddress);
const user = new Users();
user.firstName = 'Jane';
user.lastName = 'Doe';
user.login = 'janedoe';
user.addresses = savedAddress; // Set relationship
const createdUser = await jsonSdk.jsonApiSdkService.postOne(user);Update an existing resource.
// Update attributes
user.firstName = 'Updated Name';
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
// Update relationships
const newAddress = await jsonSdk.jsonApiSdkService.postOne(addressEntity);
user.addresses = newAddress;
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);Delete a resource.
await jsonSdk.jsonApiSdkService.deleteOne(user);Remove relationships without deleting the related resources.
// Remove all roles from user
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles');
// Remove manager from user
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'manager');
// Remove all comments from user
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'comments');In monorepo environments or when sharing types between frontend and backend, you may want to use plain TypeScript types/interfaces instead of classes. The SDK provides tools to work with plain objects while maintaining full type safety.
The entity() method creates a properly typed entity instance from a plain object. This is essential when:
- You share types (not classes) between frontend and backend
- The SDK needs to identify the resource type at runtime (via
constructor.name)
import { JsonApiJs } from '@klerick/json-api-nestjs-sdk';
// Shared type (not a class)
interface User {
id?: number;
firstName: string;
lastName: string;
login: string;
manager?: User | null;
}
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
// Create entity from plain object - chainable API
const createdUser = await jsonSdk.jsonApiSdkService
.entity<User>('Users', {
firstName: 'John',
lastName: 'Doe',
login: 'johndoe'
})
.postOne();
// Update entity - chainable API
const updatedUser = await jsonSdk.jsonApiSdkService
.entity<User>('Users', {
id: 1,
firstName: 'Jane'
})
.patchOne();
// Delete entity - chainable API
await jsonSdk.jsonApiSdkService
.entity<User>('Users', { id: 1 })
.deleteOne();
// Work with relationships
const userRelations = await jsonSdk.jsonApiSdkService
.entity<User>('Users', { id: 1 })
.getRelationships('manager');Raw mode - get the entity instance without chaining:
// Get raw entity instance (third argument = true)
const userEntity = jsonSdk.jsonApiSdkService.entity<User>('Users', {
firstName: 'John',
lastName: 'Doe',
login: 'johndoe'
}, true);
// Now use it with standard SDK methods
const created = await jsonSdk.jsonApiSdkService.postOne(userEntity);GET methods also accept string type names instead of classes:
// Using string type name
const users = await jsonSdk.jsonApiSdkService.getAll<User>('Users', {
include: ['manager']
});
const user = await jsonSdk.jsonApiSdkService.getOne<User>('Users', '1', {
include: ['manager']
});
const userList = await jsonSdk.jsonApiSdkService.getList<User>('Users', {
page: { number: 1, size: 10 }
});To clear a relationship (set it to null), use the nullRef() function. This is necessary because the SDK distinguishes between:
- Missing relationship - not included in the request (no change)
- Null relationship - explicitly set to
null(clear the relationship)
import { JsonApiJs, nullRef } from '@klerick/json-api-nestjs-sdk';
interface User {
id?: number;
firstName: string;
manager?: User | null;
}
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
// Clear the manager relationship
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
id: 1,
firstName: 'John',
manager: nullRef() // This will send { data: null } for the relationship
}, true);
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
// Result: user.manager is now nullHow it works:
nullRef()returns a special marker object that TypeScript sees asnull- At runtime, the SDK detects this marker and generates
{ data: null }in the JSON:API request body - The server then clears the relationship
Without nullRef:
// This won't clear the relationship - it will be ignored
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
id: 1,
firstName: 'John',
// manager is undefined - not included in request
}, true);With nullRef:
// This explicitly clears the relationship
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
id: 1,
firstName: 'John',
manager: nullRef() // Generates: relationships: { manager: { data: null } }
}, true);To clear all items from a to-many relationship (ManyToMany, OneToMany), use the emptyArrayRef() function:
import { JsonApiJs, emptyArrayRef } from '@klerick/json-api-nestjs-sdk';
interface User {
id?: number;
firstName: string;
roles?: Role[];
}
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
// Clear all roles from user
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
id: 1,
firstName: 'John',
roles: emptyArrayRef() // This will send { data: [] } for the relationship
}, true);
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
// Result: user.roles is now an empty arrayWhy emptyArrayRef is needed:
- An empty array
[]would be treated as an attribute (not a relationship) emptyArrayRef()marks it as a relationship that should be cleared- At runtime, the SDK generates
{ data: [] }in the JSON:API request body
Comparison:
// ❌ This won't work - empty array treated as attribute
const user = { id: 1, roles: [] };
// ✅ This works - explicitly clears the relationship
const user = { id: 1, roles: emptyArrayRef() };
// Generates: relationships: { roles: { data: [] } }Available operators:
enum FilterOperand {
eq = 'eq', // Equal
ne = 'ne', // Not equal
in = 'in', // In array
nin = 'nin', // Not in array
lt = 'lt', // Less than
lte = 'lte', // Less than or equal
gt = 'gt', // Greater than
gte = 'gte', // Greater than or equal
like = 'like', // SQL LIKE
re = 'regexp', // Regular expression
}Examples:
// Equal
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
isActive: { [FilterOperand.eq]: 'true' }
}
}
});
// Not equal
const inactiveUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
isActive: { [FilterOperand.ne]: 'true' }
}
}
});
// In array
const specificUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
id: { [FilterOperand.in]: ['1', '2', '3'] }
}
}
});
// LIKE search
const searchUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
login: { [FilterOperand.like]: 'john' }
}
}
});
// Check null/not null
const usersWithManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
manager: { [FilterOperand.ne]: null }
}
}
});
const usersWithoutManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
filter: {
target: {
manager: { [FilterOperand.eq]: null }
}
}
});// Sort by single field
const users = await jsonSdk.jsonApiSdkService.getList(Users, {
sort: {
target: {
id: 'ASC'
}
}
});
// Sort by multiple fields
const sortedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
sort: {
target: {
createdAt: 'DESC',
lastName: 'ASC'
}
}
});const paginatedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
page: {
number: 1, // Page number (1-indexed)
size: 20 // Items per page
}
});// Include single relationship
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
include: ['addresses']
});
// Include multiple relationships
const usersWithAll = await jsonSdk.jsonApiSdkService.getAll(Users, {
include: ['addresses', 'roles', 'comments', 'manager']
});
// Include nested relationships
const usersWithNested = await jsonSdk.jsonApiSdkService.getAll(Users, {
include: ['addresses', 'manager.addresses', 'roles']
});Request only specific fields to reduce payload size.
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
fields: {
users: ['firstName', 'lastName', 'email'],
addresses: ['city', 'country']
},
include: ['addresses']
});Execute multiple operations in a single HTTP request. All operations succeed or fail together.
const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';
const result = await jsonSdk.atomicFactory()
.postOne(newUser)
.run();
console.log(result[0]); // Created user// Create multiple related resources
const address = new Addresses();
address.city = 'New York';
address.state = 'NY';
address.country = 'USA';
const role = new Roles();
role.name = 'Admin';
role.key = 'admin';
const user = new Users();
user.firstName = 'Jane';
user.lastName = 'Doe';
user.login = 'janedoe';
user.addresses = address;
user.roles = [role];
const [createdAddress, createdRole, createdUser] = await jsonSdk
.atomicFactory()
.postOne(address)
.postOne(role)
.postOne(user)
.run();// Create user first
const newUser = new Users();
newUser.firstName = 'John';
newUser.login = 'john';
const [createdUser] = await jsonSdk.atomicFactory()
.postOne(newUser)
.run();
// Then update and manage relationships atomically
const patchUser = Object.assign(new Users(), createdUser);
patchUser.firstName = 'John Updated';
patchUser.roles = [role1];
const patchUser2 = Object.assign(new Users(), createdUser);
patchUser2.comments = [comment1];
const patchUser3 = Object.assign(new Users(), createdUser);
patchUser3.comments = [comment2];
const result = await jsonSdk
.atomicFactory()
.patchOne(patchUser) // Update user attributes and set roles
.patchOne(patchUser2) // Set comments
.patchRelationships(patchUser2, 'comments') // Replace comments (keep only comment1)
.postRelationships(patchUser3, 'comments') // Add comment2 to existing comments
.run();
// result[0] - updated user
// result[1] - updated user with comments
// result[2] - array of comment IDs after replacement
// result[3] - array of all comment IDs after additionCreate and reference resources within the same atomic request using local identifiers (lid). This allows you to establish relationships between resources being created in a single batch operation.
How it works:
- Assign a temporary ID to the resource (any unique value - number or string)
- Reference this ID in relationships of other resources in the same request
- Server automatically replaces temporary IDs with real database IDs
Example - Numeric lid:
const address = new Addresses();
address.city = 'Boston';
address.id = 10000; // Temporary ID (lid)
const user = new Users();
user.firstName = 'Alice';
user.addresses = address; // Reference resource by temporary ID
const [createdAddress, createdUser] = await jsonSdk
.atomicFactory()
.postOne(address) // First operation: create address with lid=10000
.postOne(user) // Second operation: reference address via lid
.run();
// Server assigns real IDs and maintains relationships
console.log(createdAddress.id); // Real ID (e.g., 42)
console.log(createdUser.addresses.id); // Same real ID (42)Example - UUID lid (for entities with UUID primary keys):
const book1 = new BookList();
book1.id = '550e8400-e29b-41d4-a716-446655440000'; // Temporary UUID (lid)
book1.title = 'TypeScript Handbook';
const book2 = new BookList();
book2.id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // Another temporary UUID (lid)
book2.title = 'Advanced Node.js';
const user = new Users();
user.firstName = 'John';
user.books = [book1, book2]; // Reference both books by their temporary UUIDs
const [createdBook1, createdBook2, createdUser] = await jsonSdk
.atomicFactory()
.postOne(book1)
.postOne(book2)
.postOne(user)
.run();
// All temporary IDs are used as actual IDs (for UUID fields)
// or replaced with real IDs (for autoincrement fields)
console.log(createdBook1.id); // UUID from lid
console.log(createdBook2.id); // UUID from lid
console.log(createdUser.books.map(b => b.id)); // Both book UUIDsImportant notes:
- Local identifiers (lid) are only valid within a single atomic request
- The SDK automatically handles lid assignment in the request body
- For numeric IDs: lid is replaced with the actual database-generated ID
- For UUID IDs: lid can be used as the actual ID (if server has
allowSetId: true)
The SDK supports passing meta objects with requests to send additional business logic data that doesn't belong to the entity itself, according to JSON:API specification.
// POST with meta
const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';
const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser, {
source: 'import',
batchId: '12345'
});
// Request body:
{
"data": {
"type": "users",
"attributes": { "firstName": "John", "lastName": "Doe", "login": "johndoe" }
},
"meta": {
"source": "import",
"batchId": "12345"
}
}
// PATCH with meta
user.firstName = 'Jane';
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user, {
updatedBy: 'admin',
reason: 'name correction'
});
// Relationship operations with meta
await jsonSdk.jsonApiSdkService.postRelationships(user, 'roles', {
addedBy: 'admin',
timestamp: Date.now()
});
await jsonSdk.jsonApiSdkService.patchRelationships(user, 'roles', {
source: 'sync',
syncId: 'abc123'
});
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles', {
removedBy: 'admin',
reason: 'access revoked'
});Important: deleteOne() does NOT support meta because HTTP DELETE for resources has no body according to JSON:API spec. However, deleteRelationships() DOES support meta because relationship deletion endpoints use a request body.
// Chain mode - Observable
const user = await jsonSdk.jsonApiSdkService
.entity('Users', {
firstName: 'John',
lastName: 'Doe',
login: 'johndoe'
})
.postOne({ source: 'mobile-app', version: '2.0' });
// Update with meta
await jsonSdk.jsonApiSdkService
.entity('Users', { id: 1, firstName: 'Jane' })
.patchOne({ updatedBy: 'user-123' });
// Relationship operations with meta
await jsonSdk.jsonApiSdkService
.entity('Users', user)
.patchRelationships('roles', { operation: 'bulk-update' });Each operation in an atomic request can have its own meta:
const address1 = new Addresses();
address1.id = 999; // Local identifier (lid)
address1.city = 'New York';
const address2 = new Addresses();
address2.id = 1000; // Local identifier (lid)
address2.city = 'Boston';
const user1 = new Users();
user1.firstName = 'Alice';
user1.addresses = address1;
const user2 = new Users();
user2.firstName = 'Bob';
user2.addresses = address2;
const [addr1, addr2, createdUser1, createdUser2] = await jsonSdk
.atomicFactory()
.postOne(address1)
.postOne(address2)
.postOne(user1, { source: 'import', priority: 'high' }) // User 1 with meta
.postOne(user2, { source: 'import', priority: 'normal' }) // User 2 with different meta
.run();
// Request body:
{
"atomic:operations": [
{
"op": "add",
"ref": { "type": "addresses", "lid": "999" },
"data": { "type": "addresses", "attributes": { "city": "New York" } }
},
{
"op": "add",
"ref": { "type": "addresses", "lid": "1000" },
"data": { "type": "addresses", "attributes": { "city": "Boston" } }
},
{
"op": "add",
"ref": { "type": "users" },
"data": {
"type": "users",
"attributes": { "firstName": "Alice" },
"relationships": { "addresses": { "data": { "type": "addresses", "id": "999" } } }
},
"meta": { "source": "import", "priority": "high" } // Meta for operation 3
},
{
"op": "add",
"ref": { "type": "users" },
"data": {
"type": "users",
"attributes": { "firstName": "Bob" },
"relationships": { "addresses": { "data": { "type": "addresses", "id": "1000" } } }
},
"meta": { "source": "import", "priority": "normal" } // Meta for operation 4
}
]
}const addressEntity = jsonSdk.jsonApiSdkService.entity('Addresses', {
id: 999,
city: 'New York'
}, true);
const userEntity = jsonSdk.jsonApiSdkService.entity('Users', {
firstName: 'Alice',
addresses: jsonSdk.jsonApiSdkService.entity('Addresses', { id: 999 }, true)
}, true);
const [createdAddress, createdUser] = await jsonSdk
.atomicFactory()
.postOne(addressEntity)
.postOne(userEntity, { source: 'import', batchId: '12345' })
.run();The following operations do NOT accept meta parameter:
// ❌ deleteOne - HTTP DELETE has no body
await jsonSdk.jsonApiSdkService.deleteOne(user);
// Cannot pass meta here
// ❌ deleteOne in atomic operations
await jsonSdk.atomicFactory()
.deleteOne(user) // No meta parameter
.run();
// ✅ But deleteRelationships DOES support meta (has body)
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles', {
removedBy: 'admin'
});For comprehensive real-world examples, see the E2E test suite:
- GET Operations - Fetching, filtering, pagination, sparse fieldsets
- POST Operations - Creating resources with relationships
- PATCH Operations - Updating resources and relationships
- Atomic Operations - Batch requests with rollback
- Common Decorators - Guards, interceptors, custom behavior
MIT
- @klerick/json-api-nestjs - JSON:API server implementation for NestJS
- @klerick/json-api-nestjs-typeorm - TypeORM adapter
- @klerick/json-api-nestjs-microorm - MikroORM adapter