Validation and Domain Types
Validation and Domain Types
Welcome back 👋 In the previous lesson, you established the project’s shared API contract (ApiSuccess, ApiError, ApiResponse) and the response helpers (success, error, parseJson) that make every route return consistent JSON envelopes. That foundation matters here because validation failures should also come back in the exact same structured error shape—just with a VALIDATION_ERROR code.
In this lesson, you’ll make the backend’s data shapes more explicit by introducing domain types and a small DTO for product listing inputs. Then you’ll build a tiny validation toolkit for query params and wire it into GET /api/products, so invalid inputs are rejected early with clean, consistent error responses.
Domain Types as the Backend’s Source of Truth
A backend needs a canonical “truth” for what its entities look like internally—especially once the database is involved. In this project, those canonical shapes live in src/lib/types/domain.ts. These types are designed to match the columns we’ll read from PostgreSQL later, so we don’t end up inventing new shapes at every layer.
This file defines a Currency type (kept intentionally narrow) and a Product interface that includes pricing, inventory, and status fields you’ll see in real queries and API responses.
Currencyis a narrow type instead of a plainstring, which makes the system more explicit and prevents “random” currency codes from sneaking in.- For this course, we only support
'USD', which keeps the domain model simple while still demonstrating the pattern of constrained values. - Even if we expand currency support later, starting narrow is helpful because it forces consistency across the codebase.
Now here’s the canonical Product shape.
- This interface is intentionally “database-shaped.” Fields like
price_cents,inventory_count, and timestamps are exactly the kinds of columns we’ll read from PostgreSQL rows. price_centsis an integer rather than a floating dollar amount. This avoids rounding problems that can happen when using floating point math for money.descriptionisstring | null, which mirrors how SQL databases commonly represent “no value” for optional text fields.statusis a union of'active' | 'archived'. This is important because status values show up in real filtering and business logic, and the union prevents invalid states like'deleted'or'inactive'from compiling.- Keeping these fields in the domain model makes it much easier to map DB rows to strongly-typed objects later, and it keeps your API responses aligned with the real data source.
