Skip to content

Commit f500885

Browse files
committed
docs(rest): added getting started with REST
1 parent b01b842 commit f500885

File tree

7 files changed

+186
-5
lines changed

7 files changed

+186
-5
lines changed

src/content/005-getting-started-with-repository.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
layout: post
3-
title: Build a business layer with a Repository 📦
3+
title: Build a business layer with a Repository
44
author: [gallayl]
55
tags: ['Getting Started', 'repository']
66
image: img/005-getting-started-with-repository-cover.jpg
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
layout: post
3+
title: REST in peace
4+
author: [gallayl]
5+
tags: ['Getting Started', 'rest', 'rest-service', 'rest-client-fetch', 'rest-client-got']
6+
image: img/006-getting-started-with-rest-cover.jpg
7+
date: '2021-06-23T12:58:20.257Z'
8+
draft: false
9+
excerpt: Designing and implementing APIs can be hard and consuming them can be frustrating, if they doesn't work as expected. REST API as a Typescript interface for the rescue!
10+
---
11+
12+
### The problem 🤷‍♂️
13+
14+
The old-fashioned approach is to decouple _nearly everything_, but (as a fullstack dev) when speaking about the relation between REST API's, there is a problem:
15+
16+
![Does clients depends on APIs?](img/006-joker.jpg)
17+
18+
So, let's dig deep into the good old S.O.L.I.D. principles, until the end of it, where **D** stands for **dependency inversion**... khm:
19+
20+
> Entities must depend on abstractions, not on concretions.
21+
22+
So, again, let's get this straight. The happy path should be:
23+
1. Design an abstraction (interface) for the API
24+
1. Implement your API
25+
1. The client should depend on the abstraction, not the actual implementation
26+
27+
In FuryStack, there is 3 type of REST packages which are meant to solve the scenarios. In order:
28+
1. `@furystack/rest` for the API design
29+
1. `@furystack/rest-service` for the implementation
30+
1. `@furystack/rest-client-fetch` and `@furystack/rest-client-got` for consuming the API
31+
32+
### Prerequisites 👈
33+
34+
To be able to use this ideal path, you have to share the abstraction between the service and the consumer / frontend.
35+
The preferred way can be some kind of a Monorepo (see the Boilerplate app's approacth with Yarn Workspaces).
36+
If you can't do that, you can still extract the definition to e.g. an NPM package.
37+
38+
### Step One - Design your API first. 📐
39+
40+
If you want to achieve the first step, your API should have an abstraction. An interface. And that should be a.... (drumroll 🥁 )... **Typescript Interface** 🎉
41+
42+
The following example can give you the idea how you should implement the API
43+
44+
```ts
45+
import {
46+
DeleteEndpoint,
47+
GetCollectionEndpoint,
48+
GetEntityEndpoint,
49+
PatchEndpoint,
50+
PostEndpoint,
51+
RestApi,
52+
} from '@furystack/rest'
53+
54+
export interface Mock {
55+
id: string
56+
value: string
57+
}
58+
59+
export interface CustomQuery {
60+
query: { foo: string; bar: number; baz: boolean }
61+
result: { foo: string; bar: number; baz: boolean }
62+
}
63+
export interface CustomUrl {
64+
url: { id: number }
65+
result: { id: number }
66+
}
67+
export interface CustomHeaders {
68+
headers: { foo: string; bar: number; baz: boolean }
69+
result: { foo: string; bar: number; baz: boolean }
70+
}
71+
export interface CustomBody {
72+
body: { foo: string; bar: number; baz: boolean }
73+
result: { foo: string; bar: number; baz: boolean }
74+
}
75+
76+
export interface MyApi extends RestApi {
77+
GET: {
78+
'/custom-query': CustomQuery
79+
'/custom-url/:id': CustomUrl
80+
'/custom-headers': CustomHeaders
81+
'/mock': GetCollectionEndpoint<MockEntity>
82+
'/mock/:id': GetEntityEndpoint<MockEntity, 'id'>
83+
}
84+
POST: {
85+
'/custom-body': CustomBody
86+
'/mock': PostEndpoint<Mock, 'id'>
87+
}
88+
PATCH: {
89+
'/mock/:id': PatchEndpoint<Mock, 'id'>
90+
}
91+
DELETE: {
92+
'/mock/:id': DeleteEndpoint<Mock, 'id'>
93+
}
94+
}
95+
```
96+
97+
So the interface is build on two levels. The first level defines the HTTP method.
98+
99+
The second level is actually a _key-value pair_. The key will be the URL endpoint, the second one is the type in a predefined format.
100+
The custom types can contain the following fields:
101+
- `result`: This will be the type of the response on success
102+
- `url`: An URL parameter object. This should be also part of the url (e.g. if you define the `{id: string}` as type, the `/:id` parameter)
103+
- `headers`: The required explicit headers
104+
- `query`: The query string parameters
105+
- `body`: The POST Body parameters
106+
107+
There are also some entity-related shortcut types in the example, e.g. `PatchEndpoint<T, TKey>` stands for `{ body: Partial<T>, url: { id: T[TPrimaryKey] }, result: undefined}`
108+
109+
### Step Two - Implement 🛠
110+
111+
OK, we've got a well-defined REST API with all the payloads, bodys, headers, bells and whistles. Now we can implement it, so let's move to the _service-side_.
112+
113+
So, an example API implementation overview looks like this:
114+
```ts
115+
const i = new Injector()
116+
i.useRestService<MyApi>({
117+
port: 1234, // The port that the app will use
118+
root: '/api/v1', // The root path of your API
119+
api: {
120+
GET: {
121+
"/custom-headers": customHeadersEndpoint
122+
}
123+
/*(...and you should implement all endpoints here if you want to compile it. Good luck ;) )*/
124+
}
125+
})
126+
```
127+
128+
So, the basic structure is similar to the interface, you should use the `.useRestService<T>()` injector extension with some basic options like the port or root path.
129+
The `api` should follow the same structure as the definition, but the `customHeadersEndpoint` should implement the endpoint `CustomHeaders`. So let's take a look at this:
130+
131+
```ts
132+
const customHeadersEndpoint: RequestAction<CustomHeaders> = async ({ headers }) => {
133+
console.log(headers) // The type is inferred from CustomHeaders: { foo: string; bar: number; baz: boolean }
134+
return JsonResult(headers) // This should also match the `result` type
135+
}
136+
```
137+
138+
The magical part is covered by the `RequestAction<CustomHeaders>`. This generic type tells that this
139+
- Should be an async method
140+
- Will recieve headers with a specified type
141+
- Should return the JsonResult with a specified type
142+
143+
The other properties will be also inferred:
144+
- if there is `{ url: T }` defined on the interface, you will have `getUrlParams: ()=> T` in the parameter object
145+
- if there is `{ body: T }` defined, you will get `getBody: () => Promise<T>` in the parameter object
146+
- if there is `{ query: T }` defined, you will get `getQuery: () => T`
147+
148+
### Consume your interface, not your API 🍽
149+
150+
OK, we have an interface and an implementation. The interface is shared between the service and the client. So let's take a look how the client can consume the API.
151+
Let's take a simple approacth with the native `fetch` implementation (there's also a `got`-based client implementation that can be used in node-based clients as well)
152+
153+
```ts
154+
import { createClient } from '@furystack/rest-client-fetch'
155+
156+
const callMyApi = createClient<MyApi>({
157+
endpointUrl: `http://localhost:1234/api/v1`,
158+
})
159+
160+
const result = await callMyApi({
161+
method: 'GET', // This should be the first property in order to work with Intellisense
162+
action: '/custom-headers', // After selecting the action from the list, other required properties will be inferred and required
163+
headers: {
164+
foo: 'asd',
165+
bar: 42,
166+
baz: false,
167+
},
168+
})
169+
```
170+
171+
So, step by step:
172+
1. Select a method
173+
1. All available actions will be offered by IntelliSense
174+
1. Once you've selected an action, the full payload will be inferred. You will get type checks in header, body, query, url, etc... parameters. The response type will be also there for you 💚
175+
176+
### The main gotcha: Let's break all the thingzzz!!!!4!!!💥
177+
178+
There's an ancient refactoring technique in the _type-safe dreamlands_: If you can change your type definitions and try to recompile, you will see immediately where it will break your code.
179+
The good news: Now you can do that with your API definition - your service and your frontend will be still type-protected 🙌
180+
181+
![Does clients depends on APIs?](img/006-hulk.gif)

src/content/author.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
- id: gallayl
22
avatar: avatars/gallayl.jpg
3-
bio: Lead developer, architect and demigod @FuryStack
4-
twitter: GallayLajos
5-
facebook: gallayl
3+
bio: Lead developer, architect and local demigod @FuryStack
4+
twitter: LajosGallay
5+
facebook: lajos.gallay
66
website: https://github.com/gallayl
77
location: Budapest
8-
profile_image: img/furystack-logo-512.png
8+
profile_image: avatars/gallayl-cover2.jpg
966 KB
Loading
145 KB
Loading

src/content/img/006-hulk.gif

909 KB
Loading

src/content/img/006-joker.jpg

58 KB
Loading

0 commit comments

Comments
 (0)