Princewill

Back to Blog
Cloud Engineering

Building a Production-Ready AWS S3 Image Upload System with Next.js 16 and TypeScript

July 11, 2026
5
#backend#nextjs#AWS#s3bucket#javascript#typescript
Building a Production-Ready AWS S3 Image Upload System with Next.js 16 and TypeScript

Learn how I built a production-ready image management application using Next.js 16, AWS S3, TypeScript, and presigned URLs. Instead of routing uploads through the application server, the app uses direct browser-to-S3 uploads with an automatic server-side fallback, secure validation, pagination, and a responsive image gallery.

When building applications that allow users to upload files, it’s tempting to send everything through your application server. While this works for small projects, it quickly becomes expensive, slower, and harder to scale. I wanted to build an image upload system that follows the same architectural principles used in modern SaaS products. The result is AWS S3 Image Upload, a focused image manager built with Next.js 16, React 19, TypeScript, PostgreSQL-ready architecture, and Amazon S3. The application supports direct uploads using presigned URLs, automatic fallback uploads when necessary, image previews, cursor pagination, secure validation, and object management—all while keeping the server lightweight. Rather than demonstrating only how to upload files, this project explores the engineering decisions involved in building a production-ready storage workflow.

Why Direct-to-S3 Uploads Matter

Many applications upload files like this: Browser → Application Server → AWS S3 Although simple, every file passes through your server first. That increases bandwidth usage, CPU utilization, memory consumption, and overall hosting costs. Instead, I used presigned URLs so the browser uploads files directly to Amazon S3.

Browser
    │
    │ Request upload URL
    ▼
Next.js API
    │
    │ Generate presigned URL
    ▼
Browser
    │
    │ Upload directly
    ▼
Amazon S3

Building a Reliable Upload Workflow

Direct uploads are efficient, but they introduce new challenges. Browsers may fail to upload because of CORS configuration, network interruptions, or expired signatures. Instead of simply displaying an error, the application automatically falls back to uploading through the Next.js server. The user experiences a seamless upload while the application maintains a consistent object key across both upload strategies.

Try Direct Upload
        │
        ▼
Success?
   │         │
 Yes        No
   │         │
   ▼         ▼
 Complete   Server Fallback
                │
                ▼
             Upload to S3

Protecting the Storage Bucket

Client-side validation improves the user experience, but it should never be trusted on its own. Before generating an upload URL, the API validates: File size, MIME type, Supported extensions. For the fallback upload route, the application performs an additional validation step using the file-type package to inspect the file’s binary signature rather than relying solely on browser-provided metadata. This extra layer helps prevent malicious uploads disguised as image files.

  • Maximum file size: 5 MB
  • Supported formats: JPG, PNG, GIF, WebP, AVIF
  • MIME type validation
  • Extension validation
  • Binary signature validation
  • Sanitized filenames
  • Secure object key generation

Managing Images After Upload

Uploading files is only one part of the experience. Users also need a fast way to browse and manage previously uploaded images. The application provides: Cursor-based pagination ,Optimistic UI updates, Full-screen previews, Delete confirmation, Toast notifications, Lazy image loading. Instead of requesting the entire bucket, images are loaded in small pages, making the interface responsive even as the number of uploads grows.

GET /api/files

↓

ListObjectsV2

↓

12 Images Returned

↓

Load More

↓

Next Cursor

Designing for Production Instead of Demos

One design decision I intentionally made was separating the application into clear layers. The user interface manages interactions, API routes handle authentication and upload orchestration, utility modules encapsulate AWS-specific logic, and Amazon S3 remains responsible for storing files. This separation keeps each component focused on a single responsibility, making the project easier to test, extend, and maintain.

What I Learned

This project reinforced that production-ready applications are shaped by architectural decisions rather than individual features. Using presigned URLs reduces server load, implementing a fallback upload path improves reliability, validating files at multiple layers increases security, and separating responsibilities makes the codebase easier to evolve. Building this image manager wasn’t just about uploading files to S3. it was an opportunity to explore patterns that scale beyond a demo application and are commonly used in modern cloud-native software.

Building a Production-Ready AWS S3 Image Upload System with Next.js 16 and TypeScript | Nanakumor Princewill