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