Secure, production-ready image upload, storage, and retrieval using Node.js, AWS S3, and PostgreSQL (via Prisma). Images are stored in S3; only the S3 key reference is saved in the database.
- Upload single or multiple images via REST API
- Auto-resize and convert to WebP using Sharp
- Magic-byte validation (prevents MIME spoofing)
- Referer-locked S3 bucket policy (only your domain can access images)
- Pre-signed URLs for private assets (1-hour TTL)
- Polymorphic DB schema — attach images to any entity
- Least-privilege IAM policy
- CORS locked to your domain
- Node.js 18+
- PostgreSQL (or swap Prisma provider for MySQL/SQLite)
- AWS account with S3 access
- AWS CLI configured (
aws configure)
git clone <repo-url>
cd s3-image-app
npm installcp .env.example .env
# Edit .env with your valuesnpx prisma generate
npx prisma migrate dev --name init# Create bucket
aws s3api create-bucket \
--bucket your-app-images \
--region ap-south-1 \
--create-bucket-configuration LocationConstraint=ap-south-1
# Apply security configs
chmod +x scripts/setup-s3.sh
./scripts/setup-s3.sh your-app-images https://yourwebsite.comnpm run dev # Development with hot reload
npm start # ProductionAll endpoints require Authorization: Bearer <JWT> header.
POST /api/images/upload
Content-Type: multipart/form-data
Fields:
image (file, required) — image file, max 5MB
folder (string) — S3 prefix: "avatars", "products", etc. (default: "uploads")
alt (string) — alt text
entityId (string) — ID of related entity
entityType (string) — "Product", "User", etc.
Response:
{
"success": true,
"image": {
"id": "uuid",
"s3Key": "uploads/abc123.webp",
"url": "https://bucket.s3.region.amazonaws.com/uploads/abc123.webp",
"width": 800,
"height": 600,
"sizeBytes": 42000,
"createdAt": "2024-01-01T00:00:00.000Z"
}
}POST /api/images/upload-many
Content-Type: multipart/form-data
Fields:
images[] (files, required) — up to 10 images
folder (string)
entityId (string)
entityType (string)
GET /api/images/:id
Returns DB record with a fresh pre-signed URL.
GET /api/images/:id/url?expiresIn=3600
GET /api/images/entity/:entityType/:entityId
DELETE /api/images/:id
Deletes from both S3 and the database.
The config/s3-bucket-policy.json restricts GetObject to requests with your domain in the Referer header. Direct URL access, curl, and hotlinking from other sites all return 403 Forbidden.
Replace YOUR-BUCKET-NAME and domain before applying.
aws s3api put-bucket-policy \
--bucket your-app-images \
--policy file://config/s3-bucket-policy.jsonaws s3api put-bucket-cors \
--bucket your-app-images \
--cors-configuration file://config/s3-cors.jsonAttach config/iam-policy.json to your EC2 instance role or ECS task role. This grants only the minimum permissions needed (PutObject, GetObject, DeleteObject, HeadObject, ListBucket).
In production, never use root credentials or hardcoded keys.
| Use Case | Strategy |
|---|---|
| Product images, blog covers | Store in public/ prefix; serve via bucket URL; protected by Referer policy |
| User avatars, invoices, documents | Store in private/ prefix; always generate a fresh pre-signed URL |
Use AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY in .env with a dev IAM user.
Attach an IAM Role to your instance or task. Remove the key env vars — the AWS SDK auto-discovers credentials from the metadata service.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npx prisma generate
EXPOSE 3000
CMD ["node", "src/index.js"]npm install # Install dependencies
npm run db:generate # Generate Prisma client
npm run db:migrate # Run DB migrations
npm run dev # Start dev server (nodemon)
npm start # Start production servers3-image-app/
├── src/
│ ├── index.js # Express app entry point
│ ├── services/
│ │ └── s3Service.js # S3 upload, pre-signed URL, delete
│ ├── routes/
│ │ └── images.js # REST endpoints
│ ├── middleware/
│ │ └── auth.js # JWT authentication
│ └── db/
│ └── index.js # Prisma client singleton
├── prisma/
│ └── schema.prisma # DB schema
├── config/
│ ├── s3-bucket-policy.json # S3 bucket policy (Referer lock)
│ ├── s3-cors.json # CORS configuration
│ └── iam-policy.json # IAM least-privilege policy
├── scripts/
│ └── setup-s3.sh # One-command S3 setup script
├── .env.example
├── .gitignore
└── package.json
MIT