<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[NPATI Marketplace Blog]]></title><description><![CDATA[NPATI Marketplace Blog]]></description><link>https://npati.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a395173d16d2e17759e655c/123bb7ae-7f4a-4cc1-90ee-ef2f650cd4ca.png</url><title>NPATI Marketplace Blog</title><link>https://npati.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 23 Sep 2026 18:27:58 GMT</lastBuildDate><atom:link href="https://npati.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Structure Country-Specific Routes in a Multi-Market Web Application]]></title><description><![CDATA[Building a web application for several countries requires more than translating interface text.
Each market may use a different language, currency, address format, category structure and URL pattern. ]]></description><link>https://npati.hashnode.dev/how-to-structure-country-specific-routes-in-a-multi-market-web-application</link><guid isPermaLink="true">https://npati.hashnode.dev/how-to-structure-country-specific-routes-in-a-multi-market-web-application</guid><category><![CDATA[Web Development]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[localization]]></category><category><![CDATA[SEO]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Andrii Kostashchuk]]></dc:creator><pubDate>Mon, 22 Jun 2026 15:34:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a395173d16d2e17759e655c/d62f405c-f774-49f9-8af9-764c7c24b7e5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building a web application for several countries requires more than translating interface text.</p>
<p>Each market may use a different language, currency, address format, category structure and URL pattern. If these differences are added without a clear architecture, the application can quickly become difficult to maintain.</p>
<p>This article describes a practical way to organise country-specific routes and settings in a multi-market web application.</p>
<h2>The Main Challenge</h2>
<p>Imagine an application that supports four markets:</p>
<ul>
<li><p>United States</p>
</li>
<li><p>Ukraine</p>
</li>
<li><p>Canada</p>
</li>
<li><p>United Kingdom</p>
</li>
</ul>
<p>The application needs to show different content depending on the selected country.</p>
<p>For example:</p>
<ul>
<li><p>The United States uses USD and ZIP codes.</p>
</li>
<li><p>Ukraine uses UAH and Ukrainian regional data.</p>
</li>
<li><p>Canada uses provinces and postal codes.</p>
</li>
<li><p>The United Kingdom uses GBP and UK postcodes.</p>
</li>
</ul>
<p>The application may also use different terminology.</p>
<p>A property in the United States may be described as an apartment, while British users often expect the word flat.</p>
<p>A phone number, date, price and address may also need different formatting.</p>
<p>The application should handle these differences without creating a completely separate codebase for every country.</p>
<h2>Use a Market Configuration Object</h2>
<p>A simple solution is to store market-specific settings in one configuration object.</p>
<pre><code class="language-ts">type MarketCode = "US" | "UA" | "CA" | "GB";

interface MarketConfig {
  code: MarketCode;
  path: string;
  language: string;
  currency: string;
  locale: string;
  postcodeLabel: string;
  timezone: string;
}

const markets: Record&lt;MarketCode, MarketConfig&gt; = {
  US: {
    code: "US",
    path: "",
    language: "en",
    currency: "USD",
    locale: "en-US",
    postcodeLabel: "ZIP code",
    timezone: "America/New_York",
  },
  UA: {
    code: "UA",
    path: "/ua",
    language: "uk",
    currency: "UAH",
    locale: "uk-UA",
    postcodeLabel: "Поштовий індекс",
    timezone: "Europe/Kyiv",
  },
  CA: {
    code: "CA",
    path: "/ca",
    language: "en",
    currency: "CAD",
    locale: "en-CA",
    postcodeLabel: "Postal code",
    timezone: "America/Toronto",
  },
  GB: {
    code: "GB",
    path: "/gb",
    language: "en",
    currency: "GBP",
    locale: "en-GB",
    postcodeLabel: "Postcode",
    timezone: "Europe/London",
  },
};
</code></pre>
<p>This structure keeps the main market differences in one place.</p>
<p>When a new country is added, the development team can create another configuration entry instead of changing many unrelated components.</p>
<h2>Detect the Market from the URL</h2>
<p>The next step is to identify the selected market from the URL.</p>
<p>For example:</p>
<pre><code class="language-text">/                 United States
/ua               Ukraine
/ca               Canada
/gb               United Kingdom
</code></pre>
<p>A basic resolver can inspect the first path segment.</p>
<pre><code class="language-ts">function resolveMarket(pathname: string): MarketConfig {
  const firstSegment = pathname.split("/").filter(Boolean)[0];

  if (firstSegment === "ua") {
    return markets.UA;
  }

  if (firstSegment === "ca") {
    return markets.CA;
  }

  if (firstSegment === "gb") {
    return markets.GB;
  }

  return markets.US;
}
</code></pre>
<p>This function can be called on the server or client, depending on the framework.</p>
<p>For server-rendered applications, market detection should usually happen before the page is rendered. This allows the correct language, currency and metadata to be loaded immediately.</p>
<h2>Keep Country and Language Separate</h2>
<p>Country and language should not be treated as the same value.</p>
<p>Canada, for example, may support both English and French. The United States and the United Kingdom both use English, but they use different spelling, currency and address formats.</p>
<p>A better structure separates these concepts:</p>
<pre><code class="language-ts">interface UserContext {
  market: MarketCode;
  language: string;
  currency: string;
}
</code></pre>
<p>A user may have:</p>
<pre><code class="language-ts">const context: UserContext = {
  market: "CA",
  language: "fr",
  currency: "CAD",
};
</code></pre>
<p>This makes future localisation easier.</p>
<h2>Format Prices with Intl.NumberFormat</h2>
<p>Prices should not be formatted manually.</p>
<p>JavaScript provides <code>Intl.NumberFormat</code>, which supports local currency formatting.</p>
<pre><code class="language-ts">function formatPrice(
  amount: number,
  locale: string,
  currency: string
): string {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(amount);
}
</code></pre>
<p>Example usage:</p>
<pre><code class="language-ts">formatPrice(1250, "en-US", "USD");
// $1,250.00

formatPrice(1250, "en-GB", "GBP");
// £1,250.00

formatPrice(1250, "uk-UA", "UAH");
// 1 250,00 грн
</code></pre>
<p>This approach reduces formatting errors and respects local conventions.</p>
<h2>Adapt Address Forms by Market</h2>
<p>A single address form may not work well for every country.</p>
<p>Different markets use different administrative divisions.</p>
<p>For example:</p>
<pre><code class="language-ts">const addressFields = {
  US: {
    regionLabel: "State",
    postcodeLabel: "ZIP code",
  },
  UA: {
    regionLabel: "Region",
    postcodeLabel: "Поштовий індекс",
  },
  CA: {
    regionLabel: "Province",
    postcodeLabel: "Postal code",
  },
  GB: {
    regionLabel: "Country",
    postcodeLabel: "Postcode",
  },
};
</code></pre>
<p>The form component can read these values from the selected market configuration.</p>
<pre><code class="language-tsx">function AddressForm({ market }: { market: MarketCode }) {
  const fields = addressFields[market];

  return (
    &lt;form&gt;
      &lt;label&gt;
        City
        &lt;input name="city" /&gt;
      &lt;/label&gt;

      &lt;label&gt;
        {fields.regionLabel}
        &lt;input name="region" /&gt;
      &lt;/label&gt;

      &lt;label&gt;
        {fields.postcodeLabel}
        &lt;input name="postcode" /&gt;
      &lt;/label&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>For production applications, region fields should usually use validated lists rather than unrestricted text fields.</p>
<h2>Store Categories by Country</h2>
<p>Not every category should be shared across all markets.</p>
<p>Different countries may use different names and category structures.</p>
<p>A category record can include a country field:</p>
<pre><code class="language-ts">interface Category {
  id: string;
  name: string;
  slug: string;
  country: MarketCode;
  parentId?: string;
}
</code></pre>
<p>Example:</p>
<pre><code class="language-ts">const categories: Category[] = [
  {
    id: "1",
    name: "Mobile Phones",
    slug: "mobile-phones",
    country: "GB",
  },
  {
    id: "2",
    name: "Мобільні телефони",
    slug: "mobilni-telefony",
    country: "UA",
  },
];
</code></pre>
<p>When loading categories, the application should filter them by the selected market.</p>
<pre><code class="language-ts">function getCategoriesForMarket(
  categories: Category[],
  market: MarketCode
): Category[] {
  return categories.filter(category =&gt; category.country === market);
}
</code></pre>
<p>This prevents categories from one country appearing in another market.</p>
<h2>Generate Consistent URLs</h2>
<p>A reusable URL builder helps avoid incorrect country paths.</p>
<pre><code class="language-ts">function buildMarketUrl(
  market: MarketCode,
  pathname: string
): string {
  const basePath = markets[market].path;
  const cleanPath = pathname.startsWith("/")
    ? pathname
    : `/${pathname}`;

  return `\({basePath}\){cleanPath}`;
}
</code></pre>
<p>Example:</p>
<pre><code class="language-ts">buildMarketUrl("US", "/vehicles");
// /vehicles

buildMarketUrl("UA", "/transport");
// /ua/transport

buildMarketUrl("CA", "/vehicles");
// /ca/vehicles

buildMarketUrl("GB", "/cars-vehicles");
// /gb/cars-vehicles
</code></pre>
<p>Centralising this logic reduces broken links and incorrect route prefixes.</p>
<h2>Handle Canonical URLs Carefully</h2>
<p>Country-specific pages should have their own canonical URLs when the content is intended for different markets.</p>
<p>For example:</p>
<pre><code class="language-html">&lt;link
  rel="canonical"
  href="https://example.com/gb/cars-vehicles"
/&gt;
</code></pre>
<p>Do not automatically point all country versions to the main United States page.</p>
<p>If the page has local categories, currency, content or locations, it may be a separate and useful page for search engines.</p>
<p>Applications with translated alternatives can also use <code>hreflang</code>.</p>
<pre><code class="language-html">&lt;link
  rel="alternate"
  hreflang="en-us"
  href="https://example.com/vehicles"
/&gt;

&lt;link
  rel="alternate"
  hreflang="uk-ua"
  href="https://example.com/ua/transport"
/&gt;

&lt;link
  rel="alternate"
  hreflang="en-ca"
  href="https://example.com/ca/vehicles"
/&gt;

&lt;link
  rel="alternate"
  hreflang="en-gb"
  href="https://example.com/gb/cars-vehicles"
/&gt;
</code></pre>
<p>The URLs should represent real regional content. They should not exist only for search engine optimisation.</p>
<h2>Avoid Automatic Redirects That Block Users</h2>
<p>IP-based country detection can be useful, but forced redirects may create problems.</p>
<p>A user in Ukraine may want to browse the Canadian market. A Canadian user may be travelling in another country.</p>
<p>A safer approach is:</p>
<ol>
<li><p>Detect the likely country.</p>
</li>
<li><p>Suggest the local version.</p>
</li>
<li><p>Allow the user to stay on the current market.</p>
</li>
<li><p>Save the selected market in a cookie or account preference.</p>
</li>
</ol>
<p>Example:</p>
<pre><code class="language-ts">interface MarketPreference {
  detectedMarket: MarketCode;
  selectedMarket: MarketCode;
  manuallySelected: boolean;
}
</code></pre>
<p>The manually selected market should normally have priority over automatic detection.</p>
<h2>Validate Data on the Server</h2>
<p>Client-side validation improves the user experience, but it is not enough.</p>
<p>The server should verify that:</p>
<ul>
<li><p>The selected city belongs to the selected country.</p>
</li>
<li><p>The region belongs to the correct market.</p>
</li>
<li><p>The currency matches the listing market.</p>
</li>
<li><p>The category is available in that country.</p>
</li>
<li><p>The URL path matches the stored country value.</p>
</li>
</ul>
<p>Example:</p>
<pre><code class="language-ts">function validateListingMarket(
  listingMarket: MarketCode,
  category: Category
): void {
  if (listingMarket !== category.country) {
    throw new Error("Category does not belong to the selected market");
  }
}
</code></pre>
<p>Without server validation, incorrect data can enter the database through modified API requests.</p>
<h2>A Useful Database Structure</h2>
<p>A simplified database structure may look like this:</p>
<pre><code class="language-sql">CREATE TABLE markets (
  code VARCHAR(2) PRIMARY KEY,
  language VARCHAR(10) NOT NULL,
  currency VARCHAR(3) NOT NULL,
  locale VARCHAR(20) NOT NULL,
  path VARCHAR(20) NOT NULL
);

CREATE TABLE categories (
  id UUID PRIMARY KEY,
  country_code VARCHAR(2) NOT NULL,
  name VARCHAR(255) NOT NULL,
  slug VARCHAR(255) NOT NULL,
  parent_id UUID,
  FOREIGN KEY (country_code) REFERENCES markets(code)
);

CREATE TABLE listings (
  id UUID PRIMARY KEY,
  country_code VARCHAR(2) NOT NULL,
  category_id UUID NOT NULL,
  title VARCHAR(255) NOT NULL,
  description TEXT NOT NULL,
  price DECIMAL(12, 2),
  currency VARCHAR(3) NOT NULL,
  FOREIGN KEY (country_code) REFERENCES markets(code),
  FOREIGN KEY (category_id) REFERENCES categories(id)
);
</code></pre>
<p>This design keeps market information explicit.</p>
<p>It also makes it easier to filter listings and categories without depending only on URL strings.</p>
<h2>Common Mistakes</h2>
<p>Several mistakes are common in multi-market applications.</p>
<h3>Hardcoding country logic in components</h3>
<p>Avoid code such as:</p>
<pre><code class="language-ts">if (country === "GB") {
  // many lines of UK-specific logic
}
</code></pre>
<p>If similar conditions are repeated throughout the application, maintenance becomes difficult.</p>
<p>Move differences into configuration objects or specialised services.</p>
<h3>Using one currency globally</h3>
<p>Currency should be stored with the listing or derived from a validated market.</p>
<p>Never assume that every listing uses the application's default currency.</p>
<h3>Mixing location data</h3>
<p>A city from one country should not be available in another country's address form.</p>
<p>Location data needs relationships between countries, regions and cities.</p>
<h3>Duplicating entire applications</h3>
<p>Creating a separate application for every country may seem simple at first, but it creates duplicated code and inconsistent features.</p>
<p>A shared codebase with market-specific configuration is usually easier to maintain.</p>
<h3>Translating without localisation</h3>
<p>Translation changes words. Localisation changes the full user experience.</p>
<p>Localisation includes:</p>
<ul>
<li><p>Currency</p>
</li>
<li><p>Date formats</p>
</li>
<li><p>Address formats</p>
</li>
<li><p>Category names</p>
</li>
<li><p>Spelling</p>
</li>
<li><p>Legal notices</p>
</li>
<li><p>Local terminology</p>
</li>
<li><p>Measurement units</p>
</li>
</ul>
<h2>Conclusion</h2>
<p>A multi-market application needs a clear separation between shared application logic and country-specific configuration.</p>
<p>A practical architecture should:</p>
<ul>
<li><p>Resolve the market from the URL</p>
</li>
<li><p>Store market settings centrally</p>
</li>
<li><p>Keep language and country separate</p>
</li>
<li><p>Format prices using locale-aware tools</p>
</li>
<li><p>Adapt address forms</p>
</li>
<li><p>Filter categories by market</p>
</li>
<li><p>Validate country data on the server</p>
</li>
<li><p>Generate consistent canonical URLs</p>
</li>
<li><p>Respect manual country selection</p>
</li>
</ul>
<p>This approach makes it easier to add new markets without duplicating the entire application.</p>
<p>The examples in this article are based on lessons from developing a real multi-country classifieds project.</p>
<p>Disclosure: I am building NPATI.</p>
<p>Project example:</p>
<p><a href="https://www.npati.com">https://www.npati.com</a></p>
]]></content:encoded></item></channel></rss>