Pipes
In distributed systems and web APIs, data integrity is paramount. Data flows into your application from untrusted sources (the client), often as loosely typed JSON or strings. Before this data reaches your business logic, it must be verified and transformed.
Pipes are the architectural component responsible for this gatekeeping. They operate on the arguments of a controller route handler. A pipe receives the input data, processes it, and either passes it forward or throws an exception to halt execution.
Concept: The Prep Station
The Pipe acts as the Prep Station or Quality Control line in the kitchen.
- Filtration: It creates a sieve that prevents bad ingredients (Invalid Data) from reaching the Chef.
- Preparation: It chops the vegetables (Transforms Strings to Numbers) so the Chef generally doesn't have to check if the carrot is peeled.
Validation Philosophy
Validation is not just about checking if a field exists. It is about enforcing the invariants of your domain model at the edges of your system. This is the "Fail Fast" principle. If a request contains invalid data, it should be rejected immediately with a clear error, sparing your Service layer from dealing with inconsistent state.
The Runtime vs. Compile-time Gap
TypeScript provides excellent compile-time safety, but Runtime is a different beast. When a JSON body arrives over HTTP, it is treated as any or unknown. A TypeScript interface interface User { name: string } disappears entirely after compilation. It cannot prevent a client from sending { name: 123 }.
To bridge this gap, Karin integrates with Zod, a schema validation library that creates "runtime" types.
Schema Validation (Zod)
Karin treats schema validation as a first-class citizen. By binding a Zod schema to a class (DTO), we create a Single Source of Truth for both the type system and the runtime validator.
Defining Data Transfer Objects (DTOs)
A common mistake in TypeScript development is defining a validation schema separately from the type definition. They eventually DRIFT apart. Karin provides the createZodDto helper to generate DTOs directly from your Zod schemas.
Step 1: Define the Data Structure We define the shape of valid data using Zod. This object persists at runtime.
import { z } from "zod";
import { createZodDto } from "@project-karin/core";
// 1. Define the Schema
const CreateProductSchema = z.object({
name: z.string().min(3, "Name must be at least 3 chars"),
price: z.number().positive(),
tags: z.array(z.string()).optional(),
});Step 2: Create the Class Instead of manually typing properties, we extend the helper. This ensures your TypeScript types always match your runtime validation.
// 2. Create the class extending the helper
export class CreateProductDto extends createZodDto(CreateProductSchema) {}Versatility
The createZodDto helper is generic and works for any Zod schema, allowing you to easily create DTOs for updates or even custom logic.
Example: Update DTO (Partial) You can reuse your creation schema and make it partial:
// Automatically makes all fields optional
export class UpdateProductDto extends createZodDto(
CreateProductSchema.partial(),
) {}Example: Custom Logic It works for any data structure validation, such as verifying KPI metrics or search filters:
const KPIBalanceSchema = z.object({
year: z.number(),
quarter: z.enum(["Q1", "Q2", "Q3", "Q4"]),
});
export class NewKPIBalanceDto extends createZodDto(KPIBalanceSchema) {}Implementing Validation
When you annotate a controller parameter with this DTO, Karin's checks if the DTO has a bound Zod schema.
@Post()
create(@Body() body: CreateProductDto) {
// At this line, we have a guarantee:
// 1. 'body' matches the Zod Schema (Runtime check passed).
// 2. 'body' matches the TypeScript Class (Compile-time check).
this.productService.create(body);
}IMPORTANT
When importing the DTO in your controller, ensure you import it as a value, not a type, to allow metadata emission. import { CreateProductDto } from "./dto"; ✅ import { type CreateProductDto } from "./dto"; ❌ (Metadata will be lost)
Global Validation Pipe
To enable this behavior application-wide, register the ZodValidationPipe. It is automatically aware of the zodSchema static property on your DTO classes.
// main.ts
app.useGlobalPipes(new ZodValidationPipe());Transformation Pipes
Validation ensures correctness, but Transformation ensures usability. HTTP is text-based; URLs and Query Parameters are strings. Your application logic often requires Numbers, Booleans, or complex Objects.
Pipes can seamlessly transform input data to the expected type.
Built-in Transformation
Consider a route that retrieves a resource by ID: GET /products/123. By default, the id parameter extracted from the URL is the string "123".
@Get(':id')
findOne(@Param('id') id: string) { ... }However, if your database uses numeric IDs, you would have to manually parse it (parseInt(id)). This is repetitive (imperative) logic. We can move this to a declarative Pipe.
Implementation: ParseIntPipe
A custom pipe implements the PipeTransform interface.
import {
PipeTransform,
injectable,
BadRequestException,
} from "@project-karin/core";
@injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
// If transformation fails, it is effectively a Validation failure
if (isNaN(val)) {
throw new BadRequestException(
"Validation failed: Numeric string is expected",
);
}
return val;
}
}Usage
We bind the pipe directly to the parameter decorator. The value that enters the method will be the result of the transform function.
@Get(':id')
findOne(@Param('id', new ParseIntPipe()) id: number) {
// 'id' is a primitive, safe number.
return this.service.findById(id);
}Advanced: Param-to-Entity transformation
Pipes can even perform asynchronous operations, such as looking up an entity in the database. Imagine a pipe UserByIdPipe that takes a string ID, queries the database, and either throws 404 (if not found) or returns the full User entity.
findOne(@Param('id', UserByIdPipe) user: UserEntity) {
// The controller receives the fully resolved User object!
}This pattern drastically reduces boilerplate in controllers.
