@girardmedia/bootspring 3.3.2 → 3.4.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.
Files changed (171) hide show
  1. package/assets/agents/accessibility-auditor.md +39 -0
  2. package/assets/agents/api-designer.md +40 -0
  3. package/assets/agents/auth-implementer.md +64 -0
  4. package/assets/agents/bug-hunter.md +42 -0
  5. package/assets/agents/bundle-analyzer.md +40 -0
  6. package/assets/agents/cache-optimizer.md +55 -0
  7. package/assets/agents/changelog-writer.md +55 -0
  8. package/assets/agents/ci-cd-builder.md +40 -0
  9. package/assets/agents/code-explainer.md +39 -0
  10. package/assets/agents/code-reviewer.md +39 -0
  11. package/assets/agents/cost-optimizer.md +57 -0
  12. package/assets/agents/cron-scheduler.md +51 -0
  13. package/assets/agents/data-seeder.md +56 -0
  14. package/assets/agents/database-architect.md +40 -0
  15. package/assets/agents/dependency-updater.md +40 -0
  16. package/assets/agents/deploy-checker.md +40 -0
  17. package/assets/agents/docker-optimizer.md +40 -0
  18. package/assets/agents/documentation-writer.md +40 -0
  19. package/assets/agents/email-builder.md +55 -0
  20. package/assets/agents/env-setup.md +40 -0
  21. package/assets/agents/error-handler.md +40 -0
  22. package/assets/agents/eslint-fixer.md +46 -0
  23. package/assets/agents/feature-flagger.md +69 -0
  24. package/assets/agents/git-detective.md +39 -0
  25. package/assets/agents/graphql-builder.md +60 -0
  26. package/assets/agents/incident-responder.md +59 -0
  27. package/assets/agents/log-analyzer.md +39 -0
  28. package/assets/agents/migration-planner.md +41 -0
  29. package/assets/agents/monorepo-navigator.md +39 -0
  30. package/assets/agents/nextjs-expert.md +57 -0
  31. package/assets/agents/notification-builder.md +56 -0
  32. package/assets/agents/onboarding-guide.md +39 -0
  33. package/assets/agents/performance-profiler.md +40 -0
  34. package/assets/agents/prisma-expert.md +57 -0
  35. package/assets/agents/rate-limiter.md +58 -0
  36. package/assets/agents/react-expert.md +58 -0
  37. package/assets/agents/refactorer.md +42 -0
  38. package/assets/agents/regex-builder.md +46 -0
  39. package/assets/agents/release-manager.md +40 -0
  40. package/assets/agents/s3-manager.md +58 -0
  41. package/assets/agents/schema-validator.md +40 -0
  42. package/assets/agents/search-builder.md +62 -0
  43. package/assets/agents/security-auditor.md +39 -0
  44. package/assets/agents/sitemap-generator.md +53 -0
  45. package/assets/agents/stripe-integrator.md +59 -0
  46. package/assets/agents/tailwind-expert.md +55 -0
  47. package/assets/agents/tech-debt-tracker.md +39 -0
  48. package/assets/agents/test-writer.md +42 -0
  49. package/assets/agents/type-fixer.md +45 -0
  50. package/assets/agents/webhook-builder.md +54 -0
  51. package/assets/rules/cpp.md +53 -0
  52. package/assets/rules/css.md +52 -0
  53. package/assets/rules/go.md +50 -0
  54. package/assets/rules/html.md +52 -0
  55. package/assets/rules/java.md +51 -0
  56. package/assets/rules/kotlin.md +50 -0
  57. package/assets/rules/php.md +51 -0
  58. package/assets/rules/python.md +51 -0
  59. package/assets/rules/ruby.md +51 -0
  60. package/assets/rules/rust.md +49 -0
  61. package/assets/rules/shell.md +52 -0
  62. package/assets/rules/sql.md +49 -0
  63. package/assets/rules/swift.md +50 -0
  64. package/assets/rules/typescript.md +52 -0
  65. package/assets/rules/yaml-json.md +51 -0
  66. package/assets/skills/accessibility.md +210 -0
  67. package/assets/skills/agent-patterns.md +387 -0
  68. package/assets/skills/ai-integration.md +263 -0
  69. package/assets/skills/animation-patterns.md +224 -0
  70. package/assets/skills/api-design.md +218 -0
  71. package/assets/skills/api-gateway.md +341 -0
  72. package/assets/skills/api-versioning.md +226 -0
  73. package/assets/skills/astro-patterns.md +233 -0
  74. package/assets/skills/auth-patterns.md +248 -0
  75. package/assets/skills/aws-patterns.md +171 -0
  76. package/assets/skills/background-jobs.md +162 -0
  77. package/assets/skills/browser-extensions.md +309 -0
  78. package/assets/skills/caching-patterns.md +253 -0
  79. package/assets/skills/ci-cd.md +251 -0
  80. package/assets/skills/cli-development.md +296 -0
  81. package/assets/skills/code-review.md +185 -0
  82. package/assets/skills/cron-patterns.md +327 -0
  83. package/assets/skills/data-fetching.md +231 -0
  84. package/assets/skills/database-migrations.md +346 -0
  85. package/assets/skills/database-patterns.md +219 -0
  86. package/assets/skills/debugging.md +281 -0
  87. package/assets/skills/design-system.md +289 -0
  88. package/assets/skills/django-patterns.md +182 -0
  89. package/assets/skills/docker-patterns.md +235 -0
  90. package/assets/skills/e2e-testing.md +287 -0
  91. package/assets/skills/edge-computing.md +268 -0
  92. package/assets/skills/electron-patterns.md +266 -0
  93. package/assets/skills/email-templates.md +206 -0
  94. package/assets/skills/error-handling.md +265 -0
  95. package/assets/skills/event-driven.md +232 -0
  96. package/assets/skills/express-patterns.md +239 -0
  97. package/assets/skills/fastapi-patterns.md +198 -0
  98. package/assets/skills/feature-flags.md +212 -0
  99. package/assets/skills/figma-to-code.md +298 -0
  100. package/assets/skills/file-upload.md +228 -0
  101. package/assets/skills/forms-patterns.md +264 -0
  102. package/assets/skills/gcp-patterns.md +189 -0
  103. package/assets/skills/git-workflow.md +187 -0
  104. package/assets/skills/golang-patterns.md +185 -0
  105. package/assets/skills/graphql-patterns.md +244 -0
  106. package/assets/skills/i18n-patterns.md +172 -0
  107. package/assets/skills/image-processing.md +350 -0
  108. package/assets/skills/java-springboot.md +226 -0
  109. package/assets/skills/kotlin-patterns.md +207 -0
  110. package/assets/skills/kubernetes-patterns.md +326 -0
  111. package/assets/skills/laravel-patterns.md +261 -0
  112. package/assets/skills/llm-fine-tuning.md +335 -0
  113. package/assets/skills/load-testing.md +303 -0
  114. package/assets/skills/logging-observability.md +228 -0
  115. package/assets/skills/markdown-processing.md +318 -0
  116. package/assets/skills/mcp-server-patterns.md +292 -0
  117. package/assets/skills/microservices.md +272 -0
  118. package/assets/skills/migration-patterns.md +239 -0
  119. package/assets/skills/mongodb-patterns.md +189 -0
  120. package/assets/skills/monorepo-patterns.md +287 -0
  121. package/assets/skills/nextjs-app-router.md +237 -0
  122. package/assets/skills/notification-patterns.md +348 -0
  123. package/assets/skills/oauth-patterns.md +246 -0
  124. package/assets/skills/payment-integration.md +222 -0
  125. package/assets/skills/pdf-generation.md +307 -0
  126. package/assets/skills/performance-optimization.md +277 -0
  127. package/assets/skills/php-patterns.md +210 -0
  128. package/assets/skills/prisma-patterns.md +241 -0
  129. package/assets/skills/prompt-engineering.md +193 -0
  130. package/assets/skills/pwa-patterns.md +247 -0
  131. package/assets/skills/python-patterns.md +158 -0
  132. package/assets/skills/python-testing.md +172 -0
  133. package/assets/skills/queue-patterns.md +295 -0
  134. package/assets/skills/rag-patterns.md +159 -0
  135. package/assets/skills/rate-limiting.md +319 -0
  136. package/assets/skills/react-components.md +201 -0
  137. package/assets/skills/react-native-patterns.md +299 -0
  138. package/assets/skills/real-time-patterns.md +181 -0
  139. package/assets/skills/redis-patterns.md +188 -0
  140. package/assets/skills/refactoring.md +218 -0
  141. package/assets/skills/regex-patterns.md +191 -0
  142. package/assets/skills/remix-patterns.md +262 -0
  143. package/assets/skills/responsive-design.md +199 -0
  144. package/assets/skills/ruby-rails-patterns.md +178 -0
  145. package/assets/skills/rust-patterns.md +211 -0
  146. package/assets/skills/search-patterns.md +227 -0
  147. package/assets/skills/security-hardening.md +237 -0
  148. package/assets/skills/seo-patterns.md +179 -0
  149. package/assets/skills/serverless-patterns.md +223 -0
  150. package/assets/skills/sql-optimization.md +154 -0
  151. package/assets/skills/state-management.md +254 -0
  152. package/assets/skills/storybook-patterns.md +330 -0
  153. package/assets/skills/svelte-patterns.md +258 -0
  154. package/assets/skills/swift-patterns.md +227 -0
  155. package/assets/skills/tailwind-patterns.md +272 -0
  156. package/assets/skills/tdd-workflow.md +199 -0
  157. package/assets/skills/terraform-patterns.md +270 -0
  158. package/assets/skills/testing-react.md +240 -0
  159. package/assets/skills/testing-vitest.md +232 -0
  160. package/assets/skills/typescript-strict.md +159 -0
  161. package/assets/skills/video-processing.md +340 -0
  162. package/assets/skills/vue-patterns.md +247 -0
  163. package/assets/skills/web-workers.md +327 -0
  164. package/assets/skills/webhooks-patterns.md +283 -0
  165. package/assets/skills/websocket-patterns.md +306 -0
  166. package/dist/cli/index.js +941 -958
  167. package/dist/core/index.d.ts +341 -11
  168. package/dist/core.js +58 -95
  169. package/dist/mcp/index.d.ts +33 -1
  170. package/dist/mcp-server.js +177 -255
  171. package/package.json +4 -1
@@ -0,0 +1,298 @@
1
+ ---
2
+ name: figma-to-code
3
+ description: Figma to code workflow for auto-layout mapping, design token extraction, component mapping, responsive export, and developer handoff.
4
+ ---
5
+
6
+ # Figma to Code Workflow
7
+
8
+ ## When to Use
9
+ Use this workflow when translating Figma designs into production code. It covers extracting design tokens from Figma variables, mapping auto-layout to CSS flexbox/grid, converting Figma components to React components, handling responsive breakpoints, and establishing a repeatable handoff process. Apply this workflow when starting a new design system implementation or when a design team delivers new components.
10
+
11
+ ## How It Works
12
+
13
+ ### Extracting Design Tokens via Figma API
14
+
15
+ ```typescript
16
+ // scripts/extract-figma-tokens.ts
17
+ import fetch from 'node-fetch';
18
+
19
+ const FIGMA_TOKEN = process.env.FIGMA_TOKEN!;
20
+ const FILE_KEY = process.env.FIGMA_FILE_KEY!;
21
+
22
+ interface FigmaColor { r: number; g: number; b: number; a: number }
23
+
24
+ async function extractTokens() {
25
+ const res = await fetch(
26
+ `https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
27
+ { headers: { 'X-Figma-Token': FIGMA_TOKEN } }
28
+ );
29
+ const data = await res.json();
30
+
31
+ const tokens: Record<string, Record<string, string>> = { colors: {}, spacing: {}, radius: {} };
32
+
33
+ for (const variable of Object.values(data.meta.variables) as any[]) {
34
+ const name = variable.name.replace(/\//g, '-').toLowerCase();
35
+ const value = Object.values(variable.valuesByMode)[0] as any;
36
+
37
+ if (variable.resolvedType === 'COLOR') {
38
+ tokens.colors[name] = rgbaToHex(value as FigmaColor);
39
+ } else if (variable.resolvedType === 'FLOAT') {
40
+ if (name.includes('spacing')) tokens.spacing[name] = `${value}px`;
41
+ if (name.includes('radius')) tokens.radius[name] = `${value}px`;
42
+ }
43
+ }
44
+
45
+ return tokens;
46
+ }
47
+
48
+ function rgbaToHex({ r, g, b, a }: FigmaColor): string {
49
+ const hex = [r, g, b].map((c) => Math.round(c * 255).toString(16).padStart(2, '0')).join('');
50
+ return a < 1 ? `#${hex}${Math.round(a * 255).toString(16).padStart(2, '0')}` : `#${hex}`;
51
+ }
52
+ ```
53
+
54
+ ### Auto-Layout to CSS Flexbox Mapping
55
+
56
+ ```typescript
57
+ // Figma auto-layout properties → CSS equivalents
58
+
59
+ const autoLayoutToCSS: Record<string, Record<string, string>> = {
60
+ // Layout direction
61
+ layoutMode: {
62
+ HORIZONTAL: 'flex-direction: row',
63
+ VERTICAL: 'flex-direction: column',
64
+ },
65
+ // Primary axis alignment (main axis)
66
+ primaryAxisAlignItems: {
67
+ MIN: 'justify-content: flex-start',
68
+ CENTER: 'justify-content: center',
69
+ MAX: 'justify-content: flex-end',
70
+ SPACE_BETWEEN: 'justify-content: space-between',
71
+ },
72
+ // Counter axis alignment (cross axis)
73
+ counterAxisAlignItems: {
74
+ MIN: 'align-items: flex-start',
75
+ CENTER: 'align-items: center',
76
+ MAX: 'align-items: flex-end',
77
+ BASELINE: 'align-items: baseline',
78
+ },
79
+ // Sizing
80
+ primaryAxisSizingMode: {
81
+ FIXED: 'width/height: fixed value',
82
+ AUTO: 'width/height: auto (fit-content)',
83
+ },
84
+ counterAxisSizingMode: {
85
+ FIXED: 'cross-size: fixed value',
86
+ AUTO: 'cross-size: auto (fit-content)',
87
+ },
88
+ };
89
+
90
+ // Example: Figma frame → Tailwind classes
91
+ function figmaFrameToTailwind(frame: FigmaNode): string {
92
+ const classes: string[] = ['flex'];
93
+
94
+ if (frame.layoutMode === 'VERTICAL') classes.push('flex-col');
95
+ if (frame.itemSpacing) classes.push(`gap-${pxToTailwind(frame.itemSpacing)}`);
96
+ if (frame.paddingTop) classes.push(`pt-${pxToTailwind(frame.paddingTop)}`);
97
+ if (frame.paddingRight) classes.push(`pr-${pxToTailwind(frame.paddingRight)}`);
98
+ if (frame.paddingBottom) classes.push(`pb-${pxToTailwind(frame.paddingBottom)}`);
99
+ if (frame.paddingLeft) classes.push(`pl-${pxToTailwind(frame.paddingLeft)}`);
100
+
101
+ const alignMap: Record<string, string> = {
102
+ MIN: 'items-start', CENTER: 'items-center', MAX: 'items-end',
103
+ };
104
+ if (frame.counterAxisAlignItems) {
105
+ classes.push(alignMap[frame.counterAxisAlignItems] ?? '');
106
+ }
107
+
108
+ const justifyMap: Record<string, string> = {
109
+ MIN: 'justify-start', CENTER: 'justify-center', MAX: 'justify-end',
110
+ SPACE_BETWEEN: 'justify-between',
111
+ };
112
+ if (frame.primaryAxisAlignItems) {
113
+ classes.push(justifyMap[frame.primaryAxisAlignItems] ?? '');
114
+ }
115
+
116
+ return classes.filter(Boolean).join(' ');
117
+ }
118
+
119
+ function pxToTailwind(px: number): string {
120
+ const map: Record<number, string> = { 4: '1', 8: '2', 12: '3', 16: '4', 20: '5', 24: '6', 32: '8' };
121
+ return map[px] ?? `[${px}px]`;
122
+ }
123
+ ```
124
+
125
+ ### Component Mapping (Figma to React)
126
+
127
+ ```typescript
128
+ // Map Figma component properties to React props
129
+ interface FigmaComponentMapping {
130
+ figmaName: string;
131
+ reactComponent: string;
132
+ propMapping: Record<string, { figmaProperty: string; transform?: (v: string) => string }>;
133
+ }
134
+
135
+ const componentMappings: FigmaComponentMapping[] = [
136
+ {
137
+ figmaName: 'Button',
138
+ reactComponent: 'Button',
139
+ propMapping: {
140
+ variant: { figmaProperty: 'Style', transform: (v) => v.toLowerCase() },
141
+ size: { figmaProperty: 'Size', transform: (v) => v.toLowerCase() },
142
+ disabled: { figmaProperty: 'State', transform: (v) => String(v === 'Disabled') },
143
+ },
144
+ },
145
+ {
146
+ figmaName: 'Input',
147
+ reactComponent: 'Input',
148
+ propMapping: {
149
+ label: { figmaProperty: 'Label' },
150
+ error: { figmaProperty: 'Error text' },
151
+ placeholder: { figmaProperty: 'Placeholder' },
152
+ },
153
+ },
154
+ ];
155
+
156
+ // Generate React component from Figma node
157
+ function generateComponent(node: FigmaComponentNode, mapping: FigmaComponentMapping): string {
158
+ const props: string[] = [];
159
+ for (const [reactProp, config] of Object.entries(mapping.propMapping)) {
160
+ const value = node.componentProperties[config.figmaProperty]?.value;
161
+ if (value !== undefined) {
162
+ const transformed = config.transform ? config.transform(String(value)) : String(value);
163
+ props.push(`${reactProp}="${transformed}"`);
164
+ }
165
+ }
166
+ return `<${mapping.reactComponent} ${props.join(' ')} />`;
167
+ }
168
+ ```
169
+
170
+ ### Responsive Breakpoint Strategy
171
+
172
+ ```typescript
173
+ // Map Figma frames to responsive breakpoints
174
+ interface ResponsiveFrame {
175
+ name: string;
176
+ width: number;
177
+ breakpoint: string;
178
+ tailwindPrefix: string;
179
+ }
180
+
181
+ const responsiveFrames: ResponsiveFrame[] = [
182
+ { name: 'Mobile', width: 375, breakpoint: '0px', tailwindPrefix: '' },
183
+ { name: 'Tablet', width: 768, breakpoint: '768px', tailwindPrefix: 'md:' },
184
+ { name: 'Desktop', width: 1280, breakpoint: '1024px', tailwindPrefix: 'lg:' },
185
+ { name: 'Wide', width: 1440, breakpoint: '1280px', tailwindPrefix: 'xl:' },
186
+ ];
187
+
188
+ // Generate responsive class string from multi-frame analysis
189
+ function mergeResponsiveClasses(
190
+ frames: Record<string, string[]>
191
+ ): string {
192
+ const mobile = new Set(frames['Mobile'] ?? []);
193
+ const result = [...mobile];
194
+
195
+ for (const { name, tailwindPrefix } of responsiveFrames.slice(1)) {
196
+ const classes = frames[name] ?? [];
197
+ for (const cls of classes) {
198
+ if (!mobile.has(cls)) {
199
+ result.push(`${tailwindPrefix}${cls}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ return result.join(' ');
205
+ }
206
+
207
+ // Example output: "flex flex-col gap-4 md:flex-row md:gap-8 lg:gap-12"
208
+ ```
209
+
210
+ ### Export Automation Script
211
+
212
+ ```typescript
213
+ // scripts/figma-export.ts
214
+ import * as Figma from 'figma-api';
215
+ import fs from 'fs/promises';
216
+ import path from 'path';
217
+
218
+ const api = new Figma.Api({ personalAccessToken: process.env.FIGMA_TOKEN! });
219
+
220
+ async function exportIcons(fileKey: string, nodeIds: string[]) {
221
+ const images = await api.getImage(fileKey, {
222
+ ids: nodeIds.join(','),
223
+ format: 'svg',
224
+ scale: 1,
225
+ });
226
+
227
+ for (const [nodeId, url] of Object.entries(images.images)) {
228
+ if (!url) continue;
229
+ const response = await fetch(url);
230
+ const svg = await response.text();
231
+ const name = nodeId.replace(/:/g, '-');
232
+ await fs.writeFile(
233
+ path.join('src/assets/icons', `${name}.svg`),
234
+ optimizeSvg(svg)
235
+ );
236
+ }
237
+ }
238
+
239
+ function optimizeSvg(svg: string): string {
240
+ return svg
241
+ .replace(/fill="[^"]*"/g, 'fill="currentColor"')
242
+ .replace(/\s+/g, ' ')
243
+ .trim();
244
+ }
245
+ ```
246
+
247
+ ### Developer Handoff Checklist Generator
248
+
249
+ ```typescript
250
+ // scripts/handoff-report.ts
251
+ interface HandoffReport {
252
+ component: string;
253
+ tokens: string[];
254
+ variants: string[];
255
+ states: string[];
256
+ responsive: string[];
257
+ a11y: string[];
258
+ }
259
+
260
+ function generateHandoffReport(component: FigmaComponent): HandoffReport {
261
+ return {
262
+ component: component.name,
263
+ tokens: extractUsedTokens(component),
264
+ variants: Object.keys(component.componentPropertyDefinitions ?? {}),
265
+ states: ['default', 'hover', 'focus', 'disabled', 'error'].filter(
266
+ (state) => component.children.some((c: any) => c.name.toLowerCase().includes(state))
267
+ ),
268
+ responsive: responsiveFrames
269
+ .filter((f) => component.children.some((c: any) => c.name.includes(f.name)))
270
+ .map((f) => f.name),
271
+ a11y: [
272
+ component.children.some((c: any) => c.type === 'TEXT') ? 'Has visible label' : 'Needs aria-label',
273
+ 'Check color contrast ratios',
274
+ 'Verify keyboard navigation order',
275
+ ],
276
+ };
277
+ }
278
+ ```
279
+
280
+ ## Examples
281
+
282
+ | Figma Property | CSS Equivalent | Tailwind Class |
283
+ |----------------|---------------|----------------|
284
+ | Auto-layout horizontal, gap 16 | `display: flex; gap: 16px` | `flex gap-4` |
285
+ | Auto-layout vertical, center | `flex-direction: column; align-items: center` | `flex flex-col items-center` |
286
+ | Fill container (horizontal) | `flex: 1 1 0%` | `flex-1` |
287
+ | Hug contents | `width: fit-content` | `w-fit` |
288
+ | Padding 16/24 | `padding: 16px 24px` | `py-4 px-6` |
289
+
290
+ ## Checklist
291
+ - [ ] Design tokens extracted from Figma variables, not hardcoded from visual inspection
292
+ - [ ] Auto-layout properties mapped to flexbox (not absolute positioning)
293
+ - [ ] Component variants map to React prop types with TypeScript
294
+ - [ ] Responsive layouts checked across all Figma breakpoint frames
295
+ - [ ] Icons exported as SVG with `currentColor` fill for theming
296
+ - [ ] Spacing values use token scale, not arbitrary pixel values
297
+ - [ ] Interactive states (hover, focus, disabled) accounted for in component
298
+ - [ ] Accessibility requirements documented from Figma annotations
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: file-upload
3
+ description: File upload patterns with multipart, presigned URLs, chunked upload, progress tracking, and validation.
4
+ ---
5
+
6
+ # File Upload Patterns
7
+
8
+ ## When to Use
9
+ Apply when users need to upload files -- profile pictures, documents, CSVs, or videos. Direct-to-cloud uploads via presigned URLs are the standard pattern. Never stream large files through your application server. Use chunked uploads for files over 100MB and always validate file type and size on both client and server.
10
+
11
+ ## How It Works
12
+
13
+ ### Presigned URL Upload (Direct to S3/R2)
14
+
15
+ The client uploads directly to cloud storage, bypassing your server:
16
+
17
+ ```typescript
18
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
19
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
20
+
21
+ const s3 = new S3Client({ region: "us-east-1" });
22
+
23
+ app.post("/api/upload/presign", async (req, res) => {
24
+ const { filename, contentType, size } = req.body;
25
+
26
+ const allowedTypes = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
27
+ if (!allowedTypes.includes(contentType)) {
28
+ return res.status(400).json({ error: "File type not allowed" });
29
+ }
30
+ if (size > 10 * 1024 * 1024) {
31
+ return res.status(400).json({ error: "File too large (max 10MB)" });
32
+ }
33
+
34
+ const key = `uploads/${req.user.id}/${crypto.randomUUID()}-${sanitize(filename)}`;
35
+ const command = new PutObjectCommand({
36
+ Bucket: process.env.S3_BUCKET,
37
+ Key: key,
38
+ ContentType: contentType,
39
+ Metadata: { userId: req.user.id },
40
+ });
41
+
42
+ const url = await getSignedUrl(s3, command, { expiresIn: 300 });
43
+ res.json({ uploadUrl: url, key });
44
+ });
45
+ ```
46
+
47
+ Client-side upload:
48
+
49
+ ```typescript
50
+ async function uploadFile(file: File) {
51
+ // 1. Get presigned URL
52
+ const { uploadUrl, key } = await api.post("/upload/presign", {
53
+ filename: file.name,
54
+ contentType: file.type,
55
+ size: file.size,
56
+ });
57
+
58
+ // 2. Upload directly to S3
59
+ await fetch(uploadUrl, {
60
+ method: "PUT",
61
+ headers: { "Content-Type": file.type },
62
+ body: file,
63
+ });
64
+
65
+ // 3. Confirm upload with your API
66
+ await api.post("/upload/confirm", { key });
67
+ return key;
68
+ }
69
+ ```
70
+
71
+ ### Chunked Upload for Large Files
72
+
73
+ For files over 100MB, split into chunks with resume support:
74
+
75
+ ```typescript
76
+ const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
77
+
78
+ async function chunkedUpload(
79
+ file: File,
80
+ onProgress: (percent: number) => void
81
+ ) {
82
+ const { uploadId, key } = await api.post("/upload/multipart/init", {
83
+ filename: file.name,
84
+ contentType: file.type,
85
+ });
86
+
87
+ const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
88
+ const parts: { ETag: string; PartNumber: number }[] = [];
89
+ let completed = 0;
90
+
91
+ for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
92
+ const start = (partNumber - 1) * CHUNK_SIZE;
93
+ const chunk = file.slice(start, start + CHUNK_SIZE);
94
+
95
+ const { presignedUrl } = await api.post("/upload/multipart/presign", {
96
+ key, uploadId, partNumber,
97
+ });
98
+
99
+ const res = await fetch(presignedUrl, { method: "PUT", body: chunk });
100
+ parts.push({ ETag: res.headers.get("etag")!, PartNumber: partNumber });
101
+ completed++;
102
+ onProgress(Math.round((completed / totalChunks) * 100));
103
+ }
104
+
105
+ await api.post("/upload/multipart/complete", {
106
+ key, uploadId,
107
+ parts: parts.sort((a, b) => a.PartNumber - b.PartNumber),
108
+ });
109
+ return key;
110
+ }
111
+ ```
112
+
113
+ ### Progress Tracking with XMLHttpRequest
114
+
115
+ ```typescript
116
+ function uploadWithProgress(
117
+ url: string,
118
+ file: File,
119
+ onProgress: (percent: number) => void
120
+ ): Promise<void> {
121
+ return new Promise((resolve, reject) => {
122
+ const xhr = new XMLHttpRequest();
123
+ xhr.open("PUT", url);
124
+ xhr.setRequestHeader("Content-Type", file.type);
125
+
126
+ xhr.upload.onprogress = (e) => {
127
+ if (e.lengthComputable) {
128
+ onProgress(Math.round((e.loaded / e.total) * 100));
129
+ }
130
+ };
131
+ xhr.onload = () => (xhr.status < 400 ? resolve() : reject(new Error(`Upload failed: ${xhr.status}`)));
132
+ xhr.onerror = () => reject(new Error("Network error"));
133
+ xhr.send(file);
134
+ });
135
+ }
136
+ ```
137
+
138
+ ### Image Processing After Upload
139
+
140
+ ```typescript
141
+ import sharp from "sharp";
142
+
143
+ const variants = [
144
+ { suffix: "thumb", width: 150, height: 150, quality: 80 },
145
+ { suffix: "medium", width: 600, height: 600, quality: 85 },
146
+ { suffix: "large", width: 1200, height: 1200, quality: 90 },
147
+ ];
148
+
149
+ async function processImage(inputKey: string): Promise<Record<string, string>> {
150
+ const obj = await s3.getObject({ Bucket: BUCKET, Key: inputKey });
151
+ const buffer = Buffer.from(await obj.Body!.transformToByteArray());
152
+ const results: Record<string, string> = {};
153
+
154
+ for (const v of variants) {
155
+ const processed = await sharp(buffer)
156
+ .resize(v.width, v.height, { fit: "inside", withoutEnlargement: true })
157
+ .webp({ quality: v.quality })
158
+ .toBuffer();
159
+
160
+ const outputKey = inputKey.replace(/\.[^.]+$/, `-${v.suffix}.webp`);
161
+ await s3.putObject({
162
+ Bucket: BUCKET, Key: outputKey, Body: processed,
163
+ ContentType: "image/webp",
164
+ CacheControl: "public, max-age=31536000, immutable",
165
+ });
166
+ results[v.suffix] = outputKey;
167
+ }
168
+ return results;
169
+ }
170
+ ```
171
+
172
+ ### File Validation
173
+
174
+ ```typescript
175
+ // Server-side validation beyond content-type header
176
+ import { fileTypeFromBuffer } from "file-type";
177
+
178
+ async function validateUpload(key: string): Promise<{ valid: boolean; reason?: string }> {
179
+ const obj = await s3.getObject({ Bucket: BUCKET, Key: key });
180
+ const buffer = Buffer.from(await obj.Body!.transformToByteArray());
181
+
182
+ // Check actual file type (not just extension or Content-Type header)
183
+ const type = await fileTypeFromBuffer(buffer);
184
+ const allowed = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
185
+
186
+ if (!type || !allowed.includes(type.mime)) {
187
+ return { valid: false, reason: `Disallowed file type: ${type?.mime ?? "unknown"}` };
188
+ }
189
+ return { valid: true };
190
+ }
191
+ ```
192
+
193
+ ### Storage Quotas
194
+
195
+ ```typescript
196
+ const STORAGE_LIMITS: Record<string, number> = {
197
+ free: 100 * 1024 * 1024, // 100MB
198
+ pro: 10 * 1024 * 1024 * 1024, // 10GB
199
+ enterprise: 100 * 1024 * 1024 * 1024, // 100GB
200
+ };
201
+
202
+ async function checkStorageQuota(userId: string, fileSize: number): Promise<boolean> {
203
+ const user = await db.users.findById(userId);
204
+ const usedBytes = await db.files.sumSize({ userId });
205
+ const limit = STORAGE_LIMITS[user.plan] ?? STORAGE_LIMITS.free;
206
+ return usedBytes + fileSize <= limit;
207
+ }
208
+ ```
209
+
210
+ ## Examples
211
+
212
+ | File Type | Upload Pattern | Processing |
213
+ |-----------|---------------|------------|
214
+ | Profile picture (< 5MB) | Single presigned PUT | Resize to 3 variants, WebP |
215
+ | CSV import (< 50MB) | Single presigned PUT | Parse in background job |
216
+ | Video (> 100MB) | Multipart chunked | Transcode in background |
217
+ | PDF invoice | Presigned PUT + signed URL | Virus scan, no transform |
218
+ | Bulk images (20 files) | Parallel presigned PUTs | Concurrent pipeline |
219
+
220
+ ## Checklist
221
+ - [ ] Files upload directly to S3/R2 via presigned URLs
222
+ - [ ] Presigned URLs expire within 5-15 minutes
223
+ - [ ] File type validated against allowlist (magic bytes, not just extension)
224
+ - [ ] File size enforced both client-side and server-side
225
+ - [ ] Files over 100MB use multipart/chunked upload with resume
226
+ - [ ] Images processed asynchronously (resize, compress, WebP)
227
+ - [ ] Processed files set `Cache-Control: immutable` for CDN
228
+ - [ ] Per-user storage quotas prevent abuse