Skip to Content
Presign Documentation

Presign Documentation

Overview

AWS S3 presigned URLs provide secure, time-limited access to S3 objects without requiring AWS credentials. This feature enables controlled file sharing and temporary download access.

Table of Contents

AWS S3 Presign URL Get

AWS S3 presigned URLs for downloads enable secure, temporary access to private S3 objects without exposing credentials. This approach is ideal for sharing files with expiring links, providing controlled access to protected resources, and enabling direct client downloads.

How It Works:

  1. Client requests a presigned URL from the backend with the file key
  2. Backend generates a time-limited presigned URL and returns it to the client
  3. Client downloads the file directly from S3 using the presigned URL via HTTP GET
  4. URL expires after the configured time period

Implementation:

Step 1 - Request DTO:

export class UserGetPhotoProfilePresignRequestDto { @ApiProperty({ required: true, description: 'Photo path key from S3', example: 'user/profile/unique-photo-key.jpg', }) @IsString() @IsNotEmpty() key: string; }

Step 2 - Controller Endpoint:

@Controller('users') export class UserController { constructor(private readonly userService: UserService) {} @Response('user.getPhotoProfilePresign') @UserProtected() @AuthJwtAccessProtected() @Post('/profile/get-presign/photo') @HttpCode(HttpStatus.OK) async getPhotoProfilePresign( @AuthJwtPayload('userId') userId: string, @Body() body: UserGetPhotoProfilePresignRequestDto ): Promise<IResponseReturn<AwsS3PresignDto>> { return this.userService.getPhotoProfilePresign(userId, body); } }

Step 3 - Service Implementation:

@Injectable() export class UserService { constructor( private readonly awsS3Service: AwsS3Service, private readonly userRepository: UserRepository ) {} async getPhotoProfilePresign( userId: string, { key }: UserGetPhotoProfilePresignRequestDto ): Promise<IResponseReturn<AwsS3PresignDto>> { // Verify user owns the file const user = await this.userRepository.findOneById(userId); if (user.photo?.key !== key) { throw new ForbiddenException('Access denied to this resource'); } const presign: AwsS3PresignDto = await this.awsS3Service.presignGetItem( key, { access: EnumAwsS3Accessibility.PUBLIC, expired: 3600 // 1 hour } ); return { data: presign }; } }

Step 4 - Client-Side Download:

async function downloadPhotoWithPresign(key: string) { try { // Request presigned URL const response = await fetch('/api/users/profile/get-presign/photo', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ key }) }); const { data: presignData } = await response.json(); // Download directly from S3 const fileResponse = await fetch(presignData.presignUrl, { method: 'GET' }); const blob = await fileResponse.blob(); // Create download link const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = key.split('/').pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); console.log('Photo downloaded successfully!'); } catch (error) { console.error('Download failed:', error); } }

Configuration Options:

interface IAwsS3PresignGetItemOptions { access?: EnumAwsS3Accessibility; // PUBLIC or PRIVATE expired?: number; // Expiration time in seconds (default from config) }

Response Structure:

interface AwsS3PresignDto { presignUrl: string; // The presigned URL for download key: string; // S3 object key extension: string; // File extension mime: string; // MIME type expiredIn: number; // URL expiration time in seconds }

Usage Examples:

// Example 1: Generate presign URL for private file const presign = await awsS3Service.presignGetItem( 'user/123/documents/report.pdf', { access: EnumAwsS3Accessibility.PRIVATE, expired: 300 // 5 minutes } ); // Example 2: Generate presign URL with default expiration const presign = await awsS3Service.presignGetItem( 'public/images/banner.jpg' ); // Example 3: Stream download in Node.js const presign = await awsS3Service.presignGetItem('data/export.csv'); const response = await fetch(presign.presignUrl); const stream = response.body;

AWS S3 Presigned URL Get Flow

AWS S3 Presigned URL Upload

AWS S3 presigned URLs enable secure client-side direct uploads to S3 without exposing AWS credentials. This approach is ideal for large files, reduces server bandwidth, and improves upload performance.

How It Works:

  1. Client requests a presigned URL from the backend with file metadata
  2. Backend generates a time-limited presigned URL and returns it to the client
  3. Client uploads the file directly to S3 using the presigned URL via HTTP PUT
  4. Client notifies the backend of the successful upload with the S3 key

Implementation:

Step 1 - Request DTO:

export class UserGeneratePhotoProfileRequestDto { @ApiProperty({ type: 'string', enum: EnumFileExtensionImage, default: EnumFileExtensionImage.JPG, }) @IsString() @IsEnum(EnumFileExtensionImage) @IsNotEmpty() extension: EnumFileExtensionImage; @ApiProperty({ required: true, description: 'File size in bytes', }) @IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0, }) @IsInt() @IsNotEmpty() size: number; } export class UserUpdateProfilePhotoRequestDto { @ApiProperty({ required: true, description: 'Photo path key from S3', example: 'user/profile/unique-photo-key.jpg', }) @IsString() @IsNotEmpty() photo: string; @ApiProperty({ required: true, description: 'File size in bytes', }) @IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0, }) @IsInt() @IsNotEmpty() size: number; }

Step 2 - Controller Endpoints:

@Controller('users') export class UserController { constructor(private readonly userService: UserService) {} @Response('user.generatePhotoProfilePresign') @UserProtected() @AuthJwtAccessProtected() @Post('/profile/generate-presign/photo') @HttpCode(HttpStatus.OK) async generatePhotoProfilePresign( @AuthJwtPayload('userId') userId: string, @Body() body: UserGeneratePhotoProfileRequestDto ): Promise<IResponseReturn<AwsS3PresignDto>> { return this.userService.generatePhotoProfilePresign(userId, body); } @Response('user.updatePhotoProfile') @UserProtected() @AuthJwtAccessProtected() @Put('/profile/update/photo') async updatePhotoProfile( @AuthJwtPayload('userId') userId: string, @Body() body: UserUpdateProfilePhotoRequestDto, @RequestIPAddress() ipAddress: string, @RequestUserAgent() userAgent: RequestUserAgentDto ): Promise<IResponseReturn<void>> { return this.userService.updatePhotoProfile(userId, body, { ipAddress, userAgent, }); } }

Step 3 - Service Implementation:

@Injectable() export class UserService { constructor( private readonly awsS3Service: AwsS3Service, private readonly userRepository: UserRepository, private readonly fileService: FileService ) {} async generatePhotoProfilePresign( userId: string, { extension, size }: UserGeneratePhotoProfileRequestDto ): Promise<IResponseReturn<AwsS3PresignDto>> { const key: string = this.fileService.createRandomFilename({ path: `user/${userId}/profile`, prefix: 'photo', extension, }); const presign: AwsS3PresignDto = await this.awsS3Service.presignPutItem( { key, size }, { forceUpdate: true, access: EnumAwsS3Accessibility.PUBLIC, expired: 3600 // 1 hour } ); return { data: presign }; } async updatePhotoProfile( userId: string, { photo, size }: UserUpdateProfilePhotoRequestDto, requestLog: IRequestLog ): Promise<IResponseReturn<void>> { const aws: AwsS3Dto = this.awsS3Service.mapPresign({ key: photo, size }); await this.userRepository.updatePhotoProfile(userId, aws, requestLog); return; } }

Step 4 - Client-Side Upload:

async function uploadPhotoWithPresign(file: File) { try { // Request presigned URL const response = await fetch('/api/users/profile/generate-presign/photo', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ extension: file.name.split('.').pop(), size: file.size }) }); const { data: presignData } = await response.json(); // Upload directly to S3 await fetch(presignData.presignUrl, { method: 'PUT', headers: { 'Content-Type': presignData.mime, 'Content-Length': file.size.toString(), 'x-amz-checksum-algorithm': 'SHA256' }, body: file }); // Notify backend await fetch('/api/users/profile/update/photo', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ photo: presignData.key, size: file.size }) }); console.log('Photo uploaded successfully!'); } catch (error) { console.error('Upload failed:', error); } }

Configuration Options:

interface IAwsS3PresignPutItemOptions { access?: EnumAwsS3Accessibility; // PUBLIC or PRIVATE expired?: number; // Expiration time in seconds (default from config) forceUpdate?: boolean; // Allow overwriting existing files }

Response Structure:

interface AwsS3PresignDto { presignUrl: string; // The presigned URL for upload key: string; // S3 object key extension: string; // File extension mime: string; // MIME type expiredIn: number; // URL expiration time in seconds }

AWS S3 Presigned URL Upload Flow

Flow Explanation:

  1. Generate Presigned URL Stage:

    • Client requests presigned URL with file metadata (extension, size)
    • Backend generates unique S3 key using FileService
    • AwsS3Service creates time-limited presigned URL (default 1 hour)
    • Backend returns presigned URL data to client
  2. Direct Upload Stage:

    • Client uploads file directly to S3 using presigned URL
    • No backend involvement during actual file transfer
    • S3 validates request using presigned URL signature
    • Reduces server bandwidth and improves performance
  3. Database Update Stage:

    • Client notifies backend with S3 key and file size
    • Backend maps presign data to AwsS3Dto
    • Repository updates user profile with S3 file reference
    • Transaction logged with IP address and user agent

Important Notes:

  • Presigned URLs expire after configured time (default: 1 hour)
  • Client must use exact Content-Type and Content-Length headers
  • Failed uploads don’t update database (client handles retry)
  • S3 key is generated before upload to ensure uniqueness
Last updated on