Skip to content

Latest commit

 

History

History
454 lines (343 loc) · 12.2 KB

Endpoint.md

File metadata and controls

454 lines (343 loc) · 12.2 KB
title
Endpoint
<title>Endpoint - Strongly typed API definitions</title>

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

Endpoint defines a standard interface that describes the nature of an networking endpoint. It is both strongly typed, and encapsulates runtime-relevant information.

<Tabs defaultValue="Interface" values={[ { label: 'Interface', value: 'Interface' }, { label: 'Class', value: 'Class' }, { label: 'EndpointExtraOptions', value: 'EndpointExtraOptions' }, ]}>

export interface EndpointInterface<
  F extends FetchFunction = FetchFunction,
  S extends Schema | undefined = Schema | undefined,
  M extends true | undefined = true | undefined,
> extends EndpointExtraOptions<F> {
  (...args: Parameters<F>): InferReturn<F, S>;
  key(...args: Parameters<F>): string;
  readonly sideEffect?: M;
  readonly schema?: S;
}
class Endpoint<F extends (...args: any) => Promise<any>>
  implements EndpointInterface
{
  constructor(fetchFunction: F, options: EndpointOptions);

  key(...args: Parameters<F>): string;

  readonly sideEffect?: true;

  readonly schema?: Schema;

  fetch: F;

  extend(options: EndpointOptions): Endpoint;
}

export interface EndpointOptions extends EndpointExtraOptions {
  key?: (params: any) => string;
  sideEffect?: true | undefined;
  schema?: Schema;
}
export interface EndpointExtraOptions<F extends FetchFunction = FetchFunction> {
  /** Default data expiry length, will fall back to NetworkManager default if not defined */
  readonly dataExpiryLength?: number;
  /** Default error expiry length, will fall back to NetworkManager default if not defined */
  readonly errorExpiryLength?: number;
  /** Poll with at least this frequency in miliseconds */
  readonly pollFrequency?: number;
  /** Marks cached resources as invalid if they are stale */
  readonly invalidIfStale?: boolean;
  /** Enables optimistic updates for this request - uses return value as assumed network response */
  readonly getOptimisticResponse?: (
    snap: SnapshotInterface,
    ...args: Parameters<F>
  ) => ResolveType<F>;
  /** Determines whether to throw or fallback to */
  readonly errorPolicy?: (error: any) => 'soft' | undefined;
  /** User-land extra data to send */
  readonly extra?: any;
  /** Enables optimistic updates for this request - uses return value as assumed network response
   * @deprecated use https://resthooks.io./Endpoint.md#getoptimisticresponse instead
   */
  readonly optimisticUpdate?: (...args: Parameters<F>) => ResolveType<F>;
}

Package: @rest-hooks/endpoint

:::tip

Endpoint is a protocol independent class. Try using the protocol specific patterns REST, GraphQL, or getImage instead.

:::

Lifecycle

Success

flowchart LR
  subgraph Controller.fetch
    direction TB
    key--->dispatch("dispatch(FETCH)")
  end
  subgraph managers
    NetworkManager-->endpoint("endpoint(...args)")
    endpoint--resolves-->Controller.resolve
    Controller.resolve(Controller.resolve)-->dispatchR("dispatch(RECEIVE)")
  end
  managers--FETCH-->reducer:FETCH
  Controller.fetch--FETCH-->managers
  subgraph reducer:FETCH
    getOptimisticResponse("?getOptimisticResponse()")-->RECIEVE
    subgraph RECIEVE
      normalize(normalize)-->update("update()")
    end
  end
  subgraph reducer:RECEIVE
    normalize2(normalize)-->update2("update()")
  end
  managers--RECEIVE-->reducer:RECEIVE
  click key "#key"
  click NetworkManager /docs/api/NetworkManager
  click getOptimisticResponse #getoptimisticresponse
  click update #update
  click update2 #update

Error

flowchart LR
  subgraph Controller.fetch
    direction TB
    key--->dispatch("dispatch(FETCH)")
  end
  subgraph managers
    NetworkManager-->endpoint("endpoint(...args)")
    endpoint--rejects-->Controller.resolve
    Controller.resolve(Controller.resolve)-->dispatchR("dispatch(RECEIVE)")
  end
  managers--FETCH-->reducer:FETCH
  Controller.fetch--FETCH-->managers
  subgraph reducer:FETCH
    getOptimisticResponse("?getOptimisticResponse()")-->RECIEVE
    subgraph RECIEVE
      normalize(normalize)-->update("update()")
    end
  end
  subgraph reducer:reduceError
    filterOptimistic(filterOptimistic)-->errorPolicy("errorPolicy()")
  end
  managers--RECEIVE:error-->reducer:reduceError
  click key "#key"
  click NetworkManager /docs/api/NetworkManager
  click getOptimisticResponse #getoptimisticresponse
  click update #update
  click errorPolicy #errorpolicy

Endpoint Members

Members double as options (second constructor arg). While none are required, the first few have defaults.

key: (params) => string {#key}

Serializes the parameters. This is used to build a lookup key in global stores.

Default:

`${this.fetch.name} ${JSON.stringify(params)}`;

sideEffect: true | undefined {#sideeffect}

Used to indicate endpoint might have side-effects (non-idempotent). This restricts it from being used with useSuspense() or useFetch() as those can hit the endpoint an unpredictable number of times.

schema: Schema {#schema}

Declarative definition of how to process responses

Not providing this option means no entities will be extracted.

import { Entity } from '@rest-hooks/normalizr';
import { Endpoint } from '@rest-hooks/endpoint';

class User extends Entity {
  readonly id: string = '';
  readonly username: string = '';

  pk() { return this.id;}
}

const UserDetail = new Endpoint(
    ({ id })  fetch(`/users/${id}`),
    { schema: User }
);

extend(EndpointOptions): Endpoint {#extend}

Can be used to further customize the endpoint definition

const UserDetail = new Endpoint(({ id })  fetch(`/users/${id}`));


const UserDetailNormalized = UserDetail.extend({ schema: User });

In addition to the members, fetch can be sent to override the fetch function.

EndpointExtraOptions

dataExpiryLength?: number {#dataexpirylength}

Custom data cache lifetime for the fetched resource. Will override the value set in NetworkManager.

Learn more about expiry time

errorExpiryLength?: number {#errorexpirylength}

Custom data error lifetime for the fetched resource. Will override the value set in NetworkManager.

errorPolicy?: (error: any) => 'soft' | undefined {#errorpolicy}

'soft' will use stale data (if exists) in case of error; undefined or not providing option will result in error.

Learn more about errorPolicy

invalidIfStale: boolean {#invalidifstale}

Indicates stale data should be considered unusable and thus not be returned from the cache. This means that useSuspense() will suspend when data is stale even if it already exists in cache.

pollFrequency: number {#pollfrequency}

Frequency in millisecond to poll at. Requires using useSubscription() to have an effect.

getOptimisticResponse: (snap, ...args) => fakePayload {#getoptimisticresponse}

When provided, any fetches with this endpoint will behave as though the fakePayload return value from this function was a succesful network response. When the actual fetch completes (regardless of failure or success), the optimistic update will be replaced with the actual network response.

Optimistic update guide

optimisticUpdate: (...args) => fakePayload {#optimisticupdate}

:::caution Deprecated

Use endpoint.getOptimisticResponse instead.

:::

update(normalizedResponseOfThis, ...args) => ({ [endpointKey]: (normalizedResponseOfEndpointToUpdate) => updatedNormalizedResponse) }) {#update}

type UpdateFunction<
  Source extends EndpointInterface,
  Updaters extends Record<string, any> = Record<string, any>,
> = (
  source: ResultEntry<Source>,
  ...args: Parameters<Source>
) => { [K in keyof Updaters]: (result: Updaters[K]) => Updaters[K] };

Simplest case:

const createUser = new Endpoint(postToUserFunction, {
  schema: User,
  update: (newUserId: string) => ({
    [userList.key()]: (users = []) => [newUserId, ...users],
  }),
});

More updates:

const allusers = useSuspense(userList);
const adminUsers = useSuspense(userList, { admin: true });

The endpoint below ensures the new user shows up immediately in the usages above.

const createUser = new Endpoint(postToUserFunction, {
  schema: User,
  update: (newUserId, newUser)  => {
    const updates = {
      [userList.key()]: (users = []) => [newUserId, ...users],
    ];
    if (newUser.isAdmin) {
      updates[userList.key({ admin: true })] = (users = []) => [newUserId, ...users];
    }
    return updates;
  },
});

See usage with Resource or RestEndpoint

Examples

<Tabs defaultValue="Basic" values={[ { label: 'Basic', value: 'Basic' }, { label: 'With Schema', value: 'With Schema' }, { label: 'List', value: 'List' }, ]}>

import { Endpoint } from '@rest-hooks/endpoint';

const UserDetail = new Endpoint(
  ({ id })  fetch(`/users/${id}`).then(res => res.json())
);
import { Endpoint } from '@rest-hooks/endpoint';
import { Entity } from 'rest-hooks';

class User extends Entity {
  readonly id: string = '';
  readonly username: string = '';

  pk() { return this.id; }
}

const UserDetail = new Endpoint(
  ({ id })  fetch(`/users/${id}`).then(res => res.json()),
  { schema: User }
);
import { Endpoint } from '@rest-hooks/endpoint';
import { Entity } from 'rest-hooks';

class User extends Entity {
  readonly id: string = '';
  readonly username: string = '';

  pk() { return this.id; }
}

const UserList = new Endpoint(
  ()  fetch(`/users/`).then(res => res.json()),
  { schema: [User] }
);

<Tabs defaultValue="React" values={[ { label: 'React', value: 'React' }, { label: 'JS/Node Schema', value: 'JS/Node' }, ]}>

function UserProfile() {
  const user = useSuspense(UserDetail, { id });
  const { fetch } = useController();

  return <UserForm user={user} onSubmit={() => fetch(UserDetail)} />;
}
const user = await UserDetail({ id: '5' });
console.log(user);

Additional

Motivation

There is a distinction between

  • What are networking API is
    • How to make a request, expected response fields, etc.
  • How it is used
    • Binding data, polling, triggering imperative fetch, etc.

Thus, there are many benefits to creating a distinct seperation of concerns between these two concepts.

With TypeScript Standard Endpoints, we define a standard for declaring in TypeScript the definition of a networking API.

  • Allows API authors to publish npm packages containing their API interfaces
  • Definitions can be consumed by any supporting library, allowing easy consumption across libraries like Vue, React, Angular
  • Writing codegen pipelines becomes much easier as the output is minimal
  • Product developers can use the definitions in a multitude of contexts where behaviors vary
  • Product developers can easily share code across platforms with distinct behaviors needs like React Native and React Web

What's in an Endpoint

  • A function that resolves the results
  • A function to uniquely store those results
  • Optional: information about how to store the data in a normalized cache
  • Optional: whether the request could have side effects - to prevent repeat calls

See also