Offerta a tempo limitato: piani annuali Starter e Basic al 50% di sconto 🎉

TypeScript Patterns for Scalable AI Video App Development

Oct 15, 2026

What Actually Makes an AI Video Backend Different

Text-only AI features are cheap to iterate on: a prompt goes out, a string comes back, a user reads it in under two seconds. Video generation breaks almost every assumption a conventional web framework is built around. Requests are expensive, results are large, and jobs live far longer than a single HTTP connection.

Three practical consequences follow from that.

First, a single clip can occupy a GPU for several minutes. If you run inference inside a request handler, you have built a system that collapses the moment two people press Generate at the same time.

Second, a finished file is often tens to hundreds of megabytes. Serving it from your application server is a bandwidth decision, not a code decision, and it is almost always the wrong one.

Third, jobs are long-lived and stateful. A user starts a render, closes the tab, walks away, and returns expecting the project to be exactly where they left it. That means job state has to be durable, observable, and resumable.

TypeScript does not solve any of these problems by itself. What it does is let you describe the pipeline precisely enough that the compiler stops you from writing the naive version of the code. When your job model is a discriminated union instead of a bag of optional fields, the compiler asks you a question every time you touch it. That friction is the feature.

Map the Pipeline Before You Choose a Framework

Teams often start by picking NestJS, Fastify, or a serverless platform, then discover the architecture they actually need halfway through. Do the reverse. Write down the stages, then match tools to them.

A typical generation pipeline has six stages:

  1. Intake: validate the prompt, aspect ratio, duration, reference assets, and account entitlements.
  2. Scheduling: place the job in a queue, respect concurrency limits, and assign priority.
  3. Inference: call one or more models to produce frames, motion, and audio.
  4. Assembly: stitch segments, normalize audio, burn overlays, and encode to a delivery container.
  5. Delivery: push the asset to object storage and expose a signed URL.
  6. Notification: stream progress to the client and record a durable audit trail.

Stages 1, 5, and 6 are control-plane work: fast, high-volume, and latency-sensitive. Stages 3 and 4 are compute-plane work: slow, GPU-hungry, and tolerant of retries. Stage 2 is the seam between them.

Once you draw that line explicitly, the framework question mostly answers itself. Control-plane code fits comfortably in any HTTP framework. Compute-plane code fits a worker process. Trying to run both in one deployment is where the pain starts.

A structure that survives growth

Organize by domain, not by technical layer. Folders called controllers, services, and models look tidy on day one and become a maze by month six, because every feature ends up touching every folder.

src/
  modules/
    generation/
    assets/
    timeline/
    entitlements/
  contracts/
  infra/
  workers/

Each module owns its own routes, service logic, repositories, and DTOs. Nothing outside a module imports its internals; it exposes one service interface and keeps the rest private. That single rule keeps the dependency graph acyclic and makes it possible to move the assets module into its own deployment later without rewriting imports across the repository.

The contracts folder is different. It holds every type that crosses a process boundary: job requests, progress events, webhook payloads, render manifests. Both the API and the worker import it. Change a field and the compiler tells you every consumer that needs updating, including the worker you forgot existed.

Typed Contracts Between Every Stage

This is where dynamic typing sneaks back in. Provider responses are the least predictable part of the system, so they get typed loosely under time pressure, and the looseness spreads. Keep the sloppiness to a single function per provider and normalize immediately.

Describe the request and the result

type AspectRatio = "16:9" | "9:16" | "1:1" | "4:5";

type GenerationRequest = {
  prompt: string;
  negativePrompt?: string;
  aspectRatio: AspectRatio;
  durationSeconds: number;
  seed?: number;
  referenceImageUrl?: string;
  motionStrength: number;
};

type GenerationResult = {
  assetId: string;
  width: number;
  height: number;
  durationSeconds: number;
  container: "mp4" | "webm";
  posterUrl: string;
};

The detail that matters is AspectRatio as a union rather than a plain string. It forces every call site to be explicit and lets you map ratios to render presets in exactly one place.

Model the job lifecycle as a union

Long-running jobs are where optional fields multiply. A job that is queued has no progress. A job that failed has no result. Express that in the type system instead of documenting it in a comment.

type JobState =
  | { status: "queued"; queuedAt: string; position: number }
  | { status: "running"; progress: number; stage: RenderStage }
  | { status: "failed"; reason: FailureReason; retryable: boolean }
  | { status: "complete"; result: GenerationResult };

Now a progress component has to narrow on status before it can read progress. No more defensive fallbacks scattered through the client, and no more accidental reads of result on a job that failed three attempts ago. The same pattern applies to uploads, transcodes, and exports.

Version the contract, not the field

When a shape changes, add RenderRequestV2 next to RenderRequestV1, write both during a migration window, and move consumers one at a time. Mutating a live type and hoping every consumer ships in the same deploy is how you cause an outage during a routine refactor.

Background Work: Queues, Retries, and Scheduling

If there is one decision that determines whether your AI video product survives its first traffic spike, it is whether inference runs behind a durable queue.

What to look for in a queue

Three properties matter more than raw throughput:

  • Visibility timeout. The job is not redelivered while a worker is still rendering it. Without this, a long render gets processed twice and you pay twice.
  • Delayed execution. Useful for polling an external provider on an interval instead of hammering it in a tight loop.
  • Priority lanes. An interactive preview must not sit behind a forty-minute batch export.

A Redis-backed queue, a managed cloud queue, or a database-backed queue with row locking can all work. Pick based on operational familiarity, not benchmarks.

Classify failures explicitly

Retry logic becomes unreadable when it compares raw error strings. Return a typed reason from the worker and let a small switch decide what happens next.

Failure type Retryable Strategy
Rate limited by provider Yes Exponential backoff with jitter, capped attempts
Invalid prompt or parameters No Fail fast, return a clear message to the client
GPU out of memory Conditional Retry at lower resolution or smaller batch
Asset upload interrupted Yes Resume or re-upload, then verify checksum
Model version retired No Route to a supported version and notify
Worker crashed mid-render Yes Reclaim after visibility timeout expires

Typing the failure reason is what makes this table enforceable instead of aspirational. Each row becomes a branch you can test.

Idempotency is not optional

Derive an idempotency key from the normalized request: prompt hash, parameters, model version, and account. If a duplicate delivery arrives, the worker returns the existing asset instead of starting a second render. This protects both your compute budget and the user's patience, since duplicate notifications are confusing even when the underlying work is correct.

Backpressure and admission control

Track queue depth and estimated wait time, and surface both to the client. When the median wait exceeds a threshold you have chosen deliberately, return a clear capacity response with a suggested retry window instead of accepting work you cannot finish. A typed capacity model makes this testable: drive the scheduler with synthetic load and assert that the admission decision flips exactly where you expect.

Real-Time Progress That Users Trust

Users will wait ninety seconds for a render if they can see it moving. They abandon the same ninety seconds when the spinner is meaningless. Progress reporting is a product feature, not a logging concern.

Design events, not log lines

type ProgressEvent =
  | { type: "stage"; stage: RenderStage; percent: number }
  | { type: "preview"; frameUrl: string; index: number }
  | { type: "error"; reason: FailureReason; retryable: boolean }
  | { type: "done"; assetId: string };

Emit coarse stages plus a handful of preview frames at meaningful milestones. Do not stream every frame over a socket; that is a bandwidth problem disguised as a feature. Throttle to a few updates per second per job, and drop intermediate values rather than buffering them, because the client only ever needs the latest state.

Scaling sockets without leaking data

WebSocket connections are stateful, so the worker publishing an event rarely holds the connection for that job. Publish to a channel keyed by job identifier and let whichever socket node owns the connection subscribe.

Two security rules matter here. Authenticate on the upgrade request, and revalidate authorization for each job subscription, because a shared connection must never deliver another account's render. When a client reconnects, let it fetch a snapshot of current state over HTTP and then resume the stream, so a dropped connection never leaves the interface permanently stale.

Edge Compute and GPU Workers: A Decision Guide

Edge runtimes are excellent at the fast parts of this pipeline and poor at the heavy parts. Splitting along that line keeps costs predictable.

Good fits for edge functions:

  • Validating request payloads and rejecting malformed prompts before they reach a queue.
  • Generating thumbnails from a single source frame.
  • Rewriting manifests for adaptive bitrate delivery.
  • Adding cache headers and normalizing content types.
  • Serving small metadata responses close to the user.

Poor fits for edge functions:

  • Diffusion, upscaling, frame interpolation, or anything requiring persistent GPU capacity.
  • Tasks that need long execution windows or large local scratch space.
  • Work that must warm a model into memory across many requests.

A simple test: if the task takes under 200 milliseconds and needs no GPU, it belongs at the edge. If it takes minutes or needs VRAM, it belongs on a worker.

Model Routing, Versioning, and Capability Checks

You will change model providers. Sometimes quality improves elsewhere, sometimes an endpoint is deprecated, sometimes one specific shot type needs a different model entirely. Make that swap configuration, not a refactor.

Declare capabilities instead of hardcoding names

Give every provider adapter a narrow interface and a capability descriptor. The rest of the application then asks questions like "can this route do image-to-video at 1080p with a six-second cap?" instead of checking provider names in a dozen places.

A capability registry lets a router pick a provider from requirements. New providers become a registration plus an adapter, not a scattering of conditional branches.

Migrating a live model version

Shadow mode is the cheapest way to change models safely:

  1. Route a small percentage of traffic to the candidate version.
  2. Record output metadata (dimensions, duration, failure reasons) for both versions.
  3. Compare failure rates and retry counts before comparing subjective quality.
  4. Increase traffic in steps, keeping the previous version available for rollback.
  5. Retire the old version only after the queue contains zero jobs referencing it.

That last step is easy to skip and expensive to forget. Long-running jobs can outlive a configuration change, so a version must stay loadable until the last job referencing it completes.

Testing an AI Pipeline Without Burning Compute

You cannot run real inference in every unit test, and you should not want to. Put the model call behind an interface and fake it.

Build a fake provider that misbehaves

A fake that always returns success tests almost nothing. Configure your fake to stall, to return partial output, to fail with each classified reason, and to return a response shaped slightly differently from the documented one. Then test the parts that genuinely break in production: retry classification, idempotency guards, state transitions, socket reconnection, and manifest assembly.

Contract checks against sandboxes

For each real adapter, keep a small suite that runs against a sandbox environment on demand. Assert the fields you depend on: asset URL, duration, dimensions, and error codes. When a provider quietly changes a response envelope, a scheduled run tells you before a user does.

Fast feedback for the rest of the stack

Most of your logic is not model logic. Keep the control plane free of GPU dependencies so its tests run in milliseconds, and reserve slow integration suites for the pipeline seams: intake to queue, queue to worker, worker to storage, storage to client.

Observability, Cost, and the Mistakes That Hurt Most

Video systems fail in ways ordinary web apps do not: GPU exhaustion, model deprecation, provider outages, and slow memory growth in long-lived workers.

Metrics worth tracking

Track queue wait time, per-stage duration percentiles, provider error rate by code, retry counts, GPU utilization, and cost per completed minute of output. Percentiles matter more than averages. A mean render time of forty seconds with a p99 of twelve minutes means a meaningful slice of requests is effectively broken, and the average hides it completely.

Correlate everything with a job identifier carried through logs, traces, and events. Debugging a failed render without a trace is guesswork.

Mistakes that show up again and again

  • Running inference in the request thread instead of behind a queue.
  • Trusting provider response shapes without normalizing them at the boundary.
  • Using loose typing at provider edges and paying for it months later.
  • Retrying non-retryable failures and multiplying compute spend.
  • Emitting progress events without throttling.
  • Storing finished assets without lifecycle rules, so storage costs climb every month.
  • Treating model selection as a constant instead of a routed decision based on declared capabilities.
  • Letting a worker update job state directly in the database without a compare-and-set, which allows two workers to fight over the same job.
  • Forgetting to version contracts across separately deployed API and worker processes.

Each of these has a typed, testable fix. That is the point: the fix is structural, not a matter of remembering to be careful.

FAQ: TypeScript for AI Video Workflows

Do I need NestJS to get these benefits? No. NestJS makes dependency injection and module boundaries convenient, but the structure works with Fastify, Express, Hono, or a worker-only codebase. What matters is interface boundaries and shared contracts, not the framework name.

Should I enable strict compiler settings? Yes, along with options that catch unchecked index access and inexact optional properties. Video pipelines juggle many optional fields, and those flags catch precisely the mistakes that cause crashes deep inside a worker at inconvenient hours.

How do I handle provider responses that are genuinely dynamic? Parse once at the edge of your system with a runtime schema validator, convert to your internal type, and never let the raw shape travel further than that one function. Validation plus a typed boundary keeps the rest of the code honest.

Where should socket event types live? In the shared contracts area, versioned alongside job types. If the API and client live in different repositories, publish the contracts as a versioned package so upgrades are deliberate rather than accidental.

How do I keep GPU workers from over-subscribing? Model concurrency explicitly. Each worker advertises how many concurrent jobs it can hold, and the scheduler assigns work only within that capacity. Combine that with priority lanes so interactive previews are not starved by long exports.

Is TypeScript too slow for frame-level work? The compiler never ships to production; types are erased at build time. Runtime cost comes from your architecture, not from type annotations. For per-frame processing, keep hot loops in native modules or a separate Python worker and communicate across a typed message boundary.

What if I need to migrate a live pipeline? Introduce the new shape alongside the old one, write both during the transition, and move consumers one at a time. Typed contracts make migrations boring, which is exactly what you want during a release.

How do I estimate capacity? Measure seconds of GPU time per finished minute of output for each model and resolution, then divide available GPU seconds by that figure. It is a rough number, but it turns capacity planning from an argument into arithmetic.

A Practical Ship Checklist

Before calling an AI video pipeline ready, walk through a short list.

Contracts exist for every cross-process payload, including progress events and webhook bodies. The job model is a discriminated union with no orphan optional fields. Every provider sits behind an interface with declared capabilities, and configuration is validated at startup so a missing value fails immediately rather than at the first render. Compute runs through a durable queue with idempotency keys and typed failure classification. Progress updates are throttled, reconnection-safe, and authorized per job. Assets are delivered through signed, expiring URLs with range support and lifecycle rules. Tests cover the pipeline around the model rather than the model itself. Metrics cover queue wait, stage latency, retry counts, and cost per finished minute.

None of this requires exotic tooling. It requires deciding, once, that the boundaries between your API, your queue, your workers, and your client are typed contracts rather than hopeful conventions. Teams that make that decision early spend their time improving output quality, which is the part users actually notice.

Alexander

Alexander