Tax Rates Configuration

Building the Tax Rates API

Welcome back! 👋 In this lesson, we begin building the Tax API for our e-commerce backend. Taxes are a critical part of any online store because they affect the final price customers pay. A reliable backend must apply the correct rate for a customer's location and ensure those rates are stored and managed safely.

In this lesson, we’ll implement the backend infrastructure that manages country-based tax rates. We’ll build a repository layer for database access, a service layer that validates and protects business logic, and API routes that expose the functionality to clients. By the end of the lesson, the system will support listing tax rates, retrieving a single rate, creating or updating a rate, and deleting one.

Previously in this learning path, we polished the Orders API: Checkout Workflow and Order State Transitions. That work handled the lifecycle of orders—from pending to paid or cancelled. Now we’re expanding the system to support tax configuration, which will later be used during checkout to calculate totals and snapshot tax details onto orders.

Why E-commerce Systems Store Taxes Carefully

Before diving into the code, it’s important to understand why taxes require careful backend design.

Taxes vary by country or region, and the rate can change over time. For example, one country might charge 7.25%, another 20% VAT, and governments occasionally adjust those numbers.

When a customer completes checkout, the system must record exactly which tax rate was used. Even if the rate changes later, historical orders must remain correct. This concept is called a snapshot—a copy of the tax configuration stored alongside the order.

To support this system cleanly, our backend must provide:

  • A reliable source of truth for tax rates
  • Validation to prevent invalid country codes or percentages
  • A consistent API for listing, retrieving, updating, and deleting tax configurations

The code in this lesson implements those capabilities.

Implementing the Repository Layer

The repository layer lives in src/lib/repositories/taxRatesRepo.ts. Its job is simple but extremely important: interact with PostgreSQL safely and return domain data.

Repositories should not contain business logic or HTTP concerns. Instead, they focus entirely on executing SQL queries and mapping rows to domain objects.

Below is the first part of the repository file.

import { query } from "@/lib/db/client";
import { TaxRate } from "@/lib/types/domain";

interface TaxRateRow {
  country_code: string;
  rate_bps: number;
  created_at: string;
  updated_at: string;
}
  • The query helper is imported from the database client. This utility executes parameterized SQL queries and returns typed rows from PostgreSQL.
  • The TaxRate type represents the domain object used throughout the application. Returning consistent domain types keeps the service layer and routes predictable.
  • TaxRateRow defines the exact shape of the database row. This helps TypeScript understand what the SQL query returns.
  • Separating database row types from domain types ensures flexibility. If the database schema changes later, we only need to adjust the mapping layer.

Next, we convert raw database rows into domain objects.

function mapTaxRateRow(row: TaxRateRow): TaxRate {
  return {
    country_code: row.country_code,
    rate_bps: row.rate_bps,
    created_at: row.created_at,
    updated_at: row.updated_at,
  };
}
  • This function transforms a raw database row into a domain object that the rest of the application can use safely.
  • Keeping this mapping centralized prevents duplicate transformation logic scattered across services or routes.
  • If new columns are added later, this mapping function becomes the single place to update how rows are converted.
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal