AhmadRaza365 Logo

AhmadRaza365

Blog Post

City-Based Checkout Pricing in Next.js Without Surprising the Customer

October 2, 2026
City-Based Checkout Pricing in Next.js Without Surprising the Customer

Most stores treat shipping as a single number. That works until you sell in a country where Karachi, Lahore, and a small city do not cost the same to deliver.

AffordableBags.pk needed city-based checkout pricing with layout customization on a Next.js bags store. The hard part was not the rate table. It was making sure the total the customer saw in the cart matched what they paid, and that nobody could change the city in the browser and get a cheaper total.

This post is a practical walkthrough for founders and ops leads who want city-based pricing without turning checkout into a science project.

Start with the rule, not the UI

Before you touch React components, write the business rule in one sentence:

Delivery fee (or discount) depends on the customer's city. The server is the only place that decides the final amount.

Everything else hangs off that. If the rule lives only in the client, you will get mismatches, support tickets, and eventually a customer who paid less than they should have.

On AffordableBags.pk, city drove checkout pricing. Layout customization was separate: how the store looked and how products were presented. The checkout total still had to be honest and consistent.

What the customer should see

Keep the surface area small:

  1. City (or area) selection early enough that the shipping line updates before they hit Pay.
  2. A clear shipping / delivery line in the cart summary, not buried under "other fees."
  3. The same total on cart, checkout review, and confirmation.

If the delivery fee jumps only on the payment page, trust drops. People abandon when numbers move without explanation.

A short helper under the city field helps: "Delivery fee updates when you choose your city." One line. No marketing copy.

Data model that stays maintainable

You do not need a full logistics engine on day one. You need:

  • A list of cities (or city groups) your ops team can edit.
  • A fee (or fee rule) per city or group.
  • A default for "city not listed" so checkout never soft-locks.

Store that in a place your admin can update without a deploy: CMS, admin API, or a simple config collection. Hardcoding rates in a Next.js file works for a pilot; it fails the first time ops asks for a weekend rate change.

Group cities when rates are the same. Ten groups beat eighty nearly identical rows.

App Router shape that stays honest

A pattern that holds up:

  1. Client cart holds line items and the selected city id.
  2. A Server Action or Route Handler recalculates subtotal, delivery fee, and total from trusted data.
  3. Checkout submits city + cart id (or cart snapshot) to the server.
  4. The server recomputes fees and rejects a total that does not match.

Never trust deliveryFee from the request body as the source of truth. Accept cityId, look up the fee, recompute.

With Next.js App Router, keep pricing logic in a shared server module: same function for cart preview and final order creation. One function, two call sites. That is how you avoid "preview said 300, charge charged 500."

Cart preview vs final charge

Two moments matter:

Preview. When the city changes, call the same pricing function and refresh the summary. Optimistic UI is fine if you show a brief loading state and replace with the server result.

Commit. When creating the order or payment intent, run pricing again. Persist the city, fee, and total on the order record. If the city was removed or the rate changed mid-session, return a clear error and ask them to refresh the summary.

That second pass is what protects you. The first pass is what keeps the UX calm.

Edge cases founders forget

These show up in real stores:

  • Customer changes city after applying a coupon. Re-run pricing and coupon eligibility together.
  • COD vs prepaid. Some cities support cash on delivery; others do not. Gate payment methods by city on the server, not only in the UI.
  • Partial address. If you only ask for city at first, still validate a fuller address before fulfillment.
  • Guest vs logged-in. Prefer selected city on the order over a stale profile city.
  • Free-shipping thresholds. Apply threshold after city fee rules, and say so under the shipping line only if that is actually true.

AffordableBags.pk's city-based checkout pricing only works if these edges are boring and explicit. Boring is good.

Admin UX matters as much as the storefront

Ops will own the rate list longer than engineering will. Give them:

  • A table: city (or group), fee, active/inactive.
  • Bulk edit or CSV import if the list is long.
  • A "test total" box: pick city + sample cart subtotal, then see fee and total.

If changing a Lahore fee requires a developer, the table will rot and support will invent workarounds in WhatsApp.

The Glam Bar's WordPress-to-Next.js migration was partly about simpler admin and less maintenance. City rates are the same idea in miniature: put the lever where the business can pull it.

What not to build yet

Skip these until you feel real pain:

  • Per-SKU dimensional weight by city
  • Live carrier APIs for every order
  • Neighborhood polygons when city-level is enough
  • A custom map picker

Ship city, fee, validated total. Add carrier APIs when volume or margin forces it.

Checklist before you launch

  • Single server function for delivery fee + total
  • Cart and checkout both call it
  • Order record stores city, fee, and total
  • Payment amount matches server total
  • Ops can edit city fees without a deploy
  • Unknown city has a safe default or a hard stop with a clear message
  • Payment methods that depend on city are enforced server-side

Closing

City-based pricing is not a theme setting. It is a checkout contract: show the right number early, decide the number on the server, and store what you charged.

That is the approach behind AffordableBags.pk's Next.js checkout pricing by city. Keep the storefront clear, keep the rule in one place, and let ops own the table.

If you are tightening checkout or moving a store to Next.js, the same discipline applies to gateways, SMS alerts, and admin tools. More of that work is at ahmadraza365.com.

You can find me on different platforms