Systems

supporting system

Flynt - Credit Card Points to Flights

Redeeming credit-card points for real flight bookings

A flight-search interface that turns different bank point-conversion rules into domestic flight options people can compare and book.

scale
Searches Indian domestic flights. The blog and page content are static, while flight data is requested for each search.
latency
Search speed depends on the external Flynt Flight Search API. Typical production requests complete in under one second.
reliability
Zod validates forms on the client and server. Toast messages explain API failures, missing inputs, and empty result sets.
automation
A Server Action handles newsletter subscriptions at the edge, away from the render path. Dynamic imports defer heavier sections to reduce the initial bundle.
Flynt home pagefirst proof
A consolidated points balance and flight-search entry point across bank programmes.
Context

Problem had shape before code did.

Users search domestic flights and see results in credit-card points rather than cash. After they choose an origin and destination, the app builds a query, fetches flight bundles from the Flynt API, converts them into UI-ready results, and shows them in a paginated accordion. One dialog shows booking details and another lists eligible-card conversion options.

  • Added debounced airport autocomplete backed by the external API.
  • Built a paginated result accordion with layover notes.
  • Designed a points-based booking view with bank offers and eligible-card tables.
  • Handled newsletter signups with a Zod-validated Server Action.
  • Used typed content data for blog posts and static pages.
Data source
All flight data comes from the external Flynt Flight Search API. There is no internal database.
Caching
No internal cache. Every search reaches the live API.
Rendering
Client-heavy App Router with dynamic imports. Search results are not server-rendered.
Pagination
Client-side only, synchronised with the URL's `page` parameter.
Build

Structure had to survive job.

  • The Next.js App Router runs the search interface on the client and dynamically imports heavier sections.
  • A shared Axios client adds the `X-FLYNT-KEY` header to every API request.
  • Zustand stores hold carousel mode and mobile-menu state.
  • The `newsletterService` Server Action runs at the edge, separate from rendering.
The search and booking flow needed immediate client-side interaction, so the App Router stays client-heavy rather than fully server-rendered. That makes the initial bundle larger. Flight data is transient and always comes from the Flynt API, so there is no internal database or cache. Blog and static content use a separate typed system, keeping SEO-focused pages out of the search pipeline.
Working stack

Next.js 15 (App Router, Turbopack) / React 19 / TypeScript 5 / Tailwind CSS 4 / Zustand / Axios / React Hook Form / Zod / Radix UI / Framer Motion / Vercel Analytics

Evidence

Browser becomes part of argument.

Evidence viewerKeyboard arrows change proof

01 / 06Programme details, transfer partners, and redemption sweet spots for each airline.

Decisions

Small cuts make system trustworthy.

  • React Hook Form and Zod validate forms on both the client and server.
  • `buildApiQueryString` maps URL search parameters to the backend's query parameters.
  • `transformApiBundleToFlightResult` converts raw Flynt API bundles into `FlightResult` objects before the UI renders them.
  • A reducer owns pagination state and keeps the URL's `page` parameter in sync with the router.
  • Moved dialog state from the global store into the component that owns each dialog.
Pressure

What resisted, broke, stayed expensive.

Constraints in motion

  • Banks expose different point-conversion ratios, so the app must normalise them before showing results.
  • The Flynt API returns deeply nested, inconsistent data that is not suitable for components to consume directly.
  • Pagination has to run entirely in the browser because the API provides no server-side support for it.
  • Dialog state originally lived in the global store and affected unrelated components.

Failure modes

  • An API error or missing field can interrupt the transformation pipeline.
  • Changing core query parameters can return an empty result set.
  • Before the refactor, global dialog state could open or close the wrong dialog.

Global dialog state causing unintended opens/closes

  1. Open and close state originally lived in the global Zustand store.
  2. Booking and eligible-card dialogs sometimes responded to unrelated component updates.
  3. The state moved into the local component scope.

Repair: Moved each dialog's state into the component that owns it rather than leaving it in the global store.

Held: Stopped cross-component dialog interference and made the UI state easier to follow.

Trade

  • The search and booking flow stay client-heavy for immediate interaction, at the cost of a larger initial bundle.
  • There is no internal cache, so each search depends on the external API's latency.
  • Blog content is static while flight data is dynamic. That keeps SEO content simple, but requires separate data paths.
Result

What held. What carries forward.

engineering notes

Raw API bundles become flight results before components receive them

`transformApiBundleToFlightResult` converts `FlyntFlightBundle` into the smaller `FlightResult` type before rendering.

logSnippet

Each airline programme gets a cash-equivalent value during search

Valuation example: 25,000 Avios equals $300 at 1.2 cents each, while 22,000 SkyMiles equals $242 at 1.1 cents each.

architecture notes

Newsletter subscription stays out of the render path

`newsletterService` runs as an edge Server Action and posts through Axios to the external newsletter endpoint.

If rebuilt

  • Add per-route revalidation or an edge cache to reduce repeat-search latency.
  • Move pagination to the server to reduce the client payload for large result sets.
  • Normalise API error shapes at the transformer boundary instead of handling them in every call site.
結論

Explicit transformer functions keep raw API contracts away from the UI and make bad data easier to contain. Keeping dialog state with the component that uses it also reduces coupling and makes the interaction easier to test.

Make it hold together

Complex frontend system need steady hand?

Architecture, interface, production constraints. Together.