- commit
- 917725c
- parent
- 6f1d226
- author
- Eric Bower
- date
- 2024-02-16 18:39:36 +0000 UTC
json api example
2 files changed,
+269,
-0
+257,
-0
1@@ -117,3 +117,260 @@ const fetchUsersPoll = api.get(["/users", "poll"], { supervisors: poll() });
2 The first part of the array is what is used for the router, everything else is
3 unused. This lets you create as many different variations of calling that
4 endpoint that you need.
5+
6+# Using `ctx.req`
7+
8+`ctx.req` is a helper function to merge what currently exists inside
9+`ctx.request` with new properties. It is gaurenteed to return a valid `Request`
10+object and performs a deep merge on `ctx.request` and what the user provides to
11+it.
12+
13+```ts
14+const fetchUsers = api.get("/users", function*(ctx, next) {
15+ ctx.request = ctx.req({
16+ url: "/psych",
17+ headers: {
18+ "Content-Type": "yoyo",
19+ },
20+ });
21+ yield* next();
22+}
23+```
24+
25+# Middleware
26+
27+Because endpoints use the same powerful middleware system employed by thunks, we
28+can do quite a lot of automating for API requests -- to the point where an
29+endpoint doesn't have a custom middleware function at all.
30+
31+For example, if you API leverages an API specification like JSON API, then we
32+can automate response processing.
33+
34+Given the following API response:
35+
36+```json
37+{
38+ "links": {
39+ "self": "http://example.com/articles",
40+ "next": "http://example.com/articles?page[offset]=2",
41+ "last": "http://example.com/articles?page[offset]=10"
42+ },
43+ "data": [{
44+ "type": "articles",
45+ "id": "1",
46+ "attributes": {
47+ "title": "JSON:API paints my bikeshed!"
48+ },
49+ "relationships": {
50+ "author": {
51+ "links": {
52+ "self": "http://example.com/articles/1/relationships/author",
53+ "related": "http://example.com/articles/1/author"
54+ },
55+ "data": { "type": "people", "id": "9" }
56+ },
57+ "comments": {
58+ "links": {
59+ "self": "http://example.com/articles/1/relationships/comments",
60+ "related": "http://example.com/articles/1/comments"
61+ },
62+ "data": [
63+ { "type": "comments", "id": "5" },
64+ { "type": "comments", "id": "12" }
65+ ]
66+ }
67+ },
68+ "links": {
69+ "self": "http://example.com/articles/1"
70+ }
71+ }],
72+ "included": [{
73+ "type": "people",
74+ "id": "9",
75+ "attributes": {
76+ "firstName": "Dan",
77+ "lastName": "Gebhardt",
78+ "twitter": "dgeb"
79+ },
80+ "links": {
81+ "self": "http://example.com/people/9"
82+ }
83+ }, {
84+ "type": "comments",
85+ "id": "5",
86+ "attributes": {
87+ "body": "First!"
88+ },
89+ "relationships": {
90+ "author": {
91+ "data": { "type": "people", "id": "2" }
92+ }
93+ },
94+ "links": {
95+ "self": "http://example.com/comments/5"
96+ }
97+ }, {
98+ "type": "comments",
99+ "id": "12",
100+ "attributes": {
101+ "body": "I like XML better"
102+ },
103+ "relationships": {
104+ "author": {
105+ "data": { "type": "people", "id": "9" }
106+ }
107+ },
108+ "links": {
109+ "self": "http://example.com/comments/12"
110+ }
111+ }]
112+}
113+```
114+
115+We could create a middleware:
116+
117+```ts
118+import { createApi, mdw } from "starfx";
119+import {
120+ createSchema,
121+ select,
122+ slice,
123+ storeMdw,
124+ StoreUpdater,
125+} from "starfx/store";
126+
127+interface Article {
128+ id: string;
129+ title: string;
130+ authorId: string;
131+ comments: string[];
132+}
133+
134+function deserializeArticle(art: any): Article {
135+ return {
136+ id: art.id,
137+ title: art.attributes.title,
138+ authorId: art.relationships.author.data.id,
139+ comments: art.relationships.comments.data.map((c) => c.id),
140+ };
141+}
142+
143+interface Person {
144+ id: string;
145+ firstName: string;
146+ lastName: string;
147+ twitter: string;
148+}
149+
150+function deserializePerson(per: any): Person {
151+ return {
152+ id: per.id,
153+ firstName: per.attributes.firstName,
154+ lastName: per.attributes.lastName,
155+ twitter: per.attributes.twitter,
156+ };
157+}
158+
159+interface Comment {
160+ id: string;
161+ body: string;
162+ authorId: string;
163+}
164+
165+function deserializeComment(com: any): Comment {
166+ return {
167+ id: comm.id,
168+ body: com.attributes.body,
169+ authorId: com.relationships.author.data.id,
170+ };
171+}
172+
173+const schema = createSchema({
174+ cache: slice.table(),
175+ loaders: slice.loader(),
176+ token: slice.str(),
177+ articles: slice.table<Article>(),
178+ people: slice.table<Person>(),
179+ comments: slice.table<Comment>(),
180+});
181+type WebState = typeof schema.initialState;
182+
183+const api = createApi();
184+api.use(mdw.api());
185+api.use(storeMdw.store(schema));
186+api.use(api.routes());
187+
188+// do some request setup before making fetch call
189+api.use(function* (ctx, next) {
190+ const token = yield* select(schema.token.select);
191+ ctx.request = ctx.req({
192+ headers: {
193+ "Content-Type": "application/vnd.api+json",
194+ "Authorization": `Bearer ${token}`,
195+ },
196+ });
197+
198+ yield* next();
199+});
200+
201+api.use(mdw.fetch({ baseUrl: "https://json-api.com" }));
202+
203+function process(entity: any): StoreUpdater[] {
204+ if (entity.type === "article") {
205+ const article = deserializeArticle(entity);
206+ return [schema.articles.add({ [article.id]: article })];
207+ } else if (entity.type === "people") {
208+ const person = deserializePerson(entity);
209+ return [schema.people.add({ [person.id]: person })];
210+ } else if (entity.type === "comment") {
211+ const comment = deserializeComment(entity);
212+ return [schema.comments.add({ [comment.id]: comment })];
213+ }
214+
215+ return [];
216+}
217+
218+// parse response
219+api.use(function* (ctx, next) {
220+ // wait for fetch response
221+ yield* next();
222+
223+ if (!ctx.json.ok) {
224+ // bail
225+ return;
226+ }
227+
228+ const updaters: StoreUpdater<WebState>[] = [];
229+ const jso = ctx.json.value;
230+
231+ if (Array.isArray(jso.data)) {
232+ jso.data.forEach(
233+ (entity) => updaters.push(...process(entity)),
234+ );
235+ } else {
236+ updaters.push(...process(jso.data));
237+ }
238+
239+ jso.included.forEach(
240+ (entity) => updaters.push(...process(entity)),
241+ );
242+
243+ yield* schema.update(updaters);
244+});
245+```
246+
247+Now when we create the endpoints, we really don't need a mdw function for them
248+because everything is automated higher in the mdw stack:
249+
250+```ts
251+const fetchArticles = api.get("/articles");
252+const fetchArticle = api.get<{ id: string }>("/articles/:id");
253+const fetchCommentsByArticleId = api.get<{ id: string }>(
254+ "/articles/:id/comments",
255+);
256+const fetchComment = api.get<{ id: string }>("/comments/:id");
257+```
258+
259+This is simple it is silly not to nomalize the data because we get a ton of
260+benefits from treating our front-end store like a database. CRUD operations
261+become trivial and app-wide.
+12,
-0
1@@ -93,6 +93,18 @@ makeItSo("123"); // type error!
2 makeItSo({ id: "123" }); // nice!
3 ```
4
5+If you do not provide a type for an endpoint, then the action can be dispatched
6+without a payload:
7+
8+```ts
9+const makeItSo = api.get("make-it-so", function* (ctx, next) {
10+ console.log(ctx.payload);
11+ yield* next();
12+});
13+
14+makeItSo(); // nice!
15+```
16+
17 # Custom `ctx`
18
19 End-users are able to provide a custom `ctx` object to their thunks. It must