shokupan 0.11.0 → 0.13.0
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 +47 -1815
- package/dist/{analyzer-CnKnQ5KV.js → analyzer-B0fMzeIo.js} +2 -2
- package/dist/{analyzer-CnKnQ5KV.js.map → analyzer-B0fMzeIo.js.map} +1 -1
- package/dist/{analyzer-BAhvpNY_.cjs → analyzer-BOtveWL-.cjs} +2 -2
- package/dist/{analyzer-BAhvpNY_.cjs.map → analyzer-BOtveWL-.cjs.map} +1 -1
- package/dist/{analyzer.impl-CfpMu4-g.cjs → analyzer.impl-CUDO6vpn.cjs} +82 -7
- package/dist/analyzer.impl-CUDO6vpn.cjs.map +1 -0
- package/dist/{analyzer.impl-DCiqlXI5.js → analyzer.impl-DmHe92Oi.js} +82 -7
- package/dist/analyzer.impl-DmHe92Oi.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +40 -8
- package/dist/index.cjs +2876 -506
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2911 -541
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/theme.css +4 -0
- package/dist/plugins/application/auth.d.ts +5 -0
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +9 -0
- package/dist/plugins/application/dashboard/static/requests.js +537 -251
- package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
- package/dist/plugins/application/dashboard/static/theme.css +4 -0
- package/dist/plugins/application/error-view/index.d.ts +14 -0
- package/dist/plugins/application/error-view/monkeypatch.d.ts +9 -0
- package/dist/plugins/application/error-view/util/source-reader.d.ts +10 -0
- package/dist/plugins/application/error-view/views/error.d.ts +2 -0
- package/dist/plugins/application/error-view/views/status.d.ts +2 -0
- package/dist/plugins/application/htmx/index.d.ts +39 -0
- package/dist/plugins/application/mcp-server/plugin.d.ts +38 -0
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +4 -0
- package/dist/plugins/application/openapi/test-setup.d.ts +1 -0
- package/dist/plugins/application/opentelemetry/index.d.ts +33 -0
- package/dist/plugins/middleware/compression.d.ts +12 -2
- package/dist/plugins/middleware/rate-limit.d.ts +5 -0
- package/dist/plugins/middleware/session.d.ts +4 -4
- package/dist/plugins/resilience/decorators.d.ts +23 -0
- package/dist/plugins/resilience/factory.d.ts +5 -0
- package/dist/plugins/resilience/index.d.ts +2 -0
- package/dist/router.d.ts +25 -9
- package/dist/server.d.ts +22 -0
- package/dist/shokupan.d.ts +24 -1
- package/dist/util/adapter/bun.d.ts +8 -0
- package/dist/util/adapter/index.d.ts +4 -0
- package/dist/util/adapter/interface.d.ts +12 -0
- package/dist/util/adapter/node.d.ts +8 -0
- package/dist/util/adapter/wintercg.d.ts +5 -0
- package/dist/util/body-parser.d.ts +30 -0
- package/dist/util/decorators.d.ts +58 -3
- package/dist/util/di.d.ts +3 -8
- package/dist/util/env-loader.d.ts +99 -0
- package/dist/util/mcp-protocol.d.ts +52 -0
- package/dist/util/metadata.d.ts +18 -0
- package/dist/util/promise.d.ts +16 -0
- package/dist/util/request.d.ts +1 -0
- package/dist/util/symbol.d.ts +5 -0
- package/dist/util/types.d.ts +140 -3
- package/package.json +37 -10
- package/dist/analyzer.impl-CfpMu4-g.cjs.map +0 -1
- package/dist/analyzer.impl-DCiqlXI5.js.map +0 -1
- package/dist/plugins/application/dashboard/static/failures.js +0 -85
- package/dist/plugins/application/http-server.d.ts +0 -13
- package/dist/util/adapter/adapters.d.ts +0 -19
- package/dist/util/instrumentation.d.ts +0 -9
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
**Built for Developer Experience**
|
|
6
6
|
Shokupan is designed to make building APIs delightful again. With zero-config defaults, instant startup times, and full type safety out of the box, you can focus on building your product, not configuring your framework.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
> [!CAUTION]
|
|
9
|
+
> Shokupan is still in alpha and is not guaranteed to be stable. Please use with caution. We will be adding more features and APIs in the future. Please file an issue if you find any bugs or have suggestions for improvement.
|
|
9
10
|
|
|
10
11
|
📚 **[Full documentation available at https://shokupan.dev](https://shokupan.dev)**
|
|
11
12
|
|
|
@@ -25,7 +26,7 @@ Shokupan is designed to make building APIs delightful again. With zero-config de
|
|
|
25
26
|
- 📚 **OpenAPI Docs** - Beautiful OpenAPI documentation with [Scalar](https://scalar.dev/).
|
|
26
27
|
- ⏩ **Short shift** - Very simple migration from [Express](https://expressjs.com/) or [NestJS](https://nestjs.com/) to Shokupan.
|
|
27
28
|
|
|
28
|
-

|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
## 🚀 Quick Start
|
|
@@ -48,1829 +49,60 @@ app.listen();
|
|
|
48
49
|
|
|
49
50
|
That's it! Your server is running at `http://localhost:3000` 🎉
|
|
50
51
|
|
|
51
|
-
##
|
|
52
|
+
## 💡 Core Concepts
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
- [Routing](#routing)
|
|
55
|
-
- [Controllers](#controllers)
|
|
56
|
-
- [Middleware](#middleware)
|
|
57
|
-
- [Context](#context)
|
|
58
|
-
- [Static Files](#static-files)
|
|
59
|
-
- [Plugins](#plugins)
|
|
60
|
-
- [CORS](#cors)
|
|
61
|
-
- [Compression](#compression)
|
|
62
|
-
- [Rate Limiting](#rate-limiting)
|
|
63
|
-
- [Security Headers](#security-headers)
|
|
64
|
-
- [Sessions](#sessions)
|
|
65
|
-
- [Authentication](#authentication)
|
|
66
|
-
- [Validation](#validation)
|
|
67
|
-
- [Scalar (OpenAPI)](#scalar-openapi)
|
|
68
|
-
- [Advanced Features](#advanced-features)
|
|
69
|
-
- [Dependency Injection](#dependency-injection)
|
|
70
|
-
- [OpenAPI Generation](#openapi-generation)
|
|
71
|
-
- [Sub-Requests](#sub-requests)
|
|
72
|
-
- [OpenTelemetry](#opentelemetry)
|
|
73
|
-
- [Migration Guides](#migration-guides)
|
|
74
|
-
- [From Express](#from-express)
|
|
75
|
-
- [From Koa](#from-koa)
|
|
76
|
-
- [From NestJS](#from-nestjs)
|
|
77
|
-
- [Using Express Middleware](#using-express-middleware)
|
|
78
|
-
- [Testing](#testing)
|
|
79
|
-
- [Deployment](#deployment)
|
|
80
|
-
- [Production Best Practices](https://knackstedt.github.io/shokupan/guides/production/) 📚
|
|
81
|
-
- [CLI Tools](#cli-tools)
|
|
82
|
-
- [API Reference](#api-reference)
|
|
83
|
-
- [Roadmap](#-roadmap)
|
|
54
|
+
Shokupan provides a familiar yet modern API.
|
|
84
55
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#### Basic Routes
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
import { Shokupan } from 'shokupan';
|
|
95
|
-
|
|
96
|
-
const app = new Shokupan();
|
|
97
|
-
|
|
98
|
-
// GET request
|
|
99
|
-
app.get('/users', (ctx) => {
|
|
100
|
-
return { users: ['Alice', 'Bob'] };
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// POST request
|
|
104
|
-
app.post('/users', async (ctx) => {
|
|
105
|
-
const body = await ctx.body();
|
|
106
|
-
return { created: body };
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// PUT, PATCH, DELETE
|
|
110
|
-
app.put('/users/:id', (ctx) => ({ updated: ctx.params.id }));
|
|
111
|
-
app.patch('/users/:id', (ctx) => ({ patched: ctx.params.id }));
|
|
112
|
-
app.delete('/users/:id', (ctx) => ({ deleted: ctx.params.id }));
|
|
113
|
-
|
|
114
|
-
app.listen();
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
#### Path Parameters
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
app.get('/users/:id', (ctx) => {
|
|
121
|
-
const userId = ctx.params.id;
|
|
122
|
-
return { id: userId, name: 'Alice' };
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
app.get('/posts/:postId/comments/:commentId', (ctx) => {
|
|
126
|
-
return {
|
|
127
|
-
postId: ctx.params.postId,
|
|
128
|
-
commentId: ctx.params.commentId
|
|
129
|
-
};
|
|
130
|
-
});
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### Query Strings
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
app.get('/search', (ctx) => {
|
|
137
|
-
const query = ctx.query.get('q');
|
|
138
|
-
const page = ctx.query.get('page') || '1';
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
query,
|
|
142
|
-
page: parseInt(page),
|
|
143
|
-
results: []
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// GET /search?q=shokupan&page=2
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
#### Routers
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
import { ShokupanRouter } from 'shokupan';
|
|
154
|
-
|
|
155
|
-
const apiRouter = new ShokupanRouter();
|
|
156
|
-
|
|
157
|
-
apiRouter.get('/users', (ctx) => ({ users: [] }));
|
|
158
|
-
apiRouter.get('/posts', (ctx) => ({ posts: [] }));
|
|
159
|
-
|
|
160
|
-
// Mount router under /api prefix
|
|
161
|
-
app.mount('/api', apiRouter);
|
|
162
|
-
|
|
163
|
-
// Available at:
|
|
164
|
-
// GET /api/users
|
|
165
|
-
// GET /api/posts
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Controllers
|
|
169
|
-
|
|
170
|
-
Use decorators for a more structured, class-based approach:
|
|
171
|
-
|
|
172
|
-
<!-- @Controller('/users') -->
|
|
173
|
-
```typescript
|
|
174
|
-
import { Controller, Get, Post, Put, Delete, Param, Body, Query } from 'shokupan';
|
|
175
|
-
|
|
176
|
-
export class UserController {
|
|
177
|
-
|
|
178
|
-
@Get('/')
|
|
179
|
-
async getUsers(@Query('role') role?: string) {
|
|
180
|
-
return {
|
|
181
|
-
users: ['Alice', 'Bob'],
|
|
182
|
-
filter: role || 'all'
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
@Get('/:id')
|
|
187
|
-
async getUserById(@Param('id') id: string) {
|
|
188
|
-
return {
|
|
189
|
-
id,
|
|
190
|
-
name: 'Alice',
|
|
191
|
-
email: 'alice@example.com'
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
@Post('/')
|
|
196
|
-
async createUser(@Body() body: any) {
|
|
197
|
-
return {
|
|
198
|
-
message: 'User created',
|
|
199
|
-
data: body
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
@Put('/:id')
|
|
204
|
-
async updateUser(
|
|
205
|
-
@Param('id') id: string,
|
|
206
|
-
@Body() body: any
|
|
207
|
-
) {
|
|
208
|
-
return {
|
|
209
|
-
message: 'User updated',
|
|
210
|
-
id,
|
|
211
|
-
data: body
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
@Delete('/:id')
|
|
216
|
-
async deleteUser(@Param('id') id: string) {
|
|
217
|
-
return { message: 'User deleted', id };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Mount the controller
|
|
222
|
-
app.mount('/api', UserController);
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
#### Available Decorators
|
|
226
|
-
|
|
227
|
-
<!-- - `@Controller(path)` - Define base path for controller -->
|
|
228
|
-
- `@Get(path)` - GET route
|
|
229
|
-
- `@Post(path)` - POST route
|
|
230
|
-
- `@Put(path)` - PUT route
|
|
231
|
-
- `@Patch(path)` - PATCH route
|
|
232
|
-
- `@Delete(path)` - DELETE route
|
|
233
|
-
- `@Options(path)` - OPTIONS route
|
|
234
|
-
- `@Head(path)` - HEAD route
|
|
235
|
-
- `@All(path)` - Match all HTTP methods
|
|
236
|
-
|
|
237
|
-
**Parameter Decorators:**
|
|
238
|
-
|
|
239
|
-
- `@Param(name)` - Extract path parameter
|
|
240
|
-
- `@Query(name)` - Extract query parameter
|
|
241
|
-
- `@Body()` - Parse request body
|
|
242
|
-
- `@Headers(name)` - Extract header
|
|
243
|
-
- `@Ctx()` - Access full context
|
|
244
|
-
- `@Req()` - Access request object
|
|
245
|
-
|
|
246
|
-
### WebSockets
|
|
247
|
-
|
|
248
|
-
> Notice: The WebSocket API is currently in an experimental stage and subject to change.
|
|
249
|
-
|
|
250
|
-
Shokupan provides native WebSocket handling and an HTTP Bridge feature.
|
|
251
|
-
|
|
252
|
-
#### Event Decorator
|
|
253
|
-
|
|
254
|
-
Handle WebSocket events directly in your controllers:
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
import { Controller, Event, ShokupanContext } from 'shokupan';
|
|
258
|
-
|
|
259
|
-
@Controller('/chat')
|
|
260
|
-
export class ChatController {
|
|
261
|
-
|
|
262
|
-
@Event('message')
|
|
263
|
-
async onMessage(ctx: ShokupanContext) {
|
|
264
|
-
const payload = await ctx.body();
|
|
265
|
-
console.log('Received:', payload);
|
|
266
|
-
|
|
267
|
-
// Reply using underlying socket
|
|
268
|
-
// Native Bun: ctx.socket is ServerWebSocket
|
|
269
|
-
// Socket.IO: ctx.socket is Socket
|
|
270
|
-
ctx.emit('ack', 'received');
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
#### Socket.IO Support
|
|
276
|
-
|
|
277
|
-
Easily integrate Socket.IO and wire up your event decorators via the built-in helper:
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
import { Shokupan } from 'shokupan';
|
|
281
|
-
import { Server } from 'socket.io';
|
|
282
|
-
import { attachSocketIOBridge } from 'shokupan';
|
|
283
|
-
|
|
284
|
-
const app = new Shokupan({ enableHttpBridge: false });
|
|
285
|
-
const server = await app.listen(3000);
|
|
286
|
-
|
|
287
|
-
// Attach Socket.IO
|
|
288
|
-
const io = new Server(server.nodeServer); // For Node.js
|
|
289
|
-
attachSocketIOBridge(io, app);
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
#### HTTP Bridge
|
|
293
|
-
|
|
294
|
-
Expose your HTTP API over WebSockets (set `enableHttpBridge: true` in config).
|
|
295
|
-
|
|
296
|
-
Client sends: `{ type: "HTTP", method: "GET", path: "/api/users", ... }`
|
|
297
|
-
Server responds: `{ type: "RESPONSE", status: 200, body: ... }`
|
|
298
|
-
|
|
299
|
-
### Middleware
|
|
300
|
-
|
|
301
|
-
Middleware functions have access to the context and can control request flow:
|
|
302
|
-
|
|
303
|
-
```typescript
|
|
304
|
-
import { Middleware } from 'shokupan';
|
|
305
|
-
|
|
306
|
-
// Simple logging middleware
|
|
307
|
-
const logger: Middleware = async (ctx, next) => {
|
|
308
|
-
console.log(`${ctx.method} ${ctx.path}`);
|
|
309
|
-
const start = Date.now();
|
|
310
|
-
|
|
311
|
-
const result = await next();
|
|
312
|
-
|
|
313
|
-
console.log(`${ctx.method} ${ctx.path} - ${Date.now() - start}ms`);
|
|
314
|
-
return result;
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
app.use(logger);
|
|
318
|
-
|
|
319
|
-
// Authentication middleware
|
|
320
|
-
const auth: Middleware = async (ctx, next) => {
|
|
321
|
-
const token = ctx.headers.get('Authorization');
|
|
322
|
-
|
|
323
|
-
if (!token) {
|
|
324
|
-
return ctx.json({ error: 'Unauthorized' }, 401);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Validate token and attach user to state
|
|
328
|
-
ctx.state.user = { id: '123', name: 'Alice' };
|
|
329
|
-
|
|
330
|
-
return next();
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// Apply to specific routes
|
|
334
|
-
app.get('/protected', auth, (ctx) => {
|
|
335
|
-
return { user: ctx.state.user };
|
|
336
|
-
});
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
Or use with decorators:
|
|
340
|
-
```ts
|
|
341
|
-
import { Use } from 'shokupan';
|
|
342
|
-
|
|
343
|
-
@Controller('/admin')
|
|
344
|
-
@Use(auth) // Apply to all routes in controller
|
|
345
|
-
export class AdminController {
|
|
346
|
-
|
|
347
|
-
@Get('/dashboard')
|
|
348
|
-
getDashboard(@Ctx() ctx) {
|
|
349
|
-
return { user: ctx.state.user };
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Context
|
|
355
|
-
|
|
356
|
-
The `ShokupanContext` provides a rich API for handling requests and responses:
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
app.get('/demo', async (ctx) => {
|
|
360
|
-
// Request properties
|
|
361
|
-
ctx.method; // HTTP method
|
|
362
|
-
ctx.path; // URL path
|
|
363
|
-
ctx.url; // Full URL
|
|
364
|
-
ctx.params; // Path parameters
|
|
365
|
-
ctx.query; // Query string (URLSearchParams)
|
|
366
|
-
ctx.headers; // Headers (Headers object)
|
|
367
|
-
|
|
368
|
-
// Request body
|
|
369
|
-
const body = await ctx.body(); // Auto-parsed JSON/form/multipart
|
|
370
|
-
const json = await ctx.req.json(); // JSON body
|
|
371
|
-
const text = await ctx.req.text(); // Text body
|
|
372
|
-
const form = await ctx.req.formData(); // Form data
|
|
373
|
-
|
|
374
|
-
// State (shared across middleware)
|
|
375
|
-
ctx.state.user = { id: '123' };
|
|
376
|
-
|
|
377
|
-
// Response helpers
|
|
378
|
-
return ctx.json({ message: 'Hello' }); // JSON response
|
|
379
|
-
return ctx.text('Hello World'); // Text response
|
|
380
|
-
return ctx.html('<h1>Hello</h1>'); // HTML response
|
|
381
|
-
return ctx.redirect('/new-path'); // Redirect
|
|
382
|
-
|
|
383
|
-
// Set response headers
|
|
384
|
-
ctx.set('X-Custom-Header', 'value');
|
|
385
|
-
|
|
386
|
-
// Set cookies
|
|
387
|
-
ctx.setCookie('session', 'abc123', {
|
|
388
|
-
httpOnly: true,
|
|
389
|
-
secure: true,
|
|
390
|
-
maxAge: 3600
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
// Return Response directly
|
|
394
|
-
return new Response('Custom response', {
|
|
395
|
-
status: 200,
|
|
396
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
### Static Files
|
|
402
|
-
|
|
403
|
-
Serve static files with directory listing support:
|
|
404
|
-
|
|
405
|
-
```typescript
|
|
406
|
-
// Serve static files from a directory
|
|
407
|
-
app.static('/public', {
|
|
408
|
-
root: './public',
|
|
409
|
-
listDirectory: true // Enable directory listing
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// Multiple static directories
|
|
413
|
-
app.static('/images', {
|
|
414
|
-
root: './assets/images',
|
|
415
|
-
listDirectory: true
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
app.static('/js', {
|
|
419
|
-
root: './assets/js',
|
|
420
|
-
listDirectory: false
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
// Files available at:
|
|
424
|
-
// GET /public/style.css -> ./public/style.css
|
|
425
|
-
// GET /images/logo.png -> ./assets/images/logo.png
|
|
426
|
-
```
|
|
56
|
+
- **[Routing](https://shokupan.dev/core/routing)**: Express-style routing (`app.get`, `app.post`) with a clean, intuitive API.
|
|
57
|
+
- **[Controllers](https://shokupan.dev/core/controllers)**: Decorator-based controllers (`@Controller`, `@Get`) for structured applications.
|
|
58
|
+
- **[Middleware](https://shokupan.dev/core/middleware)**: Koa-style async middleware for request processing and flow control.
|
|
59
|
+
- **[Context](https://shokupan.dev/core/context)**: A rich `ctx` object containing request, response, parameters, and shared state.
|
|
60
|
+
- **[Static Files](https://shokupan.dev/core/static-files)**: Serve static assets with ease.
|
|
61
|
+
- **[WebSockets](https://shokupan.dev/core/websockets)**: Native WebSocket handling and HTTP Bridge feature.
|
|
427
62
|
|
|
428
63
|
## 🔌 Plugins
|
|
429
64
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// Dynamic origin validation
|
|
455
|
-
app.use(Cors({
|
|
456
|
-
origin: (ctx) => {
|
|
457
|
-
const origin = ctx.headers.get('origin');
|
|
458
|
-
// Validate origin dynamically
|
|
459
|
-
return origin?.endsWith('.example.com') ? origin : false;
|
|
460
|
-
},
|
|
461
|
-
credentials: true
|
|
462
|
-
}));
|
|
463
|
-
|
|
464
|
-
// Full options
|
|
465
|
-
app.use(Cors({
|
|
466
|
-
origin: '*', // or string, string[], function
|
|
467
|
-
methods: 'GET,POST,PUT,DELETE', // or string[]
|
|
468
|
-
allowedHeaders: ['Content-Type'], // or string
|
|
469
|
-
exposedHeaders: ['X-Total-Count'], // or string
|
|
470
|
-
credentials: true,
|
|
471
|
-
maxAge: 86400 // Preflight cache duration
|
|
472
|
-
}));
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
### Compression
|
|
476
|
-
|
|
477
|
-
Enable response compression:
|
|
478
|
-
|
|
479
|
-
```typescript
|
|
480
|
-
import { Compression } from 'shokupan';
|
|
481
|
-
|
|
482
|
-
// Simple compression
|
|
483
|
-
app.use(Compression());
|
|
484
|
-
|
|
485
|
-
// Custom configuration
|
|
486
|
-
app.use(Compression({
|
|
487
|
-
threshold: 1024, // Only compress responses larger than 1KB
|
|
488
|
-
level: 6 // Compression level (1-9)
|
|
489
|
-
}));
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### Rate Limiting
|
|
493
|
-
|
|
494
|
-
Protect your API from abuse:
|
|
495
|
-
|
|
496
|
-
```typescript
|
|
497
|
-
import { RateLimit } from 'shokupan';
|
|
498
|
-
|
|
499
|
-
// Basic rate limiting - 100 requests per 15 minutes
|
|
500
|
-
app.use(RateLimit({
|
|
501
|
-
windowMs: 15 * 60 * 1000,
|
|
502
|
-
max: 100
|
|
503
|
-
}));
|
|
504
|
-
|
|
505
|
-
// Different limits for different routes
|
|
506
|
-
const apiLimiter = RateLimit({
|
|
507
|
-
windowMs: 15 * 60 * 1000,
|
|
508
|
-
max: 100,
|
|
509
|
-
message: 'Too many requests from this IP'
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
const authLimiter = RateLimit({
|
|
513
|
-
windowMs: 15 * 60 * 1000,
|
|
514
|
-
max: 5,
|
|
515
|
-
message: 'Too many login attempts'
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
app.use('/api', apiLimiter);
|
|
519
|
-
app.use('/auth/login', authLimiter);
|
|
520
|
-
|
|
521
|
-
// Custom key generator
|
|
522
|
-
app.use(RateLimit({
|
|
523
|
-
windowMs: 15 * 60 * 1000,
|
|
524
|
-
max: 100,
|
|
525
|
-
keyGenerator: (ctx) => {
|
|
526
|
-
// Rate limit by user ID instead of IP
|
|
527
|
-
return ctx.state.user?.id || ctx.ip;
|
|
528
|
-
}
|
|
529
|
-
}));
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
### Security Headers
|
|
533
|
-
|
|
534
|
-
Add security headers to responses:
|
|
535
|
-
|
|
536
|
-
```typescript
|
|
537
|
-
import { SecurityHeaders } from 'shokupan';
|
|
538
|
-
|
|
539
|
-
// Default secure headers
|
|
540
|
-
app.use(SecurityHeaders());
|
|
541
|
-
|
|
542
|
-
// Custom configuration
|
|
543
|
-
app.use(SecurityHeaders({
|
|
544
|
-
contentSecurityPolicy: {
|
|
545
|
-
directives: {
|
|
546
|
-
defaultSrc: ["'self'"],
|
|
547
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
548
|
-
scriptSrc: ["'self'", "https://trusted-cdn.com"],
|
|
549
|
-
imgSrc: ["'self'", "data:", "https:"]
|
|
550
|
-
}
|
|
551
|
-
},
|
|
552
|
-
hsts: {
|
|
553
|
-
maxAge: 31536000,
|
|
554
|
-
includeSubDomains: true,
|
|
555
|
-
preload: true
|
|
556
|
-
},
|
|
557
|
-
frameguard: {
|
|
558
|
-
action: 'deny'
|
|
559
|
-
}
|
|
560
|
-
}));
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Sessions
|
|
564
|
-
|
|
565
|
-
Session management with connect-style store support:
|
|
566
|
-
|
|
567
|
-
```typescript
|
|
568
|
-
import { Session } from 'shokupan';
|
|
569
|
-
|
|
570
|
-
// Basic session with memory store (development only)
|
|
571
|
-
app.use(Session({
|
|
572
|
-
secret: 'your-secret-key'
|
|
573
|
-
}));
|
|
574
|
-
|
|
575
|
-
// Full configuration
|
|
576
|
-
app.use(Session({
|
|
577
|
-
secret: 'your-secret-key',
|
|
578
|
-
name: 'sessionId', // Cookie name
|
|
579
|
-
resave: false, // Don't save unchanged sessions
|
|
580
|
-
saveUninitialized: false, // Don't create sessions until needed
|
|
581
|
-
cookie: {
|
|
582
|
-
httpOnly: true,
|
|
583
|
-
secure: true, // HTTPS only
|
|
584
|
-
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
585
|
-
sameSite: 'lax'
|
|
586
|
-
}
|
|
587
|
-
}));
|
|
588
|
-
|
|
589
|
-
// Use session in routes
|
|
590
|
-
app.get('/login', async (ctx) => {
|
|
591
|
-
ctx.session.user = { id: '123', name: 'Alice' };
|
|
592
|
-
return { message: 'Logged in' };
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
app.get('/profile', (ctx) => {
|
|
596
|
-
if (!ctx.session.user) {
|
|
597
|
-
return ctx.json({ error: 'Not authenticated' }, 401);
|
|
598
|
-
}
|
|
599
|
-
return ctx.session.user;
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
app.get('/logout', (ctx) => {
|
|
603
|
-
ctx.session.destroy();
|
|
604
|
-
return { message: 'Logged out' };
|
|
605
|
-
});
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
#### Using Connect-Style Session Stores
|
|
609
|
-
|
|
610
|
-
Shokupan is compatible with connect/express-session stores:
|
|
611
|
-
|
|
612
|
-
```typescript
|
|
613
|
-
import { Session } from 'shokupan';
|
|
614
|
-
import RedisStore from 'connect-redis';
|
|
615
|
-
import { createClient } from 'redis';
|
|
616
|
-
|
|
617
|
-
// Redis session store
|
|
618
|
-
const redisClient = createClient();
|
|
619
|
-
await redisClient.connect();
|
|
620
|
-
|
|
621
|
-
app.use(Session({
|
|
622
|
-
secret: 'your-secret-key',
|
|
623
|
-
store: new RedisStore({ client: redisClient }),
|
|
624
|
-
cookie: {
|
|
625
|
-
maxAge: 24 * 60 * 60 * 1000
|
|
626
|
-
}
|
|
627
|
-
}));
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
Compatible stores include:
|
|
631
|
-
- `connect-redis` - Redis
|
|
632
|
-
- `connect-mongo` - MongoDB
|
|
633
|
-
- `connect-sqlite3` - SQLite
|
|
634
|
-
- `session-file-store` - File system
|
|
635
|
-
- Any connect-compatible session store
|
|
636
|
-
|
|
637
|
-
### Authentication
|
|
638
|
-
|
|
639
|
-
Built-in OAuth2 support with multiple providers:
|
|
640
|
-
|
|
641
|
-
```typescript
|
|
642
|
-
import { AuthPlugin } from 'shokupan';
|
|
643
|
-
|
|
644
|
-
const auth = new AuthPlugin({
|
|
645
|
-
jwtSecret: 'your-jwt-secret',
|
|
646
|
-
jwtExpiration: '7d',
|
|
647
|
-
|
|
648
|
-
// Cookie configuration
|
|
649
|
-
cookieOptions: {
|
|
650
|
-
httpOnly: true,
|
|
651
|
-
secure: true,
|
|
652
|
-
sameSite: 'lax'
|
|
653
|
-
},
|
|
654
|
-
|
|
655
|
-
// GitHub OAuth
|
|
656
|
-
github: {
|
|
657
|
-
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
658
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
659
|
-
redirectUri: 'http://localhost:3000/auth/github/callback'
|
|
660
|
-
},
|
|
661
|
-
|
|
662
|
-
// Google OAuth
|
|
663
|
-
google: {
|
|
664
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
665
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
666
|
-
redirectUri: 'http://localhost:3000/auth/google/callback'
|
|
667
|
-
},
|
|
668
|
-
|
|
669
|
-
// Microsoft OAuth
|
|
670
|
-
microsoft: {
|
|
671
|
-
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
672
|
-
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
673
|
-
redirectUri: 'http://localhost:3000/auth/microsoft/callback',
|
|
674
|
-
tenantId: 'common'
|
|
675
|
-
},
|
|
676
|
-
|
|
677
|
-
// Apple OAuth
|
|
678
|
-
apple: {
|
|
679
|
-
clientId: process.env.APPLE_CLIENT_ID!,
|
|
680
|
-
clientSecret: process.env.APPLE_CLIENT_SECRET!,
|
|
681
|
-
redirectUri: 'http://localhost:3000/auth/apple/callback',
|
|
682
|
-
teamId: process.env.APPLE_TEAM_ID!,
|
|
683
|
-
keyId: process.env.APPLE_KEY_ID!
|
|
684
|
-
},
|
|
685
|
-
|
|
686
|
-
// Auth0
|
|
687
|
-
auth0: {
|
|
688
|
-
clientId: process.env.AUTH0_CLIENT_ID!,
|
|
689
|
-
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
|
|
690
|
-
redirectUri: 'http://localhost:3000/auth/auth0/callback',
|
|
691
|
-
domain: 'your-tenant.auth0.com'
|
|
692
|
-
},
|
|
693
|
-
|
|
694
|
-
// Okta
|
|
695
|
-
okta: {
|
|
696
|
-
clientId: process.env.OKTA_CLIENT_ID!,
|
|
697
|
-
clientSecret: process.env.OKTA_CLIENT_SECRET!,
|
|
698
|
-
redirectUri: 'http://localhost:3000/auth/okta/callback',
|
|
699
|
-
domain: 'your-domain.okta.com'
|
|
700
|
-
},
|
|
701
|
-
|
|
702
|
-
// Custom OAuth2
|
|
703
|
-
oauth2: {
|
|
704
|
-
clientId: 'your-client-id',
|
|
705
|
-
clientSecret: 'your-client-secret',
|
|
706
|
-
redirectUri: 'http://localhost:3000/auth/custom/callback',
|
|
707
|
-
authUrl: 'https://provider.com/oauth/authorize',
|
|
708
|
-
tokenUrl: 'https://provider.com/oauth/token',
|
|
709
|
-
userInfoUrl: 'https://provider.com/oauth/userinfo'
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// Mount auth routes at /auth
|
|
714
|
-
app.mount('/auth', auth);
|
|
715
|
-
|
|
716
|
-
// Protect routes with auth middleware
|
|
717
|
-
app.get('/protected', auth.middleware(), (ctx) => {
|
|
718
|
-
return { user: ctx.state.user };
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
// Available auth routes:
|
|
722
|
-
// GET /auth/github
|
|
723
|
-
// GET /auth/github/callback
|
|
724
|
-
// GET /auth/google
|
|
725
|
-
// GET /auth/google/callback
|
|
726
|
-
// ... (and all other providers)
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
### Validation
|
|
730
|
-
|
|
731
|
-
Validate request data with your favorite validation library:
|
|
732
|
-
|
|
733
|
-
```typescript
|
|
734
|
-
import { validate } from 'shokupan';
|
|
735
|
-
import { z } from 'zod';
|
|
736
|
-
|
|
737
|
-
// Zod validation
|
|
738
|
-
const userSchema = z.object({
|
|
739
|
-
name: z.string().min(2),
|
|
740
|
-
email: z.string().email(),
|
|
741
|
-
age: z.number().min(18)
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
app.post('/users',
|
|
745
|
-
validate({ body: userSchema }),
|
|
746
|
-
async (ctx) => {
|
|
747
|
-
const body = await ctx.body(); // Already validated!
|
|
748
|
-
return { created: body };
|
|
749
|
-
}
|
|
750
|
-
);
|
|
751
|
-
|
|
752
|
-
// Validate query parameters
|
|
753
|
-
const searchSchema = z.object({
|
|
754
|
-
q: z.string(),
|
|
755
|
-
page: z.coerce.number().default(1),
|
|
756
|
-
limit: z.coerce.number().max(100).default(10)
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
app.get('/search',
|
|
760
|
-
validate({ query: searchSchema }),
|
|
761
|
-
(ctx) => {
|
|
762
|
-
const q = ctx.query.get('q');
|
|
763
|
-
const page = ctx.query.get('page');
|
|
764
|
-
return { q, page };
|
|
765
|
-
}
|
|
766
|
-
);
|
|
767
|
-
|
|
768
|
-
// Validate path parameters
|
|
769
|
-
app.get('/users/:id',
|
|
770
|
-
validate({
|
|
771
|
-
params: z.object({
|
|
772
|
-
id: z.string().uuid()
|
|
773
|
-
})
|
|
774
|
-
}),
|
|
775
|
-
(ctx) => {
|
|
776
|
-
return { id: ctx.params.id };
|
|
777
|
-
}
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
// Validate headers
|
|
781
|
-
app.post('/webhook',
|
|
782
|
-
validate({
|
|
783
|
-
headers: z.object({
|
|
784
|
-
'x-webhook-signature': z.string()
|
|
785
|
-
})
|
|
786
|
-
}),
|
|
787
|
-
async (ctx) => {
|
|
788
|
-
// Process webhook
|
|
789
|
-
}
|
|
790
|
-
);
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
#### TypeBox Validation
|
|
794
|
-
|
|
795
|
-
```typescript
|
|
796
|
-
import { Type } from '@sinclair/typebox';
|
|
797
|
-
import { validate } from 'shokupan';
|
|
798
|
-
|
|
799
|
-
const UserSchema = Type.Object({
|
|
800
|
-
name: Type.String({ minLength: 2 }),
|
|
801
|
-
email: Type.String({ format: 'email' }),
|
|
802
|
-
age: Type.Number({ minimum: 18 })
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
app.post('/users',
|
|
806
|
-
validate({ body: UserSchema }),
|
|
807
|
-
async (ctx) => {
|
|
808
|
-
const user = await ctx.body();
|
|
809
|
-
return { created: user };
|
|
810
|
-
}
|
|
811
|
-
);
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
#### Ajv Validation
|
|
815
|
-
|
|
816
|
-
```typescript
|
|
817
|
-
import Ajv from 'ajv';
|
|
818
|
-
import { validate } from 'shokupan';
|
|
819
|
-
|
|
820
|
-
const ajv = new Ajv();
|
|
821
|
-
const userSchema = ajv.compile({
|
|
822
|
-
type: 'object',
|
|
823
|
-
properties: {
|
|
824
|
-
name: { type: 'string', minLength: 2 },
|
|
825
|
-
email: { type: 'string', format: 'email' },
|
|
826
|
-
age: { type: 'number', minimum: 18 }
|
|
827
|
-
},
|
|
828
|
-
required: ['name', 'email', 'age']
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
app.post('/users',
|
|
832
|
-
validate({ body: userSchema }),
|
|
833
|
-
async (ctx) => {
|
|
834
|
-
const user = await ctx.body();
|
|
835
|
-
return { created: user };
|
|
836
|
-
}
|
|
837
|
-
);
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
#### Valibot Validation
|
|
841
|
-
|
|
842
|
-
```typescript
|
|
843
|
-
import * as v from 'valibot';
|
|
844
|
-
import { validate, valibot } from 'shokupan';
|
|
845
|
-
|
|
846
|
-
const UserSchema = v.object({
|
|
847
|
-
name: v.pipe(v.string(), v.minLength(2)),
|
|
848
|
-
email: v.pipe(v.string(), v.email()),
|
|
849
|
-
age: v.pipe(v.number(), v.minValue(18))
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
app.post('/users',
|
|
853
|
-
validate({
|
|
854
|
-
body: valibot(UserSchema, v.parseAsync)
|
|
855
|
-
}),
|
|
856
|
-
async (ctx) => {
|
|
857
|
-
const user = await ctx.body();
|
|
858
|
-
return { created: user };
|
|
859
|
-
}
|
|
860
|
-
);
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
### Scalar (OpenAPI)
|
|
864
|
-
|
|
865
|
-
Beautiful, interactive API documentation:
|
|
866
|
-
|
|
867
|
-
```typescript
|
|
868
|
-
import { ScalarPlugin } from 'shokupan';
|
|
869
|
-
|
|
870
|
-
app.mount('/docs', new ScalarPlugin({
|
|
871
|
-
baseDocument: {
|
|
872
|
-
info: {
|
|
873
|
-
title: 'My API',
|
|
874
|
-
version: '1.0.0',
|
|
875
|
-
description: 'API documentation'
|
|
876
|
-
}
|
|
877
|
-
},
|
|
878
|
-
config: {
|
|
879
|
-
theme: 'purple',
|
|
880
|
-
layout: 'modern'
|
|
881
|
-
}
|
|
882
|
-
}));
|
|
883
|
-
|
|
884
|
-
// Access docs at http://localhost:3000/docs
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
The Scalar plugin automatically generates OpenAPI documentation from your routes and controllers!
|
|
888
|
-
|
|
889
|
-
### Proxy
|
|
890
|
-
|
|
891
|
-
Create a reverse proxy to forward requests to another server:
|
|
892
|
-
|
|
893
|
-
```typescript
|
|
894
|
-
import { Proxy } from 'shokupan';
|
|
895
|
-
|
|
896
|
-
app.use('/api/v1', Proxy({
|
|
897
|
-
target: 'https://api.example.com',
|
|
898
|
-
changeOrigin: true,
|
|
899
|
-
pathRewrite: (path) => path.replace('/api/v1', ''),
|
|
900
|
-
headers: {
|
|
901
|
-
'X-Custom-Header': 'Proxy'
|
|
902
|
-
}
|
|
903
|
-
}));
|
|
904
|
-
|
|
905
|
-
// Proxy WebSockets
|
|
906
|
-
app.use('/socket', Proxy({
|
|
907
|
-
target: 'ws://ws.example.com',
|
|
908
|
-
ws: true
|
|
909
|
-
}));
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
### OpenAPI Validator
|
|
913
|
-
|
|
914
|
-
Validate incoming requests against your generated OpenAPI specification:
|
|
915
|
-
|
|
916
|
-
```typescript
|
|
917
|
-
import { enableOpenApiValidation } from 'shokupan';
|
|
918
|
-
|
|
919
|
-
const app = new Shokupan({ enableOpenApiGen: true });
|
|
920
|
-
|
|
921
|
-
// Enable validation middleware
|
|
922
|
-
// This validates Body, Query, Params, and Headers against your OpenAPI definitions
|
|
923
|
-
enableOpenApiValidation(app);
|
|
924
|
-
|
|
925
|
-
app.post('/users', {
|
|
926
|
-
parameters: [
|
|
927
|
-
{ name: 'apiKey', in: 'header', required: true, schema: { type: 'string' } }
|
|
928
|
-
],
|
|
929
|
-
requestBody: {
|
|
930
|
-
content: {
|
|
931
|
-
'application/json': {
|
|
932
|
-
schema: {
|
|
933
|
-
type: 'object',
|
|
934
|
-
required: ['name'],
|
|
935
|
-
properties: {
|
|
936
|
-
name: { type: 'string', minLength: 3 }
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}, (ctx) => {
|
|
943
|
-
return { success: true };
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
// Invalid requests will throw a ValidationError (400 Bad Request)
|
|
947
|
-
```
|
|
948
|
-
|
|
949
|
-
### Idempotency
|
|
950
|
-
|
|
951
|
-
Ensure that multiple identical requests do not result in different outcomes (e.g., duplicate payments). This middleware caches the response of the first request and returns it for subsequent requests with the same idempotency key.
|
|
952
|
-
|
|
953
|
-
```typescript
|
|
954
|
-
import { Idempotency } from 'shokupan';
|
|
955
|
-
|
|
956
|
-
app.post('/payments',
|
|
957
|
-
Idempotency({
|
|
958
|
-
header: 'Idempotency-Key', // default
|
|
959
|
-
ttl: 24 * 60 * 60 * 1000 // default 24h
|
|
960
|
-
}),
|
|
961
|
-
async (ctx) => {
|
|
962
|
-
// Process payment...
|
|
963
|
-
return { status: 'charged' };
|
|
964
|
-
}
|
|
965
|
-
);
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
### Failed Request Recorder
|
|
969
|
-
|
|
970
|
-
Automatically record failed requests (500s) for debugging and replay purposes.
|
|
971
|
-
|
|
972
|
-
```typescript
|
|
973
|
-
import { FailedRequestRecorder } from 'shokupan';
|
|
974
|
-
|
|
975
|
-
app.use(FailedRequestRecorder({
|
|
976
|
-
maxCapacity: 1000,
|
|
977
|
-
ttl: 86400000 // 1 day
|
|
978
|
-
}));
|
|
979
|
-
```
|
|
980
|
-
|
|
981
|
-
This works great when combined with the Debug Dashboard.
|
|
982
|
-
|
|
983
|
-
### Debug Dashboard
|
|
984
|
-
|
|
985
|
-
A visual dashboard to inspect your application, view metrics, analyze the middleware graph, and replay failed requests.
|
|
986
|
-
|
|
987
|
-
```typescript
|
|
988
|
-
import { Dashboard } from 'shokupan';
|
|
989
|
-
|
|
990
|
-
// Mount the dashboard
|
|
991
|
-
app.mount('/debug', new Dashboard({
|
|
992
|
-
retentionMs: 2 * 60 * 60 * 1000, // Keep 2 hours of logs
|
|
993
|
-
getRequestHeaders: () => ({
|
|
994
|
-
'Authorization': 'Bearer ...' // Headers to using when replaying requests and accessing data APIs
|
|
995
|
-
})
|
|
996
|
-
}));
|
|
997
|
-
|
|
998
|
-
// Available at http://localhost:3000/debug
|
|
999
|
-
```
|
|
1000
|
-
|
|
65
|
+
Shokupan has a rich ecosystem of plugins.
|
|
66
|
+
|
|
67
|
+
| Plugin | Description |
|
|
68
|
+
| :--- | :--- |
|
|
69
|
+
| **[Dashboard](https://shokupan.dev/plugins/dashboard)** | Visual dashboard for debugging and analysis. |
|
|
70
|
+
| **[Error View](https://shokupan.dev/plugins/error-view)** | Beautiful, interactive error pages for development. |
|
|
71
|
+
| **[CORS](https://shokupan.dev/plugins/cors)** | Configure Cross-Origin Resource Sharing. |
|
|
72
|
+
| **[Compression](https://shokupan.dev/plugins/compression)** | Enable response compression (gzip, deflate, etc.). |
|
|
73
|
+
| **[Rate Limiting](https://shokupan.dev/plugins/rate-limiting)** | Protect your API from abuse. |
|
|
74
|
+
| **[Security Headers](https://shokupan.dev/plugins/security-headers)** | Add essential security headers (CSP, HSTS, etc.). |
|
|
75
|
+
| **[Sessions](https://shokupan.dev/plugins/sessions)** | Session management with connect-style store support. |
|
|
76
|
+
| **[Authentication](https://shokupan.dev/plugins/authentication)** | Built-in OAuth2 support (GitHub, Google, etc.). |
|
|
77
|
+
| **[Validation](https://shokupan.dev/plugins/validation)** | Validate requests with Zod, Ajv, TypeBox, or Valibot. |
|
|
78
|
+
| **[Scalar (OpenAPI)](https://shokupan.dev/plugins/scalar)** | Beautiful, interactive API documentation. |
|
|
79
|
+
| **[API Explorer](https://shokupan.dev/plugins/api-explorer)** | Built-in interactive documentation for your API. |
|
|
80
|
+
| **[AsyncAPI](https://shokupan.dev/plugins/asyncapi)** | Generate and view documentation for WebSocket APIs. |
|
|
81
|
+
| **[Cluster](https://shokupan.dev/plugins/cluster)** | Utilize multiple CPU cores for better performance. |
|
|
82
|
+
| **[GraphQL](https://shokupan.dev/plugins/graphql)** | Support for Apollo Server and GraphQL Yoga. |
|
|
83
|
+
| **[HTTP Server](https://shokupan.dev/plugins/http-server)** | Use standard Node.js HTTP/HTTPS servers. |
|
|
84
|
+
| **[MCP Server](https://shokupan.dev/plugins/mcp-server)** | Expose your API as tools to LLMs. |
|
|
85
|
+
| **[Socket.IO](https://shokupan.dev/plugins/socket-io)** | Easy integration with Socket.IO. |
|
|
86
|
+
| **[Proxy](https://shokupan.dev/plugins/proxy)** | Create reverse proxies. |
|
|
87
|
+
| **[OpenAPI Validator](https://shokupan.dev/plugins/openapi-validation)** | Validate requests against OpenAPI specs. |
|
|
88
|
+
| **[Idempotency](https://shokupan.dev/plugins/idempotency)** | Ensure safe retries for non-idempotent operations. |
|
|
1001
89
|
|
|
1002
90
|
## 🚀 Advanced Features
|
|
1003
91
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
import { Container } from 'shokupan';
|
|
1010
|
-
|
|
1011
|
-
// Register services
|
|
1012
|
-
class Database {
|
|
1013
|
-
query(sql: string) {
|
|
1014
|
-
return [];
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
class UserService {
|
|
1019
|
-
constructor(private db: Database) {}
|
|
1020
|
-
|
|
1021
|
-
getUsers() {
|
|
1022
|
-
return this.db.query('SELECT * FROM users');
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
Container.register('db', Database);
|
|
1027
|
-
Container.register('userService', UserService);
|
|
1028
|
-
|
|
1029
|
-
// Use in controllers
|
|
1030
|
-
@Controller('/users')
|
|
1031
|
-
export class UserController {
|
|
1032
|
-
constructor(
|
|
1033
|
-
private userService: UserService = Container.resolve('userService')
|
|
1034
|
-
) {}
|
|
1035
|
-
|
|
1036
|
-
@Get('/')
|
|
1037
|
-
getUsers() {
|
|
1038
|
-
return this.userService.getUsers();
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
### OpenAPI Generation
|
|
1044
|
-
|
|
1045
|
-
Generate OpenAPI specs automatically and add custom documentation:
|
|
1046
|
-
|
|
1047
|
-
```typescript
|
|
1048
|
-
// Add OpenAPI metadata to routes
|
|
1049
|
-
app.get('/users/:id', {
|
|
1050
|
-
summary: 'Get user by ID',
|
|
1051
|
-
description: 'Retrieves a single user by their unique identifier',
|
|
1052
|
-
tags: ['Users'],
|
|
1053
|
-
parameters: [{
|
|
1054
|
-
name: 'id',
|
|
1055
|
-
in: 'path',
|
|
1056
|
-
required: true,
|
|
1057
|
-
schema: { type: 'string' }
|
|
1058
|
-
}],
|
|
1059
|
-
responses: {
|
|
1060
|
-
200: {
|
|
1061
|
-
description: 'User found',
|
|
1062
|
-
content: {
|
|
1063
|
-
'application/json': {
|
|
1064
|
-
schema: {
|
|
1065
|
-
type: 'object',
|
|
1066
|
-
properties: {
|
|
1067
|
-
id: { type: 'string' },
|
|
1068
|
-
name: { type: 'string' },
|
|
1069
|
-
email: { type: 'string' }
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
},
|
|
1075
|
-
404: {
|
|
1076
|
-
description: 'User not found'
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}, (ctx) => {
|
|
1080
|
-
return { id: ctx.params.id, name: 'Alice' };
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
// Generate OpenAPI spec
|
|
1084
|
-
const spec = app.computeOpenAPISpec({
|
|
1085
|
-
info: {
|
|
1086
|
-
title: 'My API',
|
|
1087
|
-
version: '1.0.0'
|
|
1088
|
-
}
|
|
1089
|
-
});
|
|
1090
|
-
```
|
|
1091
|
-
|
|
1092
|
-
### Sub-Requests
|
|
1093
|
-
|
|
1094
|
-
Make internal requests without HTTP overhead:
|
|
1095
|
-
|
|
1096
|
-
```typescript
|
|
1097
|
-
import { ShokupanRouter } from 'shokupan';
|
|
1098
|
-
|
|
1099
|
-
const router = new ShokupanRouter();
|
|
1100
|
-
|
|
1101
|
-
// Service endpoints
|
|
1102
|
-
router.get('/wines/red', async (ctx) => {
|
|
1103
|
-
const response = await fetch('https://api.sampleapis.com/wines/reds');
|
|
1104
|
-
return response.json();
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
router.get('/wines/white', async (ctx) => {
|
|
1108
|
-
const response = await fetch('https://api.sampleapis.com/wines/whites');
|
|
1109
|
-
return response.json();
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// Aggregate endpoint using sub-requests
|
|
1113
|
-
router.get('/wines/all', async (ctx) => {
|
|
1114
|
-
// Make parallel sub-requests
|
|
1115
|
-
const [redResponse, whiteResponse] = await Promise.all([
|
|
1116
|
-
router.internalRequest('/wines/red'),
|
|
1117
|
-
router.internalRequest('/wines/white')
|
|
1118
|
-
]);
|
|
1119
|
-
|
|
1120
|
-
const red = await redResponse.json();
|
|
1121
|
-
const white = await whiteResponse.json();
|
|
1122
|
-
|
|
1123
|
-
return { red, white };
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
app.mount('/api', router);
|
|
1127
|
-
|
|
1128
|
-
// GET /api/wines/all
|
|
1129
|
-
// Returns both red and white wines aggregated
|
|
1130
|
-
```
|
|
1131
|
-
|
|
1132
|
-
Sub-requests are great for:
|
|
1133
|
-
- Service composition
|
|
1134
|
-
- Backend-for-Frontend (BFF) patterns
|
|
1135
|
-
- Internal API aggregation
|
|
1136
|
-
- Testing
|
|
1137
|
-
|
|
1138
|
-
### OpenTelemetry
|
|
1139
|
-
|
|
1140
|
-
Built-in distributed tracing support:
|
|
1141
|
-
|
|
1142
|
-
```typescript
|
|
1143
|
-
const app = new Shokupan({
|
|
1144
|
-
port: 3000,
|
|
1145
|
-
development: true,
|
|
1146
|
-
enableAsyncLocalStorage: true // Enable for better trace context
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
// Tracing is automatic!
|
|
1150
|
-
// All routes and middleware are instrumented
|
|
1151
|
-
// Sub-requests maintain trace context
|
|
1152
|
-
```
|
|
1153
|
-
|
|
1154
|
-
Configure OpenTelemetry exporters in your environment:
|
|
1155
|
-
|
|
1156
|
-
```typescript
|
|
1157
|
-
// src/instrumentation.ts
|
|
1158
|
-
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
1159
|
-
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
|
|
1160
|
-
|
|
1161
|
-
const provider = new NodeTracerProvider();
|
|
1162
|
-
provider.addSpanProcessor(
|
|
1163
|
-
new BatchSpanProcessor(
|
|
1164
|
-
new OTLPTraceExporter({
|
|
1165
|
-
url: 'http://localhost:4318/v1/traces'
|
|
1166
|
-
})
|
|
1167
|
-
)
|
|
1168
|
-
);
|
|
1169
|
-
provider.register();
|
|
1170
|
-
```
|
|
1171
|
-
|
|
1172
|
-
### Server Factory (Node.js & Deno)
|
|
1173
|
-
|
|
1174
|
-
Run Shokupan on Node.js or Deno using the server adapter:
|
|
1175
|
-
|
|
1176
|
-
```typescript
|
|
1177
|
-
import { Shokupan, createHttpServer } from 'shokupan';
|
|
1178
|
-
|
|
1179
|
-
const app = new Shokupan({
|
|
1180
|
-
// Use Node.js http module
|
|
1181
|
-
serverFactory: createHttpServer()
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
app.get('/', () => ({ message: 'Running on Node!' }));
|
|
1185
|
-
|
|
1186
|
-
app.listen(3000);
|
|
1187
|
-
```
|
|
1188
|
-
|
|
1189
|
-
### Automatic Backpressure
|
|
1190
|
-
|
|
1191
|
-
Protect your server from overload by shedding load when CPU usage is high:
|
|
1192
|
-
|
|
1193
|
-
```typescript
|
|
1194
|
-
const app = new Shokupan({
|
|
1195
|
-
// Monitor CPU and reject requests when usage > 80%
|
|
1196
|
-
autoBackpressureFeedback: true,
|
|
1197
|
-
autoBackpressureLevel: 80
|
|
1198
|
-
});
|
|
1199
|
-
```
|
|
1200
|
-
|
|
1201
|
-
When the threshold is reached, the server will return `429 Too Many Requests`.
|
|
1202
|
-
|
|
1203
|
-
## 📦 Migration Guides
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
### From Express
|
|
1207
|
-
|
|
1208
|
-
Shokupan is designed to feel familiar to Express developers. Here's how to migrate:
|
|
1209
|
-
|
|
1210
|
-
#### Basic Server
|
|
1211
|
-
|
|
1212
|
-
**Express:**
|
|
1213
|
-
```typescript
|
|
1214
|
-
import express from 'express';
|
|
1215
|
-
|
|
1216
|
-
const app = express();
|
|
1217
|
-
|
|
1218
|
-
app.get('/', (req, res) => {
|
|
1219
|
-
res.json({ message: 'Hello' });
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
app.listen(3000);
|
|
1223
|
-
```
|
|
1224
|
-
|
|
1225
|
-
**Shokupan:**
|
|
1226
|
-
```typescript
|
|
1227
|
-
import { Shokupan } from 'shokupan';
|
|
1228
|
-
|
|
1229
|
-
const app = new Shokupan({ port: 3000 });
|
|
1230
|
-
|
|
1231
|
-
app.get('/', (ctx) => {
|
|
1232
|
-
return { message: 'Hello' };
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
app.listen();
|
|
1236
|
-
```
|
|
1237
|
-
|
|
1238
|
-
#### Request/Response
|
|
1239
|
-
|
|
1240
|
-
**Express:**
|
|
1241
|
-
```typescript
|
|
1242
|
-
app.get('/users/:id', (req, res) => {
|
|
1243
|
-
const id = req.params.id;
|
|
1244
|
-
const page = req.query.page;
|
|
1245
|
-
const token = req.headers.authorization;
|
|
1246
|
-
|
|
1247
|
-
res.status(200).json({
|
|
1248
|
-
id,
|
|
1249
|
-
page,
|
|
1250
|
-
authenticated: !!token
|
|
1251
|
-
});
|
|
1252
|
-
});
|
|
1253
|
-
```
|
|
1254
|
-
|
|
1255
|
-
**Shokupan:**
|
|
1256
|
-
```typescript
|
|
1257
|
-
app.get('/users/:id', (ctx) => {
|
|
1258
|
-
const id = ctx.params.id;
|
|
1259
|
-
const page = ctx.query.get('page');
|
|
1260
|
-
const token = ctx.headers.get('authorization');
|
|
1261
|
-
|
|
1262
|
-
return ctx.json({
|
|
1263
|
-
id,
|
|
1264
|
-
page,
|
|
1265
|
-
authenticated: !!token
|
|
1266
|
-
}, 200);
|
|
1267
|
-
|
|
1268
|
-
// Or simply return an object (auto JSON, status 200)
|
|
1269
|
-
return { id, page, authenticated: !!token };
|
|
1270
|
-
});
|
|
1271
|
-
```
|
|
1272
|
-
|
|
1273
|
-
#### Middleware
|
|
1274
|
-
|
|
1275
|
-
**Express:**
|
|
1276
|
-
```typescript
|
|
1277
|
-
app.use((req, res, next) => {
|
|
1278
|
-
console.log(`${req.method} ${req.path}`);
|
|
1279
|
-
next();
|
|
1280
|
-
});
|
|
1281
|
-
|
|
1282
|
-
app.use(express.json());
|
|
1283
|
-
app.use(cors());
|
|
1284
|
-
```
|
|
1285
|
-
|
|
1286
|
-
**Shokupan:**
|
|
1287
|
-
```typescript
|
|
1288
|
-
import { Cors } from 'shokupan';
|
|
1289
|
-
|
|
1290
|
-
app.use(async (ctx, next) => {
|
|
1291
|
-
console.log(`${ctx.method} ${ctx.path}`);
|
|
1292
|
-
return next();
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
// Body parsing is built-in, no middleware needed
|
|
1296
|
-
app.use(Cors());
|
|
1297
|
-
```
|
|
1298
|
-
|
|
1299
|
-
#### Static Files
|
|
1300
|
-
|
|
1301
|
-
**Express:**
|
|
1302
|
-
```typescript
|
|
1303
|
-
app.use('/public', express.static('public'));
|
|
1304
|
-
```
|
|
1305
|
-
|
|
1306
|
-
**Shokupan:**
|
|
1307
|
-
```typescript
|
|
1308
|
-
app.static('/public', {
|
|
1309
|
-
root: './public',
|
|
1310
|
-
listDirectory: true
|
|
1311
|
-
});
|
|
1312
|
-
```
|
|
1313
|
-
|
|
1314
|
-
#### Key Differences
|
|
1315
|
-
|
|
1316
|
-
1. **Context vs Req/Res**: Shokupan uses a single `ctx` object
|
|
1317
|
-
2. **Return vs Send**: Return values directly instead of calling `res.json()` or `res.send()`
|
|
1318
|
-
3. **Built-in Parsing**: Body parsing is automatic, no need for `express.json()`
|
|
1319
|
-
4. **Async by Default**: All handlers and middleware are naturally async
|
|
1320
|
-
5. **Web Standard APIs**: Uses `Headers`, `URL`, `Response` etc. from web standards
|
|
1321
|
-
|
|
1322
|
-
### From Koa
|
|
1323
|
-
|
|
1324
|
-
Shokupan's context-based approach is heavily inspired by Koa:
|
|
1325
|
-
|
|
1326
|
-
#### Basic Differences
|
|
1327
|
-
|
|
1328
|
-
**Koa:**
|
|
1329
|
-
```typescript
|
|
1330
|
-
import Koa from 'koa';
|
|
1331
|
-
|
|
1332
|
-
const app = new Koa();
|
|
1333
|
-
|
|
1334
|
-
app.use(async (ctx, next) => {
|
|
1335
|
-
ctx.body = { message: 'Hello' };
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
app.listen(3000);
|
|
1339
|
-
```
|
|
1340
|
-
|
|
1341
|
-
**Shokupan:**
|
|
1342
|
-
```typescript
|
|
1343
|
-
import { Shokupan } from 'shokupan';
|
|
1344
|
-
|
|
1345
|
-
const app = new Shokupan({ port: 3000 });
|
|
1346
|
-
|
|
1347
|
-
app.get('/', async (ctx) => {
|
|
1348
|
-
return { message: 'Hello' };
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
app.listen();
|
|
1352
|
-
```
|
|
1353
|
-
|
|
1354
|
-
#### Middleware
|
|
1355
|
-
|
|
1356
|
-
**Koa:**
|
|
1357
|
-
```typescript
|
|
1358
|
-
app.use(async (ctx, next) => {
|
|
1359
|
-
const start = Date.now();
|
|
1360
|
-
await next();
|
|
1361
|
-
const ms = Date.now() - start;
|
|
1362
|
-
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
|
|
1363
|
-
});
|
|
1364
|
-
```
|
|
1365
|
-
|
|
1366
|
-
**Shokupan:**
|
|
1367
|
-
```typescript
|
|
1368
|
-
app.use(async (ctx, next) => {
|
|
1369
|
-
const start = Date.now();
|
|
1370
|
-
const result = await next();
|
|
1371
|
-
const ms = Date.now() - start;
|
|
1372
|
-
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
|
|
1373
|
-
return result; // Don't forget to return!
|
|
1374
|
-
});
|
|
1375
|
-
```
|
|
1376
|
-
|
|
1377
|
-
#### Routing
|
|
1378
|
-
|
|
1379
|
-
**Koa (with koa-router):**
|
|
1380
|
-
```typescript
|
|
1381
|
-
import Router from '@koa/router';
|
|
1382
|
-
|
|
1383
|
-
const router = new Router();
|
|
1384
|
-
|
|
1385
|
-
router.get('/users/:id', (ctx) => {
|
|
1386
|
-
ctx.body = { id: ctx.params.id };
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
app.use(router.routes());
|
|
1390
|
-
```
|
|
1391
|
-
|
|
1392
|
-
**Shokupan:**
|
|
1393
|
-
```typescript
|
|
1394
|
-
import { ShokupanRouter } from 'shokupan';
|
|
1395
|
-
|
|
1396
|
-
const router = new ShokupanRouter();
|
|
1397
|
-
|
|
1398
|
-
router.get('/users/:id', (ctx) => {
|
|
1399
|
-
return { id: ctx.params.id };
|
|
1400
|
-
});
|
|
1401
|
-
|
|
1402
|
-
app.mount('/', router);
|
|
1403
|
-
```
|
|
1404
|
-
|
|
1405
|
-
#### Key Differences
|
|
1406
|
-
|
|
1407
|
-
1. **Return Value**: Shokupan requires returning the response from middleware
|
|
1408
|
-
2. **Routing**: Built-in routing, no need for external router package
|
|
1409
|
-
3. **Context Properties**: Some property names differ (`ctx.path` vs `ctx.url`)
|
|
1410
|
-
4. **Body Parsing**: Built-in, no need for koa-bodyparser
|
|
1411
|
-
|
|
1412
|
-
### From NestJS
|
|
1413
|
-
|
|
1414
|
-
Moving from NestJS to Shokupan:
|
|
1415
|
-
|
|
1416
|
-
#### Controllers
|
|
1417
|
-
|
|
1418
|
-
**NestJS:**
|
|
1419
|
-
```typescript
|
|
1420
|
-
import { Controller, Get, Post, Param, Body } from '@nestjs/common';
|
|
1421
|
-
|
|
1422
|
-
@Controller('users')
|
|
1423
|
-
export class UserController {
|
|
1424
|
-
@Get(':id')
|
|
1425
|
-
getUser(@Param('id') id: string) {
|
|
1426
|
-
return { id, name: 'Alice' };
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
@Post()
|
|
1430
|
-
createUser(@Body() body: CreateUserDto) {
|
|
1431
|
-
return { created: body };
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
```
|
|
1435
|
-
|
|
1436
|
-
**Shokupan:**
|
|
1437
|
-
```typescript
|
|
1438
|
-
import { Controller, Get, Post, Param, Body } from 'shokupan';
|
|
1439
|
-
|
|
1440
|
-
@Controller('/users')
|
|
1441
|
-
export class UserController {
|
|
1442
|
-
@Get('/:id')
|
|
1443
|
-
getUser(@Param('id') id: string) {
|
|
1444
|
-
return { id, name: 'Alice' };
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
@Post('/')
|
|
1448
|
-
createUser(@Body() body: CreateUserDto) {
|
|
1449
|
-
return { created: body };
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
```
|
|
1453
|
-
|
|
1454
|
-
#### Dependency Injection
|
|
1455
|
-
|
|
1456
|
-
**NestJS:**
|
|
1457
|
-
```typescript
|
|
1458
|
-
import { Injectable } from '@nestjs/common';
|
|
1459
|
-
|
|
1460
|
-
@Injectable()
|
|
1461
|
-
export class UserService {
|
|
1462
|
-
getUsers() {
|
|
1463
|
-
return [];
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
@Controller('users')
|
|
1468
|
-
export class UserController {
|
|
1469
|
-
constructor(private userService: UserService) {}
|
|
1470
|
-
|
|
1471
|
-
@Get()
|
|
1472
|
-
getUsers() {
|
|
1473
|
-
return this.userService.getUsers();
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
```
|
|
1477
|
-
|
|
1478
|
-
**Shokupan:**
|
|
1479
|
-
```typescript
|
|
1480
|
-
import { Container } from 'shokupan';
|
|
1481
|
-
|
|
1482
|
-
class UserService {
|
|
1483
|
-
getUsers() {
|
|
1484
|
-
return [];
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
Container.register('userService', UserService);
|
|
1489
|
-
|
|
1490
|
-
@Controller('/users')
|
|
1491
|
-
export class UserController {
|
|
1492
|
-
constructor(
|
|
1493
|
-
private userService: UserService = Container.resolve('userService')
|
|
1494
|
-
) {}
|
|
1495
|
-
|
|
1496
|
-
@Get('/')
|
|
1497
|
-
getUsers() {
|
|
1498
|
-
return this.userService.getUsers();
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
```
|
|
1502
|
-
|
|
1503
|
-
#### Guards
|
|
1504
|
-
|
|
1505
|
-
**NestJS:**
|
|
1506
|
-
```typescript
|
|
1507
|
-
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
1508
|
-
|
|
1509
|
-
export class AuthGuard implements CanActivate {
|
|
1510
|
-
canActivate(context: ExecutionContext): boolean {
|
|
1511
|
-
const request = context.switchToHttp().getRequest();
|
|
1512
|
-
return validateToken(request.headers.authorization);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
@Controller('admin')
|
|
1517
|
-
@UseGuards(AuthGuard)
|
|
1518
|
-
export class AdminController {}
|
|
1519
|
-
```
|
|
1520
|
-
|
|
1521
|
-
**Shokupan:**
|
|
1522
|
-
```typescript
|
|
1523
|
-
import { Middleware, Use } from 'shokupan';
|
|
1524
|
-
|
|
1525
|
-
const authGuard: Middleware = async (ctx, next) => {
|
|
1526
|
-
if (!validateToken(ctx.headers.get('authorization'))) {
|
|
1527
|
-
return ctx.json({ error: 'Unauthorized' }, 401);
|
|
1528
|
-
}
|
|
1529
|
-
return next();
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
@Controller('/admin')
|
|
1533
|
-
@Use(authGuard)
|
|
1534
|
-
export class AdminController {}
|
|
1535
|
-
```
|
|
1536
|
-
|
|
1537
|
-
#### Validation
|
|
1538
|
-
|
|
1539
|
-
**NestJS:**
|
|
1540
|
-
```typescript
|
|
1541
|
-
import { IsString, IsEmail, IsNumber } from 'class-validator';
|
|
1542
|
-
|
|
1543
|
-
export class CreateUserDto {
|
|
1544
|
-
@IsString()
|
|
1545
|
-
name: string;
|
|
1546
|
-
|
|
1547
|
-
@IsEmail()
|
|
1548
|
-
email: string;
|
|
1549
|
-
|
|
1550
|
-
@IsNumber()
|
|
1551
|
-
age: number;
|
|
1552
|
-
}
|
|
1553
|
-
```
|
|
1554
|
-
|
|
1555
|
-
**Shokupan:**
|
|
1556
|
-
```typescript
|
|
1557
|
-
import { z } from 'zod';
|
|
1558
|
-
import { validate } from 'shokupan';
|
|
1559
|
-
|
|
1560
|
-
const createUserSchema = z.object({
|
|
1561
|
-
name: z.string(),
|
|
1562
|
-
email: z.string().email(),
|
|
1563
|
-
age: z.number()
|
|
1564
|
-
});
|
|
1565
|
-
|
|
1566
|
-
@Post('/')
|
|
1567
|
-
@Use(validate({ body: createUserSchema }))
|
|
1568
|
-
createUser(@Body() body: any) {
|
|
1569
|
-
return { created: body };
|
|
1570
|
-
}
|
|
1571
|
-
```
|
|
1572
|
-
|
|
1573
|
-
#### Key Differences
|
|
1574
|
-
|
|
1575
|
-
1. **Lighter DI**: Manual registration vs automatic
|
|
1576
|
-
2. **Middleware over Guards**: Use middleware pattern instead of guards
|
|
1577
|
-
3. **Validation Libraries**: Use Zod/Ajv/TypeBox instead of class-validator
|
|
1578
|
-
4. **Module System**: No modules, simpler structure
|
|
1579
|
-
5. **Less Boilerplate**: More straightforward setup
|
|
1580
|
-
|
|
1581
|
-
### Using Express Middleware
|
|
1582
|
-
|
|
1583
|
-
Many Express middleware packages work with Shokupan:
|
|
1584
|
-
|
|
1585
|
-
```typescript
|
|
1586
|
-
import { Shokupan, useExpress } from 'shokupan';
|
|
1587
|
-
import helmet from 'helmet';
|
|
1588
|
-
import compression from 'compression';
|
|
1589
|
-
|
|
1590
|
-
const app = new Shokupan();
|
|
1591
|
-
|
|
1592
|
-
// Use Express middleware
|
|
1593
|
-
app.use(useExpress(helmet()));
|
|
1594
|
-
app.use(useExpress(compression()));
|
|
1595
|
-
```
|
|
1596
|
-
|
|
1597
|
-
**Note**: While many Express middleware will work, native Shokupan plugins are recommended for better performance and TypeScript support.
|
|
1598
|
-
|
|
1599
|
-
## 🧪 Testing
|
|
1600
|
-
|
|
1601
|
-
Shokupan applications are easy to test using Bun's built-in test runner.
|
|
1602
|
-
It can directly pass requests to the application or a router without requiring the
|
|
1603
|
-
server to fully start, bind to a port or listen for connections. This makes mocking
|
|
1604
|
-
the server unnecessary and allows for faster and more reliable testing. Additionally
|
|
1605
|
-
you can directly test authenticated endpoints without the need for a session or cookie.
|
|
1606
|
-
|
|
1607
|
-
```typescript
|
|
1608
|
-
import { describe, it, expect } from 'bun:test';
|
|
1609
|
-
import { Shokupan } from 'shokupan';
|
|
1610
|
-
|
|
1611
|
-
describe('My App', () => {
|
|
1612
|
-
it('should return hello world', async () => {
|
|
1613
|
-
const app = new Shokupan();
|
|
1614
|
-
|
|
1615
|
-
app.get('/', () => ({ message: 'Hello' }));
|
|
1616
|
-
|
|
1617
|
-
// Process a request without starting the server
|
|
1618
|
-
const res = await app.testRequest({
|
|
1619
|
-
method: 'GET',
|
|
1620
|
-
path: '/'
|
|
1621
|
-
});
|
|
1622
|
-
|
|
1623
|
-
expect(res.status).toBe(200);
|
|
1624
|
-
expect(res.data).toEqual({ message: 'Hello' });
|
|
1625
|
-
});
|
|
1626
|
-
});
|
|
1627
|
-
```
|
|
1628
|
-
|
|
1629
|
-
## 🚢 Deployment
|
|
1630
|
-
|
|
1631
|
-
Since Shokupan is built on Bun, deployment is straightforward.
|
|
1632
|
-
|
|
1633
|
-
### Using Bun
|
|
1634
|
-
|
|
1635
|
-
```bash
|
|
1636
|
-
bun run src/main.ts
|
|
1637
|
-
```
|
|
1638
|
-
|
|
1639
|
-
### Docker
|
|
1640
|
-
|
|
1641
|
-
```dockerfile
|
|
1642
|
-
FROM oven/bun:1-alpine
|
|
1643
|
-
|
|
1644
|
-
WORKDIR /app
|
|
1645
|
-
|
|
1646
|
-
COPY . .
|
|
1647
|
-
RUN bun install --production
|
|
1648
|
-
|
|
1649
|
-
EXPOSE 3000
|
|
1650
|
-
|
|
1651
|
-
CMD ["bun", "run", "src/main.ts"]
|
|
1652
|
-
```
|
|
1653
|
-
|
|
1654
|
-
## 🛠️ CLI Tools
|
|
1655
|
-
|
|
1656
|
-
Shokupan includes a CLI for scaffolding:
|
|
1657
|
-
|
|
1658
|
-
```bash
|
|
1659
|
-
# Install globally
|
|
1660
|
-
bun add -g shokupan
|
|
1661
|
-
|
|
1662
|
-
# Or use with bunx
|
|
1663
|
-
bunx shokupan
|
|
1664
|
-
```
|
|
1665
|
-
|
|
1666
|
-
### Generate Controller
|
|
1667
|
-
|
|
1668
|
-
```bash
|
|
1669
|
-
shokupan generate controller User
|
|
1670
|
-
# or
|
|
1671
|
-
skp g controller User
|
|
1672
|
-
```
|
|
1673
|
-
|
|
1674
|
-
Generates:
|
|
1675
|
-
```typescript
|
|
1676
|
-
import { Controller, Get, Post, Put, Delete, Param, Body } from 'shokupan';
|
|
1677
|
-
|
|
1678
|
-
@Controller('/user')
|
|
1679
|
-
export class UserController {
|
|
1680
|
-
|
|
1681
|
-
@Get('/')
|
|
1682
|
-
async getAll() {
|
|
1683
|
-
return { users: [] };
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
@Get('/:id')
|
|
1687
|
-
async getById(@Param('id') id: string) {
|
|
1688
|
-
return { id };
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
@Post('/')
|
|
1692
|
-
async create(@Body() body: any) {
|
|
1693
|
-
return { created: body };
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
@Put('/:id')
|
|
1697
|
-
async update(@Param('id') id: string, @Body() body: any) {
|
|
1698
|
-
return { id, updated: body };
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
@Delete('/:id')
|
|
1702
|
-
async delete(@Param('id') id: string) {
|
|
1703
|
-
return { id, deleted: true };
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
```
|
|
1707
|
-
|
|
1708
|
-
### Generate Middleware
|
|
1709
|
-
|
|
1710
|
-
```bash
|
|
1711
|
-
shokupan generate middleware auth
|
|
1712
|
-
# or
|
|
1713
|
-
skp g middleware auth
|
|
1714
|
-
```
|
|
1715
|
-
|
|
1716
|
-
### Generate Plugin
|
|
1717
|
-
|
|
1718
|
-
```bash
|
|
1719
|
-
shokupan generate plugin custom
|
|
1720
|
-
# or
|
|
1721
|
-
skp g plugin custom
|
|
1722
|
-
```
|
|
1723
|
-
|
|
1724
|
-
## 📚 API Reference
|
|
1725
|
-
|
|
1726
|
-
### Shokupan Class
|
|
1727
|
-
|
|
1728
|
-
Main application class.
|
|
1729
|
-
|
|
1730
|
-
```typescript
|
|
1731
|
-
const app = new Shokupan(config?: ShokupanConfig);
|
|
1732
|
-
```
|
|
1733
|
-
|
|
1734
|
-
**Config Options:**
|
|
1735
|
-
- `port?: number` - Port to listen on (default: 3000)
|
|
1736
|
-
- `hostname?: string` - Hostname (default: "localhost")
|
|
1737
|
-
- `development?: boolean` - Development mode (default: auto-detect)
|
|
1738
|
-
- `enableAsyncLocalStorage?: boolean` - Enable async context tracking
|
|
1739
|
-
- `enableTracing?: boolean` - Enable OpenTelemetry tracing
|
|
1740
|
-
- `enableOpenApiGen?: boolean` - Enable OpenAPI spec generation (default: true)
|
|
1741
|
-
- `controllersOnly?: boolean` - If true, only allows controllers, disabling app.get/post/etc (default: false)
|
|
1742
|
-
- `requestTimeout?: number` - Global request timeout (ms)
|
|
1743
|
-
- `readTimeout?: number` - Request body read timeout (ms)
|
|
1744
|
-
- `serverFactory?: ServerFactory` - Custom server factory (for Node.js/Deno support)
|
|
1745
|
-
- `autoBackpressureFeedback?: boolean` - Enable automatic load shedding based on CPU usage
|
|
1746
|
-
- `autoBackpressureLevel?: number` - CPU usage % threshold for backpressure (default: 60)
|
|
1747
|
-
- `logger?: Logger` - Custom logger instance
|
|
1748
|
-
|
|
1749
|
-
**Methods:**
|
|
1750
|
-
- `add({ method, path, spec, handler, regex, group)` - Add a route with any HTTP method.
|
|
1751
|
-
- `get(path, spec?, ...handlers)` - Add GET route
|
|
1752
|
-
- `post(path, spec?, ...handlers)` - Add POST route
|
|
1753
|
-
- `put(path, spec?, ...handlers)` - Add PUT route
|
|
1754
|
-
- `patch(path, spec?, ...handlers)` - Add PATCH route
|
|
1755
|
-
- `delete(path, spec?, ...handlers)` - Add DELETE route
|
|
1756
|
-
- `options(path, spec?, ...handlers)` - Add OPTIONS route
|
|
1757
|
-
- `head(path, spec?, ...handlers)` - Add HEAD route
|
|
1758
|
-
- `use(middleware)` - Add middleware
|
|
1759
|
-
- `mount(path, controller)` - Mount controller or router
|
|
1760
|
-
- `static(path, options)` - Serve static files
|
|
1761
|
-
- `listen(port?)` - Start server
|
|
1762
|
-
- `testRequest(options)` - Process request (for testing purposes)
|
|
1763
|
-
- `internalRequest(options)` - Make sub-request
|
|
1764
|
-
- `computeOpenAPISpec(base)` - Generate OpenAPI spec
|
|
1765
|
-
|
|
1766
|
-
### ShokupanRouter Class
|
|
1767
|
-
|
|
1768
|
-
Router for grouping operations, applying middleware, and mounting controllers. Additionally
|
|
1769
|
-
they are effective for creating sub-applications that are independently tested. Routers can
|
|
1770
|
-
have OpenAPI spec applied to all endpoints of the router. Additionally they can be mounted
|
|
1771
|
-
onto the main application or other routers.
|
|
1772
|
-
|
|
1773
|
-
When a router is mounted to an app, if you are using the DebugView plugin you will be able to
|
|
1774
|
-
see it under the Registry tab and the Graph tab.
|
|
1775
|
-
|
|
1776
|
-
```typescript
|
|
1777
|
-
const router = new ShokupanRouter(config?: ShokupanRouteConfig);
|
|
1778
|
-
```
|
|
1779
|
-
|
|
1780
|
-
**Config Options:**
|
|
1781
|
-
- `name?: string` - Name of the router
|
|
1782
|
-
- `group?: string` - Group of the router
|
|
1783
|
-
- `openapi?: boolean` - OpenAPI spec applied to all endpoints of the router
|
|
1784
|
-
|
|
1785
|
-
**Methods:**
|
|
1786
|
-
- `add({ method, path, spec, handler, regex, group)` - Add a route with any HTTP method.
|
|
1787
|
-
- `get(path, spec?, ...handlers)` - Add GET route
|
|
1788
|
-
- `post(path, spec?, ...handlers)` - Add POST route
|
|
1789
|
-
- `put(path, spec?, ...handlers)` - Add PUT route
|
|
1790
|
-
- `patch(path, spec?, ...handlers)` - Add PATCH route
|
|
1791
|
-
- `delete(path, spec?, ...handlers)` - Add DELETE route
|
|
1792
|
-
- `options(path, spec?, ...handlers)` - Add OPTIONS route
|
|
1793
|
-
- `head(path, spec?, ...handlers)` - Add HEAD route
|
|
1794
|
-
- `mount(path, controller)` - Mount controller or router
|
|
1795
|
-
- `static(path, options)` - Serve static files
|
|
1796
|
-
- `testRequest(options)` - Process request (for testing purposes)
|
|
1797
|
-
- `internalRequest(options)` - Make sub-request
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
### ShokupanContext
|
|
1801
|
-
|
|
1802
|
-
Request context object.
|
|
1803
|
-
|
|
1804
|
-
**Properties:**
|
|
1805
|
-
- `req: Request` - Request object
|
|
1806
|
-
- `method: string` - HTTP method
|
|
1807
|
-
- `path: string` - URL path
|
|
1808
|
-
- `url: URL` - Full URL
|
|
1809
|
-
- `params: Record<string, string>` - Path parameters
|
|
1810
|
-
- `query: URLSearchParams` - Query parameters
|
|
1811
|
-
- `headers: Headers` - Request headers
|
|
1812
|
-
- `state: Record<string, any>` - Shared state object
|
|
1813
|
-
- `session: any` - Session data (with session plugin)
|
|
1814
|
-
- `response: ShokupanResponse` - Response builder
|
|
1815
|
-
- `ip: string` - Client IP address
|
|
1816
|
-
- `hostname: string` - Hostname (e.g. "localhost")
|
|
1817
|
-
- `host: string` - Host (e.g. "localhost:3000")
|
|
1818
|
-
- `protocol: string` - Protocol (http/https)
|
|
1819
|
-
- `secure: boolean` - Whether the request is secure over HTTPS
|
|
1820
|
-
- `origin: string` - Origin URL
|
|
1821
|
-
- `signal: AbortSignal` - Request abort signal (for standard fetch requests)
|
|
1822
|
-
|
|
1823
|
-
**Methods:**
|
|
1824
|
-
- `set(name: string, value: string): ShokupanContext` - Set a response header
|
|
1825
|
-
- `setCookie(name: string, value: string, options?: CookieOptions): ShokupanContext` - Set a response cookie
|
|
1826
|
-
- `send(body?: BodyInit, options?: ResponseInit): Response` - Return response
|
|
1827
|
-
- `status(code: number): Response` - Return status code default response
|
|
1828
|
-
- `body(): Promise<any>` - Parse request body
|
|
1829
|
-
- `json(data: any, status?: number): ShokupanContext` - Return JSON response
|
|
1830
|
-
- `text(data: string, status?: number): ShokupanContext` - Return text response
|
|
1831
|
-
- `html(data: string, status?: number): ShokupanContext` - Return HTML response
|
|
1832
|
-
- `jsx(element: any): ShokupanContext` - Render a JSX element
|
|
1833
|
-
- `redirect(url: string, status?: number): ShokupanContext` - Redirect response
|
|
1834
|
-
- `file(path: string, fileOptions?: BlobPropertyBag, responseOptions?: ResponseInit): Response` - Return file response
|
|
1835
|
-
|
|
1836
|
-
### Container
|
|
1837
|
-
|
|
1838
|
-
Dependency injection container. This feature is still experimental and subject to change.
|
|
1839
|
-
|
|
1840
|
-
```typescript
|
|
1841
|
-
Container.register(name: string, classOrFactory: any);
|
|
1842
|
-
Container.resolve<T>(name: string): T;
|
|
1843
|
-
Container.clear();
|
|
1844
|
-
```
|
|
1845
|
-
|
|
1846
|
-
## 🗺️ Roadmap
|
|
1847
|
-
|
|
1848
|
-
### Current Features
|
|
1849
|
-
|
|
1850
|
-
- ✅ **Built for Bun** - Native performance
|
|
1851
|
-
- ✅ **Express Ecosystem** - Middleware support
|
|
1852
|
-
- ✅ **TypeScript First** - Decorators, Generics, Type Safety
|
|
1853
|
-
- ✅ **Auto OpenAPI** - [Scalar](https://github.com/scalar/scalar) documentation
|
|
1854
|
-
- ✅ **Rich Plugin System** - CORS, Session, Validation, Rate Limiting etc.
|
|
1855
|
-
- ✅ **Dependency Injection** - Container for dependency injection
|
|
1856
|
-
- ✅ **OpenTelemetry** - Built-in [OpenTelemetry](https://opentelemetry.io/) traces
|
|
1857
|
-
- ✅ **OAuth2** - Built-in [OAuth2](https://oauth.net/2/) support
|
|
1858
|
-
- ✅ **Request-Scoped Globals** - Request-scoped values via [AsyncLocalStorage](https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage)
|
|
1859
|
-
- ✅ **Runtime Compatibility** - Support for [Deno](https://deno.com/) and [Node.js](https://nodejs.org/)
|
|
1860
|
-
- ✅ **Deep Introspection** - Type analysis for enhanced OpenAPI generation
|
|
1861
|
-
- ✅ **Controller Mode** - Option for controller-only mode
|
|
1862
|
-
- ✅ **Supports Node/Deno** - Shokupan can run on Node.js or Deno
|
|
1863
|
-
- ✅ **OpenAPI Validation** - Built-in [OpenAPI](https://www.openapis.org/) validation
|
|
92
|
+
- **[Dependency Injection](https://shokupan.dev/guides/advanced)**: Built-in container for managing dependencies.
|
|
93
|
+
- **[OpenAPI Generation](https://shokupan.dev/core/controllers)**: Auto-generate specs from your code.
|
|
94
|
+
- **[Sub-Requests](https://shokupan.dev/core/routing)**: Make internal requests without HTTP overhead.
|
|
95
|
+
- **[OpenTelemetry](https://shokupan.dev/guides/production)**: Built-in distributed tracing.
|
|
96
|
+
- **[Type Augmentation](https://shokupan.dev/guides/global-type-augmentation)**: Extend global types for type-safety.
|
|
1864
97
|
|
|
1865
|
-
|
|
98
|
+
## 📚 Guides & Reference
|
|
1866
99
|
|
|
1867
|
-
-
|
|
1868
|
-
-
|
|
1869
|
-
-
|
|
1870
|
-
-
|
|
1871
|
-
-
|
|
1872
|
-
-
|
|
1873
|
-
- 🚧 **Standardized Errors** - Consistent 4xx/5xx error formats
|
|
100
|
+
- **[Migration Guides](https://shokupan.dev/migration)**: Detailed guides for migrating from Express, Koa, or NestJS.
|
|
101
|
+
- **[Testing](https://shokupan.dev/guides/testing)**: How to test your Shokupan application.
|
|
102
|
+
- **[Deployment](https://shokupan.dev/guides/deployment)**: Deploying to Bun, Docker, and more.
|
|
103
|
+
- **[CLI Reference](https://shokupan.dev/guides/cli)**: Documentation for the Shokupan CLI.
|
|
104
|
+
- **[API Reference](https://shokupan.dev/api)**: Complete API documentation.
|
|
105
|
+
- **[Roadmap](https://shokupan.dev/reference/roadmap)**: Future plans and features.
|
|
1874
106
|
|
|
1875
107
|
## 🤝 Contributing
|
|
1876
108
|
|