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 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**