At a high level, this is an attempt to define a "good enough" standard set of types for requests, responses,
pagination, sorting, and filtering. It is somewhat experimental and under active development, but may still provide
value, at least as an idea.
The basic idea here is that you make a request, and the response you receive contains all of the information you
might need to either do the thing you wanted to do, get the next page of results, or - in the event of an error -
communicate clearly to the user what went wrong and how to fix it. Additionally, the response is structured in such
a way that you can easily build powerful HTTP client libraries around it
(see https://luminous-money.github.io/ts-client for an example, and in particular
https://luminous-money.github.io/ts-client/classes/Client.html#next).
In this particular case, I've left requests up to you. The important thing (for collections) is that on the server,
you return the input params along with the response. This is what allows the client to know how to get the next page
of results.
Here's an example of a request-response cycle using these types:
// These are arbitrary, but just to demonstrate how you _could_ format a request constparams = { 'pg[size]':25, 'pg[cursor]':'bnVtOjY=', 'sort':'-createdDate,+name', 'include':'friends.id,friends.name,friends.email', 'filter[createdDateGt]':newDate('2023-01-01T00:00:00.000Z').getTime(), } conststringifyParams = (params: Record<string, string>) =>Object.entries(params) .map(([k, v]) =>`${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); constendpoint = 'https://my-service.com/api/v1/users'; constres: Response<User> = awaithttpClient.get(`${endpoint}?${stringifyParams(params)}`);
if (res.t !== 'collection') thrownewError('Expected a collection response');
// Do something with the users collection .....
// Now if the response has a nextCursor, there's another page if (res.meta.pg.nextCursor) { params['pg[cursor]'] = res.meta.pg.nextCursor; constnextRes: Response<User> = awaithttpClient.get(`${endpoint}?${stringifyParams(params)}`); // .... }
The pagination scheme detailed here is cursor-based; HOWEVER, that does not necessarily mean that it does not still
use page numbers under the hood. The intention here was to leave that up to you. In the simplest case, a "cursor" can
be something like Buffer.from('num:3').toString("base64") - which is to say, a base64-encoded string representing
the page number. However, using the idea of a cursor accommodates more complex pagination schemes as well and is thus
preferable to having different shapes based on the pagination scheme you're using.
Pagination is by far the trickiest problem this structure attempts to solve. Since the unpaginated result set is
defined by both the filter and sort, those parameters are essential for the paginated result set as well. This is why
they are included in the response. The idea is that you can simply pass them back in to the next request to get the
next page of results.
(TODO: Currently filter is not included in the result. sort IS included because sometimes the server will
automatically apply a sort to the result set. Why is filter not included?? And should we include it?)
This module defines three "success" response types and an error response type. In general, your API client should
throw an error if it receives an error response. The t property on these responses allows you to easily distinguish
between them. The three success response types are "single", "collection" and "null". Null responses may happen on
delete or on some other API call that doesn't really return any data. In my opinion, it's nice to have consistent
response shapes, so I've included it here. However, you could argue it doesn't actually add much.
General types having to do with API interactions.
At a high level, this is an attempt to define a "good enough" standard set of types for requests, responses, pagination, sorting, and filtering. It is somewhat experimental and under active development, but may still provide value, at least as an idea.
High-Level Overview
The basic idea here is that you make a request, and the response you receive contains all of the information you might need to either do the thing you wanted to do, get the next page of results, or - in the event of an error - communicate clearly to the user what went wrong and how to fix it. Additionally, the response is structured in such a way that you can easily build powerful HTTP client libraries around it (see https://luminous-money.github.io/ts-client for an example, and in particular https://luminous-money.github.io/ts-client/classes/Client.html#next).
In this particular case, I've left requests up to you. The important thing (for collections) is that on the server, you return the input params along with the response. This is what allows the client to know how to get the next page of results.
Here's an example of a request-response cycle using these types:
Pagination
The pagination scheme detailed here is cursor-based; HOWEVER, that does not necessarily mean that it does not still use page numbers under the hood. The intention here was to leave that up to you. In the simplest case, a "cursor" can be something like
Buffer.from('num:3').toString("base64")
- which is to say, a base64-encoded string representing the page number. However, using the idea of a cursor accommodates more complex pagination schemes as well and is thus preferable to having different shapes based on the pagination scheme you're using.Pagination is by far the trickiest problem this structure attempts to solve. Since the unpaginated result set is defined by both the filter and sort, those parameters are essential for the paginated result set as well. This is why they are included in the response. The idea is that you can simply pass them back in to the next request to get the next page of results.
(TODO: Currently
filter
is not included in the result.sort
IS included because sometimes the server will automatically apply a sort to the result set. Why is filter not included?? And should we include it?)Response Types
This module defines three "success" response types and an error response type. In general, your API client should throw an error if it receives an error response. The
t
property on these responses allows you to easily distinguish between them. The three success response types are "single", "collection" and "null". Null responses may happen on delete or on some other API call that doesn't really return any data. In my opinion, it's nice to have consistent response shapes, so I've included it here. However, you could argue it doesn't actually add much.