Tax Rates Configuration API

Why Tax Rates Live Behind an API

Welcome back! 👋 Up to this point, you’ve built out the core order workflow—checkout creates orders, and pay/cancel moves them through a basic lifecycle. Now we’re going to add the missing piece that makes checkout totals feel “real”: configurable tax rates that can be managed through an API and stored in Postgres.

In this lesson, you’ll build a Tax Rates Configuration API that lets clients list all tax rates, fetch one country’s rate, save (create/update) a rate, and delete a rate. You’ll see how this codebase keeps routes thin, centralizes rules and validation in the service layer, and uses a repository layer for parameterized SQL.

Previously… you completed the Orders API and its action-based state transitions (pay/cancel). That gave us a stable order lifecycle; now we’re ready to support pricing rules that vary by location by adding a tax rate configuration system that other parts of the backend (like checkout) can rely on.

In e-commerce, tax rules vary by country (and often by region). Hardcoding a single percentage doesn’t scale—and it’s risky because tax rates change.

This project solves that by storing tax rates in a tax_rates table and exposing them through a small API:

  • GET /api/tax/rates → list all configured rates (deterministic ordering)
  • GET /api/tax/rates/:country → fetch one rate
  • PUT /api/tax/rates/:country → create or update a rate (“save” behavior)
  • DELETE /api/tax/rates/:country → delete a rate

A key detail: rates are stored as basis points (rate_bps)—an integer representation of a percentage—so we avoid floating point rounding issues in financial logic.

Repository Layer: Talking to Postgres Safely

The repository is responsible for direct database access. It does not validate HTTP input or decide what response status to return—that’s the service layer’s job. In this codebase, repositories focus on: parameterized SQL, mapping DB rows to domain types, and returning “raw outcomes” like null or boolean.

The repository for tax rates lives in src/lib/repositories/taxRatesRepo.ts.

Mapping Rows Into Domain Types

This first part defines the database row shape and converts it into the domain model TaxRate. The rest of the repository functions build on this mapping to keep everything consistent.

// src/lib/repositories/taxRatesRepo.ts
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;
}

function mapTaxRateRow(r: TaxRateRow): TaxRate {
  return {
    country_code: r.country_code,
    rate_bps: r.rate_bps,
    created_at: r.created_at,
    updated_at: r.updated_at,
  };
}
  • TaxRateRow represents the exact column names coming back from Postgres (country_code, rate_bps, timestamps). Keeping this separate from the domain type makes it explicit what we get from SQL.
  • mapTaxRateRow is a small but important abstraction: it centralizes how DB rows become a TaxRate, so every repository function returns a consistent shape.
  • Even though the mapping looks “1:1” here, having a mapper pays off later if the DB schema changes or if you want to rename/transform fields at the boundary.
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