In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
When building modern web applications, file uploads are a common requirement. Whether you're creating a voice recording app, image gallery, or document management system, you'll often need to store files in cloud storage services like Google Cloud Storage (GCS).
In this comprehensive guide, we'll build a file upload system for a web application where users can record audio using the HTML5 Audio API and upload it directly to Google Cloud Storage. Instead of storing files on our server (which can be expensive and resource-intensive), we'll leverage GCS for scalable, reliable file storage.
We'll explore two different approaches for uploading files to GCS and implement the more robust server proxy method that eliminates CORS issues and provides better security.
Understanding Upload Approaches
Before diving into implementation, it's important to understand the different approaches for uploading files to Google Cloud Storage:
Approach A: Direct Client Upload
In this approach, the client (browser) uploads files directly to Google Cloud Storage using pre-signed URLs.
Benefits:
Faster uploads (no server bottleneck)
Reduces server bandwidth usage
Lower server resource consumption
Scales better for high-traffic applications
Downsides:
CORS configuration complexity
Security concerns (credentials visible to client)
Browser compatibility issues
Limited upload validation options
Harder to implement retry logic
Approach B: Server Proxy Upload (Our Choice)
In this approach, files are uploaded to your server first, then your server uploads them to Google Cloud Storage.
Benefits:
No CORS issues
Better security (credentials stay on server)
Server-side validation and processing
Reliable error handling and retry logic
Complete control over upload flow
Better monitoring and logging
Downsides:
Uses server bandwidth
Slightly higher latency
Server resource consumption
Potential bottleneck for concurrent uploads
For this tutorial, we'll implement Approach B (Server Proxy) because it provides the most reliable and secure solution for production applications.
Prerequisites
Before we start, make sure you have:
A Google Cloud Platform account
A Next.js project set up
Basic knowledge of React and API routes
Step 1: Setting Up Google Cloud Storage
In this step, we'll create a Google Cloud project, set up a storage bucket, and configure the necessary permissions for our application to upload files.
Give your project a name (e.g., "file-upload-demo")
Click "Create"
1.2 Enable Cloud Storage API
In your project dashboard, go to "APIs & Services" > "Library"
Search for "Cloud Storage API"
Click on it and press "Enable"
1.3 Create a Storage Bucket
Navigate to "Cloud Storage" > "Buckets"
Click "Create Bucket"
Choose a globally unique bucket name (e.g., "my-app-uploads-bucket")
Select a location (choose based on your users' geography)
Click "Create"
1.4 Configure Bucket Access Control
This is a crucial step that many developers miss. By default, Google Cloud Storage uses uniform bucket-level access, but we need fine-grained access control.
Go to your bucket's details page
Click on the "Permissions" tab
In the "Access control" section, click "Edit"
Select "Fine-grained" instead of "Uniform"
Check "Allow public access" (we'll configure this properly in the next steps)
Click "Save"
bucket settings
Important Concept: Fine-grained access control allows us to set permissions at the object level, which is necessary for our upload mechanism to work properly.
1.5 Create a Service Account
A service account is what our application will use to authenticate with Google Cloud Storage.
Go to "IAM & Admin" > "Service Accounts"
Click "Create Service Account"
Give it a name (e.g., "file-upload-service")
Add description: "Service account for file uploads"
Click "Create and Continue"
In the "Grant this service account access to project" section, add the role "Storage Object Admin"
Click "Continue" and then "Done"
Create new service account
1.6 Generate Service Account Key
Click on your newly created service account
Go to the "Keys" tab
Click "Add Key" > "Create new key"
Select "JSON" format
Click "Create" (this will download a JSON file)
Security Note: Keep this JSON file secure and never commit it to version control.
1.7 Configure Service Account Permissions
Now we need to give our service account permission to create objects in our bucket:
Go back to your bucket's "Permissions" tab
Click "Grant Access"
In "New principals", enter your service account email (found in the JSON file)
In "Select a role", choose "Storage Legacy Bucket Owner and Storage Legacy Object Owner"
Click "Save"
Important Concept: The "Storage Object Creator" role gives our service account the minimum necessary permissions to upload files to the bucket while maintaining security best practices.
Add role to service account
1.8 Convert Service Account Key to Base64
For easier environment variable management, we'll convert our JSON key to Base64:
@google-cloud/storage: Official Google Cloud Storage client library
uuid: For generating unique file names
@types/uuid: TypeScript types for UUID
2.2 Configure Environment Variables
Create a .env.local file in your project root:
env
# Google Cloud Storage Configuration
GOOGLE_CLOUD_PROJECT_ID=your-project-id
GOOGLE_CLOUD_STORAGE_BUCKET=your-bucket-name
GOOGLE_CLOUD_CREDENTIALS_BASE64=your-base64-encoded-json-key
Security Tip: The Base64 approach makes it easier to deploy to platforms like Vercel or Netlify without dealing with file uploads for service account keys.
Key Concept: We're initializing the Google Cloud Storage client with our decoded service account credentials. This client will be used on the server-side to generate pre-signed URLs and handle uploads.
Step 3: Creating Upload Credential Generation
In this step, we'll create a function that generates the necessary credentials for uploading files to Google Cloud Storage. This includes creating pre-signed URLs that allow our server to upload files on behalf of our application.
Pre-signed URLs: These are time-limited URLs that allow uploads without exposing your credentials
File Organization: We organize files by user and date for better management
URL Expiration: 15-minute expiration balances security with usability
Step 4: Building the Server Upload Logic
In this step, we'll create the server-side logic that handles file uploads. Our server will receive files from the client, then upload them to Google Cloud Storage using the pre-signed URLs we generate.
4.1 Create Server Upload Handler
Create src/lib/server-upload.ts:
typescript
import { generateUploadCredentials, UploadCredentials } from'./google-storage';
exportinterfaceUploadResult {
success: boolean;
fileId?: string;
gcsUri?: string;
error?: string;
}
/**
* Uploads a file to Google Cloud Storage from the server
* @paramfileBuffer - The file data as a Buffer
* @paramuserId - User identifier
* @paramfileName - Original filename
* @paramcontentType - MIME type of the file
* @returns Upload result with success status and file information
*/exportconst performServerUpload = async (
fileBuffer: Buffer,
userId: string,
fileName: string,
contentType: string
): Promise<UploadResult> => {
try {
console.log('[Server Upload] Starting upload process', {
bufferSize: fileBuffer.length,
fileName,
contentType,
userId
});
// Generate upload credentialsconst credentials = awaitgenerateUploadCredentials(userId, fileName);
console.log('[Server Upload] Generated credentials successfully');
// Upload file to Google Cloud Storageconst uploadResult = awaituploadToGCS(fileBuffer, credentials, contentType);
console.log('[Server Upload] Upload completed successfully');
return {
success: true,
fileId: credentials.fileId,
gcsUri: credentials.gcsUri,
};
} catch (error: any) {
console.error('[Server Upload] Upload failed:', error);
return {
success: false,
error: error.message,
};
}
};
/**
* Performs the actual upload to Google Cloud Storage
* @paramfileBuffer - File data
* @paramcredentials - Upload credentials from generateUploadCredentials
* @paramcontentType - MIME type
*/const uploadToGCS = async (
fileBuffer: Buffer,
credentials: UploadCredentials,
contentType: string
): Promise<void> => {
console.log('[GCS Upload] Starting upload to Google Cloud Storage');
try {
const response = awaitfetch(credentials.uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': fileBuffer.length.toString(),
},
body: fileBuffer,
});
if (!response.ok) {
const errorText = await response.text();
console.error('[GCS Upload] Upload failed:', {
status: response.status,
statusText: response.statusText,
error: errorText
});
thrownewError(`GCS upload failed with status ${response.status}: ${errorText}`);
}
console.log('[GCS Upload] Upload successful');
} catch (error: any) {
console.error('[GCS Upload] Upload error:', error);
thrownewError(`GCS upload failed: ${error.message}`);
}
};
Key Concepts:
Buffer Handling: We work with Buffer objects for efficient file handling on the server
Error Handling: Comprehensive error handling with detailed logging for debugging
Content-Type: We use 'application/octet-stream' for maximum compatibility
Step 5: Creating the API Route
In this step, we'll create the Next.js API route that serves as the entry point for file uploads. This route will handle incoming requests from the client, validate files, and coordinate the upload process.
Input Validation: We validate file size, type, and required fields before processing
Rate Limiting: Basic protection against abuse (use Redis in production)
Error Handling: Comprehensive error responses with unique error IDs for debugging
CORS Headers: Proper CORS handling for cross-origin requests
Step 6: Building the Client-Side Upload Component
In this step, we'll create a React component that allows users to select and upload files. This component will handle the user interface and communicate with our API route.
6.1 Create Upload Hook
First, let's create a custom hook for handling uploads. Create src/hooks/useFileUpload.ts:
Since we mentioned building a voice recording app, let's add audio recording functionality. This step shows how to integrate HTML5 Audio API with our upload system.
Component Integration: Clean separation of file upload and audio recording
State Management: Centralized upload history tracking
User Feedback: Clear visual feedback for all upload states
Conclusion
We've successfully built a complete file upload system using Google Cloud Storage with a server proxy approach. This implementation provides:
Reliable uploads without CORS complications
Secure credential handling on the server-side
Real-time progress tracking for better user experience
Comprehensive error handling with detailed logging
Audio recording capabilities using HTML5 APIs
The server proxy approach we chose offers maximum reliability and security, making it ideal for production applications. While it uses server resources, the trade-offs in reliability and security make it the preferred choice for most use cases.
What's Next?
We covered the server proxy approach in this article, but there's more to explore! If you're interested in learning about the direct client upload approach (which can be faster but requires more complex CORS configuration), subscribe to our newsletter. We'll cover:
Direct client uploads with pre-signed URLs
Advanced CORS configuration techniques
Handling large file uploads with multipart uploads
Adding file processing pipelines
The complete implementation provides a solid foundation for any application requiring file uploads to Google Cloud Storage. You can extend this further by adding features like file type validation, image resizing, or automatic backup strategies.