Project 03 / 05 · Spirit Airlines · 2021–2022

The booking funnel where most of the revenue lives

Spirit.com's booking funnel — homepage widget, fare display, baggage selector, seat map, bundles, checkout — drives nearly all of the airline's e-commerce revenue, and the legacy stack underneath it was the bottleneck on ancillary upsell experimentation (bags, seats and bundles are the low-cost carrier's primary margin levers, not the base fare). I led the React 18 + TypeScript rebuild of those highest-traffic surfaces, on top of a shared TypeScript component library consumed across promotions, search, booking and checkout — and end-to-end TypeScript contracts from the Lambda edge through to UI prop types. Ancillary feature delivery time dropped about 30%. The booking widget holds a sub-300ms render budget. The entire purchase experience meets WCAG 2.1 from keyboard navigation through screen-reader output.

Role
Senior Frontend Engineer
Dates
Aug 2021 – Nov 2022
Location
Fort Lauderdale, FL
Domain
Airline e-commerce / consumer
  • React 18
  • TypeScript
  • Tailwind CSS
  • NgRx
  • AWS Lambda
  • AWS API Gateway
  • Jest
  • WCAG 2.1
At a glance
Widget render budget
Sub-300ms

Homepage booking widget held to a strict budget — critical-render priority, deep checkout modules lazy-loaded.

Faster ancillary delivery
30%

Shared TypeScript component library — booking widget, fare chips, seat map, baggage selector, bundle cards consumed across promotions, search, booking and checkout.

Type contracts
End-to-end

Lambda handler signatures shared through to UI prop types — runtime checkout errors caught at compile time, not by customers.

Accessibility
WCAG 2.1

Keyboard navigation and screen-reader support across the entire purchase experience — built in from the start, not retrofitted.

The interface

Illustrative recreation

An interactive recreation of the booking-funnel entry surface — fictional fares, real UI behavior. The booking widget on top, fare tiers below, an ancillary bundle upsell card to the right. The shared component library is what made this kind of surface ship-once across promotions, search, and checkout.

spirit.com/booking
Open full screen ↗

Shared library across every surface

Booking widget, fare-display chips and bundle cards in this view are the same components consumed by promotions, search results, and checkout summary — one source of truth.

Typed end-to-end to the API edge

Every prop in this view traces back to a TypeScript contract shared with the Lambda handler that serves the fare data. A response-shape change is a compile error, not a production undefined.

Sub-300ms widget, lazy-loaded checkout

The booking widget on top is critical-render priority. Deep checkout modules (seat selection, baggage details, payment) load on demand so they don't block the homepage paint.

Recreation · stylized stand-in for the airline brand, faithful to the funnel's UI behavior, not the shipped source.

The problem

The funnel where the revenue actually lives

Spirit Airlines is a low-cost carrier. The base fare is a teaser; the actual revenue comes from ancillaries — checked bags, carry-on bags, seat selection, bundles, priority boarding, change fees. The booking funnel is where every one of those upsells happens, and where each one either lands or doesn't. The team that owns it has to ship ancillary feature experiments fast, measure the conversion delta cleanly, and never regress the base purchase flow under all the experimentation.

The legacy stack underneath was the bottleneck. Untyped APIs at the Lambda edge meant production runtime errors in checkout — the most expensive place in the funnel to have them. Per-team component duplicates of the booking widget, fare chips, and bundle cards drifted out of sync, so the same change had to ship five times across promotions, search and checkout. No clean lazy-loading boundary between the homepage widget and the deep checkout modules meant the widget paid for code paths it didn't use. Accessibility was retrofitted after the fact, painfully and incompletely.

Replacing all of that on a revenue-critical funnel while keeping the funnel live for the airline's daily booking traffic — that's the work.

Base fare is a teaser; the actual revenue comes from ancillaries — and the funnel is where every one of those upsells either lands or doesn't.
The defining constraint

Key decisions

Four calls that shaped the rebuild

Building a shared component library, typing the Lambda boundary end-to-end, keeping the existing state machine through migration, and getting accessibility right from day one — each one trades short-term complexity for long-term resilience on a funnel that ships ancillary experiments constantly.

01Decision

Shared TypeScript component library over per-team duplicates

The booking widget appears on the homepage, in search results, in fare display, in checkout summary. Fare-display chips render in promotions, in search, in the funnel itself. Baggage selector, seat map, bundle cards — all of them get consumed by multiple surfaces. The shared TypeScript component library is one source of truth so ancillary teams ship a feature change once and it appears everywhere consistently. Ancillary feature delivery time dropped roughly 30% after migration.

The catch· Coordinating a shared library across multiple consuming teams takes governance. In exchange, revenue-critical surfaces stay visually consistent and a feature change ships once, not five times.

02Decision

End-to-end TypeScript contracts at the Lambda boundary

The previous untyped API edge was the source of multiple runtime type errors in checkout — the most expensive place in the funnel to have them. A Lambda handler quietly dropped a field from its response once and the UI silently rendered undefined in a fare-display chip on the live homepage. The fix wasn't a defensive null guard; it was end-to-end TypeScript contracts from the Lambda handler signature through to UI prop types. A contract change anywhere in the chain becomes a compile-time error before it can ship to production.

The catch· Shared type contracts mean coordination between Lambda and UI teams on every API change. In exchange, the entire class of “undefined silently rendered in production” bugs is gone.

03Decision

Keep the existing state library through the migration

The booking-flow state machine is genuinely complex — multi-step form, conditional pricing, real-time fare updates, ancillary cross-sells. The team had deep muscle memory with the existing state library from the prior Angular era. Switching to a different state library mid-migration would have meant a parallel learning curve at the worst possible time. The booking flow kept its existing state machine; newer modules adopted Redux Toolkit and TanStack Query as the team transitioned.

The catch· Two state-management patterns coexisting in the same codebase isn't a long-term architecture. In exchange, the migration shipped on schedule and the booking-flow team didn't lose velocity learning new patterns mid-flight.

04Decision

Accessibility from the start, not retrofitted

A multi-step purchase form with conditional fields, dynamic pricing and ancillary cross-sells is one of the hardest things to retrofit for accessibility. We built it in from day one — keyboard navigation, screen-reader announcements on state changes, focus management across step transitions, sufficient contrast across every surface — so the end-to-end purchase experience works for assistive technology without a separate accessibility sprint after launch.

The catch· Accessibility-first slows the first wireframe-to-prototype loop slightly. In exchange, the funnel ships compliant on day one instead of requiring a remediation pass post-launch.

What I built

The funnel, the shared library, and the type contracts

I built the homepage booking widget (sub-300ms render budget on the highest-traffic surface, critical-render priority on the entry path, deep checkout modules lazy-loaded with React.lazy + Suspense). I owned the baggage selection flow, seat selection with its custom seat-map renderer, the bundles upsell flow, fare display with conditional pricing and dynamic ancillary cross-sells, and the checkout path with end-to-end TypeScript contracts from the Lambda handler signature through to UI prop types.

I co-built the shared TypeScript component library that ancillary teams consume across promotions, search, booking and checkout — one source of truth that cut ancillary feature delivery time about 30%. WCAG 2.1 accessibility (keyboard navigation, screen-reader announcements on state changes, focus management across step transitions, sufficient contrast across every surface) was built in from the start across the entire funnel, not retrofitted after launch. Jest + React Testing Library coverage on every consuming component, Lighthouse CI gating PR merges so the render-budget commitment stays enforced as the codebase grows.

Project 04 / 05 · Next

Resume Studio