Image API Security: Best Practices for Production
Your image generation API works. The templates render, the images come back, and everything looks great in development. But is it secure?
Most teams skip security until something goes wrong. Maybe someone finds your API key in a public GitHub repo. Maybe a bot hammers your endpoint and burns through your monthly credits in an hour. Maybe a user injects a script tag into a text field and it ends up rendered in an image shared across thousands of users.
These aren't hypothetical problems. They happen all the time. The good news: fixing them isn't hard if you know what to look for. This guide walks through the security practices you should have in place before your image generation API integration goes to production.
Api key managementAPI Key Management
Your API key is the single most important secret in your integration. If someone gets it, they can generate images on your account, rack up charges, and potentially access your templates. Protecting it starts with how you store it.
Environment variablesEnvironment Variables
Never hardcode API keys in your source code. Not in a config file, not in a constant, not even in a "temporary" test file. Use environment variables instead.
# .env file (add to .gitignore!)
IMEJIS_API_KEY=your_api_key_here// Load from environment
const apiKey = process.env.IMEJIS_API_KEY
if (!apiKey) {
throw new Error("IMEJIS_API_KEY is not set")
}Every hosting platform has its own way to set environment variables:
- Vercel: Settings > Environment Variables
- Railway: Variables tab in your service
- AWS: Parameter Store or Secrets Manager
- Docker:
--env-fileflag or Docker secrets
Add .env to your .gitignore immediately. If you've already committed a .env file, rotate your keys right away. Git history is forever.
The backend proxy patternThe Backend Proxy Pattern
This is non-negotiable: never call the image API directly from your frontend. Any API key in client-side JavaScript is visible to anyone who opens browser DevTools. It takes about ten seconds to find.
Instead, create a backend endpoint that acts as a proxy. Your frontend calls your server, your server adds the API key and calls the image API.
// Backend proxy endpoint (Express.js)
app.post("/api/generate-image", authenticate, async (req, res) => {
try {
const response = await fetch("https://api.imejis.io/v1/images", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IMEJIS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
template: req.body.template,
data: req.body.data,
}),
})
const image = await response.json()
res.json(image)
} catch (error) {
res.status(500).json({ error: "Image generation failed" })
}
})// Frontend - calls YOUR server, not the API directly
const response = await fetch("/api/generate-image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template: "social-card", data: cardData }),
})If you're working in Next.js, you can use API routes or Server Actions to keep the key server-side. For React apps, the proxy pattern is covered in detail here.
Key rotation is another thing most teams skip. Here's how to do it without downtime:
- Generate a new API key in your Imejis dashboard.
- Update the environment variable on your server.
- Deploy and verify the new key works.
- Revoke the old key.
Keep both keys active during the transition. Set a calendar reminder to rotate keys every 90 days.
Input validationInput Validation
Every piece of data that flows through your proxy to the image API should be validated. Don't trust user input. Don't trust frontend code. Validate everything server-side before it leaves your backend.
Text fieldsText Fields
Text inputs are the most common attack vector. Set length limits, strip dangerous characters, and reject anything that looks like code injection.
function validateTextField(value, maxLength = 200) {
if (typeof value !== "string") {
throw new Error("Expected string value")
}
// Trim and enforce length
const trimmed = value.trim()
if (trimmed.length === 0) {
throw new Error("Text field cannot be empty")
}
if (trimmed.length > maxLength) {
throw new Error(`Text exceeds ${maxLength} characters`)
}
// Strip HTML/script tags
const sanitized = trimmed
.replace(/<[^>]*>/g, "")
.replace(/javascript:/gi, "")
.replace(/on\w+=/gi, "")
return sanitized
}Why does this matter for image generation? Because user-supplied text gets rendered into images that might be shared publicly, embedded in emails, or displayed on websites. Malicious content in an image can damage your brand or violate platform policies.
Image urlsImage URLs
If your template accepts user-supplied image URLs (for avatars, logos, etc.), you need to validate those too.
function validateImageUrl(url) {
// Check it's a valid URL
let parsed
try {
parsed = new URL(url)
} catch {
throw new Error("Invalid URL format")
}
// HTTPS only
if (parsed.protocol !== "https:") {
throw new Error("Only HTTPS URLs are allowed")
}
// Allowlist domains
const allowedDomains = [
"images.example.com",
"cdn.example.com",
"avatars.githubusercontent.com",
"lh3.googleusercontent.com",
]
if (
!allowedDomains.some(
(d) => parsed.hostname === d || parsed.hostname.endsWith(`.${d}`)
)
) {
throw new Error("Image domain not allowed")
}
// Check file extension
const allowedExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"]
const hasValidExt = allowedExtensions.some((ext) =>
parsed.pathname.toLowerCase().endsWith(ext)
)
if (!hasValidExt) {
throw new Error("Invalid image format")
}
return url
}Domain allowlisting is important. Without it, someone could pass a URL pointing to an internal service (SSRF attack) or to inappropriate content.
Numeric fieldsNumeric Fields
Numeric inputs like font sizes, dimensions, or opacity values should be type-checked and range-validated.
function validateNumber(value, min, max, fieldName) {
const num = Number(value)
if (isNaN(num)) {
throw new Error(`${fieldName} must be a number`)
}
if (num < min || num > max) {
throw new Error(`${fieldName} must be between ${min} and ${max}`)
}
return num
}
// Usage
const fontSize = validateNumber(req.body.data.fontSize, 8, 120, "fontSize")
const width = validateNumber(req.body.data.width, 100, 2000, "width")Put all your validation in a single function that runs before any API call:
function validateTemplateData(data) {
return {
title: validateTextField(data.title, 100),
subtitle: validateTextField(data.subtitle, 200),
avatarUrl: validateImageUrl(data.avatarUrl),
fontSize: validateNumber(data.fontSize, 8, 120, "fontSize"),
}
}Rate limiting your proxyRate Limiting Your Proxy
Without rate limiting, a single user or bot can burn through your entire API quota. Rate limiting is your first line of defense against abuse, and it protects your wallet.
Per user limitsPer-User Limits
For authenticated endpoints, limit by user ID. This is the most fair approach since it ties usage to an account.
A reasonable starting point: 30 images per user per hour for standard accounts. Adjust based on your product's actual usage patterns.
Per ip limitsPer-IP Limits
For public or semi-public endpoints, limit by IP address. This catches unauthenticated abuse. Be more conservative here, maybe 10 requests per IP per minute.
ImplementationImplementation
Here's a practical setup using express-rate-limit:
import rateLimit from "express-rate-limit"
// General rate limit by IP
const ipLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10,
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many requests. Try again in a minute." },
})
// Per-user rate limit (after authentication)
const userLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 30,
keyGenerator: (req) => req.user.id, // Use authenticated user ID
message: { error: "Hourly image limit reached." },
})
app.post(
"/api/generate-image",
ipLimiter,
authenticate,
userLimiter,
async (req, res) => {
// ... image generation logic
}
)For production workloads, consider a Redis-backed rate limiter like rate-limit-redis. In-memory limiters don't work across multiple server instances.
For more on rate limiting strategies and caching generated images, see the rate limits and caching guide.
Abuse preventionAbuse Prevention
Rate limiting stops brute-force abuse, but there are subtler problems to watch for.
Content moderationContent Moderation
If users can supply text or images that end up in generated images, you're responsible for what gets created. Someone will eventually try to generate offensive content.
Options for moderation:
- Keyword filtering: Maintain a blocklist of terms. Simple but easy to circumvent.
- AI-based moderation: Services like OpenAI's Moderation API or AWS Rekognition can flag problematic text and images before generation.
- Manual review queue: For high-risk use cases, queue generated images for human review before delivery.
At minimum, log what's being generated so you can investigate if something goes wrong.
Spending capsSpending Caps
Set spending alerts in your Imejis dashboard so you're notified before you hit unexpected charges. Most API abuse shows up as a sudden spike in usage.
Build this into your proxy too:
// Track daily usage
let dailyCount = 0
const DAILY_LIMIT = 1000
// Reset at midnight
setInterval(() => {
dailyCount = 0
}, 24 * 60 * 60 * 1000)
app.post("/api/generate-image", authenticate, async (req, res) => {
if (dailyCount >= DAILY_LIMIT) {
// Alert your team
await notifySlack("Daily image generation limit reached!")
return res.status(429).json({ error: "Service temporarily unavailable" })
}
dailyCount++
// ... proceed with generation
})In production, store this counter in Redis or your database, not in application memory.
User level quotasUser-Level Quotas
Global limits protect your overall budget, but user-level quotas keep things fair and catch individual abuse early.
async function checkUserQuota(userId) {
const today = new Date().toISOString().split("T")[0]
const key = `quota:${userId}:${today}`
const count = await redis.incr(key)
if (count === 1) {
await redis.expire(key, 86400) // Expire after 24 hours
}
const limit = await getUserPlanLimit(userId) // e.g., 50 for free, 500 for pro
if (count > limit) {
return { allowed: false, remaining: 0 }
}
return { allowed: true, remaining: limit - count }
}Return the remaining quota in your API response headers so the frontend can show users how many generations they have left.
Logging and monitoringLogging and Monitoring
You can't fix what you can't see. Log every image generation request with enough context to investigate problems.
What to log:
- User ID and IP address
- Template name and input data (sanitize sensitive fields)
- Timestamp and response time
- Success or failure with error details
- Image API response status
function logGeneration(req, result) {
const entry = {
timestamp: new Date().toISOString(),
userId: req.user?.id || "anonymous",
ip: req.ip,
template: req.body.template,
status: result.success ? "success" : "error",
responseTime: result.duration,
error: result.error || null,
}
logger.info("image_generation", entry)
}Set up alerts for:
- More than 100 requests from a single user in an hour
- Error rate above 10% over a 5-minute window
- Total daily usage exceeding 80% of your budget
- Any 401/403 responses (someone trying bad API keys)
Review your logs weekly. Most security issues are obvious in hindsight. You just have to look.
The production security checklistThe Production Security Checklist
Before you go live, walk through each of these:
- API key is stored in environment variables, not in source code or config files.
.envis in.gitignoreand has never been committed to the repository.- Frontend never touches the API key. All image generation goes through your backend proxy.
- Authentication is required on your proxy endpoint. Anonymous users can't generate images.
- All input fields are validated server-side: text length, URL format, numeric ranges.
- Image URLs are restricted to an allowlist of trusted domains.
- Rate limiting is active on your proxy, both per-IP and per-user.
- User-level quotas are enforced based on plan or account type.
- Spending alerts are configured in your API dashboard.
- A global daily cap exists that kills generation before your budget is blown.
- Every generation request is logged with user ID, template, and timestamp.
- Alerts fire on anomalies: usage spikes, high error rates, auth failures.
Print this list. Tape it to your monitor. Check every item before launch day.
Whats nextWhat's Next
Security isn't a one-time setup. It's something you maintain. Review your logs, update your allowlists, rotate your keys, and keep an eye on usage patterns.
If you're just getting started with image generation APIs, the getting started tutorial covers the basics. For framework-specific proxy patterns, check out the guides for React and Next.js. And for optimizing performance alongside security, the rate limits and caching guide covers what you need.
Ready to build? Start generating images with Imejis, and keep them secure from day one.
FaqFAQ
Should i expose my api key in frontend codeShould I expose my API key in frontend code?
Never. API keys in client-side JavaScript are visible to anyone who opens DevTools. Use a backend proxy that adds the key server-side. Your frontend calls your proxy, not the image API directly.
How do i rotate api keys safelyHow do I rotate API keys safely?
Generate a new key first, update your backend config, verify it works, then revoke the old key. Keep both active during the transition window to avoid downtime.
What if someone abuses my image generation endpointWhat if someone abuses my image generation endpoint?
Add rate limiting per user and per IP, require authentication, validate all inputs, and set spending alerts on your API dashboard. A rate limiter catching abuse at your proxy layer protects your API credits.
Should i validate template data before sending to the apiShould I validate template data before sending to the API?
Yes. Validate data types, string lengths, and URL formats. Reject anything unexpected before it reaches the API. This prevents injection attacks and wasted credits on malformed requests.
How do i monitor for security issuesHow do I monitor for security issues?
Track API usage by user, set alerts for unusual spikes, log all generation requests with user IDs, and review logs weekly. Most abuse shows up as sudden volume increases.