Building TicketFlow: A Progressive Karin Tutorial
Welcome to the TicketFlow tutorial! In this guide, we will build a production-ready event booking API step-by-step. Instead of copy-pasting final code, we will evolve the application incrementally, adding features and complexity as we go.
Prerequisites
Before starting, ensure you have:
# Bun 1.2.10 or higher
bun --version
# MongoDB running locally
docker run -d -p 27017:27017 --name mongodb mongo
# Redis running locally
docker run -d -p 6379:6379 --name redis redisPhase 1: Project Initialization
Let's start by creating a new Karin project using the CLI.
Step 1: Scaffold the Project
# Install CLI globally
bun install -g @project-karin/cli
# Create project
karin new ticket-flowSelect the following options:
- Project Name:
ticket-flow - Environment:
Traditional Server (Bun) - Framework Adapter:
H3(High Performance) - Init Git & Install Deps:
Yes
Navigate to the folder:
cd ticket-flowStep 2: Verify Installation
Run the development server to make sure everything works:
bun run devVisit http://localhost:3000 and you should see a JSON response.
Phase 2: Database Integration (MongoDB)
Now we will add persistence to our application using MongoDB.
Step 3: Install Dependencies
We need the Mongoose plugin and the Config plugin to manage environment variables.
bun add @project-karin/mongoose @project-karin/configStep 4: Generate Events Resource
Use the CLI to generate the structure for our "Events" feature.
karin g resource eventsSelect Yes when asked to generate CRUD entry points.
This creates:
src/events/entities/events.entity.tssrc/events/events.controller.tssrc/events/events.service.ts- DTOs folder
Step 5: Define the Event Entity
Open src/events/entities/events.entity.ts and define the schema:
import { Schema, Prop } from "@project-karin/mongoose";
@Schema("Events")
export class Events {
@Prop({ required: true, index: true })
title: string;
@Prop({ required: true })
date: Date;
@Prop({ required: true, min: 0 })
price: number;
@Prop({ required: true, min: 0 })
stock: number;
}Step 6: Configure main.ts (Version 1)
Now we need to register the plugins and the model in our application entry point.
Open src/main.ts:
import { ConfigPlugin } from "@project-karin/config";
import { KarinFactory, Logger } from "@project-karin/core";
import { MongoosePlugin } from "@project-karin/mongoose";
import { H3Adapter } from "@project-karin/platform-h3";
import { Events } from "./events/entities/events.entity"; // Import entity
async function bootstrap() {
const logger = new Logger("Bootstrap");
// 1. Initialize Config
const config = new ConfigPlugin({
requiredKeys: ["MONGO_URI"],
});
// 2. Initialize Mongoose
const mongoose = new MongoosePlugin({
uri: () => config.get("MONGO_URI"), // Lazy resolution
options: () => ({ dbName: "tickets" }),
models: [Events], // ✅ Explicitly register the model
});
// 3. Create App
const app = await KarinFactory.create(new H3Adapter(), {
scan: "./src/**/*.ts",
plugins: [config, mongoose],
});
app.listen(3000, () => {
logger.info(`Server running on http://localhost:3000 🚀`);
});
}
bootstrap();Create a .env file in the root:
MONGO_URI=mongodb://localhost:27017Step 7: Implement Basic Service
Open src/events/events.service.ts and implement the create method:
import { Service } from "@project-karin/core";
import { InjectModel, Model } from "@project-karin/mongoose";
import { Events } from "./entities/events.entity";
import type { CreateEventsDto } from "./dtos/create-events.dto";
@Service()
export class EventsService {
constructor(
@InjectModel("Events") private readonly eventModel: Model<Events>,
) {}
async create(data: CreateEventsDto) {
return this.eventModel.create(data);
}
async findAll() {
return this.eventModel.find().exec();
}
}Phase 3: Validation & Error Handling
To make our API robust, we need to validate inputs and handle database errors gracefully.
Step 8: Define Validation Schema
Open src/events/dtos/create-events.dto.ts:
import { z } from "zod";
export const CreateEventsSchema = z.object({
title: z.string().min(3, "Title must be at least 3 characters"),
date: z.coerce.date(),
price: z.number().min(0),
stock: z.number().int().min(1),
});
export type CreateEventsDto = z.infer<typeof CreateEventsSchema>;Step 9: Apply Validation Pipe
Update src/events/events.controller.ts to use ZodValidationPipe:
import { Controller, Post, Body, ZodValidationPipe } from "@project-karin/core";
import { CreateEventsSchema, CreateEventsDto } from "./dtos/create-events.dto";
// ... imports
@Controller("/events")
export class EventsController {
constructor(private readonly service: EventsService) {}
@Post("/")
create(
// ✅ Validate body against schema
@Body(new ZodValidationPipe(CreateEventsSchema)) body: CreateEventsDto,
) {
return this.service.create(body);
}
// ...
}Step 10: Create Global Exception Filter
Create src/common/filters/mongoose.filter.ts to handle DB errors:
import { Catch, ExceptionFilter, ArgumentsHost } from "@project-karin/core";
import { Error as MongooseError } from "mongoose";
@Catch(MongooseError.CastError, MongooseError.ValidationError)
export class MongooseExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
return new Response(
JSON.stringify({
statusCode: 400,
message: "Database Validation Error",
details: exception.message,
}),
{ status: 400 },
);
}
}Step 11: Register Filter Globally
Update src/main.ts (Version 2):
import { MongooseExceptionFilter } from "./common/filters/mongoose.filter";
// ... inside bootstrap
const app = await KarinFactory.create(new H3Adapter(), {
scan: "./src/**/*.ts",
plugins: [config, mongoose],
globalFilters: [new MongooseExceptionFilter()], // ✅ Register global filter
});Phase 4: Performance Optimization (Redis)
Our API works, but we want it to be fast. Let's add Redis caching.
Step 12: Install Redis Plugin
bun add @project-karin/redis ioredisStep 13: Update main.ts (Version 3)
// ... imports
import { RedisPlugin } from "@project-karin/redis";
async function bootstrap() {
// ... config setup
const redis = new RedisPlugin({
url: "redis://localhost:6379",
failureStrategy: "warn",
});
const app = await KarinFactory.create(new H3Adapter(), {
scan: "./src/**/*.ts",
plugins: [config, mongoose, redis], // Add redis here
globalFilters: [new MongooseExceptionFilter()],
});
}Step 14: Add Caching to Service
Update src/events/events.service.ts:
import { InjectRedis, Redis } from "@project-karin/redis";
@Service()
export class EventsService {
constructor(
@InjectModel("Events") private readonly eventModel: Model<Events>,
@InjectRedis() private redis: Redis,
) {}
async findAll() {
const cached = await this.redis.get("events:all");
if (cached) return JSON.parse(cached);
const events = await this.eventModel.find().exec();
await this.redis.set("events:all", JSON.stringify(events), "EX", 60);
return events;
}
}Phase 5: Complex Logic (Bookings)
Now let's add the booking system with atomic operations.
Step 15: Generate Bookings Resource
karin g resource bookingsStep 16: Define Booking Entity
src/bookings/entities/bookings.entity.ts:
import { Schema, Prop } from "@project-karin/mongoose";
@Schema("Bookings")
export class Bookings {
@Prop({ required: true }) eventId: string;
@Prop({ required: true }) userId: string;
@Prop({ required: true }) quantity: number;
@Prop({ required: true }) totalPrice: number;
@Prop({ default: Date.now }) createdAt: Date;
}Step 17: Update main.ts (Version 4)
Register the new model:
import { Bookings } from "./bookings/entities/bookings.entity";
// ...
const mongoose = new MongoosePlugin({
// ...
models: [Events, Bookings], // ✅ Add Bookings
});Step 18: Implement Atomic Booking
In src/bookings/bookings.service.ts:
import { Service, BadRequestException } from "@project-karin/core";
import { InjectModel, Model } from "@project-karin/mongoose";
import { Bookings } from "./entities/bookings.entity";
import { Events } from "../../events/entities/events.entity";
@Service()
export class BookingsService {
constructor(
@InjectModel("Bookings") private bookingModel: Model<Bookings>,
@InjectModel("Events") private eventModel: Model<Events>,
) {}
async create(userId: string, eventId: string, quantity: number) {
// Atomic operation: Decrement stock ONLY if enough exists
const event = await this.eventModel.findOneAndUpdate(
{ _id: eventId, stock: { $gte: quantity } },
{ $inc: { stock: -quantity } },
{ new: true },
);
if (!event) throw new BadRequestException("Sold out");
return this.bookingModel.create({
userId,
eventId,
quantity,
totalPrice: event.price * quantity,
});
}
}Phase 6: Security (Guards)
Step 19: Create Auth Guard
src/common/auth.guard.ts:
import {
CanActivate,
ExecutionContext,
UnauthorizedException,
} from "@project-karin/core";
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
if (!req.headers.get("authorization")) throw new UnauthorizedException();
return true;
}
}Step 20: Protect Controller
src/bookings/bookings.controller.ts:
import { Controller, Post, Body, UseGuards } from "@project-karin/core";
import { AuthGuard } from "../common/auth.guard";
import { BookingsService } from "./bookings.service";
@Controller("/bookings")
@UseGuards(AuthGuard) // 🛡️ Protect routes
export class BookingsController {
constructor(private service: BookingsService) {}
@Post()
create(@Body() body: any) {
return this.service.create("user_123", body.eventId, body.quantity);
}
}Phase 7: Documentation (OpenAPI)
Finally, let's generate automatic documentation for our API.
Step 21: Install OpenAPI Plugin
bun add @project-karin/openapiStep 22: Final main.ts
Update main.ts to include the OpenAPI plugin.
import { OpenApiPlugin } from "@project-karin/openapi";
import { AnalyticsPlugin } from "./common/plugins/analytics.plugin";
async function bootstrap() {
// ... previous config
const openapi = new OpenApiPlugin({
title: "TicketFlow API",
version: "1.0.0",
path: "/docs",
});
const app = await KarinFactory.create(new H3Adapter(), {
scan: "./src/**/*.ts",
plugins: [config, mongoose, redis, openapi],
globalFilters: [new MongooseExceptionFilter()],
});
const port = parseInt(config.get("PORT") || "3000", 10);
app.listen(port, () => {
console.log(`Docs available at http://localhost:${port}/docs`);
});
}Now visit http://localhost:3000/docs to see your interactive API documentation! 🎉
Conclusion
You have built a complete, production-ready API with:
- ✅ Validation (Zod)
- ✅ Error Handling (Global Filters)
- ✅ Database (Mongoose with Atomic Ops)
- ✅ Caching (Redis)
- ✅ Security (Guards)
- ✅ Documentation (OpenAPI)
Happy coding with Karin! 🦊
