fets 0.0.1-alpha-20230302123305-a76a7ea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +728 -0
- package/client/createClient.d.ts +14 -0
- package/client/index.d.ts +2 -0
- package/client/types.d.ts +70 -0
- package/createRouter.d.ts +6 -0
- package/index.d.ts +6 -0
- package/index.js +517 -0
- package/index.mjs +488 -0
- package/internal-plugins/ajv.d.ts +6 -0
- package/internal-plugins/openapi.d.ts +8 -0
- package/package.json +41 -0
- package/swagger-ui-html.d.ts +2 -0
- package/typed-fetch.d.ts +69 -0
- package/types.d.ts +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
# FETS
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/fets)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
FETS is a fully type-safe, web standards compliant, platform independent, and lightweight HTTP
|
|
7
|
+
framework written in TypeScript, it includes a fully type-safe server and OpenAPI client.
|
|
8
|
+
|
|
9
|
+
FETS works with the WHATWG Fetch API that works in any JavaScript environment including Node.js,
|
|
10
|
+
Deno, Bun, Cloudflare Workers, Next.js, Fastify, Express, AWS Lambdas and even in browsers.
|
|
11
|
+
|
|
12
|
+
- FETS Client is a fully type-safe HTTP client that accepts any valid OpenAPI document.
|
|
13
|
+
|
|
14
|
+
- FETS Server is a platform independent HTTP server that can be deployed to any JavaScript
|
|
15
|
+
environment.
|
|
16
|
+
|
|
17
|
+
It doesn't need **ANY CODE GENERATION**.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yarn add fets
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why should I use this package instead of other packages like `itty-router`, `express` or `fastify`?
|
|
26
|
+
|
|
27
|
+
There are many packages out there that allow you to create HTTP routers, but they are not platform
|
|
28
|
+
agnostic. They are only for Node.js or only for those specific environments. But if you use this
|
|
29
|
+
package, your router will work in any environment that uses JavaScript.
|
|
30
|
+
|
|
31
|
+
## Why should I use this package instead of tRPC?
|
|
32
|
+
|
|
33
|
+
tRPC doesn't need a code generation like FETS, but FETS allows you export an OpenAPI document based
|
|
34
|
+
on the JSON Schema definitions if you don't want to share TypeScript definitions between the client
|
|
35
|
+
and the server. And FETS uses [JSON Schema](https://json-schema.org/) instead of a programmatic
|
|
36
|
+
schema solution like zod which is more portable and has bigger ecosystem. So even if you don't want
|
|
37
|
+
to write JSON Schemas manually, you can use `@sinclair/typebox` to generate them by using an API
|
|
38
|
+
like `zod` has.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
It uses `.route()` method to add routes to the router with the following parameters;
|
|
43
|
+
|
|
44
|
+
- `method`: The HTTP method of the request. This is optional and it handles all methods if it's not
|
|
45
|
+
given.
|
|
46
|
+
- `path`: The URL pattern that the request should match. The url pattern is a string that follows
|
|
47
|
+
the [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) standard. You can
|
|
48
|
+
learn more about URL patterns
|
|
49
|
+
[here](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/URLPattern#matching_a_pathname).
|
|
50
|
+
- `schemas`: An object that contains the schemas for the request and response. This brings a
|
|
51
|
+
type-safety and validation to your requests and responses. You can learn more about schemas
|
|
52
|
+
[here](#end-to-end-type-safety-and-validation-with-json-schema).
|
|
53
|
+
- `handler`: The function that will be called when the request matches the given method and path.
|
|
54
|
+
This function can be either synchronous or asynchronous. If it's asynchronous, it should return a
|
|
55
|
+
`Promise` that resolves to a `Response` object.
|
|
56
|
+
|
|
57
|
+
`Router` gives you an extended version of the regular `Request` object that has the following
|
|
58
|
+
properties:
|
|
59
|
+
|
|
60
|
+
- `request.params`: An object that contains the parameters that are given in the url pattern.
|
|
61
|
+
- `request.query`: An object that contains the query parameters that are given in the url.
|
|
62
|
+
|
|
63
|
+
You can learn more about the original `Request` object
|
|
64
|
+
[here](https://developer.mozilla.org/en-US/docs/Web/API/Request).
|
|
65
|
+
|
|
66
|
+
### Basic Routing
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Then use it in any environment
|
|
70
|
+
import { createServer } from 'http'
|
|
71
|
+
import { createRouter, Response, Router } from 'fets'
|
|
72
|
+
|
|
73
|
+
const router = createRouter()
|
|
74
|
+
// GET collection index
|
|
75
|
+
.route({
|
|
76
|
+
method: 'GET',
|
|
77
|
+
path: '/todos',
|
|
78
|
+
handler: () => new Response('Todos Index!')
|
|
79
|
+
})
|
|
80
|
+
// GET item
|
|
81
|
+
.route({
|
|
82
|
+
method: 'GET',
|
|
83
|
+
path: '/todos/:id',
|
|
84
|
+
handler: ({ params }) => new Response(`Todo #${params.id}`)
|
|
85
|
+
})
|
|
86
|
+
// POST to the collection (we'll use async here)
|
|
87
|
+
.route({
|
|
88
|
+
method: 'POST',
|
|
89
|
+
path: '/todos',
|
|
90
|
+
handler: async request => {
|
|
91
|
+
const content = await request.json()
|
|
92
|
+
return new Response('Creating Todo: ' + JSON.stringify(content))
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Redirect to a URL
|
|
97
|
+
.route({
|
|
98
|
+
method: 'GET',
|
|
99
|
+
path: '/google',
|
|
100
|
+
handler: () => Response.redirect('http://www.google.com')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// 404 for everything else
|
|
104
|
+
.route({
|
|
105
|
+
path: '*',
|
|
106
|
+
handler: () => new Response('Not Found.', { status: 404 })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const httpServer = createServer(router)
|
|
110
|
+
httpServer.listen(4000)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Example
|
|
114
|
+
|
|
115
|
+
Let's create a basic REST API that manages users.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createRouter, Response } from 'fets'
|
|
119
|
+
|
|
120
|
+
const users = [
|
|
121
|
+
{ id: '1', name: 'John' },
|
|
122
|
+
{ id: '2', name: 'Jane' }
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
const router = createRouter()
|
|
126
|
+
.route({
|
|
127
|
+
method: 'GET',
|
|
128
|
+
path: '/users',
|
|
129
|
+
handler: () => Response.json(users)
|
|
130
|
+
})
|
|
131
|
+
// Parameters are given in the `request.params` object
|
|
132
|
+
.route({
|
|
133
|
+
method: 'GET',
|
|
134
|
+
path: '/users/:id',
|
|
135
|
+
handler: request => {
|
|
136
|
+
const user = users.find(user => user.id === request.params.id)
|
|
137
|
+
|
|
138
|
+
if (!user) {
|
|
139
|
+
return new Response(null, {
|
|
140
|
+
status: 404
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Response.json(user)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.route({
|
|
148
|
+
method: 'DELETE',
|
|
149
|
+
path: '/users/:id',
|
|
150
|
+
handler: request => {
|
|
151
|
+
const user = users.find(user => user.id === request.params.id)
|
|
152
|
+
|
|
153
|
+
if (!user) {
|
|
154
|
+
return new Response(null, {
|
|
155
|
+
status: 404
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
users.splice(users.indexOf(user), 1)
|
|
160
|
+
|
|
161
|
+
return new Response(null, {
|
|
162
|
+
status: 204
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
// Handle JSON bodies
|
|
167
|
+
.route({
|
|
168
|
+
method: 'PUT',
|
|
169
|
+
path: '/users',
|
|
170
|
+
handler: async request => {
|
|
171
|
+
const body = await request.json()
|
|
172
|
+
|
|
173
|
+
const user = {
|
|
174
|
+
id: String(users.length + 1),
|
|
175
|
+
name: body.name
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
users.push(user)
|
|
179
|
+
|
|
180
|
+
return Response.json(user)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
// Handle both parameters and JSON body
|
|
184
|
+
.route({
|
|
185
|
+
method: 'PATCH',
|
|
186
|
+
path: '/users/:id',
|
|
187
|
+
handler: async request => {
|
|
188
|
+
const user = users.find(user => user.id === request.params.id)
|
|
189
|
+
|
|
190
|
+
if (!user) {
|
|
191
|
+
return new Response(null, {
|
|
192
|
+
status: 404
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const body = await request.json()
|
|
197
|
+
|
|
198
|
+
user.name = body.name
|
|
199
|
+
|
|
200
|
+
return Response.json(user)
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Middlewares
|
|
206
|
+
|
|
207
|
+
You can also use middlewares to handle requests. Middlewares are functions that are called before
|
|
208
|
+
the request is handled by the router. You can use them to handle authentication, logging, etc.
|
|
209
|
+
|
|
210
|
+
If a handler function doesn't return a `Response` object, the request will be passed to the next
|
|
211
|
+
handler.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// In the following example, we are checking if the request has an `Authorization` header.
|
|
215
|
+
const router = createRouter()
|
|
216
|
+
.route({
|
|
217
|
+
path: '*',
|
|
218
|
+
handler: request => {
|
|
219
|
+
if (!request.headers.get('Authorization')) {
|
|
220
|
+
return new Response(null, {
|
|
221
|
+
status: 401
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
.route({
|
|
227
|
+
path: '/users',
|
|
228
|
+
method: 'GET',
|
|
229
|
+
handler: request => {
|
|
230
|
+
// It doesn't reach here if the request doesn't have an `Authorization` header.
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Handler chaining
|
|
235
|
+
|
|
236
|
+
You can also chain multiple handlers to a single route. In the following example, we are checking if
|
|
237
|
+
the request has an `Authorization` header and if the user is an admin.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { RouteHandler } from 'fets'
|
|
241
|
+
|
|
242
|
+
const withAuth: RouteHandler = request => {
|
|
243
|
+
if (!request.headers.get('Authorization')) {
|
|
244
|
+
return new Response(null, {
|
|
245
|
+
status: 401
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const router = createRouter().route({
|
|
251
|
+
path: '/users',
|
|
252
|
+
method: 'GET',
|
|
253
|
+
handler: [
|
|
254
|
+
withAuth,
|
|
255
|
+
request => {
|
|
256
|
+
// It doesn't reach here if the request doesn't have an `Authorization` header.
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Error handling
|
|
263
|
+
|
|
264
|
+
If an unexpected error is thrown, the response will have a `500` status code. You can use the
|
|
265
|
+
`try/catch` method to handle errors. Or you can use the plugins to handle errors like below.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { HTTPError, useErrorHandling } from 'fets'
|
|
269
|
+
|
|
270
|
+
const router = createRouter({
|
|
271
|
+
plugins: [useErrorHandling()]
|
|
272
|
+
}).route({
|
|
273
|
+
path: '/users',
|
|
274
|
+
method: 'GET',
|
|
275
|
+
handler: request => {
|
|
276
|
+
if (!request.headers.get('Authorization')) {
|
|
277
|
+
// You can use `HTTPError` to return a custom error response.
|
|
278
|
+
// It accepts a status code, a message, headers and a body.
|
|
279
|
+
// If you pass a json object as the body, the response will be a json response.
|
|
280
|
+
throw new HTTPError(
|
|
281
|
+
401,
|
|
282
|
+
'Unauthorized',
|
|
283
|
+
{
|
|
284
|
+
'WWW-Authenticate': 'Basic'
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
message: 'You need to be authenticated to access this resource.'
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Plugins to handle CORS, cookies and more
|
|
296
|
+
|
|
297
|
+
FETS also provides a plugin system that allows you hook into the request/response lifecycle.
|
|
298
|
+
|
|
299
|
+
- `onRequest` - Called before the request is handled by the router
|
|
300
|
+
- - It has `endResponse` method that accepts a `Response` object to short-circuit the request
|
|
301
|
+
- `onResponse` - Called after the request is handled by the router
|
|
302
|
+
- - It allows you to modify the response before it is sent to the client
|
|
303
|
+
|
|
304
|
+
### Cookie Management
|
|
305
|
+
|
|
306
|
+
You can use `useCookies` to parse cookies from the request header and set cookies in the response by
|
|
307
|
+
using Web Standard [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore).
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { createRouter, Response, useCookies } from 'fets'
|
|
311
|
+
|
|
312
|
+
const router = createRouter({
|
|
313
|
+
plugins: [useCookies()]
|
|
314
|
+
})
|
|
315
|
+
.route({
|
|
316
|
+
path: '/users',
|
|
317
|
+
method: 'GET',
|
|
318
|
+
handler: request => {
|
|
319
|
+
const sessionId = await request.cookieStore.get('session_id')
|
|
320
|
+
if (!sessionId) {
|
|
321
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
322
|
+
}
|
|
323
|
+
const user = await getUserBySessionId(sessionId)
|
|
324
|
+
return Response.json(user)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
.route({
|
|
328
|
+
path: '/login',
|
|
329
|
+
method: 'POST',
|
|
330
|
+
handler: async request => {
|
|
331
|
+
const { username, password } = await request.json()
|
|
332
|
+
const sessionId = await createSessionForUser({ username, password })
|
|
333
|
+
await request.cookieStore.set('session_id', sessionId)
|
|
334
|
+
return Response.json({ message: 'ok' })
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### CORS Management
|
|
340
|
+
|
|
341
|
+
You can also setup a CORS middleware to handle preflight CORS requests.
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { createRouter, useCORS } from 'fets'
|
|
345
|
+
|
|
346
|
+
const router = createRouter({
|
|
347
|
+
plugins: [
|
|
348
|
+
useCORS({
|
|
349
|
+
origin: '*',
|
|
350
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
351
|
+
headers: ['Content-Type', 'Authorization']
|
|
352
|
+
})
|
|
353
|
+
]
|
|
354
|
+
})
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Custom plugins
|
|
358
|
+
|
|
359
|
+
You can also create your own plugins to handle errors, logging, etc.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { createRouter } from 'fets'
|
|
363
|
+
|
|
364
|
+
const useRequestId = (): RoutePlugin => {
|
|
365
|
+
return {
|
|
366
|
+
onRequest({ request, fetchAPI }) {
|
|
367
|
+
let requestId = request.headers.get('X-Request-ID')
|
|
368
|
+
if (!requestId) {
|
|
369
|
+
requestId = fetchAPI.crypto.randomUUID()
|
|
370
|
+
request.headers.set('X-Request-ID', requestId)
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
onResponse({ response, fetchAPI }) {
|
|
374
|
+
response.headers.set('X-Request-ID', request.headers.get('X-Request-ID'))
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const router = createRouter({
|
|
380
|
+
plugins: [useRequestId()]
|
|
381
|
+
})
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## End-to-End Type Safety and Validation with JSON Schema
|
|
385
|
+
|
|
386
|
+
Even if the library provides you some type safety with TypeScript's type inference, you can still
|
|
387
|
+
use JSON Schemas to have a better type safety on both request and response.
|
|
388
|
+
|
|
389
|
+
To define type-safe routes, we use `schemas` parameters
|
|
390
|
+
|
|
391
|
+
### Typing the request
|
|
392
|
+
|
|
393
|
+
You can type individual parts of the `Request` object including JSON body, headers, query
|
|
394
|
+
parameters, and URL parameters.
|
|
395
|
+
|
|
396
|
+
#### JSON Body
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { createRouter, Response } from 'fets'
|
|
400
|
+
|
|
401
|
+
const router = createRouter().route(
|
|
402
|
+
{
|
|
403
|
+
method: 'post',
|
|
404
|
+
path: '/todos',
|
|
405
|
+
// Define the request body schema
|
|
406
|
+
schemas: {
|
|
407
|
+
request: {
|
|
408
|
+
json: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
title: { type: 'string' },
|
|
412
|
+
completed: { type: 'boolean' }
|
|
413
|
+
},
|
|
414
|
+
additionalProperties: false,
|
|
415
|
+
required: ['title']
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
async request => {
|
|
421
|
+
// This part is fully typed
|
|
422
|
+
const { title, completed } = await request.json()
|
|
423
|
+
// ...
|
|
424
|
+
return Response.json({ message: 'ok' })
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### Headers
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
import { createRouter, Response } from 'fets'
|
|
433
|
+
|
|
434
|
+
const router = createRouter().route(
|
|
435
|
+
{
|
|
436
|
+
method: 'post',
|
|
437
|
+
path: '/todos',
|
|
438
|
+
// Define the request body schema
|
|
439
|
+
schemas: {
|
|
440
|
+
request: {
|
|
441
|
+
headers: {
|
|
442
|
+
type: 'object',
|
|
443
|
+
properties: {
|
|
444
|
+
'x-api-key': { type: 'string' }
|
|
445
|
+
},
|
|
446
|
+
additionalProperties: false,
|
|
447
|
+
required: ['x-api-key']
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
async request => {
|
|
453
|
+
// This part is fully typed
|
|
454
|
+
const apiKey = request.headers.get('x-api-key')
|
|
455
|
+
// Would fail on TypeScript compilation
|
|
456
|
+
const wrongHeaderName = request.headers.get('x-api-key-wrong')
|
|
457
|
+
// ...
|
|
458
|
+
return Response.json({ message: 'ok' })
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### Path Parameters
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
import { createRouter, Response } from 'fets'
|
|
467
|
+
|
|
468
|
+
const router = createRouter().route(
|
|
469
|
+
{
|
|
470
|
+
method: 'get',
|
|
471
|
+
path: '/todos/:id',
|
|
472
|
+
// Define the request body schema
|
|
473
|
+
schemas: {
|
|
474
|
+
request: {
|
|
475
|
+
params: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
id: { type: 'string' }
|
|
479
|
+
},
|
|
480
|
+
additionalProperties: false,
|
|
481
|
+
required: ['id']
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
async request => {
|
|
487
|
+
// This part is fully typed
|
|
488
|
+
const { id } = request.params
|
|
489
|
+
// ...
|
|
490
|
+
return Response.json({ message: 'ok' })
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
#### Query Parameters
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
import { createRouter, Response } from 'fets'
|
|
499
|
+
|
|
500
|
+
const router = createRouter().addRoute({
|
|
501
|
+
method: 'get',
|
|
502
|
+
path: '/todos',
|
|
503
|
+
// Define the request body schema
|
|
504
|
+
schemas: {
|
|
505
|
+
request: {
|
|
506
|
+
query: {
|
|
507
|
+
type: 'object',
|
|
508
|
+
properties: {
|
|
509
|
+
limit: { type: 'number' },
|
|
510
|
+
offset: { type: 'number' }
|
|
511
|
+
},
|
|
512
|
+
additionalProperties: false,
|
|
513
|
+
required: ['limit']
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}, async request => {
|
|
518
|
+
// This part is fully typed
|
|
519
|
+
const { limit, offset } = request.query
|
|
520
|
+
// You can also use `URLSearchParams` API
|
|
521
|
+
const limit = request.parsedURL.searchParams.get('limit')
|
|
522
|
+
// ...
|
|
523
|
+
return Response.json({ message: 'ok' })
|
|
524
|
+
})
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Typing the response
|
|
528
|
+
|
|
529
|
+
You can also type the response body by the status code. We strongly recommend to explicitly define
|
|
530
|
+
the status codes.
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
import { createRouter, Response } from 'fets'
|
|
534
|
+
|
|
535
|
+
const router = createRouter().addRoute(
|
|
536
|
+
{
|
|
537
|
+
method: 'get',
|
|
538
|
+
path: '/todos',
|
|
539
|
+
// Define the request body schema
|
|
540
|
+
schemas: {
|
|
541
|
+
request: {
|
|
542
|
+
headers: {
|
|
543
|
+
type: 'object',
|
|
544
|
+
properties: {
|
|
545
|
+
'x-api-key': { type: 'string' }
|
|
546
|
+
},
|
|
547
|
+
additionalProperties: false,
|
|
548
|
+
required: ['x-api-key']
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
response: {
|
|
552
|
+
200: {
|
|
553
|
+
type: 'array',
|
|
554
|
+
items: {
|
|
555
|
+
type: 'object',
|
|
556
|
+
properties: {
|
|
557
|
+
id: { type: 'string' },
|
|
558
|
+
title: { type: 'string' },
|
|
559
|
+
completed: { type: 'boolean' }
|
|
560
|
+
},
|
|
561
|
+
additionalProperties: false,
|
|
562
|
+
required: ['id', 'title', 'completed']
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
401: {
|
|
566
|
+
type: 'object',
|
|
567
|
+
properties: {
|
|
568
|
+
message: { type: 'string' }
|
|
569
|
+
},
|
|
570
|
+
additionalProperties: false,
|
|
571
|
+
required: ['message']
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
async request => {
|
|
577
|
+
const apiKey = request.headers.get('x-api-key')
|
|
578
|
+
if (!apiKey) {
|
|
579
|
+
return Response.json(
|
|
580
|
+
{ message: 'API key is required' },
|
|
581
|
+
{
|
|
582
|
+
status: 401
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
const todos = await getTodos({
|
|
587
|
+
apiKey
|
|
588
|
+
})
|
|
589
|
+
// This part is fully typed
|
|
590
|
+
return Response.json(todos, {
|
|
591
|
+
status: 200
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Runtime validation with Ajv
|
|
598
|
+
|
|
599
|
+
The library itself doesn't include a runtime validation by default. But you can use that plugin to
|
|
600
|
+
have a runtime validation with the provided JSON Schemas above. This plugin uses
|
|
601
|
+
[Ajv](https://ajv.js.org/) under the hood. All you have to do is to install `ajv` and `ajv-formats`
|
|
602
|
+
packages and pass `Ajv` to the router.
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
import Ajv from 'ajv'
|
|
606
|
+
import { addFormats } from 'ajv-formats'
|
|
607
|
+
import { createRouter } from 'fets'
|
|
608
|
+
|
|
609
|
+
const ajv = new Ajv()
|
|
610
|
+
// Some type issues with Ajv
|
|
611
|
+
addFormats(ajv as any)
|
|
612
|
+
|
|
613
|
+
const router = createRouter({
|
|
614
|
+
ajv
|
|
615
|
+
})
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### OpenAPI Generation
|
|
619
|
+
|
|
620
|
+
You can generate OpenAPI specification from the defined routes by using OpenAPI plugin. This plugin
|
|
621
|
+
also provides you a [Swagger UI](https://swagger.io/tools/swagger-ui/) to test the API.
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
import { createRouter } from 'fets'
|
|
625
|
+
|
|
626
|
+
const router = createRouter({
|
|
627
|
+
plugins: [
|
|
628
|
+
title: 'Todo List Example',
|
|
629
|
+
description: 'A simple todo list example with fets',
|
|
630
|
+
version: '1.0.0'
|
|
631
|
+
// You can access the Swagger UI at `/docs`
|
|
632
|
+
swaggerUIPath: '/docs',
|
|
633
|
+
// You can download the OpenAPI specification as a JSON file
|
|
634
|
+
oasPath: '/openapi.json'
|
|
635
|
+
]
|
|
636
|
+
})
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Using a programmatic JSON Schema builder (`zod`-like API)
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
import { Static, Type } from '@sinclair/typebox'
|
|
643
|
+
|
|
644
|
+
const Todo = Type.Object({
|
|
645
|
+
id: Type.String(),
|
|
646
|
+
title: Type.String(),
|
|
647
|
+
completed: Type.Boolean()
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
type Todo = Static<typeof Todo>
|
|
651
|
+
|
|
652
|
+
const router = createRouter().route({
|
|
653
|
+
path: '/todos',
|
|
654
|
+
schemas: {
|
|
655
|
+
request: {
|
|
656
|
+
body: Todo
|
|
657
|
+
},
|
|
658
|
+
response: {
|
|
659
|
+
200: Type.Array(Todo)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Type-safety on the client side
|
|
666
|
+
|
|
667
|
+
With FETS, you can also type the request and response on the client side. But you have two options;
|
|
668
|
+
|
|
669
|
+
1. You can infer the types from the router itself.
|
|
670
|
+
2. You can use OpenAPI specification
|
|
671
|
+
|
|
672
|
+
#### Using the `router`
|
|
673
|
+
|
|
674
|
+
```ts file=examples/client.ts
|
|
675
|
+
import { createClient } from 'fets'
|
|
676
|
+
// Notice `type` in the import to avoid to import it on runtime
|
|
677
|
+
import type { router } from '../fets'
|
|
678
|
+
|
|
679
|
+
const client = createClient<typeof router>({
|
|
680
|
+
endpoint: 'http://localhost:3000'
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
// Everything below is fully typed
|
|
684
|
+
const response = await client['/todo'].put({
|
|
685
|
+
json: {
|
|
686
|
+
title: 'Buy milk',
|
|
687
|
+
completed: false
|
|
688
|
+
}
|
|
689
|
+
})
|
|
690
|
+
const responseJson = await response.json()
|
|
691
|
+
console.table(responseJson)
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
#### Using OpenAPI
|
|
695
|
+
|
|
696
|
+
You need to save the OpenAPI document to a code file like below;
|
|
697
|
+
|
|
698
|
+
```ts
|
|
699
|
+
export default { openapi: '3.0.1' /* ... */ }
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Then you need to import the OpenAPI document to the client code;
|
|
703
|
+
|
|
704
|
+
```ts file=examples/client.ts
|
|
705
|
+
import { createOASClient, Mutable } from 'fets'
|
|
706
|
+
// Notice `type` in the import to avoid to import it on runtime
|
|
707
|
+
import type oas from './saved_openapi'
|
|
708
|
+
|
|
709
|
+
const client = createOASClient<Mutable<typeof oas>>({
|
|
710
|
+
endpoint: 'http://localhost:3000'
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
// Everything below is fully typed
|
|
714
|
+
const response = await client['/todo'].put({
|
|
715
|
+
json: {
|
|
716
|
+
title: 'Buy milk',
|
|
717
|
+
completed: false
|
|
718
|
+
}
|
|
719
|
+
})
|
|
720
|
+
const responseJson = await response.json()
|
|
721
|
+
console.table(responseJson)
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## Usage in environments
|
|
725
|
+
|
|
726
|
+
`Router` is actually an instance of `ServerAdapter` of `@whatwg-node/server` package. So you can use
|
|
727
|
+
it in any environment just like `ServerAdapter`. See the [documentation](../server/README.md) of
|
|
728
|
+
`@whatwg-node/server` package for more information.
|