Mobile Image API: iOS, Android & React Native

Mobile Image API: iOS, Android & React Native

Mobile apps thrive on dynamic visuals. Personalized greeting cards, share cards for social media, achievement badges, profile banners. These aren't things you can ship as static assets. You need to generate them on the fly, tailored to each user.

The problem? Mobile comes with constraints that web doesn't. Spotty connections, limited memory, battery concerns, and the ever-present risk of leaking API keys in a decompiled APK.

I've shipped image generation in iOS, Android, and React Native apps. Here's what actually works, with full code examples for all three platforms. If you're brand new to image generation APIs, start with how to generate images with an API first.

Architecture mobile to backend to image apiArchitecture: Mobile to Backend to Image API

Let's get this out of the way: don't call the image API directly from your mobile app in production.

Here's why. Any API key embedded in a mobile app can be extracted. It doesn't matter how clever you are about hiding it. Someone will decompile your app, run strings on the binary, and find it. For more on this, read our guide on image API security best practices.

The correct architecture looks like this:

Mobile App → Your Backend (holds API key) → Image Generation API

Your backend acts as a proxy. The mobile app sends a request to your server, your server calls the Imejis API with the secret key, and returns the generated image URL back to the app.

For prototyping and hackathons, calling the API directly is fine. Just don't ship it to the App Store that way.

Here's a minimal Express proxy to illustrate:

// backend/proxy.ts
import express from "express"
 
const app = express()
app.use(express.json())
 
app.post("/api/generate-image", async (req, res) => {
  const { templateId, modifications } = req.body
 
  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({ templateId, modifications }),
  })
 
  const data = await response.json()
  res.json({ imageUrl: data.imageUrl })
})

Now let's build the mobile clients.

Ios swiftiOS (Swift)

Urlsession implementationURLSession Implementation

Swift's modern concurrency with async/await makes API calls clean. Here's a service class that talks to your backend proxy:

// ImageGenerationService.swift
import Foundation
 
struct GeneratedImage: Codable {
    let imageUrl: String
}
 
struct ImageRequest: Codable {
    let templateId: String
    let modifications: [String: String]
}
 
class ImageGenerationService {
    private let baseURL: URL
    private let session: URLSession
 
    init(baseURL: URL, session: URLSession = .shared) {
        self.baseURL = baseURL
        self.session = session
    }
 
    func generateImage(
        templateId: String,
        modifications: [String: String]
    ) async throws -> URL {
        let endpoint = baseURL.appendingPathComponent("/api/generate-image")
 
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.timeoutInterval = 20 // generous for mobile
 
        let body = ImageRequest(
            templateId: templateId,
            modifications: modifications
        )
        request.httpBody = try JSONEncoder().encode(body)
 
        let (data, response) = try await session.data(for: request)
 
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw ImageGenerationError.serverError
        }
 
        let result = try JSONDecoder().decode(GeneratedImage.self, from: data)
        guard let url = URL(string: result.imageUrl) else {
            throw ImageGenerationError.invalidURL
        }
 
        return url
    }
}
 
enum ImageGenerationError: Error {
    case serverError
    case invalidURL
    case cacheMiss
}

Notice the 20-second timeout. On mobile networks, especially cellular, you need more patience than on desktop.

Caching with urlcacheCaching with URLCache

iOS has a built-in caching layer that most developers underuse. Configure it once at app startup:

// AppDelegate.swift or App init
func setupCache() {
    let memoryCapacity = 50 * 1024 * 1024  // 50 MB
    let diskCapacity = 200 * 1024 * 1024    // 200 MB
    let cache = URLCache(
        memoryCapacity: memoryCapacity,
        diskCapacity: diskCapacity,
        diskPath: "image_cache"
    )
    URLCache.shared = cache
}

For generated images, you'll also want your own mapping layer so you can look up cached images by template + parameters:

class ImageCacheManager {
    static let shared = ImageCacheManager()
    private let cache = NSCache<NSString, UIImage>()
    private let fileManager = FileManager.default
 
    private var cacheDirectory: URL {
        fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("generated_images")
    }
 
    func cachedImage(for key: String) -> UIImage? {
        // Check memory first
        if let image = cache.object(forKey: key as NSString) {
            return image
        }
 
        // Check disk
        let fileURL = cacheDirectory.appendingPathComponent(key.sha256 + ".png")
        guard let data = try? Data(contentsOf: fileURL),
              let image = UIImage(data: data) else {
            return nil
        }
 
        cache.setObject(image, forKey: key as NSString)
        return image
    }
 
    func store(_ image: UIImage, for key: String) {
        cache.setObject(image, forKey: key as NSString)
 
        try? fileManager.createDirectory(
            at: cacheDirectory,
            withIntermediateDirectories: true
        )
 
        let fileURL = cacheDirectory.appendingPathComponent(key.sha256 + ".png")
        try? image.pngData()?.write(to: fileURL)
    }
}

Swiftui image view componentSwiftUI Image View Component

Here's a reusable SwiftUI view that ties generation, loading, and caching together:

struct GeneratedImageView: View {
    let templateId: String
    let modifications: [String: String]
 
    @State private var image: UIImage?
    @State private var isLoading = false
    @State private var error: Error?
 
    private let service = ImageGenerationService(
        baseURL: URL(string: "https://your-backend.com")!
    )
 
    var body: some View {
        Group {
            if let image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else if isLoading {
                ProgressView()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else if error != nil {
                VStack(spacing: 8) {
                    Image(systemName: "exclamationmark.triangle")
                    Text("Failed to load image")
                        .font(.caption)
                    Button("Retry") { Task { await loadImage() } }
                }
            } else {
                Color.gray.opacity(0.1)
            }
        }
        .task { await loadImage() }
    }
 
    private func loadImage() async {
        let cacheKey = "\(templateId)_\(modifications.description)"
 
        // Check cache first
        if let cached = ImageCacheManager.shared.cachedImage(for: cacheKey) {
            self.image = cached
            return
        }
 
        isLoading = true
        defer { isLoading = false }
 
        do {
            let imageURL = try await service.generateImage(
                templateId: templateId,
                modifications: modifications
            )
            let (data, _) = try await URLSession.shared.data(from: imageURL)
            guard let uiImage = UIImage(data: data) else { return }
 
            ImageCacheManager.shared.store(uiImage, for: cacheKey)
            self.image = uiImage
        } catch {
            self.error = error
        }
    }
}

Usage is dead simple:

GeneratedImageView(
    templateId: "welcome-banner",
    modifications: ["username": "Sarah", "level": "Pro"]
)
.frame(height: 200)

Android kotlinAndroid (Kotlin)

Okhttp retrofit implementationOkHttp / Retrofit Implementation

On Android, Retrofit with coroutines is the standard approach. Here's the setup:

// ImageApiService.kt
import retrofit2.http.Body
import retrofit2.http.POST
 
data class ImageGenerationRequest(
    val templateId: String,
    val modifications: Map<String, String>
)
 
data class ImageGenerationResponse(
    val imageUrl: String
)
 
interface ImageApiService {
    @POST("/api/generate-image")
    suspend fun generateImage(
        @Body request: ImageGenerationRequest
    ): ImageGenerationResponse
}
// ImageRepository.kt
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
 
class ImageRepository {
    private val client = OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .build()
 
    private val api = Retrofit.Builder()
        .baseUrl("https://your-backend.com")
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(ImageApiService::class.java)
 
    suspend fun generateImage(
        templateId: String,
        modifications: Map<String, String>
    ): String {
        val response = api.generateImage(
            ImageGenerationRequest(templateId, modifications)
        )
        return response.imageUrl
    }
}

Caching with coilCaching with Coil

Coil is the modern image loading library for Android. It handles disk caching, memory caching, and lifecycle awareness out of the box. Configure it at the application level:

// MyApplication.kt
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import coil.memory.MemoryCache
 
class MyApplication : Application(), ImageLoaderFactory {
    override fun newImageLoader(): ImageLoader {
        return ImageLoader.Builder(this)
            .memoryCache {
                MemoryCache.Builder(this)
                    .maxSizePercent(0.20) // 20% of app memory
                    .build()
            }
            .diskCache {
                DiskCache.Builder()
                    .directory(cacheDir.resolve("generated_images"))
                    .maxSizeBytes(200L * 1024 * 1024) // 200 MB
                    .build()
            }
            .build()
    }
}

Jetpack compose componentJetpack Compose Component

Here's a Compose component that mirrors what we built in SwiftUI:

@Composable
fun GeneratedImage(
    templateId: String,
    modifications: Map<String, String>,
    modifier: Modifier = Modifier
) {
    val repository = remember { ImageRepository() }
    var imageUrl by remember { mutableStateOf<String?>(null) }
    var isLoading by remember { mutableStateOf(false) }
    var hasError by remember { mutableStateOf(false) }
 
    LaunchedEffect(templateId, modifications) {
        isLoading = true
        hasError = false
        try {
            imageUrl = repository.generateImage(templateId, modifications)
        } catch (e: Exception) {
            hasError = true
        } finally {
            isLoading = false
        }
    }
 
    Box(modifier = modifier, contentAlignment = Alignment.Center) {
        when {
            isLoading -> {
                CircularProgressIndicator()
            }
            hasError -> {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Icon(
                        Icons.Default.Warning,
                        contentDescription = "Error"
                    )
                    Text("Failed to load", style = MaterialTheme.typography.bodySmall)
                    TextButton(onClick = {
                        // Trigger reload
                        hasError = false
                        isLoading = true
                    }) {
                        Text("Retry")
                    }
                }
            }
            imageUrl != null -> {
                AsyncImage(
                    model = imageUrl,
                    contentDescription = "Generated image",
                    contentScale = ContentScale.Fit,
                    modifier = Modifier.fillMaxSize()
                )
            }
        }
    }
}

Use it like this:

GeneratedImage(
    templateId = "welcome-banner",
    modifications = mapOf("username" to "Sarah", "level" to "Pro"),
    modifier = Modifier.height(200.dp).fillMaxWidth()
)

React native typescriptReact Native (TypeScript)

Fetch backend proxyFetch + Backend Proxy

React Native's fetch works the same as web, so the API layer is straightforward:

// services/imageGeneration.ts
const API_BASE = "https://your-backend.com"
 
interface GenerateImageParams {
  templateId: string
  modifications: Record<string, string>
}
 
export async function generateImage(
  params: GenerateImageParams
): Promise<string> {
  const controller = new AbortController()
  const timeout = setTimeout(() => controller.abort(), 20000)
 
  try {
    const response = await fetch(`${API_BASE}/api/generate-image`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(params),
      signal: controller.signal,
    })
 
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`)
    }
 
    const data = await response.json()
    return data.imageUrl
  } finally {
    clearTimeout(timeout)
  }
}

Fastimage component with cacheFastImage Component with Cache

react-native-fast-image handles caching and performance far better than the built-in Image component. Here's a full wrapper:

// components/GeneratedImage.tsx
import React, { useCallback, useEffect, useState } from "react"
import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"
import FastImage from "react-native-fast-image"
 
import { generateImage } from "../services/imageGeneration"
 
interface Props {
  templateId: string
  modifications: Record<string, string>
  style?: object
}
 
export function GeneratedImage({ templateId, modifications, style }: Props) {
  const [imageUrl, setImageUrl] = useState<string | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(false)
 
  const load = useCallback(async () => {
    setLoading(true)
    setError(false)
    try {
      const url = await generateImage({ templateId, modifications })
      // Pre-cache the image
      FastImage.preload([{ uri: url }])
      setImageUrl(url)
    } catch {
      setError(true)
    } finally {
      setLoading(false)
    }
  }, [templateId, JSON.stringify(modifications)])
 
  useEffect(() => {
    load()
  }, [load])
 
  if (loading) {
    return (
      <View style={[style, { justifyContent: "center", alignItems: "center" }]}>
        <ActivityIndicator />
      </View>
    )
  }
 
  if (error || !imageUrl) {
    return (
      <View style={[style, { justifyContent: "center", alignItems: "center" }]}>
        <Text>Failed to load image</Text>
        <TouchableOpacity onPress={load}>
          <Text style={{ color: "#007AFF", marginTop: 8 }}>Retry</Text>
        </TouchableOpacity>
      </View>
    )
  }
 
  return (
    <FastImage
      source={{
        uri: imageUrl,
        priority: FastImage.priority.normal,
        cache: FastImage.cacheControl.immutable,
      }}
      style={style}
      resizeMode={FastImage.resizeMode.contain}
    />
  )
}

Usage:

<GeneratedImage
  templateId="welcome-banner"
  modifications={{ username: "Sarah", level: "Pro" }}
  style={{ height: 200, width: "100%" }}
/>

For more on building React components around image generation, check out our guide on React image generation components.

Offline support and pre generationOffline Support and Pre-Generation

Mobile apps need to work when the signal drops. You can't generate images without a network connection, but you can prepare for offline use.

The strategy: pre-generate images when you have a good connection, then serve from cache.

Here's a practical approach for iOS:

class ImagePreloader {
    private let service: ImageGenerationService
    private let cacheManager = ImageCacheManager.shared
 
    func preloadImagesIfNeeded() async {
        // Only preload on WiFi
        guard isOnWiFi() else { return }
 
        let templates = [
            ("achievement-badge", ["type": "streak", "count": "7"]),
            ("achievement-badge", ["type": "streak", "count": "30"]),
            ("share-card", ["username": currentUser.name]),
        ]
 
        for (templateId, mods) in templates {
            let key = "\(templateId)_\(mods.description)"
            guard cacheManager.cachedImage(for: key) == nil else { continue }
 
            do {
                let url = try await service.generateImage(
                    templateId: templateId,
                    modifications: mods
                )
                let (data, _) = try await URLSession.shared.data(from: url)
                if let image = UIImage(data: data) {
                    cacheManager.store(image, for: key)
                }
            } catch {
                // Non-critical — skip and try again later
                continue
            }
        }
    }
 
    private func isOnWiFi() -> Bool {
        // Use NWPathMonitor to check connection type
        // Return true only if on WiFi
    }
}

Call this from your app's background refresh handler or when the app enters the foreground on WiFi. The key insight is that generated images for known inputs (like achievement badges) are perfectly deterministic, so you can cache them forever.

Performance tips for mobilePerformance Tips for Mobile

Request the right image size. If you're displaying an image in a 300x200 card, don't request a 2048x1024 image. Most image APIs, including Imejis, let you specify dimensions. Smaller images mean faster downloads and less memory usage.

Pre-fetch during idle time. If you know the user will see a generated image on the next screen, start generating it as soon as the current screen loads. By the time they tap through, the image is ready.

Handle memory warnings. On iOS, listen for didReceiveMemoryWarning and clear your in-memory image cache. On Android, override onTrimMemory in your Application class. Let the disk cache survive. Only purge RAM.

// iOS memory management
NotificationCenter.default.addObserver(
    forName: UIApplication.didReceiveMemoryWarningNotification,
    object: nil,
    queue: .main
) { _ in
    ImageCacheManager.shared.clearMemoryCache()
}
// Android memory management
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    if (level >= TRIM_MEMORY_MODERATE) {
        imageLoader.memoryCache?.clear()
    }
}

Respect data saver modes. Both iOS and Android have low-data modes. Check for them and skip non-essential image generation when they're active. Your users will appreciate it.

For more strategies around API usage patterns and caching, see our guide on image API rate limits and caching.

Common patternsCommon Patterns

Here are the patterns I see most often in production mobile apps:

Share cards. When a user wants to share an achievement or content, generate a branded image card with their stats, profile photo, and your app's branding. These work great on Instagram Stories, Twitter, and iMessage.

Achievement badges. Gaming and fitness apps generate badges when users hit milestones. Pre-generate these since you know the possible badge types in advance. Fitness apps use workout cards with exercise stats, streaks, and progress visuals that users share on social media.

Personalized welcome screens. First-launch screens that greet the user by name with a custom illustration. Generate on signup, cache immediately.

Dynamic thumbnails. Content apps that generate preview images for user-created content (playlists, collections, mood boards). Generate when the content is created or updated, not when it's viewed.

The common thread: generate ahead of time whenever possible. Don't make the user wait for image generation in real-time unless the input is truly dynamic and unknown until that moment.

Get startedGet Started

The Imejis API works with all three platforms covered here. Create a template in the dashboard, grab your API key, set up a backend proxy, and you're generating images from mobile in minutes.

Start with the simplest use case, maybe a share card or welcome banner, and expand from there. The patterns in this guide scale well from a single image type to dozens.

Related tutorials:

FaqFAQ

Should i call the image api directly from the mobile appShould I call the image API directly from the mobile app?

Ideally no. Use a backend proxy to keep your API key secure. If you must call directly (prototyping), use certificate pinning and obfuscate the key. Never hardcode keys in source.

How do i handle slow mobile connectionsHow do I handle slow mobile connections?

Set generous timeouts (15-30 seconds), show loading placeholders, cache aggressively, and pre-generate images when on WiFi. The API returns images in 1-2 seconds on good connections.

Can i generate images offlineCan I generate images offline?

No, the API requires an internet connection. But you can pre-generate and cache images while online, then serve from cache when offline. Core Data (iOS) or Room (Android) work well for this.

What image format should i requestWhat image format should I request?

The API returns PNG by default, which works on all platforms. For smaller file sizes on mobile, consider converting to WebP client-side after receiving the image.

How do i display the generated image efficientlyHow do I display the generated image efficiently?

On iOS, use URLSession with URLCache. On Android, use Coil or Glide with disk caching. React Native has react-native-fast-image. All handle caching, memory management, and placeholder display.