ai newsDo., 5. Nov.
EilmeldungClaude Reads Screenshots at 2,576px Now. The Same PNG Can Cost 3x the Tokens

Building RapVideoGo with TanStack Start and Cloudflare: Beyond the AI API Call

An AI video product has a deceptively simple interface: upload a photo, describe an idea, and click Generate. Behind that button, the application needs to answer several questions. What happens if the user submits twice? What if the provider accepts a request but the connection drops? When should c

dev.toDonnerstag 03:44 · 05.11.5 Min. Lesezeit
Bild: dev.to

An AI video product has a deceptively simple interface: upload a photo, describe an idea, and click Generate.

Behind that button, the application needs to answer several questions. What happens if the user submits twice? What if the provider accepts a request but the connection drops? When should credits be deducted, and how should they be restored after a failure?

I’m the creator of RapVideoGo, a web application for making personalized rap videos from reference photos and text prompts. This post covers some of the implementation decisions behind it, particularly the parts that sit between the interface and the generation provider.

I built on an existing SaaS foundation for accounts, payments, and credits. That let me focus product development on the creation workflow and its failure cases.

The video model runs through an external provider. The application’s responsibility is to turn a user’s creative input into a valid request, track its progress, and keep the surrounding account state consistent.

pending → processing → success ↘ failed Enter fullscreen mode Exit fullscreen mode A task record stores the owner, generation settings, reference image URLs, prompt, provider information, and credit cost.

This record gives the application a durable identity for the work. The browser does not have to be the only place that remembers a generation is running.

Repeated requests also need more than a check that an ID already exists. In RapVideoGo, the existing task must match the requesting user, product module, and request fingerprint. Reusing an ID with different input produces a conflict.

That distinction matters: retrying the same request should recover existing work, while changing the request should not silently reuse an unrelated task.

One consistency problem is easy to describe:

Deduct credits Create task Enter fullscreen mode Exit fullscreen mode If the second operation fails, the user has spent credits without getting a task. Reversing the order creates a different partial state: a task without its corresponding charge.

The implementation plans credit consumption first, then inserts the task and applies the credit operations through one atomic database operation.

On D1, the project’s database helper uses a batch of statements. Cloudflare documents that a failed statement in a batch causes the sequence to roll back.

Reference: Cloudflare D1 batch documentation.

Reading a balance before writing is not enough, however. Two requests can read the same available credits.

The credit operations therefore include database guards. If another request consumes credits that the current operation expected to use, the guard prevents the stale plan from completing. Task creation can then re-plan within a bounded retry loop.

The provider request happens separately. A database batch cannot make an external API call transactional, so provider submission still needs its own recovery logic.

A timeout does not always mean the provider rejected the job.

There is a meaningful difference between these cases:

Automatically submitting again in the second case could create duplicate paid work.

The implementation distinguishes these situations. Where the provider supports an idempotency key, recovery uses the same task identity and saved request. Other paths require waiting for confirmation or expiration rather than blindly creating another job.

Task updates are conditional, too. A completion update checks the expected state and provider task ID, helping prevent an older response from overwriting a newer state.

Failure handling also needs repeat protection. The credit restoration path claims the original consumption record before restoring credits. A repeated failure notification should not restore the same charge twice.

This does not make every external failure disappear. It gives the application explicit rules for handling uncertainty.

Another problem lives closer to the interface.

Imagine a duet with two reference slots:

@1 performs the opening line. @2 responds. Enter fullscreen mode Exit fullscreen mode If the user uploads the second slot first, upload completion order cannot become performer order.

The application preserves explicit slot numbers and derives the active reference list from those slots. A simplified version of the helper looks like this:

function activePhotos < T extends { slot?: number } > ( photos: T [], count: number ): T [] { return Array. from ({ length: count }, ( _, slot ) => photos. find (( photo ) => photo. slot === slot ) ). filter (( photo ): photo is T => photo!== undefined ); } Enter fullscreen mode Exit fullscreen mode The important detail is that the output follows slot order, regardless of the order in which uploads finish.

Switching to a single-photo mode also needs to exclude inactive references without deleting them from the draft.

These are small UI decisions with direct consequences for the generation request. The tests include uploading the second slot first and switching between photo counts.

RapVideoGo has English and Chinese interfaces. Localization includes navigation and page metadata as well as buttons and form fields.

The routes remain locale-free internally, while the routing layer handles localized URLs. Route loaders resolve the locale used for titles and descriptions.

Keeping that logic in a shared layer reduces the number of places where individual components need to construct language-specific paths.

It also makes the relationship between the page URL, displayed content, and metadata easier to maintain.

The project includes tests around credit guards, atomic writes, generation requests, reference ordering, and creation history. It also has a separate test path for exercising atomic operations against local D1.

For an application that spends credits and calls paid services, these boundaries deserve attention.

A successful preview alone cannot demonstrate that duplicate submissions or concurrent spending are handled correctly. Those behaviors need tests that exercise the relevant state transitions and database operations.

The most useful lesson from this project has been to treat generation as a workflow with durable state. The interface collects creative intent; the application must preserve that intent through uploads, retries, provider responses, and account updates.

For developers building similar products: how do you handle a provider request that may have succeeded when your application never received its response?

Quelle: dev.to (dev.to). Original öffnen