Skip to main content
Version: 5.0

Custom endpoints

Previously we saw how we could use the useSuspense() and Controller.fetch() hooks to read and mutate data. The first argument of these hooks is known as a Endpoint. Endpoints are the minimal definition of instructions needed to tell Rest Hooks how to handle those types of requests.

Resource comes with a small handleful Endpoints for each of the typical CRUD operations. This is often not enough.

caution

TypeScript does not infer super properly. When overriding you can either include the schema, or explicitly specify the return type of the endpoint.

Overriding endpoints

By default the list() assumes an array of entities returned while detail() assumes just the entity returned.

Default schema

{
"postId": 1,
"id": 1,
"name": "id labore ex et quam laborum",
"email": "[email protected]",
"body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
}

GET /comments/1

Example schema

However, often the data is not returned quite so simply. For instance, maybe it can be found in a 'data' key of an object:

{
"data": {
"postId": 1,
"id": 1,
"name": "id labore ex et quam laborum",
"email": "[email protected]",
"body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
}
}

GET /comments/1

Resource definition

In this case, you'll need to override your detail() and list() definitions to reflect the structure of your data. This is known as a 'schema' definition.

import { Resource } from '@rest-hooks/rest';

export default class CommentResource extends Resource {
static detail<T extends typeof Resource>(this: T) {
return super.detail().extend({
schema: { data: this },
});
}
static list<T extends typeof Resource>(this: T) {
return super.list().extend({
schema: { data: [this] },
});
}
}

Here we only overrode the 'schema' part of the Endpoint - taking advantage of super to keep the other pieces the same. See pagination, nested resources and mutation side-effects guide for more examples of custom schemas and overriding endpoints.

Additional endpoints

In many cases there are more means of interacting with a given resource than the basic CRUD operations. Often this means a custom RPC call, or even a custom retrieval endpoint. We'll demonstrate a few examples here, but be sure to learn more about Endpoints to define exactly what your endpoint needs.

RPC

In this example, we have an RPC endpoint located at /users/[id]/make_manager. This endpoint doesn't expect any body, but is a POST request. Because it is so similar to a create() we'll be extended that schema definition.

import { Resource } from '@rest-hooks/rest';

export default class UserResource extends Resource {
static makeManager<T extends typeof Resource>(this: T) {
const endpoint = this.create();
return endpoint.extend({
url({ id }: { id: number }) { return `/users/${id}/make_manager` },
fetch({ id }: { id: number }) {
return endpoint.fetch.call(this, { id });
}
});
}
}

We customized the following:

  • Custom type:
    • Params of { id: number }
    • No Body
  • Custom url

Custom GET

Normally we can look up user resources like any other - with their 'id'. However, how do we get the currently logged in user? One way is to define a special url just for retrieving the current user. In this case - /current_user/. Since there is only one - we won't need to send any params.

import { Resource } from '@rest-hooks/rest';

export default class UserResource extends Resource {
/** Retrieves current logged in user */
static current<T extends typeof Resource>(this: T) {
const endpoint = this.detail();
return endpoint.extend({
fetch() { return endpoint.call(this); }
url() { return '/current_user/' },
})
}
}

We customized the following:

  • Custom type:
    • No params
  • Custom url

Usage

import { useSuspense } from 'rest-hooks';

import UserResource from 'resources/user';

export default function CurrentUserProfilePage() {
const loggedInUser = useSuspense(UserResource.current());

return <ProfileDetail user={loggedInUser} />;
}

Custom List Endpoints

Sometimes we have endpoints that select particular results. We set the url in our custom Endpoint function, and ensure the data is normalized and typed correctly via the schema definition.

import { Resource } from '@rest-hooks/rest';

export default class BirthdayResource extends BaseResource {
readonly id: string | undefined = undefined;
readonly name: string = '';
readonly image: string = '';
readonly source: string = '';
readonly date: Date = new Date();

pk() {
return this.id;
}

static urlRoot = '/api/birthdays/';

/** Lists all upcoming birthdays */
static upcoming<T extends typeof Resource>(this: T) {
const endpoint = this.list();
return this.list().extend({
fetch() { return endpoint.fetch.call(this); }
url() { return '/current_user/' },
schema: {
withinSevenDays: [this],
withinThirtyDays: [this],
},
});
}
}

Usage

import { useSuspense } from 'rest-hooks';

import BirthdayResource from 'resources/user';

export default function UpcomingBirthdays() {
const { withinSevenDays, withinThirtyDays } = useSuspense(
BirthdayResource.upcoming(),
{},
);

return (
<div>
<h2>Next Seven</h2>
<div>
{withinSevenDays.map(birthday => (
<Birthday key={birthday.pk()} birthday={birthday} />
))}
</div>
<h2>Next Thirty</h2>
<div>
{withinThirtyDays.map(birthday => (
<Birthday key={birthday.pk()} birthday={birthday} />
))}
</div>
</div>
);
}