1. Cấu trúc thư mục
src/
├── routes/
│ ├── (admin)/
│ │ └── admin/
│ │ ├── dashboard/+page.svelte
│ │ ├── events/
│ │ │ ├── +page.svelte
│ │ │ ├── new/+page.svelte
│ │ │ └── [id]/+page.svelte
│ │ └── +layout.svelte
│ ├── (customer)/
│ │ ├── events/
│ │ │ ├── [id]/
│ │ │ │ ├── +page.svelte
│ │ │ │ ├── seats/+page.svelte
│ │ │ │ └── queue/+page.svelte
│ │ │ └── +page.svelte # trang chủ
│ │ ├── checkout/[id]/+page.svelte
│ │ ├── me/tickets/+page.svelte
│ │ └── +layout.svelte
│ ├── login/+page.svelte
│ ├── register/+page.svelte
│ └── api/
│ ├── auth/
│ │ ├── register/+server.ts
│ │ └── login/+server.ts
│ ├── events/
│ │ ├── +server.ts # GET list
│ │ └── [id]/
│ │ ├── +server.ts # GET detail
│ │ ├── queue/+server.ts # POST join, GET status
│ │ └── seats/
│ │ ├── +server.ts # GET seats
│ │ ├── hold/+server.ts # POST hold
│ │ └── stream/+server.ts # GET SSE
│ ├── orders/
│ │ └── [id]/checkout/+server.ts
│ ├── me/tickets/+server.ts
│ └── stats/
│ ├── revenue/+server.ts # middleware: requireAdmin
│ ├── occupancy/+server.ts
│ └── demographics/+server.ts
├── lib/
│ ├── server/
│ │ ├── db/
│ │ │ ├── index.ts # drizzle client
│ │ │ ├── schema.ts # all tables
│ │ │ └── seed.ts
│ │ ├── auth/
│ │ │ ├── jwt.ts
│ │ │ └── password.ts
│ │ ├── mq/
│ │ │ ├── connection.ts
│ │ │ ├── producer.ts
│ │ │ └── consumer.ts
│ │ └── services/
│ │ ├── event.service.ts
│ │ ├── seat.service.ts
│ │ ├── order.service.ts
│ │ ├── queue.service.ts
│ │ └── stats.service.ts
│ ├── components/
│ │ ├── ui/ # Button, Input, Modal,...
│ │ ├── SeatGrid.svelte
│ │ ├── CountdownTimer.svelte
│ │ ├── Toast.svelte
│ │ └── Navbar.svelte
│ ├── stores/
│ │ └── toast.ts
│ └── utils/
| ├── api.ts
│ ├── format.ts # formatCurrency, formatDate
│ └── constants.ts
├── hooks.server.ts # auth middleware
└── app.d.ts # type declarations
2. Quy tắc đặt tên
Files & Folders
────────────────────────────────────────
Svelte components PascalCase.svelte SeatGrid.svelte
TS modules kebab-case.ts event.service.ts
Route folders kebab-case checkout/[id]/
Variables & Functions
────────────────────────────────────────
Variables camelCase activeCount
Functions camelCase holdSeats()
Constants UPPER_SNAKE MAX_CONCURRENT_USERS
Types / Interfaces PascalCase SeatStatus
Database
────────────────────────────────────────
Tables snake_case (số nhiều) seat_sections
Columns snake_case locked_at
Enums lowercase 'available' | 'locked' | 'sold'
3. TypeScript
// ✅ Dùng type cho data shape
type Seat = {
id: number;
row: string;
col: number;
status: 'available' | 'locked' | 'sold';
};
// ✅ Dùng interface cho contract / extensible
interface ApiResponse<T> {
data: T;
message?: string;
}
// ❌ Không dùng any
// ❌ Không dùng @ts-ignore (trừ khi comment lý do)
// ✅ Bật strict mode trong tsconfig.json
4. API Response Format
// Thành công
return json({ data: event }, { status: 200 });
return json({ data: order }, { status: 201 });
// Lỗi
return json({ error: 'Ghế đã được người khác chọn' }, { status: 409 });
return json({ error: 'Unauthorized' }, { status: 401 });
return json({ error: 'Forbidden' }, { status: 403 });
return json({ error: 'Not found' }, { status: 404 });
// Validation error
return json({
error: 'Validation failed',
details: { email: 'Email đã tồn tại' }
}, { status: 400 });
5. API Handler Pattern
// src/routes/api/seats/hold/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { holdSeats } from '$lib/server/services/seat.service';
export const POST: RequestHandler = async ({ request, locals }) => {
// 1. Auth check
if (!locals.user) throw error(401, 'Unauthorized');
// 2. Parse & validate input
const body = await request.json();
const { seat_ids } = body;
if (!seat_ids?.length) throw error(400, 'seat_ids is required');
// 3. Call service
const result = await holdSeats(locals.user.id, seat_ids);
// 4. Return response
return json({ data: result }, { status: 200 });
};
6. Service Layer Pattern
// src/lib/server/services/seat.service.ts
import { db } from '$lib/server/db';
import { seats, orders, orderItems } from '$lib/server/db/schema';
import { eq, inArray, sql } from 'drizzle-orm';
export async function holdSeats(userId: number, seatIds: number[]) {
return await db.transaction(async (tx) => {
// Row lock
const available = await tx
.select()
.from(seats)
.where(inArray(seats.id, seatIds))
.for('update');
// Validate
const allAvailable = available.every((s) => s.status === 'available');
if (!allAvailable || available.length !== seatIds.length) {
throw new Error('SEAT_CONFLICT');
}
// Update seats
await tx
.update(seats)
.set({ status: 'locked', lockedBy: userId, lockedAt: new Date() })
.where(inArray(seats.id, seatIds));
// Create order
// ...
return { orderId, expiresAt };
});
}
7. Svelte Components
<!-- ✅ Thứ tự trong file .svelte -->
<script lang="ts">
// 1. Imports
// 2. Props (export let)
// 3. State ($state / let)
// 4. Derived ($derived / $:)
// 5. Functions
// 6. Lifecycle (onMount, onDestroy)
</script>
<!-- HTML -->
<style>
/* Scoped styles — ưu tiên dùng Tailwind class thay vì viết CSS ở đây */
</style>