Build a Free Flux Text-to-Image API on Cloudflare in 5 Minutes
This guide shows how to use Cloudflare Workers AI's free daily quota to quickly create a custom Flux‑1‑Schnell text‑to‑image API, covering project initialization, AI binding configuration, request validation, error handling, authentication, deployment, and testing with curl.
Cloudflare offers a generous free daily quota for Workers AI, which is sufficient for low‑frequency usage. Leveraging this, the article walks through building a dedicated Flux‑1‑Schnell text‑to‑image API that can be integrated into an MCP Server for unrestricted image generation.
Prerequisites
Node.js installed locally
A Cloudflare account
Project Initialization
Run the following command to create a new Cloudflare Workers project: pnpm create cloudflare@latest cf-flux-schnell Follow the prompts to select a starter template and programming language (TypeScript).
Configure Workers AI
Add an AI binding in wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cf-flux-schnell",
"main": "src/index.ts",
"compatibility_date": "2025-03-12",
"observability": { "enabled": true },
"ai": { "binding": "AI" }
}Understand the Flux‑1‑Schnell API
The official schema defines the input and output objects. The input requires a prompt string (1‑2048 characters) and an optional steps integer (1‑8, default 4). The output returns an image field containing a Base64‑encoded JPEG.
{
"type": "object",
"properties": {
"prompt": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "A text description of the image you want to generate." },
"steps": { "type": "integer", "default": 4, "maximum": 8, "description": "The number of diffusion steps; higher values can improve quality but take longer." }
},
"required": ["prompt"]
}Develop the Flux‑1‑Schnell API
1. Add request validation using the zod library:
pnpm add zod import { z } from 'zod';
const GenerateImageSchema = z.object({
prompt: z.string().min(1, 'Prompt cannot be empty').max(2048, 'Prompt must not exceed 2048 characters'),
steps: z.number().int().min(1).max(8).default(4).optional()
});2. Define custom error classes for validation, unauthorized access, and method not allowed:
export class ApiError extends Error {
constructor(public status: number, message: string, public details?: unknown) {
super(message);
this.name = 'ApiError';
}
}
export class ValidationError extends ApiError {
constructor(details: z.ZodError) {
super(400, 'Validation error', details.errors);
this.name = 'ValidationError';
}
}
export class UnauthorizedError extends ApiError {
constructor() { super(401, 'Unauthorized'); this.name = 'UnauthorizedError'; }
}
export class MethodNotAllowedError extends ApiError {
constructor() { super(405, 'Method not allowed'); this.name = 'MethodNotAllowedError'; }
}3. Enforce POST method and Authorization header :
const FLUX_TOKEN = 'Y2YtZmx1eC1hcGk=';
async function validateRequest(request: Request): Promise<z.infer<typeof GenerateImageSchema>> {
if (request.method !== 'POST') throw new MethodNotAllowedError();
const authHeader = request.headers.get('Authorization');
if (!authHeader || authHeader !== `Bearer ${FLUX_TOKEN}`) throw new UnauthorizedError();
const body = await request.json();
try { return GenerateImageSchema.parse(body); }
catch (error) {
if (error instanceof z.ZodError) throw new ValidationError(error);
throw new ApiError(400, 'Invalid request body');
}
}4. Utility functions for JSON responses and error handling :
const createJsonResponse = (data: unknown, status = 200): Response =>
new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } });
const handleError = (error: unknown): Response => {
if (error instanceof ApiError) {
return createJsonResponse({ error: error.message, details: error.details ?? undefined }, error.status);
}
return createJsonResponse({ error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' }, 500);
};5. Update the fetch handler to validate input, call the AI model, and return the image data URI:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const validatedData = await validateRequest(request);
const response = await env.AI.run('@cf/black-forest-labs/flux-1-schnell', {
prompt: validatedData.prompt,
steps: validatedData.steps,
});
const result = { image: response.image ? `data:image/png;base64,${response.image}` : '' };
return createJsonResponse(result);
} catch (error) {
return handleError(error);
}
},
} satisfies ExportedHandler<Env>;Testing and Deployment
1. Run the local development server: pnpm dev 2. Deploy to Cloudflare Workers: pnpm run deploy 3. Test the deployed endpoint with curl (replace the URL with your worker's domain):
curl --request POST \
--url http://your-site.workers.dev/ \
--header 'Authorization: Bearer Y2YtZmx1eC1hcGk=' \
--header 'Content-Type: application/json' \
--data '{
"prompt": "1girl, cute, drinking water, glass cup, morning light, cartoon style, simple background",
"steps": 4
}'After a successful response, the image field contains a data URI that can be used directly in an <img> tag.
https://gist.github.com/bytefer/6ea44e54c37c276b9c2cca4244ec32e2
The complete source code is available in the linked Gist.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Cultivation Path
Focused on sharing practical tech content about TypeScript, Vue 3, front-end architecture, and source code analysis.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
