Back to Blog
Tutorial2026-07-075 min read

Deploy a 2026 Serverless AI Image Captioning API with Bun.js, Elysia.js, and Cloudflare R2 Using Llama 3.2-Vision: A Developer Tutorial

A step-by-step tutorial to build and deploy a globally distributed image captioning API using Bun.js, Elysia.js, Cloudflare R2, and Meta's Llama 3.2-Vision model in 2026.

Agentic Academy Labs

Author

Why Serverless AI in 2026?

The landscape of web development has shifted dramatically. In 2026, edge computing and serverless architectures are no longer experimental—they are the default for startups and enterprises alike. Combining Bun.js's blistering runtime performance, Elysia.js's type-safe and ergonomic API layer, and Cloudflare R2's zero-egress storage creates a trifecta that slashes both latency and cost. Traditional cloud setups penalize you with egress fees; R2 eliminates them entirely, making media-heavy AI apps affordable.

In this developer tutorial, we will build a production-ready image captioning API that accepts uploaded images, persists them in R2, and leverages Meta's open-weight Llama 3.2-Vision multimodal model to generate accurate, human-like descriptions. The entire backend runs on Cloudflare's global network, meaning your users get responses from the nearest edge location.


Architecture Overview

Our system follows a clean, event-driven flow:

  • A client sends a multipart/form-data POST request with an image file.
  • The Elysia.js server (running on Bun) validates the input and streams the file to an R2 bucket.
  • A unique object key is generated and a public or signed URL is constructed.
  • The API then calls a Llama 3.2-Vision inference endpoint with the image URL and a prompt.
  • The returned caption is stored alongside metadata and returned to the client as JSON.

This design decouples storage from compute and allows independent scaling of each concern.


Why Llama 3.2-Vision?

Meta's Llama 3.2-Vision is a 2026 flagship open multimodal model. It delivers near-frontier quality on image understanding tasks while running efficiently on commodity GPUs and edge inference clusters. Its permissive license allows commercial use, making it ideal for indie hackers and enterprises alike. In our API, we prompt it to produce concise captions, but you could easily adapt the same endpoint for OCR, scene segmentation, or accessibility descriptions.


Prerequisites

Before writing code, ensure you have the following:

  • Bun.js v1.3+ (2026 stable release) installed locally
  • A Cloudflare account with R2 storage enabled and a created bucket
  • The wrangler CLI installed globally via bun install -g wrangler
  • An API token for a Llama 3.2-Vision provider (Cloudflare Workers AI, Replicate, or self-hosted)
  • Fundamental knowledge of TypeScript and REST principles

Step 1: Scaffold the Project with Bun.js

Create a new directory and initialize a Bun project. Bun's built-in package manager is faster than npm or pnpm.

mkdir caption-api && cd caption-api
bun init -y
bun add elysia @elysiajs/cors

We also add the official CORS plugin so the API can be consumed by browser applications without preflight errors.


Step 2: Configure Cloudflare R2 Bindings

Create a wrangler.toml at the project root. This file declares the R2 bucket binding and runtime compatibility. In 2026, Cloudflare supports the Bun runtime natively via the bun compatibility flag.

name = "caption-api"
main = "src/index.ts"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat", "bun"]

[r2_buckets]
binding = "IMAGES"
bucket_name = "my-caption-images"

[vars]
LLAMA_API_URL = "https://api.your-provider.com/v1/llama-3.2-vision"

Remember to set your secret API key via wrangler secret put LLAMA_API_KEY rather than hardcoding it.


Step 3: Build the Elysia.js Server

Create src/index.ts. The following code sets up an Elysia instance, enables CORS, and defines the /caption route.

import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'

const app = new Elysia()
  .use(cors())
  .post('/caption', async ({ request, env }) => {
    const formData = await request.formData()
    const file = formData.get('image') as File | null
    if (!file) return new Response('No image provided', { status: 400 })

    const key = `${crypto.randomUUID()}.${file.name.split('.').pop()}`
    await env.IMAGES.put(key, file.stream(), { contentType: file.type })

    const imageUrl = `https://images.my-cdn.com/${key}`
    const caption = await generateCaption(imageUrl, env)

    return new Response(JSON.stringify({ key, caption }), {
      headers: { 'content-type': 'application/json' }
    })
  })
  .listen(3000)

console.log('🚀 Server running on http://localhost:3000')

Note how we use crypto.randomUUID()—available globally in both Bun and Cloudflare Workers—to avoid filename collisions.


Step 4: Integrate Llama 3.2-Vision

The generateCaption helper performs a standards-compliant chat completion request. Llama 3.2-Vision accepts interleaved text and image parts.

async function generateCaption(imageUrl: string, env: any): Promise<string> {
  const res = await fetch(env.LLAMA_API_URL, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'authorization': `Bearer ${env.LLAMA_API_KEY}`
    },
    body: JSON.stringify({
      model: 'llama-3.2-vision',
      messages: [
        {
          role: 'user',
          content: [
            { type: 'text', text: 'Describe this image in one concise sentence.' },
            { type: 'image_url', image_url: { url: imageUrl } }
          ]
        }
      ],
      max_tokens: 100
    })
  })

  const data = await res.json()
  return data.choices?.[0]?.message?.content?.trim() ?? 'No caption'
}

Security tip: The API key never leaves the server environment. You can rotate it using wrangler secret put without redeploying code.


Step 5: Local Development & Testing

Run the API locally with hot reload:

bun run --hot src/index.ts

Then, in another terminal, send a test image:

curl -F "image=@./test.jpg" http://localhost:3000/caption

You should see a JSON response containing the generated caption and the R2 key. If you encounter CORS issues, verify the cors() plugin is applied.


Step 6: Deploy to the Edge

Deployment is trivial with Wrangler. Because we declared the bun compatibility flag, the bundle is optimized for Cloudflare's runtime.

wrangler deploy

Within seconds, your endpoint is live at https://caption-api.your-subdomain.workers.dev. Scale is automatic and you pay only for requests and storage.


Step 7: Production Enhancements

To harden the service, consider these patterns taught in Agentic Academy Labs courses:

  • Rate limiting with Cloudflare's ratelimit binding to prevent abuse
  • KV caching of captions keyed by image hash to reduce model spend
  • Queues for asynchronous batch processing of large albums
  • Observability via Workers Analytics Engine and structured logs
  • Signed URLs instead of public R2 objects for private datasets

Each of these can be added with a few lines of code and dramatically improves reliability.


Final Thoughts

You have just deployed a complete serverless AI pipeline using Bun.js, Elysia.js, Cloudflare R2, and Llama 3.2-Vision. This stack exemplifies the 2026 paradigm: fast runtimes, edge storage, and open multimodal models working in harmony.

At Agentic Academy Labs, we empower developers to master these tools through hands-on projects. Fork the code, extend it, and ship something amazing today.

Enjoyed this article?

Read More Articles