Overview
Implement a reusable image upload service that accepts an image file, resizes it into three variants (thumbnail, medium, full), uploads all three to AWS S3, and returns their public URLs. This pipeline is used by both the restaurant module (logo, cover image) and the food item module (item photos).
Figma Reference
📐 Figma: [Upload UX flow & image size specifications — placeholder, link to be added by design lead]
File Location
```
apps/api/src/shared/upload/
upload.service.ts # resize + S3 logic
upload.middleware.ts # multer config
upload.types.ts # UploadResult, ImageVariant types
apps/api/src/modules/upload/
controllers/upload.controller.ts
routes/upload.routes.ts
```
The `upload.service` lives in `shared/` because it is reused by multiple modules. The route/controller are their own thin module.
Image Variants
| Variant |
Max dimension |
Format |
Use |
| `thumbnail` |
200 × 200 px |
WebP |
Feed cards, list views |
| `medium` |
600 × 600 px |
WebP |
Detail screens |
| `full` |
1200 × 1200 px |
WebP |
Hero images |
All variants maintain aspect ratio (object-fit: cover). Use the `sharp` package for resizing.
API Endpoint
```
POST /api/upload
Content-Type: multipart/form-data
Authorization: Bearer
Field name: file
Max file size: 5 MB
Accepted MIME types: image/jpeg, image/png, image/webp
```
Response (200)
```json
{
"thumbnail": "https://cdn.example.com/uploads/abc123-thumb.webp",
"medium": "https://cdn.example.com/uploads/abc123-medium.webp",
"full": "https://cdn.example.com/uploads/abc123-full.webp"
}
```
S3 Key Pattern
```
uploads/{uuid}-{variant}.webp
```
Example: `uploads/550e8400-e29b-41d4-a716-446655440000-thumb.webp`
Environment Variables
Add to `apps/api/.env.example`:
```
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_BUCKET=
AWS_S3_PUBLIC_BASE_URL=https://.s3..amazonaws.com
```
Acceptance Criteria
Dependencies to Install
```bash
npm install sharp multer @aws-sdk/client-s3 uuid --workspace @discoverly/api
npm install @types/multer @types/uuid --save-dev --workspace @discoverly/api
```
Error Responses
| Scenario |
Status |
| No file attached |
400 |
| File exceeds 5 MB |
413 |
| Unsupported MIME type |
415 |
| S3 upload failure |
500 |
| Unauthenticated |
401 |
Testing Notes
Unit-test `upload.service.ts` by mocking the S3 client and verifying:
- Three PutObject calls are made per upload
- Keys follow the UUID pattern
- Returned URLs use `AWS_S3_PUBLIC_BASE_URL` as the base
Do not make real S3 calls in CI. Use `vi.mock` to stub `@aws-sdk/client-s3`.
Out of Scope
- Video uploads
- Avatar/profile images (attached to user — future sprint)
- Image deletion / cleanup
Overview
Implement a reusable image upload service that accepts an image file, resizes it into three variants (thumbnail, medium, full), uploads all three to AWS S3, and returns their public URLs. This pipeline is used by both the restaurant module (logo, cover image) and the food item module (item photos).
Figma Reference
File Location
```
apps/api/src/shared/upload/
upload.service.ts # resize + S3 logic
upload.middleware.ts # multer config
upload.types.ts # UploadResult, ImageVariant types
apps/api/src/modules/upload/
controllers/upload.controller.ts
routes/upload.routes.ts
```
The `upload.service` lives in `shared/` because it is reused by multiple modules. The route/controller are their own thin module.
Image Variants
All variants maintain aspect ratio (object-fit: cover). Use the `sharp` package for resizing.
API Endpoint
```
POST /api/upload
Content-Type: multipart/form-data
Authorization: Bearer
Field name: file
Max file size: 5 MB
Accepted MIME types: image/jpeg, image/png, image/webp
```
Response (200)
```json
{
"thumbnail": "https://cdn.example.com/uploads/abc123-thumb.webp",
"medium": "https://cdn.example.com/uploads/abc123-medium.webp",
"full": "https://cdn.example.com/uploads/abc123-full.webp"
}
```
S3 Key Pattern
```
uploads/{uuid}-{variant}.webp
```
Example: `uploads/550e8400-e29b-41d4-a716-446655440000-thumb.webp`
Environment Variables
Add to `apps/api/.env.example`:
```
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_BUCKET=
AWS_S3_PUBLIC_BASE_URL=https://.s3..amazonaws.com
```
Acceptance Criteria
Dependencies to Install
```bash
npm install sharp multer @aws-sdk/client-s3 uuid --workspace @discoverly/api
npm install @types/multer @types/uuid --save-dev --workspace @discoverly/api
```
Error Responses
Testing Notes
Unit-test `upload.service.ts` by mocking the S3 client and verifying:
Do not make real S3 calls in CI. Use `vi.mock` to stub `@aws-sdk/client-s3`.
Out of Scope