Skip to main content

Resource

Resources are a collection of RestEndpoints that operate on a common data by sharing a schema

Usage

resources/Todo.ts
export class Todo extends Entity {
id = '';
title = '';
completed = false;

static key = 'Todo';
}

const TodoResource = resource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
});
Resources start with 6 Endpoints
const todo = useSuspense(TodoResource.get, { id: '5' });
const todos = useSuspense(TodoResource.getList);
controller.fetch(TodoResource.getList.push, {
title: 'finish installing reactive data client',
});
controller.fetch(
TodoResource.update,
{ id: '5' },
{ ...todo, completed: true },
);
controller.fetch(
TodoResource.partialUpdate,
{ id: '5' },
{ completed: true },
);
controller.fetch(TodoResource.delete, { id: '5' });

Arguments

{
path: string;
schema: Schema;
urlPrefix?: string;
body?: any;
searchParams?: any;
paginationField?: string;
optimistic?: boolean;
Endpoint?: typeof RestEndpoint;
Collection?: typeof Collection;
} & EndpointExtraOptions

path

Passed to RestEndpoint.path for single item .

Create (getList.push/getList.unshift) and getList remove the last argument.

const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
});

// GET /react/posts/abc
PostResource.get({ group: 'react', id: 'abc' });
// GET /react/posts
PostResource.getList({ group: 'react' });

schema

Passed to RestEndpoint.schema representing a single item. This is usually an Entity or Union.

urlPrefix

Passed to RestEndpoint.urlPrefix

searchParams

Passed to RestEndpoint.searchParams for getList and getList.push

body

Passed to RestEndpoint.body for getList.push update and partialUpdate

paginationField

If specified, will add Resource.getList.getPage method on the Resource.

optimistic

true makes all mutation endpoints optimistic, making UI updates immediate, even before fetch completion.

Endpoint

Class used to construct the members.

import { RestEndpoint } from '@data-client/rest';

export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async getRequestInit(body: any): Promise<RequestInit> {
return {
...(await super.getRequestInit(body)),
credentials: 'same-origin',
};
}
}
const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
Endpoint: AuthdEndpoint,
});

Collection

Collection Class used to construct getList schema.

import { schema, resource } from '@data-client/rest';

class MyCollection<
S extends any[] | PolymorphicInterface = any,
Parent extends any[] = [urlParams: any, body?: any],
> extends schema.Collection<S, Parent> {
// getList.push should add to Collections regardless of its 'orderBy' argument
// in other words: `orderBy` is a non-filtering argument - it does not influence which results are returned
nonFilterArgumentKeys(key: string) {
return key === 'orderBy';
}
}
const TodoResource = resource({
path: '/todos/:id',
searchParams: {} as { userId?: string; orderBy?: string } | undefined,
schema: Todo,
Collection: MyCollection,
});

EndpointExtraOptions

Members

These provide the standard CRUD endpointss common in REST APIs. Feel free to customize or add new endpoints based to match your API.

const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
searchParams: {} as { author?: string },
paginationField: 'page',
});
NameMethodArgsSchema
getGET[{group: string; id: string}]Post
getListGET[{group: string; author?: string}]Collection([Post])
getList.pushPOST[{group: string; author?: string}, Partial<Post>]Collection([Post]).push
getList.unshiftPOST[{group: string; author?: string}, Partial<Post>]Collection([Post]).unshift
getList.getPageGET[{group: string; author?: string; page: string}]Collection([Post]).addWith
updatePUT[{group: string; id: string }, Partial<Post>]Post
partialUpdatePATCH[{group: string; id: string }, Partial<Post>]Post
deleteDELETE[{group: string; id: string }]Invalidate(Post)

get

Retrieve a singular entity.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
Request
import { PostResource } from './Resource';
PostResource.get({
  group: 'react',
  id: '1',
});
Request
GET /react/posts/1
Content-Type: application/json
Response200
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
FieldValue
method'GET'
pathpath
schemaschema

Commonly used with useSuspense(), Controller.invalidate, Controller.expireAll

getList

Retrieve a list of entities.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
Request
import { PostResource } from './Resource';
PostResource.getList({
  group: 'react',
  author: 'clara',
});
Request
GET /react/posts?author=clara
Content-Type: application/json
Response200
[
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
]
FieldValue
method'GET'
pathremoveLastArg(path)
searchParamssearchParams
paginationFieldpaginationField
schemanew schema.Collection([schema])
resource({ path: '/:first/:second' }).getList.path === '/:first';
resource({ path: '/:first' }).getList.path === '/';

Commonly used with useSuspense(), Controller.invalidate, Controller.expireAll

getList.push

RestEndpoint.push creates a new entity and pushes it to the end of getList. Use getList.unshift to place at the beginning instead.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
Request
import { PostResource } from './Resource';
PostResource.getList.push(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
Request
POST /react/posts?author=clara
Content-Type: application/json
Body: {"title":"winning"}
Response201
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
FieldValue
method'POST'
pathremoveLastArg(path)
searchParamssearchParams
bodybody
schemagetList.schema.push

Commonly used with Controller.fetch

getList.unshift

RestEndpoint.unshift creates a new entity and pushes it to the beginning of getList.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
Request
import { PostResource } from './Resource';
PostResource.getList.unshift(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
Request
POST /react/posts?author=clara
Content-Type: application/json
Body: {"title":"winning"}
Response201
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
FieldValue
method'POST'
pathremoveLastArg(path)
searchParamssearchParams
bodybody
schemagetList.schema.unshift

Commonly used with Controller.fetch

getList.getPage

RestEndpoint.getPage retrieves another page appending to getList ensuring there are no duplicates.

This member is only available when paginationField is specified.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
  paginationField: 'page',
});
Request
import { PostResource } from './Resource';
PostResource.getList.getPage({
  group: 'react',
  author: 'clara',
  page: 2,
});
Request
GET /react/posts?author=clara&page=2
Content-Type: application/json
Response200
[
{
"id": "5",
"group": "react",
"title": "second page",
"author": "clara"
}
]
FieldValue
method'GET'
pathremoveLastArg(path)
searchParamssearchParams
paginationFieldpaginationField
schemagetList.schema.addWith

args: PathToArgs(shortenPath(path)) & searchParams & \{ [paginationField]: string | number \}

Commonly used with Controller.fetch

update

Update an entity.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
Request
import { PostResource } from './Resource';
PostResource.update(
  { group: 'react', id: '1' },
  { title: 'updated title', author: 'clara' },
);
Request
PUT /react/posts/1
Content-Type: application/json
Body: {"title":"updated title","author":"clara"}
Response200
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
FieldValue
method'PUT'
pathpath
bodybody
schemaschema

Commonly used with Controller.fetch

partialUpdate

Update some subset of fields of an entity.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
Request
import { PostResource } from './Resource';
PostResource.partialUpdate(
  { group: 'react', id: '1' },
  { title: 'updated title' },
);
Request
PATCH /react/posts/1
Content-Type: application/json
Body: {"title":"updated title"}
Response200
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
FieldValue
method'PATCH'
pathpath
bodybody
schemaschema

Commonly used with Controller.fetch

delete

Deletes an entity.

import Post from './Post';
export const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
});
Request
import { PostResource } from './Resource';
PostResource.delete({ group: 'react', id: '1' });
Request
DELETE /react/posts/1
Content-Type: application/json
Response200
{
"id": "1"
}
FieldValue
method'DELETE'
pathpath
schemanew schema.Invalidate(schema)
process
(value, params) {
return value && Object.keys(value).length ? value : params;
},

Commonly used with Controller.fetch

Response

{ "id": "xyz" }

Response should either be the pk as a string (like 'xyz'). Or an object with the members needed to compute Entity.pk (like {id: 'xyz'}).

If no response is provided, the process implementation will attempt to use the url parameters sent as an object to compute the Entity.pk. This enables the default implementation to still work with no response, so long as standard arguments are used.

This allows schema.Invalidate to remove the entity from the entity table

extend()

resource builds a great starting point, but often endpoints need to be further customized.

extend() is polymorphic with three forms:

Batch extension of known members

export const CommentResource = resource({
path: '/repos/:owner/:repo/issues/comments/:id',
schema: Comment,
}).extend({
getList: { path: '/repos/:owner/:repo/issues/:number/comments' },
update: { body: { body: '' } },
});

Adding new members

export const UserResource = createGithubResource({
path: '/users/:login',
schema: User,
}).extend('current', {
path: '/user',
schema: User,
});

Function form (to get BaseResource/super)

export const IssueResource= resource({
path: '/repos/:owner/:repo/issues/:number',
schema: Issue,
pollFrequency: 60000,
searchParams: {} as IssueFilters | undefined,
}).extend(BaseResource => ({
search: BaseResource.getList.extend({
path: '/search/issues\\?q=:q?%20repo\\::owner/:repo&page=:page?',
schema: {
results: {
incompleteResults: false,
items: BaseIssueResource.getList.schema.results,
totalCount: 0,
},
link: '',
},
})
)});

Github CommentResource

More Demos

Function Inheritance Patterns

To reuse code related to Resource definitions, you can create your own function that calls resource(). This has similar effects as class-based inheritance, with the added benefit of allowing for complete typing overrides.

import {
resource,
RestEndpoint,
type EndpointExtraOptions,
type RestGenerics,
type ResourceGenerics,
type ResourceOptions,
} from '@data-client/rest';

export class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
urlPrefix = process.env.API_SERVER ?? 'http://localhost:8000';

async getRequestInit(body: any): Promise<RequestInit> {
return {
...(await super.getRequestInit(body)),
credentials: 'same-origin',
};
}
}

export function myResource<O extends ResourceGenerics = any>({
schema,
Endpoint = AuthdEndpoint,
...extraOptions
}: Readonly<O> & ResourceOptions) {
return resource({
Endpoint,
schema,
...extraOptions,
}).extend({
getList: {
schema: {
results: new schema.Collection([schema]),
total: 0,
limit: 0,
skip: 0,
},
},
});
}

Github Example

More Demos