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 upload/download access with built-in encryption and security.
Related Documents
- Message Documentation - For internationalization and error message translation
- Handling Error Documentation - For exception handling and response formatting
- Doc Documentation - For API documentation integration with DTOs
- File Upload Documentation - For file validation pipes
Table of Contents
- Overview
- Related Documents
- AWS S3 Presigned URL Get
- AWS S3 Presigned URL Upload
- Error Handling
- Troubleshooting
AWS S3 Presigned 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
- Client requests a presigned URL from the backend with the file key
- Backend validates user authorization and generates a time-limited presigned URL
- Client downloads the file directly from S3 using the presigned URL via HTTP GET
- URL expires after the configured time period (default: 1 hour)
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.private,
expired: 3600 // 1 hour
}
);
return { data: presign };
}
}Step 4 - Client-Side Download:
async function downloadPhotoWithPresign(key: string) {
try {
// Step 1: 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 })
});
if (!response.ok) {
throw new Error('Failed to get presigned URL');
}
const { data: presignData } = await response.json();
// Step 2: Download directly from S3
const fileResponse = await fetch(presignData.presignUrl, {
method: 'GET'
});
if (!fileResponse.ok) {
throw new Error('Failed to download file from S3');
}
const blob = await fileResponse.blob();
// Step 3: Trigger browser download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = key.split('/').pop() || 'download';
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);
// Handle error appropriately (show user notification, etc.)
}
}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 (5 minutes)
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 (1 hour)
const presign = await awsS3Service.presignGetItem(
'user/456/images/banner.jpg',
{
access: EnumAwsS3Accessibility.private
}
);
// Example 3: Stream download in Node.js
const presign = await awsS3Service.presignGetItem('data/export.csv');
const response = await fetch(presign.presignUrl);
const buffer = await response.arrayBuffer();Flow Diagram
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
- Client requests a presigned URL from the backend with file metadata (extension, size)
- Backend generates a unique S3 key and time-limited presigned URL
- Client uploads the file directly to S3 using the presigned URL via HTTP PUT
- Client notifies the backend of successful upload with the S3 key
- Backend saves file reference to database with audit trail
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>> {
// Generate unique S3 key
const key: string = this.fileService.createRandomFilename({
path: `user/${userId}/profile`,
prefix: 'photo',
extension,
});
// Generate presigned URL (1 hour expiration, auto-encrypted)
const presign: AwsS3PresignDto = await this.awsS3Service.presignPutItem(
{ key, size },
{
forceUpdate: true,
access: EnumAwsS3Accessibility.private,
expired: 3600 // 1 hour
}
);
return { data: presign };
}
async updatePhotoProfile(
userId: string,
{ photo, size }: UserUpdateProfilePhotoRequestDto,
requestLog: IRequestLog
): Promise<IResponseReturn<void>> {
// Map presign data to AWS S3 DTO
const aws: AwsS3Dto = this.awsS3Service.mapPresign({ key: photo, size });
// Save to database with audit trail
await this.userRepository.updatePhotoProfile(userId, aws, requestLog);
return;
}
}Step 4 - Client-Side Upload:
async function uploadPhotoSimple(file: File) {
try {
// Step 1: 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();
// Step 2: Upload to S3 (simple PUT request)
const uploadResponse = await fetch(presignData.presignUrl, {
method: 'PUT',
headers: {
'Content-Type': presignData.mime,
},
body: file
});
if (!uploadResponse.ok) {
throw new Error('S3 upload failed');
}
// Step 3: 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('Upload complete!');
} catch (error) {
console.error('Upload failed:', error);
throw 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 (save this for later reference)
extension: string; // File extension
mime: string; // MIME type (use this as Content-Type header)
expiredIn: number; // URL expiration time in seconds
}Flow Diagram
Flow Explanation:
-
Generate Presigned URL Stage:
- Client requests presigned URL with file metadata (extension, size)
- Backend generates unique S3 key using
FileService AwsS3Servicecreates time-limited presigned URL with encryption enabled- Backend returns presigned URL data to client
-
Direct Upload Stage:
- Client uploads file directly to S3 using presigned URL
- Only
Content-Typeheader needed (encryption is automatic) - No backend involvement during actual file transfer
- S3 encrypts file at rest with AES-256
- Reduces server bandwidth and improves performance
-
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 for audit trail