A serverless file upload API built with AWS Lambda, S3, and DynamoDB using the SST framework. This API provides secure file upload functionality with presigned URLs and automatic file tracking.
├── src/
│ ├── config/
│ │ └── env.ts # Environment configuration
│ ├── domain/
│ │ └── uploads/
│ │ ├── factories/ # Factory pattern implementations
│ │ │ ├── prepare-upload-factory.ts
│ │ │ └── uploaded-file-factory.ts
│ │ └── use-cases/ # Business logic
│ │ ├── prepare-upload-use-case.ts
│ │ └── uploaded-file-use-case.ts
│ ├── infra/
│ │ ├── adapters/
│ │ │ └── http.ts # HTTP adapter for Lambda
│ │ ├── errors/
│ │ │ └── http-error.ts # Custom HTTP error class
│ │ ├── functions/ # Lambda function handlers
│ │ │ ├── prepare-upload.ts
│ │ │ └── uploaded-file.ts
│ │ └── middlewares/
│ │ └── error-handler.ts # Error handling middleware
│ ├── lib/
│ │ ├── dynamo.ts # DynamoDB client configuration
│ │ └── s3.ts # S3 client configuration
│ └── types/
│ └── http.ts # HTTP type definitions
├── stacks/ # SST infrastructure as code
│ ├── apigw.ts # API Gateway configuration
│ ├── config.ts # App configuration
│ ├── dynamo.ts # DynamoDB table definition
│ ├── index.ts # Stack exports
│ ├── lambda.ts # Lambda function definitions
│ ├── s3.ts # S3 bucket configuration
│ └── utils/
│ └── lambda.ts # Lambda utility functions
├── package.json # Dependencies and scripts
├── sst.config.ts # SST configuration
└── tsconfig.json # TypeScript configuration
- Runtime: Node.js with TypeScript
- Framework: SST (Serverless Stack) for infrastructure as code
- Cloud Provider: AWS
- AWS Lambda: Serverless compute for API endpoints and file processing
- Amazon S3: Object storage for file uploads
- Amazon DynamoDB: NoSQL database for file metadata tracking
- API Gateway V2: HTTP API with CORS support
- @aws-sdk/client-s3: S3 operations and presigned URL generation
- @aws-sdk/client-dynamodb: DynamoDB operations
- @aws-sdk/lib-dynamodb: High-level DynamoDB document client
- @aws-sdk/s3-request-presigner: Secure URL generation for uploads
- @middy/core: Lambda middleware framework
- @middy/http-cors: CORS handling
- @middy/http-json-body-parser: JSON request parsing
- @middy/http-response-serializer: Response formatting
- Handler:
src/infra/functions/prepare-upload.ts
- Endpoint:
POST /prepare-upload
- Purpose: Generates presigned URLs for secure file uploads to S3
- Timeout: 10 seconds
Functionality:
- Accepts an array of file names in the request body
- Generates unique file keys with UUID prefixes
- Creates presigned URLs valid for a configurable duration
- Stores file metadata in DynamoDB with
PENDING
status - Returns array of presigned URLs for client-side uploads
Permissions:
s3:PutObject
on uploads folderdynamodb:PutItem
on the files table
- Handler:
src/infra/functions/uploaded-file.ts
- Trigger: S3 ObjectCreated:Put events with
uploads/
prefix - Purpose: Updates file status when uploads complete
- Timeout: 10 seconds
Functionality:
- Triggered automatically when files are uploaded to S3
- Updates file status from
PENDING
toUPLOADED
in DynamoDB - Removes expiration timestamp to make records permanent
- Processes multiple files in parallel
Permissions:
dynamodb:UpdateItem
on the files table
Here's how to integrate the Upaki API in your frontend application:
class UpakiClient {
constructor(apiUrl) {
this.apiUrl = apiUrl;
}
async uploadFiles(files) {
try {
// Step 1: Prepare upload URLs
const fileNames = Array.from(files).map(file => file.name);
const response = await fetch(`${this.apiUrl}/prepare-upload`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ fileNames }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const { urls } = await response.json();
// Step 2: Upload files to S3 using presigned URLs
const uploadPromises = files.map(async (file, index) => {
const uploadResponse = await fetch(urls[index], {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed for ${file.name}`);
}
return {
fileName: file.name,
url: urls[index].split('?')[0], // Remove query params to get final URL
status: 'uploaded',
};
});
const results = await Promise.all(uploadPromises);
return results;
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
}
}
// Usage
const client = new UpakiClient('https://your-api-gateway-url');
// Handle file input
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const files = event.target.files;
if (files.length > 0) {
try {
const results = await client.uploadFiles(files);
console.log('Upload successful:', results);
} catch (error) {
console.error('Upload failed:', error);
}
}
});
import { useState, useCallback } from 'react';
export const useFileUpload = (apiUrl) => {
const [uploading, setUploading] = useState(false);
const [error, setError] = useState(null);
const uploadFiles = useCallback(async (files) => {
setUploading(true);
setError(null);
try {
// Prepare upload
const fileNames = Array.from(files).map(file => file.name);
const response = await fetch(`${apiUrl}/prepare-upload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileNames }),
});
const { urls } = await response.json();
// Upload files
const uploadPromises = files.map((file, index) =>
fetch(urls[index], {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
})
);
await Promise.all(uploadPromises);
return urls.map(url => url.split('?')[0]);
} catch (err) {
setError(err.message);
throw err;
} finally {
setUploading(false);
}
}, [apiUrl]);
return { uploadFiles, uploading, error };
};
// Component usage
const FileUploader = () => {
const { uploadFiles, uploading, error } = useFileUpload(process.env.REACT_APP_API_URL);
const handleFileChange = async (event) => {
const files = event.target.files;
if (files.length > 0) {
try {
const fileUrls = await uploadFiles(files);
console.log('Files uploaded:', fileUrls);
} catch (error) {
console.error('Upload failed:', error);
}
}
};
return (
<div>
<input
type="file"
multiple
onChange={handleFileChange}
disabled={uploading}
/>
{uploading && <p>Uploading...</p>}
{error && <p>Error: {error}</p>}
</div>
);
};
# Step 1: Get presigned URLs
curl -X POST https://your-api-gateway-url/prepare-upload \
-H "Content-Type: application/json" \
-d '{"fileNames": ["document.pdf", "image.jpg"]}'
# Response:
# {
# "urls": [
# "https://bucket.s3.amazonaws.com/uploads/uuid-document.pdf?presigned-params...",
# "https://bucket.s3.amazonaws.com/uploads/uuid-image.jpg?presigned-params..."
# ]
# }
# Step 2: Upload files using the presigned URLs
curl -X PUT "https://bucket.s3.amazonaws.com/uploads/uuid-document.pdf?presigned-params..." \
-H "Content-Type: application/pdf" \
--data-binary @document.pdf
curl -X PUT "https://bucket.s3.amazonaws.com/uploads/uuid-image.jpg?presigned-params..." \
-H "Content-Type: image/jpeg" \
--data-binary @image.jpg
- Node.js 18+ and npm/yarn
- AWS CLI configured with appropriate credentials
- SST CLI installed globally:
npm install -g sst
# Install dependencies
npm install
# Start local development
sst dev
# Deploy to AWS
sst deploy
# Remove all resources
sst remove
The API uses environment variables configured through SST:
BUCKET_NAME
: S3 bucket for file storageTABLE_NAME
: DynamoDB table for file metadata- Upload expiration time is configurable in the environment configuration
MIT