Skip to content

abnerpersio/upaki-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Upaki API

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.

Project Structure

├── 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

Tech Stack

Core Technologies

  • Runtime: Node.js with TypeScript
  • Framework: SST (Serverless Stack) for infrastructure as code
  • Cloud Provider: AWS

AWS Services

  • 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

Key Dependencies

  • @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

Lambda Functions

1. Prepare Upload Function

  • 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 folder
  • dynamodb:PutItem on the files table

2. Uploaded File Function

  • 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 to UPLOADED in DynamoDB
  • Removes expiration timestamp to make records permanent
  • Processes multiple files in parallel

Permissions:

  • dynamodb:UpdateItem on the files table

API Usage Examples

Frontend Client Integration

Here's how to integrate the Upaki API in your frontend application:

JavaScript/TypeScript Example

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);
    }
  }
});

React Hook Example

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>
  );
};

cURL Example

# 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

Development

Prerequisites

  • Node.js 18+ and npm/yarn
  • AWS CLI configured with appropriate credentials
  • SST CLI installed globally: npm install -g sst

Local Development

# Install dependencies
npm install

# Start local development
sst dev

# Deploy to AWS
sst deploy

# Remove all resources
sst remove

Environment Variables

The API uses environment variables configured through SST:

  • BUCKET_NAME: S3 bucket for file storage
  • TABLE_NAME: DynamoDB table for file metadata
  • Upload expiration time is configurable in the environment configuration

License

MIT

About

API to manage S3 presigned upload URLs | Built on JStack fullstack course

Topics

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •