AI photo editors look simple from the outside: upload an image, describe a change, and download the result. The hard part is everything around the model call.
If you are building or evaluating a browser-based image editor, the workflow needs to protect the original file, reject bad inputs early, make retries safe, and help the user compare the result with the source. This article walks through a small implementation pattern that does that without turning the UI into a complex desktop editor.
1. Validate the image before upload
Do not rely on the file extension. Check the MIME type, file size, and whether the browser can actually decode the image.
const ACCEPTED_TYPES = new Set([
"image/jpeg",
"image/png",
"image/webp",
]);
async function validateImage(file) {
if (!ACCEPTED_TYPES.has(file.type)) {
throw new Error("Use a JPG, PNG, or WebP image.");
}
const maxBytes = 10 * 1024 * 1024;
if (file.size > maxBytes) {
throw new Error("The image must be smaller than 10 MB.");
}
const bitmap = await createImageBitmap(file);
const dimensions = { width: bitmap.width, height: bitmap.height };
bitmap.close();
if (dimensions.width < 64 || dimensions.height < 64) {
throw new Error("The image is too small for a useful edit.");
}
return dimensions;
}
This catches renamed files, broken images, and tiny inputs before they consume bandwidth or model credits.
2. Treat the prompt as a single edit contract
Open-ended chat is useful, but it can make image editing unpredictable. A clearer UI asks for one concrete change at a time:
- remove the person on the right;
- replace the background with a plain white wall;
- repair the crease across the top-left corner;
- extend the image to a 16:9 frame.
The request object should preserve that intent without mixing it with UI state:
function buildEditRequest(file, prompt, options = {}) {
const normalizedPrompt = prompt.trim().replace(/\s+/g, " ");
if (normalizedPrompt.length < 5) {
throw new Error("Describe one visible change.");
}
return {
requestId: crypto.randomUUID(),
file,
prompt: normalizedPrompt,
aspectRatio: options.aspectRatio ?? "original",
resolution: options.resolution ?? "1k",
};
}
A client-generated request ID is especially useful. It lets the server recognize a retry and prevents a double charge when the network drops after the model has already finished.
3. Make cancellation and retries explicit
Uploads and generation calls can take long enough that users will navigate away or try again. Use an AbortController, and keep retry behavior separate from creating a new request.
async function submitEdit(request, signal) {
const form = new FormData();
form.set("image", request.file);
form.set("prompt", request.prompt);
form.set("aspectRatio", request.aspectRatio);
form.set("resolution", request.resolution);
const response = await fetch("/api/edit", {
method: "POST",
headers: { "Idempotency-Key": request.requestId },
body: form,
signal,
});
if (!response.ok) {
throw new Error(`Edit failed with status ${response.status}`);
}
return response.json();
}
If the user changes the prompt, create a new request ID. If the same request is being retried after a timeout, keep the existing one.
4. Compare the result instead of replacing the original
The safest result screen shows both images. A before/after slider is more useful than a success message because it exposes changes the model made outside the requested area.
Ask the user to check:
- faces, hands, and text;
- reflective surfaces;
- shadows around removed objects;
- edges such as hair, fur, and transparent glass;
- details that must remain factually accurate.
Keep the original object URL until the comparison is finished, then revoke it:
const originalUrl = URL.createObjectURL(file);
try {
renderComparison({ originalUrl, resultUrl });
} finally {
// Revoke this when the comparison view is closed.
URL.revokeObjectURL(originalUrl);
}
5. Test the privacy claims, not only the output
When evaluating a hosted editor, check what happens before and after the generation call:
- Can the first edit run without creating an account?
- Does the service state how long uploads and results are retained?
- Is there a watermark or resolution limit on the downloaded file?
- Can the user compare the original and result before downloading?
- Does a failed request consume credits?
The important part is not the specific model. It is the contract around the model: validate early, make retries idempotent, preserve the original for comparison, and delete temporary data on a predictable schedule.
Final checklist
Before shipping an AI photo editor, verify these four paths:
- a valid edit completes and can be compared with the original;
- an invalid file fails before upload;
- a network retry does not create a duplicate charge;
- closing the result view releases local object URLs and triggers server-side cleanup.
That small amount of engineering makes a prompt-based image editor feel much more trustworthy than a single upload button connected directly to a model endpoint.
For a concrete browser-based workflow, use Editara as a test case.
Top comments (1)
do you think this workflow would slow down if you're dealing with really high res raw files?