@burger-api/cli 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1120 +1,1116 @@
1
- /**
2
- * Template Management System
3
- *
4
- * Handles downloading and caching project templates.
5
- * Templates are the starter projects users get when running `burger-api create`
6
- *
7
- */
8
-
9
- import { join } from 'path';
10
-
11
- import type { CreateOptions } from '../types/index';
12
- import { spinner } from './logger';
13
- import { downloadFile } from './github';
14
-
15
- /**
16
- * Generate package.json content for a new project
17
- * This includes the burger-api dependency and basic scripts
18
- *
19
- * @param projectName - Name of the project
20
- * @returns package.json content as a string
21
- */
22
- export function generatePackageJson(projectName: string): string {
23
- const packageJson = {
24
- name: projectName,
25
- version: '0.1.0',
26
- type: 'module',
27
- scripts: {
28
- dev: 'bun --watch src/index.ts',
29
- start: 'bun src/index.ts',
30
- build: 'bun build src/index.ts --outdir ./dist',
31
- },
32
- dependencies: {
33
- 'burger-api': '^0.6.2',
34
- },
35
- devDependencies: {
36
- '@types/bun': 'latest',
37
- typescript: '^5',
38
- },
39
- };
40
-
41
- return JSON.stringify(packageJson, null, 2);
42
- }
43
-
44
- /**
45
- * Generate tsconfig.json content for a new project
46
- * This sets up TypeScript properly for Bun
47
- *
48
- * @returns tsconfig.json content as a string
49
- */
50
- export function generateTsConfig(): string {
51
- const tsconfig = {
52
- compilerOptions: {
53
- lib: ['ESNext'],
54
- target: 'ESNext',
55
- module: 'ESNext',
56
- moduleDetection: 'force',
57
- jsx: 'react-jsx',
58
- allowJs: true,
59
-
60
- // Best practices for type safety
61
- strict: true,
62
- noUncheckedIndexedAccess: true,
63
- noImplicitOverride: true,
64
-
65
- // Module resolution for Bun
66
- moduleResolution: 'bundler',
67
- allowImportingTsExtensions: true,
68
- verbatimModuleSyntax: true,
69
- noEmit: true,
70
-
71
- // Interop
72
- allowSyntheticDefaultImports: true,
73
- esModuleInterop: true,
74
- forceConsistentCasingInFileNames: true,
75
-
76
- // Skip type checking for dependencies
77
- skipLibCheck: true,
78
-
79
- // Types
80
- types: ['bun-types'],
81
- },
82
- };
83
-
84
- return JSON.stringify(tsconfig, null, 2);
85
- }
86
-
87
- /**
88
- * Generate .gitignore content
89
- *
90
- * @returns .gitignore content as a string
91
- */
92
- export function generateGitIgnore(): string {
93
- return `# Bun
94
- node_modules/
95
- bun.lockb
96
- .env*
97
-
98
- # Build output
99
- dist/
100
- .build/
101
- *.exe
102
-
103
- # OS files
104
- .DS_Store
105
- Thumbs.db
106
-
107
- # Editor
108
- .vscode/
109
- .idea/
110
- *.swp
111
- *.swo
112
- `;
113
- }
114
-
115
- /**
116
- * Generate .prettierrc content
117
- * This matches the burger-api project style
118
- *
119
- * @returns .prettierrc content as a string
120
- */
121
- export function generatePrettierConfig(): string {
122
- const prettierConfig = {
123
- semi: true,
124
- singleQuote: true,
125
- tabWidth: 4,
126
- trailingComma: 'es5',
127
- printWidth: 80,
128
- arrowParens: 'always',
129
- };
130
-
131
- return JSON.stringify(prettierConfig, null, 2);
132
- }
133
-
134
- /**
135
- * Generate index.ts content based on user options
136
- * This is the main entry point for the user's project
137
- *
138
- * @param options - Project configuration from user prompts
139
- * @returns index.ts content as a string
140
- */
141
- export function generateIndexFile(options: CreateOptions): string {
142
- const lines: string[] = [];
143
-
144
- // Import statement
145
- lines.push("import { Burger } from 'burger-api';");
146
- lines.push('');
147
-
148
- // Configuration object
149
- lines.push('const app = new Burger({');
150
-
151
- if (options.useApi) {
152
- lines.push(` apiDir: './src/${options.apiDir || 'api'}',`);
153
- if (options.apiPrefix && options.apiPrefix !== '/api') {
154
- lines.push(` apiPrefix: '${options.apiPrefix}',`);
155
- }
156
- }
157
-
158
- if (options.usePages) {
159
- lines.push(` pageDir: './src/${options.pageDir || 'pages'}',`);
160
- if (options.pagePrefix && options.pagePrefix !== '/') {
161
- lines.push(` pagePrefix: '${options.pagePrefix}',`);
162
- }
163
- }
164
-
165
- if (options.debug) {
166
- lines.push(' debug: true,');
167
- }
168
-
169
- lines.push(' globalMiddleware: [],');
170
- lines.push('});');
171
- lines.push('');
172
-
173
- // Start server - uses PORT env variable for flexibility (e.g., burger-api serve --port 4000)
174
- lines.push('const port = Number(process.env.PORT) || 4000;');
175
- lines.push('app.serve(port, () => {');
176
- lines.push(
177
- ' console.log(`Server running on http://localhost:${port}`);'
178
- );
179
- lines.push('});');
180
-
181
- return lines.join('\n');
182
- }
183
-
184
- /**
185
- * Generate a comprehensive API route file with full examples
186
- * Includes: OpenAPI metadata, Zod schemas, all HTTP methods, middleware
187
- * Every line has beginner-friendly comments explaining what it does
188
- *
189
- * @returns route.ts content as a string
190
- */
191
- export function generateApiRoute(): string {
192
- return `/**
193
- * =============================================================================
194
- * BURGER API - EXAMPLE ROUTE FILE
195
- * =============================================================================
196
- *
197
- * This file shows you everything you can do with BurgerAPI routes!
198
- *
199
- * KEY CONCEPTS:
200
- * - This file is automatically loaded because it's named "route.ts"
201
- * - The folder path becomes the URL path (e.g., /api/route.ts → /api)
202
- * - Export functions named after HTTP methods: GET, POST, PUT, DELETE, etc.
203
- *
204
- * =============================================================================
205
- */
206
-
207
-
208
- import { z } from 'zod';
209
- import type { BurgerRequest, Middleware, BurgerNext } from 'burger-api';
210
-
211
-
212
- /*
213
- -----------------------------------------------------------------------------
214
- OPENAPI METADATA (Optional but recommended!)
215
- -----------------------------------------------------------------------------
216
-
217
- - This creates automatic documentation for your API!
218
- - Visit /docs in your browser to see beautiful Swagger UI documentation.
219
- - Each HTTP method (get, post, put, delete) can have its own documentation.
220
- -----------------------------------------------------------------------------
221
- */
222
- export const openapi = {
223
- // Documentation for the GET method
224
- get: {
225
- // 'summary' - A short title shown in the docs (keep it brief!)
226
- summary: 'Get all items',
227
-
228
- // 'description' - A longer explanation of what this endpoint does
229
- description: 'Fetches a list of items. You can filter results using query parameters.',
230
-
231
- // 'tags' - Groups related endpoints together in the docs
232
- // All endpoints with the same tag appear in the same section
233
- tags: ['Items'],
234
-
235
- // 'operationId' - A unique ID for this endpoint (useful for code generation)
236
- operationId: 'getItems',
237
-
238
- // 'responses' - Documents what responses the endpoint can return
239
- responses: {
240
- '200': { description: 'Successfully retrieved items' },
241
- '400': { description: 'Invalid query parameters' },
242
- },
243
- },
244
-
245
- // Documentation for the POST method
246
- post: {
247
- summary: 'Create a new item',
248
- description: 'Creates a new item with the provided data. Returns the created item.',
249
- tags: ['Items'],
250
- operationId: 'createItem',
251
- responses: {
252
- '201': { description: 'Item created successfully' },
253
- '400': { description: 'Invalid request body' },
254
- },
255
- },
256
-
257
- // Documentation for the PUT method
258
- put: {
259
- summary: 'Update an item',
260
- description: 'Updates an existing item. Provide the item ID in the query string.',
261
- tags: ['Items'],
262
- operationId: 'updateItem',
263
- responses: {
264
- '200': { description: 'Item updated successfully' },
265
- '400': { description: 'Invalid request data' },
266
- '404': { description: 'Item not found' },
267
- },
268
- },
269
-
270
- // Documentation for the DELETE method
271
- delete: {
272
- summary: 'Delete an item',
273
- description: 'Permanently deletes an item by ID.',
274
- tags: ['Items'],
275
- operationId: 'deleteItem',
276
- responses: {
277
- '200': { description: 'Item deleted successfully' },
278
- '404': { description: 'Item not found' },
279
- },
280
- },
281
- };
282
-
283
-
284
- /*
285
- -----------------------------------------------------------------------------
286
- SCHEMA VALIDATION (Using Zod)
287
- -----------------------------------------------------------------------------
288
-
289
- - Schemas define what data your API accepts. BurgerAPI automatically:
290
- - Validates incoming data against these schemas
291
- - Returns a 400 error if validation fails
292
- - Puts the validated data in req.validated for you to use
293
-
294
- - You can validate:
295
- - 'query' → URL query parameters like ?search=hello&page=1
296
- - 'body' → Request body (for POST/PUT requests)
297
- - 'params' → URL parameters like /items/[id] → { id: "123" }
298
- -----------------------------------------------------------------------------
299
- */
300
- export const schema = {
301
- // Schema for GET requests - validates query parameters
302
- get: {
303
- // 'query' - Validates the URL query string
304
- // Example URL: /api?search=burger&limit=10&page=2
305
- query: z.object({
306
- // 'search' - Optional text to search for
307
- // .optional() means this field isn't required
308
- search: z.string().optional(),
309
-
310
- // 'limit' - How many items to return (default: 10)
311
- // .coerce.number() converts string "10" to number 10
312
- // .min(1) means it must be at least 1
313
- // .max(100) means it can't be more than 100
314
- // .default(10) uses 10 if not provided
315
- limit: z.coerce.number().min(1).max(100).default(10),
316
-
317
- // 'page' - Which page of results to return
318
- page: z.coerce.number().min(1).default(1),
319
- }),
320
- },
321
-
322
- // Schema for POST requests - validates the request body
323
- post: {
324
- // 'body' - Validates JSON data sent in the request body
325
- body: z.object({
326
- // 'name' - Required, must be at least 1 character
327
- // .min(1, '...') shows a custom error message if too short
328
- name: z.string().min(1, 'Name is required'),
329
-
330
- // 'description' - Optional text field
331
- description: z.string().optional(),
332
-
333
- // 'price' - Required, must be a positive number
334
- // .positive() ensures the number is greater than 0
335
- price: z.number().positive('Price must be greater than 0'),
336
-
337
- // 'category' - Must be one of these specific values
338
- // .enum() only allows the listed values
339
- category: z.enum(['food', 'drink', 'dessert']),
340
-
341
- // 'isAvailable' - Optional boolean, defaults to true
342
- isAvailable: z.boolean().default(true),
343
- }),
344
- },
345
-
346
- // Schema for PUT requests - validates both query and body
347
- put: {
348
- // Which item to update (ID in query string)
349
- query: z.object({
350
- id: z.string().min(1, 'Item ID is required'),
351
- }),
352
-
353
- // What to update (in the request body)
354
- // .partial() makes all fields optional (for partial updates)
355
- body: z.object({
356
- name: z.string().min(1),
357
- description: z.string(),
358
- price: z.number().positive(),
359
- category: z.enum(['food', 'drink', 'dessert']),
360
- isAvailable: z.boolean(),
361
- }).partial(), // .partial() = all fields become optional
362
- },
363
-
364
- // Schema for DELETE requests - validates query parameters
365
- delete: {
366
- query: z.object({
367
- id: z.string().min(1, 'Item ID is required'),
368
- }),
369
- },
370
- };
371
-
372
-
373
- /*
374
- -----------------------------------------------------------------------------
375
- ROUTE-SPECIFIC MIDDLEWARE (Optional)
376
- -----------------------------------------------------------------------------
377
-
378
- - Middleware runs BEFORE your route handler. Use it for:
379
- - Logging requests
380
- - Checking authentication
381
- - Modifying the request
382
- - Blocking unauthorized access
383
-
384
- - Return 'undefined' to continue to the next middleware/handler
385
- - Return a 'Response' to stop and send that response immediately
386
- -----------------------------------------------------------------------------
387
- */
388
- export const middleware: Middleware[] = [
389
- // Example: Log every request to this route
390
- async (req: BurgerRequest): Promise<BurgerNext> => {
391
- console.log(\`[\${new Date().toISOString()}] \${req.method} \${req.url}\`);
392
-
393
- // Return undefined to continue to the next middleware/handler
394
- // If you return a Response here, it stops and sends that response
395
- return undefined;
396
- },
397
- ];
398
-
399
- /*
400
- -----------------------------------------------------------------------------
401
- HTTP HANDLERS
402
- -----------------------------------------------------------------------------
403
-
404
- - These functions handle the actual requests. They receive:
405
- - req: The request object with validated data in req.validated
406
- - They must return a Response object. Use Response.json() for JSON responses.
407
- -----------------------------------------------------------------------------
408
- */
409
-
410
- /**
411
- * GET - Fetch items with optional filtering
412
- *
413
- * Example requests:
414
- * - GET /api → Get first 10 items
415
- * - GET /api?limit=5 → Get first 5 items
416
- * - GET /api?search=burger&page=2 → Search for "burger", page 2
417
- */
418
- export async function GET(req: BurgerRequest<{ query: z.infer<typeof schema.get.query> }>) {
419
- // Access validated query parameters from the schema
420
- const { search, limit, page } = req.validated.query;
421
-
422
- // Mock data (replace with your database query)
423
- const mockItems = [
424
- { id: '1', name: 'Classic Burger', price: 9.99, category: 'food' },
425
- { id: '2', name: 'Cheese Burger', price: 11.99, category: 'food' },
426
- { id: '3', name: 'Cola', price: 2.99, category: 'drink' },
427
- ];
428
-
429
- // Filter items if search is provided
430
- let items = mockItems;
431
- if (search) {
432
- items = items.filter(item =>
433
- item.name.toLowerCase().includes(search.toLowerCase())
434
- );
435
- }
436
-
437
- // Calculate pagination
438
- const startIndex = (page - 1) * limit;
439
- const paginatedItems = items.slice(startIndex, startIndex + limit);
440
-
441
- // Return JSON response with status 200 (default)
442
- return Response.json({
443
- success: true,
444
- data: paginatedItems,
445
- pagination: {
446
- page,
447
- limit,
448
- total: items.length,
449
- totalPages: Math.ceil(items.length / limit),
450
- },
451
- });
452
- }
453
-
454
- /**
455
- * POST - Create a new item
456
- *
457
- * Example request body:
458
- * {
459
- * "name": "Veggie Burger",
460
- * "description": "Delicious plant-based burger",
461
- * "price": 12.99,
462
- * "category": "food"
463
- * }
464
- */
465
- export async function POST(req: BurgerRequest<{ body: z.infer<typeof schema.post.body> }>) {
466
- // Get validated body data - already checked by Zod schema!
467
- const { name, description, price, category, isAvailable } = req.validated.body;
468
-
469
- // Create the item (replace with your database insert)
470
- const newItem = {
471
- id: crypto.randomUUID(), // Generate unique ID
472
- name,
473
- description: description || null,
474
- price,
475
- category,
476
- isAvailable,
477
- createdAt: new Date().toISOString(),
478
- };
479
-
480
- // Return the created item with status 201 (Created)
481
- return Response.json({
482
- success: true,
483
- message: 'Item created successfully',
484
- data: newItem,
485
- }, { status: 201 });
486
- }
487
-
488
- /**
489
- * PUT - Update an existing item
490
- *
491
- * Example: PUT /api?id=123
492
- * Body: { "name": "Updated Name", "price": 15.99 }
493
- */
494
- export async function PUT(req: BurgerRequest<{ query: z.infer<typeof schema.put.query>, body: z.infer<typeof schema.put.body> }>) {
495
- // Get the item ID from query parameters
496
- const { id } = req.validated.query;
497
-
498
- // Get the fields to update from the request body
499
- const updates = req.validated.body;
500
-
501
- // Find and update the item (replace with your database update)
502
- // Here we're just simulating an update
503
- const updatedItem = {
504
- id,
505
- ...updates,
506
- updatedAt: new Date().toISOString(),
507
- };
508
-
509
- return Response.json({
510
- success: true,
511
- message: 'Item updated successfully',
512
- data: updatedItem,
513
- });
514
- }
515
-
516
- /**
517
- * DELETE - Remove an item
518
- *
519
- * Example: DELETE /api?id=123
520
- */
521
- export async function DELETE(req: BurgerRequest<{ query: z.infer<typeof schema.delete.query> }>) {
522
- // Get the item ID from query parameters
523
- const { id } = req.validated.query;
524
-
525
- // Delete the item (replace with your database delete)
526
- // Here we're just returning a success message
527
- return Response.json({
528
- success: true,
529
- message: \`Item \${id} deleted successfully\`,
530
- });
531
- }
532
- `;
533
- }
534
-
535
- /**
536
- * Generate a CSS file with modern styling for the landing page
537
- *
538
- * @returns style.css content as a string
539
- */
540
- export function generateSampleCss(): string {
541
- return `
542
- :root {
543
- --color-primary: hsl(30, 75%, 90%);
544
- --color-primary-dark: hsl(30, 75%, 80%);
545
- --color-bg: #09090b;
546
- --color-surface: hsl(240, 10%, 3.9%);
547
- --color-border: hsl(240, 3.7%, 15.9%);
548
- --color-success: hsl(120, 50%, 40%);
549
- --color-text-muted: hsl(240, 5%, 50%);
550
- }
551
-
552
- * {
553
- margin: 0;
554
- padding: 0;
555
- box-sizing: border-box;
556
- }
557
-
558
- body {
559
- font-family: 'Poppins', system-ui, sans-serif;
560
- min-height: 100vh;
561
- background: var(--color-bg);
562
- color: #fff;
563
- display: flex;
564
- flex-direction: column;
565
- align-items: center;
566
- padding: 60px 20px 40px;
567
- }
568
-
569
- .hero {
570
- text-align: center;
571
- max-width: 600px;
572
- margin-bottom: 48px;
573
- }
574
-
575
- .logo-wrapper {
576
- display: flex;
577
- flex-wrap: wrap;
578
- margin-bottom: 32px;
579
- }
580
-
581
- .logo {
582
- width: 80px;
583
- height: 80px;
584
- }
585
-
586
- .logo-text {
587
- font-size: 3.5rem;
588
- font-weight: 600;
589
- color: var(--color-primary);
590
- }
591
-
592
- h1 {
593
- font-size: 2.5rem;
594
- font-weight: 600;
595
- margin-bottom: 12px;
596
- color: #fff;
597
- }
598
-
599
- .subtitle {
600
- color: var(--color-text-muted);
601
- font-size: 1.1rem;
602
- margin-bottom: 24px;
603
- }
604
-
605
- .status {
606
- display: inline-flex;
607
- align-items: center;
608
- gap: 8px;
609
- background: hsla(120, 50%, 40%, 0.1);
610
- border: 1px solid hsla(120, 50%, 40%, 0.3);
611
- padding: 8px 16px;
612
- border-radius: 20px;
613
- font-size: 0.875rem;
614
- color: var(--color-success);
615
- }
616
-
617
- .status::before {
618
- content: '';
619
- width: 8px;
620
- height: 8px;
621
- background: var(--color-success);
622
- border-radius: 50%;
623
- animation: pulse 2s infinite;
624
- }
625
-
626
- @keyframes pulse {
627
- 0%, 100% { opacity: 1; }
628
- 50% { opacity: 0.5; }
629
- }
630
-
631
- /* Edit hint section */
632
- .edit-hint {
633
- background: var(--color-surface);
634
- border: 1px solid var(--color-border);
635
- border-radius: 12px;
636
- padding: 24px 32px;
637
- margin-bottom: 48px;
638
- max-width: 500px;
639
- text-align: center;
640
- }
641
-
642
- .edit-hint p {
643
- color: var(--color-text-muted);
644
- font-size: 0.95rem;
645
- margin-bottom: 8px;
646
- }
647
-
648
- .edit-hint code {
649
- color: var(--color-primary);
650
- font-family: 'JetBrains Mono', monospace;
651
- font-size: 0.9rem;
652
- }
653
-
654
- .edit-hint .hint {
655
- font-size: 0.8rem;
656
- color: hsl(240, 5%, 40%);
657
- margin-top: 12px;
658
- }
659
-
660
- /* Quick start section */
661
- .quick-start {
662
- max-width: 500px;
663
- width: 100%;
664
- margin-bottom: 48px;
665
- }
666
-
667
- .quick-start h2 {
668
- font-size: 1rem;
669
- font-weight: 500;
670
- color: var(--color-text-muted);
671
- margin-bottom: 16px;
672
- text-align: center;
673
- }
674
-
675
- .commands {
676
- display: flex;
677
- flex-direction: column;
678
- gap: 8px;
679
- }
680
-
681
- .command {
682
- display: flex;
683
- align-items: center;
684
- background: var(--color-surface);
685
- border: 1px solid var(--color-border);
686
- border-radius: 8px;
687
- padding: 12px 16px;
688
- font-family: 'JetBrains Mono', monospace;
689
- font-size: 0.85rem;
690
- transition: border-color 0.2s;
691
- }
692
-
693
- .command:hover {
694
- border-color: var(--color-primary-dark);
695
- }
696
-
697
- .command .prefix {
698
- color: var(--color-success);
699
- margin-right: 8px;
700
- }
701
-
702
- .command .cmd {
703
- color: var(--color-primary);
704
- }
705
-
706
- .command .comment {
707
- color: var(--color-text-muted);
708
- margin-left: auto;
709
- font-size: 0.75rem;
710
- }
711
-
712
- /* Links section */
713
- .links {
714
- display: flex;
715
- gap: 12px;
716
- justify-content: center;
717
- flex-wrap: wrap;
718
- margin-bottom: 48px;
719
- }
720
-
721
- .link {
722
- color: var(--color-text-muted);
723
- text-decoration: none;
724
- font-size: 0.9rem;
725
- padding: 10px 20px;
726
- border: 1px solid var(--color-border);
727
- border-radius: 8px;
728
- transition: all 0.2s;
729
- }
730
-
731
- .link:hover {
732
- color: var(--color-primary);
733
- border-color: var(--color-primary-dark);
734
- background: var(--color-surface);
735
- }
736
-
737
- .link.primary {
738
- background: var(--color-primary);
739
- border-color: var(--color-primary);
740
- color: #000;
741
- }
742
-
743
- .link.primary:hover {
744
- background: var(--color-primary-dark);
745
- border-color: var(--color-primary-dark);
746
- }
747
-
748
- /* Documentation links */
749
- .docs-links {
750
- display: flex;
751
- gap: 32px;
752
- justify-content: center;
753
- flex-wrap: wrap;
754
- margin-bottom: 48px;
755
- padding-top: 32px;
756
- border-top: 1px solid var(--color-border);
757
- max-width: 600px;
758
- width: 100%;
759
- }
760
-
761
- .docs-section h3 {
762
- font-size: 0.8rem;
763
- font-weight: 500;
764
- color: var(--color-text-muted);
765
- margin-bottom: 12px;
766
- text-transform: uppercase;
767
- letter-spacing: 0.5px;
768
- }
769
-
770
- .docs-section a {
771
- display: block;
772
- color: hsl(240, 5%, 60%);
773
- text-decoration: none;
774
- font-size: 0.85rem;
775
- padding: 4px 0;
776
- transition: color 0.2s;
777
- }
778
-
779
- .docs-section a:hover {
780
- color: var(--color-primary);
781
- }
782
-
783
- /* Footer */
784
- .footer {
785
- margin-top: auto;
786
- text-align: center;
787
- padding-top: 32px;
788
- }
789
-
790
- .version {
791
- font-size: 0.75rem;
792
- color: hsl(240, 5%, 35%);
793
- margin-bottom: 16px;
794
- }
795
-
796
- .social-links {
797
- display: flex;
798
- gap: 16px;
799
- justify-content: center;
800
- margin-bottom: 16px;
801
- }
802
-
803
- .social-links a {
804
- color: var(--color-text-muted);
805
- text-decoration: none;
806
- font-size: 0.85rem;
807
- transition: color 0.2s;
808
- }
809
-
810
- .social-links a:hover {
811
- color: var(--color-primary);
812
- }
813
-
814
- .powered-by {
815
- color: hsl(240, 5%, 35%);
816
- font-size: 0.8rem;
817
- }
818
-
819
- .powered-by a {
820
- color: var(--color-primary-dark);
821
- text-decoration: none;
822
- }
823
-
824
- .powered-by a:hover {
825
- color: var(--color-primary);
826
- }
827
-
828
- @media (max-width: 600px) {
829
- h1 { font-size: 2rem; }
830
- .docs-links { flex-direction: column; gap: 24px; text-align: center; }
831
- .command .comment { display: none; }
832
- }
833
- `;
834
- }
835
-
836
- /**
837
- * Generate a sample JavaScript file with useful utilities
838
- *
839
- * @returns app.js content as a string
840
- */
841
- export function generateSampleJs(): string {
842
- return 'console.log("Hello from app.js");';
843
- }
844
-
845
- /**
846
- * Generate a minimal, clean landing page
847
- * Uses official BurgerAPI color scheme
848
- *
849
- * @param projectName - Name of the project
850
- * @returns index.html content as a string
851
- */
852
- export function generateIndexPage(projectName: string): string {
853
- return `<!DOCTYPE html>
854
- <html lang="en">
855
- <head>
856
- <meta charset="UTF-8">
857
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
858
- <title>${projectName}</title>
859
- <link rel="icon" type="image/png" href="https://burger-api.com/img/logo.png">
860
- <link rel="preconnect" href="https://fonts.googleapis.com">
861
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
862
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=JetBrains+Mono&display=swap" rel="stylesheet">
863
- <!-- Assets: Styles -->
864
- <link rel="stylesheet" href="./assets/css/style.css" />
865
- <!-- Assets: Scripts -->
866
- <script src="./assets/js/app.js" type="module"></script>
867
- </head>
868
- <body>
869
- <!-- Hero Section -->
870
- <section class="hero">
871
- <div class="logo-wrapper">
872
- <img src="https://burger-api.com/img/logo.png" alt="BurgerAPI Logo" class="logo">
873
- <span class="logo-text">BurgerAPI</span>
874
- </div>
875
- <p class="subtitle">Your Project ${projectName} is ready</p>
876
- <div class="status">Server running</div>
877
- </section>
878
-
879
- <!-- Edit Hint -->
880
- <div class="edit-hint">
881
- <p>Edit <code>src/pages/index.html</code> and save to reload the page.</p>
882
- <p>Edit <code>src/api/route.ts</code> and save to reload the API endpoint.</p>
883
- <p class="hint">Your changes will automatically refresh the server.</p>
884
- </div>
885
-
886
- <!-- Quick Start Commands -->
887
- <section class="quick-start">
888
- <h2>Quick Start</h2>
889
- <div class="commands">
890
- <div class="command">
891
- <span class="prefix">$</span>
892
- <span class="cmd">burger-api add cors logger</span>
893
- <span class="comment"># Add middleware</span>
894
- </div>
895
- <div class="command">
896
- <span class="prefix">$</span>
897
- <span class="cmd">burger-api build src/index.ts</span>
898
- <span class="comment"># Build for production</span>
899
- </div>
900
- </div>
901
- </section>
902
-
903
- <!-- Action Links -->
904
- <div class="links">
905
- <a href="/docs" class="link primary">API Docs</a>
906
- <a href="/api" class="link">Try API</a>
907
- <a href="/openapi.json" class="link">OpenAPI</a>
908
- </div>
909
-
910
- <!-- Documentation Links -->
911
- <div class="docs-links">
912
- <div class="docs-section">
913
- <h3>Documentation</h3>
914
- <a href="https://burger-api.com/docs" target="_blank">Getting Started</a>
915
- <a href="https://burger-api.com/docs/core/configuration" target="_blank">Configuration</a>
916
- <a href="https://burger-api.com/docs/request-handling/middleware" target="_blank">Middleware</a>
917
- </div>
918
- <div class="docs-section">
919
- <h3>Resources</h3>
920
- <a href="https://github.com/isfhan/burger-api" target="_blank">GitHub</a>
921
- <a href="https://github.com/isfhan/burger-api/issues" target="_blank">Report Issue</a>
922
- <a href="https://www.npmjs.com/package/burger-api" target="_blank">NPM Package</a>
923
- </div>
924
- <div class="docs-section">
925
- <h3>Community</h3>
926
- <a href="https://github.com/isfhan/burger-api" target="_blank">Contribute</a>
927
- <a href="https://github.com/isfhan/burger-api/discussions" target="_blank">Discussions</a>
928
- <a href="https://github.com/isfhan/burger-api/stargazers" target="_blank">Star on GitHub</a>
929
- </div>
930
- </div>
931
-
932
- <!-- Footer -->
933
- <footer class="footer">
934
- <div class="version">BurgerAPI v0.6.6 • Bun v1.3+</div>
935
- <div class="social-links">
936
- <a href="https://github.com/isfhan/burger-api" target="_blank">GitHub</a>
937
- <a href="https://www.npmjs.com/package/burger-api" target="_blank">NPM</a>
938
- <a href="https://burger-api.com" target="_blank">Website</a>
939
- </div>
940
- <p class="powered-by">
941
- Built with ❤️ using <a href="https://burger-api.com">BurgerAPI</a>
942
- </p>
943
- </footer>
944
- </body>
945
- </html>
946
- `;
947
- }
948
-
949
- /**
950
- * Generate middleware index file
951
- * This is where users will export their middleware
952
- *
953
- * @returns middleware/index.ts content as a string
954
- */
955
- export function generateMiddlewareIndex(): string {
956
- return `/**
957
- * Global Middleware Configuration
958
- *
959
- * Import and export middleware here to use them in your app.
960
- * Example:
961
- *
962
- * import { cors } from './cors/cors';
963
- * import { logger } from './logger/logger';
964
- *
965
- * export const globalMiddleware = [
966
- * logger(),
967
- * cors(),
968
- * ];
969
- */
970
-
971
- export const globalMiddleware: any[] = [];
972
- `;
973
- }
974
-
975
- /**
976
- * Download the .llm-context folder from GitHub to the target project
977
- * This includes context files for AI assistants working with Burger API
978
- *
979
- * @param targetDir - Where to create the project
980
- */
981
- async function downloadLlmFolder(targetDir: string): Promise<void> {
982
- try {
983
- const llmDir = join(targetDir, 'ecosystem', '.llm-context');
984
-
985
- // Download all three .llm files from GitHub
986
- const files = [
987
- 'llms.txt',
988
- 'llms-small.txt',
989
- 'llms-full.txt',
990
- ];
991
-
992
- for (const fileName of files) {
993
- const sourcePath = `ecosystem/.llm-context/${fileName}`;
994
- const destPath = join(llmDir, fileName);
995
- await downloadFile(sourcePath, destPath);
996
- }
997
- } catch (err) {
998
- // Log warning but don't fail project creation if download fails
999
- // This allows projects to be created even if GitHub is unreachable
1000
- console.warn(
1001
- `Warning: Could not download .llm-context folder: ${
1002
- err instanceof Error ? err.message : 'Unknown error'
1003
- }`
1004
- );
1005
- }
1006
- }
1007
-
1008
- /**
1009
- * Create a new project with all necessary files
1010
- * This is the main function that sets up everything
1011
- *
1012
- * @param targetDir - Where to create the project
1013
- * @param options - Project configuration from user prompts
1014
- */
1015
- export async function createProject(
1016
- targetDir: string,
1017
- options: CreateOptions
1018
- ): Promise<void> {
1019
- const spin = spinner('Creating project structure...');
1020
-
1021
- try {
1022
- // Create base files that every project needs
1023
- await Bun.write(
1024
- join(targetDir, 'package.json'),
1025
- generatePackageJson(options.name)
1026
- );
1027
- await Bun.write(join(targetDir, 'tsconfig.json'), generateTsConfig());
1028
- await Bun.write(join(targetDir, '.gitignore'), generateGitIgnore());
1029
- await Bun.write(
1030
- join(targetDir, '.prettierrc'),
1031
- generatePrettierConfig()
1032
- );
1033
-
1034
- // Create src directory and index file
1035
- await Bun.write(
1036
- join(targetDir, 'src', 'index.ts'),
1037
- generateIndexFile(options)
1038
- );
1039
-
1040
- // Create API directory and files if requested
1041
- if (options.useApi) {
1042
- const apiDir = join(targetDir, 'src', options.apiDir || 'api');
1043
- await Bun.write(join(apiDir, 'route.ts'), generateApiRoute());
1044
- }
1045
-
1046
- // Create Pages directory and files if requested
1047
- if (options.usePages) {
1048
- const pagesDir = join(targetDir, 'src', options.pageDir || 'pages');
1049
- await Bun.write(
1050
- join(pagesDir, 'index.html'),
1051
- generateIndexPage(options.name)
1052
- );
1053
- }
1054
-
1055
- // Create sample assets inside pages directory (so they're served by page router)
1056
- if (options.usePages) {
1057
- const pagesDir = join(targetDir, 'src', options.pageDir || 'pages');
1058
- await Bun.write(
1059
- join(pagesDir, 'assets', 'css', 'style.css'),
1060
- generateSampleCss()
1061
- );
1062
- await Bun.write(
1063
- join(pagesDir, 'assets', 'js', 'app.js'),
1064
- generateSampleJs()
1065
- );
1066
- // Logo is loaded from https://burger-api.com/img/logo.png
1067
- }
1068
-
1069
- // Create ecosystem/middleware directory for installed middleware
1070
- // Users can also create their own middleware/ folder for custom middleware
1071
- const ecosystemMiddlewareDir = join(
1072
- targetDir,
1073
- 'ecosystem',
1074
- 'middleware'
1075
- );
1076
- await Bun.write(
1077
- join(ecosystemMiddlewareDir, 'index.ts'),
1078
- generateMiddlewareIndex()
1079
- );
1080
-
1081
- // Download .llm-context folder with context files for AI assistants
1082
- await downloadLlmFolder(targetDir);
1083
-
1084
- spin.stop('Project created successfully!');
1085
- } catch (err) {
1086
- spin.stop('Failed to create project', true);
1087
- throw err;
1088
- }
1089
- }
1090
-
1091
- /**
1092
- * Install dependencies in a project directory
1093
- * Runs `bun install` to install all packages
1094
- *
1095
- * @param projectDir - Directory containing package.json
1096
- */
1097
- export async function installDependencies(projectDir: string): Promise<void> {
1098
- const spin = spinner('Installing dependencies...');
1099
-
1100
- try {
1101
- // Run bun install using Bun.spawn
1102
- const proc = Bun.spawn(['bun', 'install'], {
1103
- cwd: projectDir,
1104
- stdout: 'ignore',
1105
- stderr: 'pipe',
1106
- });
1107
-
1108
- // Wait for it to complete
1109
- const exitCode = await proc.exited;
1110
-
1111
- if (exitCode !== 0) {
1112
- throw new Error('bun install failed');
1113
- }
1114
-
1115
- spin.stop('Dependencies installed!');
1116
- } catch (err) {
1117
- spin.stop('Failed to install dependencies', true);
1118
- throw err;
1119
- }
1120
- }
1
+ /**
2
+ * Template Management System
3
+ *
4
+ * Handles downloading and caching project templates.
5
+ * Templates are the starter projects users get when running `burger-api create`
6
+ *
7
+ */
8
+
9
+ import { join } from 'path';
10
+
11
+ import type { CreateOptions } from '../types/index';
12
+ import { spinner } from './logger';
13
+ import { downloadFile } from './github';
14
+
15
+ /**
16
+ * Generate package.json content for a new project
17
+ * This includes the burger-api dependency and basic scripts
18
+ *
19
+ * @param projectName - Name of the project
20
+ * @returns package.json content as a string
21
+ */
22
+ export function generatePackageJson(projectName: string): string {
23
+ const packageJson = {
24
+ name: projectName,
25
+ version: '0.1.0',
26
+ type: 'module',
27
+ scripts: {
28
+ dev: 'bun --watch src/index.ts',
29
+ start: 'bun src/index.ts',
30
+ build: 'bun build src/index.ts --outdir ./dist',
31
+ },
32
+ dependencies: {
33
+ 'burger-api': '^0.6.2',
34
+ },
35
+ devDependencies: {
36
+ '@types/bun': 'latest',
37
+ typescript: '^5',
38
+ },
39
+ };
40
+
41
+ return JSON.stringify(packageJson, null, 2);
42
+ }
43
+
44
+ /**
45
+ * Generate tsconfig.json content for a new project
46
+ * This sets up TypeScript properly for Bun
47
+ *
48
+ * @returns tsconfig.json content as a string
49
+ */
50
+ export function generateTsConfig(): string {
51
+ const tsconfig = {
52
+ compilerOptions: {
53
+ lib: ['ESNext'],
54
+ target: 'ESNext',
55
+ module: 'ESNext',
56
+ moduleDetection: 'force',
57
+ jsx: 'react-jsx',
58
+ allowJs: true,
59
+
60
+ // Best practices for type safety
61
+ strict: true,
62
+ noUncheckedIndexedAccess: true,
63
+ noImplicitOverride: true,
64
+
65
+ // Module resolution for Bun
66
+ moduleResolution: 'bundler',
67
+ allowImportingTsExtensions: true,
68
+ verbatimModuleSyntax: true,
69
+ noEmit: true,
70
+
71
+ // Interop
72
+ allowSyntheticDefaultImports: true,
73
+ esModuleInterop: true,
74
+ forceConsistentCasingInFileNames: true,
75
+
76
+ // Skip type checking for dependencies
77
+ skipLibCheck: true,
78
+
79
+ // Types
80
+ types: ['bun-types'],
81
+ },
82
+ };
83
+
84
+ return JSON.stringify(tsconfig, null, 2);
85
+ }
86
+
87
+ /**
88
+ * Generate .gitignore content
89
+ *
90
+ * @returns .gitignore content as a string
91
+ */
92
+ export function generateGitIgnore(): string {
93
+ return `# Bun
94
+ node_modules/
95
+ bun.lockb
96
+ .env*
97
+
98
+ # Build output
99
+ dist/
100
+ .build/
101
+ *.exe
102
+
103
+ # OS files
104
+ .DS_Store
105
+ Thumbs.db
106
+
107
+ # Editor
108
+ .vscode/
109
+ .idea/
110
+ *.swp
111
+ *.swo
112
+ `;
113
+ }
114
+
115
+ /**
116
+ * Generate .prettierrc content
117
+ * This matches the burger-api project style
118
+ *
119
+ * @returns .prettierrc content as a string
120
+ */
121
+ export function generatePrettierConfig(): string {
122
+ const prettierConfig = {
123
+ semi: true,
124
+ singleQuote: true,
125
+ tabWidth: 4,
126
+ trailingComma: 'es5',
127
+ printWidth: 80,
128
+ arrowParens: 'always',
129
+ };
130
+
131
+ return JSON.stringify(prettierConfig, null, 2);
132
+ }
133
+
134
+ /**
135
+ * Generate index.ts content based on user options
136
+ * This is the main entry point for the user's project
137
+ *
138
+ * @param options - Project configuration from user prompts
139
+ * @returns index.ts content as a string
140
+ */
141
+ export function generateIndexFile(options: CreateOptions): string {
142
+ const lines: string[] = [];
143
+
144
+ // Import statement
145
+ lines.push("import { Burger } from 'burger-api';");
146
+ lines.push('');
147
+
148
+ // Configuration object
149
+ lines.push('const app = new Burger({');
150
+
151
+ if (options.useApi) {
152
+ lines.push(` apiDir: './src/${options.apiDir || 'api'}',`);
153
+ if (options.apiPrefix && options.apiPrefix !== '/api') {
154
+ lines.push(` apiPrefix: '${options.apiPrefix}',`);
155
+ }
156
+ }
157
+
158
+ if (options.usePages) {
159
+ lines.push(` pageDir: './src/${options.pageDir || 'pages'}',`);
160
+ if (options.pagePrefix && options.pagePrefix !== '/') {
161
+ lines.push(` pagePrefix: '${options.pagePrefix}',`);
162
+ }
163
+ }
164
+
165
+ if (options.debug) {
166
+ lines.push(' debug: true,');
167
+ }
168
+
169
+ lines.push(' globalMiddleware: [],');
170
+ lines.push('});');
171
+ lines.push('');
172
+
173
+ // Start server - uses PORT env variable for flexibility (e.g., burger-api serve --port 4000)
174
+ lines.push('const port = Number(process.env.PORT) || 4000;');
175
+ lines.push('app.serve(port, () => {');
176
+ lines.push(
177
+ ' console.log(`Server running on http://localhost:${port}`);'
178
+ );
179
+ lines.push('});');
180
+
181
+ return lines.join('\n');
182
+ }
183
+
184
+ /**
185
+ * Generate a comprehensive API route file with full examples
186
+ * Includes: OpenAPI metadata, Zod schemas, all HTTP methods, middleware
187
+ * Every line has beginner-friendly comments explaining what it does
188
+ *
189
+ * @returns route.ts content as a string
190
+ */
191
+ export function generateApiRoute(): string {
192
+ return `/**
193
+ * =============================================================================
194
+ * BURGER API - EXAMPLE ROUTE FILE
195
+ * =============================================================================
196
+ *
197
+ * This file shows you everything you can do with BurgerAPI routes!
198
+ *
199
+ * KEY CONCEPTS:
200
+ * - This file is automatically loaded because it's named "route.ts"
201
+ * - The folder path becomes the URL path (e.g., /api/route.ts → /api)
202
+ * - Export functions named after HTTP methods: GET, POST, PUT, DELETE, etc.
203
+ *
204
+ * =============================================================================
205
+ */
206
+
207
+
208
+ import { z } from 'zod';
209
+ import type { BurgerRequest, Middleware, BurgerNext } from 'burger-api';
210
+
211
+
212
+ /*
213
+ -----------------------------------------------------------------------------
214
+ OPENAPI METADATA (Optional but recommended!)
215
+ -----------------------------------------------------------------------------
216
+
217
+ - This creates automatic documentation for your API!
218
+ - Visit /docs in your browser to see beautiful Swagger UI documentation.
219
+ - Each HTTP method (get, post, put, delete) can have its own documentation.
220
+ -----------------------------------------------------------------------------
221
+ */
222
+ export const openapi = {
223
+ // Documentation for the GET method
224
+ get: {
225
+ // 'summary' - A short title shown in the docs (keep it brief!)
226
+ summary: 'Get all items',
227
+
228
+ // 'description' - A longer explanation of what this endpoint does
229
+ description: 'Fetches a list of items. You can filter results using query parameters.',
230
+
231
+ // 'tags' - Groups related endpoints together in the docs
232
+ // All endpoints with the same tag appear in the same section
233
+ tags: ['Items'],
234
+
235
+ // 'operationId' - A unique ID for this endpoint (useful for code generation)
236
+ operationId: 'getItems',
237
+
238
+ // 'responses' - Documents what responses the endpoint can return
239
+ responses: {
240
+ '200': { description: 'Successfully retrieved items' },
241
+ '400': { description: 'Invalid query parameters' },
242
+ },
243
+ },
244
+
245
+ // Documentation for the POST method
246
+ post: {
247
+ summary: 'Create a new item',
248
+ description: 'Creates a new item with the provided data. Returns the created item.',
249
+ tags: ['Items'],
250
+ operationId: 'createItem',
251
+ responses: {
252
+ '201': { description: 'Item created successfully' },
253
+ '400': { description: 'Invalid request body' },
254
+ },
255
+ },
256
+
257
+ // Documentation for the PUT method
258
+ put: {
259
+ summary: 'Update an item',
260
+ description: 'Updates an existing item. Provide the item ID in the query string.',
261
+ tags: ['Items'],
262
+ operationId: 'updateItem',
263
+ responses: {
264
+ '200': { description: 'Item updated successfully' },
265
+ '400': { description: 'Invalid request data' },
266
+ '404': { description: 'Item not found' },
267
+ },
268
+ },
269
+
270
+ // Documentation for the DELETE method
271
+ delete: {
272
+ summary: 'Delete an item',
273
+ description: 'Permanently deletes an item by ID.',
274
+ tags: ['Items'],
275
+ operationId: 'deleteItem',
276
+ responses: {
277
+ '200': { description: 'Item deleted successfully' },
278
+ '404': { description: 'Item not found' },
279
+ },
280
+ },
281
+ };
282
+
283
+
284
+ /*
285
+ -----------------------------------------------------------------------------
286
+ SCHEMA VALIDATION (Using Zod)
287
+ -----------------------------------------------------------------------------
288
+
289
+ - Schemas define what data your API accepts. BurgerAPI automatically:
290
+ - Validates incoming data against these schemas
291
+ - Returns a 400 error if validation fails
292
+ - Puts the validated data in req.validated for you to use
293
+
294
+ - You can validate:
295
+ - 'query' → URL query parameters like ?search=hello&page=1
296
+ - 'body' → Request body (for POST/PUT requests)
297
+ - 'params' → URL parameters like /items/[id] → { id: "123" }
298
+ -----------------------------------------------------------------------------
299
+ */
300
+ export const schema = {
301
+ // Schema for GET requests - validates query parameters
302
+ get: {
303
+ // 'query' - Validates the URL query string
304
+ // Example URL: /api?search=burger&limit=10&page=2
305
+ query: z.object({
306
+ // 'search' - Optional text to search for
307
+ // .optional() means this field isn't required
308
+ search: z.string().optional(),
309
+
310
+ // 'limit' - How many items to return (default: 10)
311
+ // .coerce.number() converts string "10" to number 10
312
+ // .min(1) means it must be at least 1
313
+ // .max(100) means it can't be more than 100
314
+ // .default(10) uses 10 if not provided
315
+ limit: z.coerce.number().min(1).max(100).default(10),
316
+
317
+ // 'page' - Which page of results to return
318
+ page: z.coerce.number().min(1).default(1),
319
+ }),
320
+ },
321
+
322
+ // Schema for POST requests - validates the request body
323
+ post: {
324
+ // 'body' - Validates JSON data sent in the request body
325
+ body: z.object({
326
+ // 'name' - Required, must be at least 1 character
327
+ // .min(1, '...') shows a custom error message if too short
328
+ name: z.string().min(1, 'Name is required'),
329
+
330
+ // 'description' - Optional text field
331
+ description: z.string().optional(),
332
+
333
+ // 'price' - Required, must be a positive number
334
+ // .positive() ensures the number is greater than 0
335
+ price: z.number().positive('Price must be greater than 0'),
336
+
337
+ // 'category' - Must be one of these specific values
338
+ // .enum() only allows the listed values
339
+ category: z.enum(['food', 'drink', 'dessert']),
340
+
341
+ // 'isAvailable' - Optional boolean, defaults to true
342
+ isAvailable: z.boolean().default(true),
343
+ }),
344
+ },
345
+
346
+ // Schema for PUT requests - validates both query and body
347
+ put: {
348
+ // Which item to update (ID in query string)
349
+ query: z.object({
350
+ id: z.string().min(1, 'Item ID is required'),
351
+ }),
352
+
353
+ // What to update (in the request body)
354
+ // .partial() makes all fields optional (for partial updates)
355
+ body: z.object({
356
+ name: z.string().min(1),
357
+ description: z.string(),
358
+ price: z.number().positive(),
359
+ category: z.enum(['food', 'drink', 'dessert']),
360
+ isAvailable: z.boolean(),
361
+ }).partial(), // .partial() = all fields become optional
362
+ },
363
+
364
+ // Schema for DELETE requests - validates query parameters
365
+ delete: {
366
+ query: z.object({
367
+ id: z.string().min(1, 'Item ID is required'),
368
+ }),
369
+ },
370
+ };
371
+
372
+
373
+ /*
374
+ -----------------------------------------------------------------------------
375
+ ROUTE-SPECIFIC MIDDLEWARE (Optional)
376
+ -----------------------------------------------------------------------------
377
+
378
+ - Middleware runs BEFORE your route handler. Use it for:
379
+ - Logging requests
380
+ - Checking authentication
381
+ - Modifying the request
382
+ - Blocking unauthorized access
383
+
384
+ - Return 'undefined' to continue to the next middleware/handler
385
+ - Return a 'Response' to stop and send that response immediately
386
+ -----------------------------------------------------------------------------
387
+ */
388
+ export const middleware: Middleware[] = [
389
+ // Example: Log every request to this route
390
+ async (req: BurgerRequest): Promise<BurgerNext> => {
391
+ console.log(\`[\${new Date().toISOString()}] \${req.method} \${req.url}\`);
392
+
393
+ // Return undefined to continue to the next middleware/handler
394
+ // If you return a Response here, it stops and sends that response
395
+ return undefined;
396
+ },
397
+ ];
398
+
399
+ /*
400
+ -----------------------------------------------------------------------------
401
+ HTTP HANDLERS
402
+ -----------------------------------------------------------------------------
403
+
404
+ - These functions handle the actual requests. They receive:
405
+ - req: The request object with validated data in req.validated
406
+ - They must return a Response object. Use Response.json() for JSON responses.
407
+ -----------------------------------------------------------------------------
408
+ */
409
+
410
+ /**
411
+ * GET - Fetch items with optional filtering
412
+ *
413
+ * Example requests:
414
+ * - GET /api → Get first 10 items
415
+ * - GET /api?limit=5 → Get first 5 items
416
+ * - GET /api?search=burger&page=2 → Search for "burger", page 2
417
+ */
418
+ export async function GET(req: BurgerRequest<{ query: z.infer<typeof schema.get.query> }>) {
419
+ // Access validated query parameters from the schema
420
+ const { search, limit, page } = req.validated.query;
421
+
422
+ // Mock data (replace with your database query)
423
+ const mockItems = [
424
+ { id: '1', name: 'Classic Burger', price: 9.99, category: 'food' },
425
+ { id: '2', name: 'Cheese Burger', price: 11.99, category: 'food' },
426
+ { id: '3', name: 'Cola', price: 2.99, category: 'drink' },
427
+ ];
428
+
429
+ // Filter items if search is provided
430
+ let items = mockItems;
431
+ if (search) {
432
+ items = items.filter(item =>
433
+ item.name.toLowerCase().includes(search.toLowerCase())
434
+ );
435
+ }
436
+
437
+ // Calculate pagination
438
+ const startIndex = (page - 1) * limit;
439
+ const paginatedItems = items.slice(startIndex, startIndex + limit);
440
+
441
+ // Return JSON response with status 200 (default)
442
+ return Response.json({
443
+ success: true,
444
+ data: paginatedItems,
445
+ pagination: {
446
+ page,
447
+ limit,
448
+ total: items.length,
449
+ totalPages: Math.ceil(items.length / limit),
450
+ },
451
+ });
452
+ }
453
+
454
+ /**
455
+ * POST - Create a new item
456
+ *
457
+ * Example request body:
458
+ * {
459
+ * "name": "Veggie Burger",
460
+ * "description": "Delicious plant-based burger",
461
+ * "price": 12.99,
462
+ * "category": "food"
463
+ * }
464
+ */
465
+ export async function POST(req: BurgerRequest<{ body: z.infer<typeof schema.post.body> }>) {
466
+ // Get validated body data - already checked by Zod schema!
467
+ const { name, description, price, category, isAvailable } = req.validated.body;
468
+
469
+ // Create the item (replace with your database insert)
470
+ const newItem = {
471
+ id: crypto.randomUUID(), // Generate unique ID
472
+ name,
473
+ description: description || null,
474
+ price,
475
+ category,
476
+ isAvailable,
477
+ createdAt: new Date().toISOString(),
478
+ };
479
+
480
+ // Return the created item with status 201 (Created)
481
+ return Response.json({
482
+ success: true,
483
+ message: 'Item created successfully',
484
+ data: newItem,
485
+ }, { status: 201 });
486
+ }
487
+
488
+ /**
489
+ * PUT - Update an existing item
490
+ *
491
+ * Example: PUT /api?id=123
492
+ * Body: { "name": "Updated Name", "price": 15.99 }
493
+ */
494
+ export async function PUT(req: BurgerRequest<{ query: z.infer<typeof schema.put.query>, body: z.infer<typeof schema.put.body> }>) {
495
+ // Get the item ID from query parameters
496
+ const { id } = req.validated.query;
497
+
498
+ // Get the fields to update from the request body
499
+ const updates = req.validated.body;
500
+
501
+ // Find and update the item (replace with your database update)
502
+ // Here we're just simulating an update
503
+ const updatedItem = {
504
+ id,
505
+ ...updates,
506
+ updatedAt: new Date().toISOString(),
507
+ };
508
+
509
+ return Response.json({
510
+ success: true,
511
+ message: 'Item updated successfully',
512
+ data: updatedItem,
513
+ });
514
+ }
515
+
516
+ /**
517
+ * DELETE - Remove an item
518
+ *
519
+ * Example: DELETE /api?id=123
520
+ */
521
+ export async function DELETE(req: BurgerRequest<{ query: z.infer<typeof schema.delete.query> }>) {
522
+ // Get the item ID from query parameters
523
+ const { id } = req.validated.query;
524
+
525
+ // Delete the item (replace with your database delete)
526
+ // Here we're just returning a success message
527
+ return Response.json({
528
+ success: true,
529
+ message: \`Item \${id} deleted successfully\`,
530
+ });
531
+ }
532
+ `;
533
+ }
534
+
535
+ /**
536
+ * Generate a CSS file with modern styling for the landing page
537
+ *
538
+ * @returns style.css content as a string
539
+ */
540
+ export function generateSampleCss(): string {
541
+ return `
542
+ :root {
543
+ --color-primary: hsl(30, 75%, 90%);
544
+ --color-primary-dark: hsl(30, 75%, 80%);
545
+ --color-bg: #09090b;
546
+ --color-surface: hsl(240, 10%, 3.9%);
547
+ --color-border: hsl(240, 3.7%, 15.9%);
548
+ --color-success: hsl(120, 50%, 40%);
549
+ --color-text-muted: hsl(240, 5%, 50%);
550
+ }
551
+
552
+ * {
553
+ margin: 0;
554
+ padding: 0;
555
+ box-sizing: border-box;
556
+ }
557
+
558
+ body {
559
+ font-family: 'Poppins', system-ui, sans-serif;
560
+ min-height: 100vh;
561
+ background: var(--color-bg);
562
+ color: #fff;
563
+ display: flex;
564
+ flex-direction: column;
565
+ align-items: center;
566
+ padding: 60px 20px 40px;
567
+ }
568
+
569
+ .hero {
570
+ text-align: center;
571
+ max-width: 600px;
572
+ margin-bottom: 48px;
573
+ }
574
+
575
+ .logo-wrapper {
576
+ display: flex;
577
+ flex-wrap: wrap;
578
+ margin-bottom: 32px;
579
+ }
580
+
581
+ .logo {
582
+ width: 80px;
583
+ height: 80px;
584
+ }
585
+
586
+ .logo-text {
587
+ font-size: 3.5rem;
588
+ font-weight: 600;
589
+ color: var(--color-primary);
590
+ }
591
+
592
+ h1 {
593
+ font-size: 2.5rem;
594
+ font-weight: 600;
595
+ margin-bottom: 12px;
596
+ color: #fff;
597
+ }
598
+
599
+ .subtitle {
600
+ color: var(--color-text-muted);
601
+ font-size: 1.1rem;
602
+ margin-bottom: 24px;
603
+ }
604
+
605
+ .status {
606
+ display: inline-flex;
607
+ align-items: center;
608
+ gap: 8px;
609
+ background: hsla(120, 50%, 40%, 0.1);
610
+ border: 1px solid hsla(120, 50%, 40%, 0.3);
611
+ padding: 8px 16px;
612
+ border-radius: 20px;
613
+ font-size: 0.875rem;
614
+ color: var(--color-success);
615
+ }
616
+
617
+ .status::before {
618
+ content: '';
619
+ width: 8px;
620
+ height: 8px;
621
+ background: var(--color-success);
622
+ border-radius: 50%;
623
+ animation: pulse 2s infinite;
624
+ }
625
+
626
+ @keyframes pulse {
627
+ 0%, 100% { opacity: 1; }
628
+ 50% { opacity: 0.5; }
629
+ }
630
+
631
+ /* Edit hint section */
632
+ .edit-hint {
633
+ background: var(--color-surface);
634
+ border: 1px solid var(--color-border);
635
+ border-radius: 12px;
636
+ padding: 24px 32px;
637
+ margin-bottom: 48px;
638
+ max-width: 500px;
639
+ text-align: center;
640
+ }
641
+
642
+ .edit-hint p {
643
+ color: var(--color-text-muted);
644
+ font-size: 0.95rem;
645
+ margin-bottom: 8px;
646
+ }
647
+
648
+ .edit-hint code {
649
+ color: var(--color-primary);
650
+ font-family: 'JetBrains Mono', monospace;
651
+ font-size: 0.9rem;
652
+ }
653
+
654
+ .edit-hint .hint {
655
+ font-size: 0.8rem;
656
+ color: hsl(240, 5%, 40%);
657
+ margin-top: 12px;
658
+ }
659
+
660
+ /* Quick start section */
661
+ .quick-start {
662
+ max-width: 500px;
663
+ width: 100%;
664
+ margin-bottom: 48px;
665
+ }
666
+
667
+ .quick-start h2 {
668
+ font-size: 1rem;
669
+ font-weight: 500;
670
+ color: var(--color-text-muted);
671
+ margin-bottom: 16px;
672
+ text-align: center;
673
+ }
674
+
675
+ .commands {
676
+ display: flex;
677
+ flex-direction: column;
678
+ gap: 8px;
679
+ }
680
+
681
+ .command {
682
+ display: flex;
683
+ align-items: center;
684
+ background: var(--color-surface);
685
+ border: 1px solid var(--color-border);
686
+ border-radius: 8px;
687
+ padding: 12px 16px;
688
+ font-family: 'JetBrains Mono', monospace;
689
+ font-size: 0.85rem;
690
+ transition: border-color 0.2s;
691
+ }
692
+
693
+ .command:hover {
694
+ border-color: var(--color-primary-dark);
695
+ }
696
+
697
+ .command .prefix {
698
+ color: var(--color-success);
699
+ margin-right: 8px;
700
+ }
701
+
702
+ .command .cmd {
703
+ color: var(--color-primary);
704
+ }
705
+
706
+ .command .comment {
707
+ color: var(--color-text-muted);
708
+ margin-left: auto;
709
+ font-size: 0.75rem;
710
+ }
711
+
712
+ /* Links section */
713
+ .links {
714
+ display: flex;
715
+ gap: 12px;
716
+ justify-content: center;
717
+ flex-wrap: wrap;
718
+ margin-bottom: 48px;
719
+ }
720
+
721
+ .link {
722
+ color: var(--color-text-muted);
723
+ text-decoration: none;
724
+ font-size: 0.9rem;
725
+ padding: 10px 20px;
726
+ border: 1px solid var(--color-border);
727
+ border-radius: 8px;
728
+ transition: all 0.2s;
729
+ }
730
+
731
+ .link:hover {
732
+ color: var(--color-primary);
733
+ border-color: var(--color-primary-dark);
734
+ background: var(--color-surface);
735
+ }
736
+
737
+ .link.primary {
738
+ background: var(--color-primary);
739
+ border-color: var(--color-primary);
740
+ color: #000;
741
+ }
742
+
743
+ .link.primary:hover {
744
+ background: var(--color-primary-dark);
745
+ border-color: var(--color-primary-dark);
746
+ }
747
+
748
+ /* Documentation links */
749
+ .docs-links {
750
+ display: flex;
751
+ gap: 32px;
752
+ justify-content: center;
753
+ flex-wrap: wrap;
754
+ margin-bottom: 48px;
755
+ padding-top: 32px;
756
+ border-top: 1px solid var(--color-border);
757
+ max-width: 600px;
758
+ width: 100%;
759
+ }
760
+
761
+ .docs-section h3 {
762
+ font-size: 0.8rem;
763
+ font-weight: 500;
764
+ color: var(--color-text-muted);
765
+ margin-bottom: 12px;
766
+ text-transform: uppercase;
767
+ letter-spacing: 0.5px;
768
+ }
769
+
770
+ .docs-section a {
771
+ display: block;
772
+ color: hsl(240, 5%, 60%);
773
+ text-decoration: none;
774
+ font-size: 0.85rem;
775
+ padding: 4px 0;
776
+ transition: color 0.2s;
777
+ }
778
+
779
+ .docs-section a:hover {
780
+ color: var(--color-primary);
781
+ }
782
+
783
+ /* Footer */
784
+ .footer {
785
+ margin-top: auto;
786
+ text-align: center;
787
+ padding-top: 32px;
788
+ }
789
+
790
+ .version {
791
+ font-size: 0.75rem;
792
+ color: hsl(240, 5%, 35%);
793
+ margin-bottom: 16px;
794
+ }
795
+
796
+ .social-links {
797
+ display: flex;
798
+ gap: 16px;
799
+ justify-content: center;
800
+ margin-bottom: 16px;
801
+ }
802
+
803
+ .social-links a {
804
+ color: var(--color-text-muted);
805
+ text-decoration: none;
806
+ font-size: 0.85rem;
807
+ transition: color 0.2s;
808
+ }
809
+
810
+ .social-links a:hover {
811
+ color: var(--color-primary);
812
+ }
813
+
814
+ .powered-by {
815
+ color: hsl(240, 5%, 35%);
816
+ font-size: 0.8rem;
817
+ }
818
+
819
+ .powered-by a {
820
+ color: var(--color-primary-dark);
821
+ text-decoration: none;
822
+ }
823
+
824
+ .powered-by a:hover {
825
+ color: var(--color-primary);
826
+ }
827
+
828
+ @media (max-width: 600px) {
829
+ h1 { font-size: 2rem; }
830
+ .docs-links { flex-direction: column; gap: 24px; text-align: center; }
831
+ .command .comment { display: none; }
832
+ }
833
+ `;
834
+ }
835
+
836
+ /**
837
+ * Generate a sample JavaScript file with useful utilities
838
+ *
839
+ * @returns app.js content as a string
840
+ */
841
+ export function generateSampleJs(): string {
842
+ return 'console.log("Hello from app.js");';
843
+ }
844
+
845
+ /**
846
+ * Generate a minimal, clean landing page
847
+ * Uses official BurgerAPI color scheme
848
+ *
849
+ * @param projectName - Name of the project
850
+ * @returns index.html content as a string
851
+ */
852
+ export function generateIndexPage(projectName: string): string {
853
+ return `<!DOCTYPE html>
854
+ <html lang="en">
855
+ <head>
856
+ <meta charset="UTF-8">
857
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
858
+ <title>${projectName}</title>
859
+ <link rel="icon" type="image/png" href="https://burger-api.com/img/logo.png">
860
+ <link rel="preconnect" href="https://fonts.googleapis.com">
861
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
862
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=JetBrains+Mono&display=swap" rel="stylesheet">
863
+ <!-- Assets: Styles -->
864
+ <link rel="stylesheet" href="./assets/css/style.css" />
865
+ <!-- Assets: Scripts -->
866
+ <script src="./assets/js/app.js" type="module"></script>
867
+ </head>
868
+ <body>
869
+ <!-- Hero Section -->
870
+ <section class="hero">
871
+ <div class="logo-wrapper">
872
+ <img src="https://burger-api.com/img/logo.png" alt="BurgerAPI Logo" class="logo">
873
+ <span class="logo-text">BurgerAPI</span>
874
+ </div>
875
+ <p class="subtitle">Your Project ${projectName} is ready</p>
876
+ <div class="status">Server running</div>
877
+ </section>
878
+
879
+ <!-- Edit Hint -->
880
+ <div class="edit-hint">
881
+ <p>Edit <code>src/pages/index.html</code> and save to reload the page.</p>
882
+ <p>Edit <code>src/api/route.ts</code> and save to reload the API endpoint.</p>
883
+ <p class="hint">Your changes will automatically refresh the server.</p>
884
+ </div>
885
+
886
+ <!-- Quick Start Commands -->
887
+ <section class="quick-start">
888
+ <h2>Quick Start</h2>
889
+ <div class="commands">
890
+ <div class="command">
891
+ <span class="prefix">$</span>
892
+ <span class="cmd">burger-api add cors logger</span>
893
+ <span class="comment"># Add middleware</span>
894
+ </div>
895
+ <div class="command">
896
+ <span class="prefix">$</span>
897
+ <span class="cmd">burger-api build src/index.ts</span>
898
+ <span class="comment"># Build for production</span>
899
+ </div>
900
+ </div>
901
+ </section>
902
+
903
+ <!-- Action Links -->
904
+ <div class="links">
905
+ <a href="/docs" class="link primary">API Docs</a>
906
+ <a href="/api" class="link">Try API</a>
907
+ <a href="/openapi.json" class="link">OpenAPI</a>
908
+ </div>
909
+
910
+ <!-- Documentation Links -->
911
+ <div class="docs-links">
912
+ <div class="docs-section">
913
+ <h3>Documentation</h3>
914
+ <a href="https://burger-api.com/docs" target="_blank">Getting Started</a>
915
+ <a href="https://burger-api.com/docs/core/configuration" target="_blank">Configuration</a>
916
+ <a href="https://burger-api.com/docs/request-handling/middleware" target="_blank">Middleware</a>
917
+ </div>
918
+ <div class="docs-section">
919
+ <h3>Resources</h3>
920
+ <a href="https://github.com/isfhan/burger-api" target="_blank">GitHub</a>
921
+ <a href="https://github.com/isfhan/burger-api/issues" target="_blank">Report Issue</a>
922
+ <a href="https://www.npmjs.com/package/burger-api" target="_blank">NPM Package</a>
923
+ </div>
924
+ <div class="docs-section">
925
+ <h3>Community</h3>
926
+ <a href="https://github.com/isfhan/burger-api" target="_blank">Contribute</a>
927
+ <a href="https://github.com/isfhan/burger-api/discussions" target="_blank">Discussions</a>
928
+ <a href="https://github.com/isfhan/burger-api/stargazers" target="_blank">Star on GitHub</a>
929
+ </div>
930
+ </div>
931
+
932
+ <!-- Footer -->
933
+ <footer class="footer">
934
+ <div class="version">BurgerAPI v0.7.0 • Bun v1.3+</div>
935
+ <div class="social-links">
936
+ <a href="https://github.com/isfhan/burger-api" target="_blank">GitHub</a>
937
+ <a href="https://www.npmjs.com/package/burger-api" target="_blank">NPM</a>
938
+ <a href="https://burger-api.com" target="_blank">Website</a>
939
+ </div>
940
+ <p class="powered-by">
941
+ Built with ❤️ using <a href="https://burger-api.com">BurgerAPI</a>
942
+ </p>
943
+ </footer>
944
+ </body>
945
+ </html>
946
+ `;
947
+ }
948
+
949
+ /**
950
+ * Generate middleware index file
951
+ * This is where users will export their middleware
952
+ *
953
+ * @returns middleware/index.ts content as a string
954
+ */
955
+ export function generateMiddlewareIndex(): string {
956
+ return `/**
957
+ * Global Middleware Configuration
958
+ *
959
+ * Import and export middleware here to use them in your app.
960
+ * Example:
961
+ *
962
+ * import { cors } from './cors/cors';
963
+ * import { logger } from './logger/logger';
964
+ *
965
+ * export const globalMiddleware = [
966
+ * logger(),
967
+ * cors(),
968
+ * ];
969
+ */
970
+
971
+ export const globalMiddleware: any[] = [];
972
+ `;
973
+ }
974
+
975
+ /**
976
+ * Download the .llm-context folder from GitHub to the target project
977
+ * This includes context files for AI assistants working with Burger API
978
+ *
979
+ * @param targetDir - Where to create the project
980
+ */
981
+ async function downloadLlmFolder(targetDir: string): Promise<void> {
982
+ try {
983
+ const llmDir = join(targetDir, 'ecosystem', '.llm-context');
984
+
985
+ // Download all three .llm files from GitHub
986
+ const files = ['llms.txt', 'llms-small.txt', 'llms-full.txt'];
987
+
988
+ for (const fileName of files) {
989
+ const sourcePath = `ecosystem/.llm-context/${fileName}`;
990
+ const destPath = join(llmDir, fileName);
991
+ await downloadFile(sourcePath, destPath);
992
+ }
993
+ } catch (err) {
994
+ // Log warning but don't fail project creation if download fails
995
+ // This allows projects to be created even if GitHub is unreachable
996
+ console.warn(
997
+ `Warning: Could not download .llm-context folder: ${
998
+ err instanceof Error ? err.message : 'Unknown error'
999
+ }`
1000
+ );
1001
+ }
1002
+ }
1003
+
1004
+ /**
1005
+ * Create a new project with all necessary files
1006
+ * This is the main function that sets up everything
1007
+ *
1008
+ * @param targetDir - Where to create the project
1009
+ * @param options - Project configuration from user prompts
1010
+ */
1011
+ export async function createProject(
1012
+ targetDir: string,
1013
+ options: CreateOptions
1014
+ ): Promise<void> {
1015
+ const spin = spinner('Creating project structure...');
1016
+
1017
+ try {
1018
+ // Create base files that every project needs
1019
+ await Bun.write(
1020
+ join(targetDir, 'package.json'),
1021
+ generatePackageJson(options.name)
1022
+ );
1023
+ await Bun.write(join(targetDir, 'tsconfig.json'), generateTsConfig());
1024
+ await Bun.write(join(targetDir, '.gitignore'), generateGitIgnore());
1025
+ await Bun.write(
1026
+ join(targetDir, '.prettierrc'),
1027
+ generatePrettierConfig()
1028
+ );
1029
+
1030
+ // Create src directory and index file
1031
+ await Bun.write(
1032
+ join(targetDir, 'src', 'index.ts'),
1033
+ generateIndexFile(options)
1034
+ );
1035
+
1036
+ // Create API directory and files if requested
1037
+ if (options.useApi) {
1038
+ const apiDir = join(targetDir, 'src', options.apiDir || 'api');
1039
+ await Bun.write(join(apiDir, 'route.ts'), generateApiRoute());
1040
+ }
1041
+
1042
+ // Create Pages directory and files if requested
1043
+ if (options.usePages) {
1044
+ const pagesDir = join(targetDir, 'src', options.pageDir || 'pages');
1045
+ await Bun.write(
1046
+ join(pagesDir, 'index.html'),
1047
+ generateIndexPage(options.name)
1048
+ );
1049
+ }
1050
+
1051
+ // Create sample assets inside pages directory (so they're served by page router)
1052
+ if (options.usePages) {
1053
+ const pagesDir = join(targetDir, 'src', options.pageDir || 'pages');
1054
+ await Bun.write(
1055
+ join(pagesDir, 'assets', 'css', 'style.css'),
1056
+ generateSampleCss()
1057
+ );
1058
+ await Bun.write(
1059
+ join(pagesDir, 'assets', 'js', 'app.js'),
1060
+ generateSampleJs()
1061
+ );
1062
+ // Logo is loaded from https://burger-api.com/img/logo.png
1063
+ }
1064
+
1065
+ // Create ecosystem/middleware directory for installed middleware
1066
+ // Users can also create their own middleware/ folder for custom middleware
1067
+ const ecosystemMiddlewareDir = join(
1068
+ targetDir,
1069
+ 'ecosystem',
1070
+ 'middleware'
1071
+ );
1072
+ await Bun.write(
1073
+ join(ecosystemMiddlewareDir, 'index.ts'),
1074
+ generateMiddlewareIndex()
1075
+ );
1076
+
1077
+ // Download .llm-context folder with context files for AI assistants
1078
+ await downloadLlmFolder(targetDir);
1079
+
1080
+ spin.stop('Project created successfully!');
1081
+ } catch (err) {
1082
+ spin.stop('Failed to create project', true);
1083
+ throw err;
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Install dependencies in a project directory
1089
+ * Runs `bun install` to install all packages
1090
+ *
1091
+ * @param projectDir - Directory containing package.json
1092
+ */
1093
+ export async function installDependencies(projectDir: string): Promise<void> {
1094
+ const spin = spinner('Installing dependencies...');
1095
+
1096
+ try {
1097
+ // Run bun install using Bun.spawn
1098
+ const proc = Bun.spawn(['bun', 'install'], {
1099
+ cwd: projectDir,
1100
+ stdout: 'ignore',
1101
+ stderr: 'pipe',
1102
+ });
1103
+
1104
+ // Wait for it to complete
1105
+ const exitCode = await proc.exited;
1106
+
1107
+ if (exitCode !== 0) {
1108
+ throw new Error('bun install failed');
1109
+ }
1110
+
1111
+ spin.stop('Dependencies installed!');
1112
+ } catch (err) {
1113
+ spin.stop('Failed to install dependencies', true);
1114
+ throw err;
1115
+ }
1116
+ }