Architecture
Deep dive into BrewHoard's project structure, design patterns, and architectural decisions.
Understanding BrewHoard’s architecture helps you navigate the codebase efficiently and make informed decisions when extending functionality.
High-Level Overview
BrewHoard follows a layered architecture with clear separation of concerns:
Directory Structure
beer-vault/
├── src/
│ ├── routes/ # SvelteKit routing
│ │ ├── (app)/ # Authenticated routes (route group)
│ │ │ ├── collection/ # Beer collection pages
│ │ │ ├── marketplace/ # Trading marketplace
│ │ │ ├── scan/ # Beer scanner
│ │ │ ├── settings/ # User settings
│ │ │ └── +layout.svelte # App shell with nav
│ │ ├── api/ # REST API endpoints
│ │ │ └── v1/ # Versioned API
│ │ ├── docs/ # Documentation (you're here!)
│ │ └── +layout.svelte # Root layout
│ │
│ ├── lib/ # Shared code
│ │ ├── auth/ # Authentication logic
│ │ ├── beer/ # Beer-related utilities
│ │ ├── collection/ # Collection management
│ │ ├── marketplace/ # Marketplace logic
│ │ ├── scanner/ # Image recognition
│ │ ├── ratings/ # Review system
│ │ ├── notifications/ # Email/push notifications
│ │ ├── payments/ # Stripe integration
│ │ ├── storage/ # File uploads
│ │ ├── server/ # Server-only code
│ │ │ └── db.js # Database connection
│ │ ├── utils/ # Shared utilities
│ │ ├── components/ # Shared UI components
│ │ │ └── ui/ # shadcn-svelte components
│ │ └── paraglide/ # Generated i18n code
│ │
│ ├── components/ # Feature-specific components
│ │ ├── auth/ # Login, registration forms
│ │ ├── beer/ # Beer cards, lists
│ │ ├── collection/ # Collection UI
│ │ ├── marketplace/ # Listing forms, cards
│ │ └── ratings/ # Review forms, displays
│ │
│ ├── hooks.js # Client hooks
│ ├── hooks.server.js # Server hooks (auth, etc.)
│ ├── app.css # Global styles
│ ├── app.html # HTML template
│ └── app.d.ts # TypeScript declarations
│
├── messages/ # i18n message files
│ ├── en.json # English
│ ├── nl.json # Dutch
│ └── ... # Other languages
│
├── migrations/ # Database migrations
│ ├── 00001_initial/
│ ├── 00002_marketplace_fields/
│ └── ...
│
├── static/ # Static assets
│ ├── pwa-192x192.png
│ └── ...
│
└── tests/ # Test files
├── unit/
└── e2e/Key Concepts
Route Groups
SvelteKit uses route groups (directories in parentheses) to organize routes without affecting URLs:
routes/
├── (app)/ # Authenticated routes → /collection, /marketplace
│ ├── collection/
│ └── +layout.svelte # Shared app layout with navigation
├── (marketing)/ # Public routes → /about, /pricing
│ └── +layout.svelte # Marketing layout
└── +layout.svelte # Root layout (always applied)The (app) group shares a layout with navigation, authentication checks, and the app shell.
Server vs Client Code
BrewHoard carefully separates server-only and client-safe code:
// src/lib/server/db.js - SERVER ONLY
// This file can only be imported in +server.js or +page.server.js
import postgres from 'postgres';
export default postgres(process.env.DATABASE_URL);
// src/lib/utils/format.js - SHARED
// Safe to use anywhere
export function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
}SvelteKit enforces this at build time - importing server code in client code causes an error.
Hooks Architecture
Hooks intercept requests at various lifecycle points:
// src/hooks.server.js
import { sequence } from '@sveltejs/kit/hooks';
import { authHook } from '$lib/middleware/auth.js';
import { rateLimitHook } from '$lib/middleware/rate-limit.js';
// Hooks run in sequence for every request
export const handle = sequence(
authHook, // 1. Parse session, set locals.user
rateLimitHook // 2. Check rate limits
);The locals object passes data between hooks and route handlers:
// In hooks.server.js
event.locals.user = await validateSession(sessionId);
// In +page.server.js
export function load({ locals }) {
if (!locals.user) redirect(303, '/login');
return { user: locals.user };
}Data Flow
Page Load Flow
API Request Flow
State Management
Svelte 5 Runes
BrewHoard uses Svelte 5’s rune-based reactivity:
<script>
// Reactive state
let beers = $state([]);
let searchQuery = $state('');
// Derived state (computed values)
let filteredBeers = $derived(
beers.filter(b =>
b.name.toLowerCase().includes(searchQuery.toLowerCase())
)
);
// Side effects
$effect(() => {
// Runs when dependencies change
console.log(`Found ${filteredBeers.length} beers`);
});
// Cleanup on unmount
$effect(() => {
const interval = setInterval(checkUpdates, 30000);
return () => clearInterval(interval);
});
</script>Server State (Load Functions)
Data fetched in +page.server.js is passed to components via props:
// +page.server.js
export async function load({ locals }) {
const collection = await getCollection(locals.user.id);
return { collection };
}<!-- +page.svelte -->
<script>
let { data } = $props();
// data.collection is available here
</script>Form Actions
Mutations use SvelteKit form actions for progressive enhancement:
// +page.server.js
export const actions = {
addBeer: async ({ request, locals }) => {
const formData = await request.formData();
const beerId = formData.get('beerId');
await addToCollection(locals.user.id, beerId);
return { success: true };
}
};<!-- +page.svelte -->
<form method="POST" action="?/addBeer">
<input type="hidden" name="beerId" value={beer.id} />
<button type="submit">Add to Collection</button>
</form>Component Architecture
Component Organization
Components are organized by feature and reusability:
components/
├── auth/ # Auth feature components
│ ├── LoginForm.svelte
│ └── RegisterForm.svelte
├── collection/ # Collection feature
│ ├── AddBeerForm.svelte
│ ├── CollectionGrid.svelte
│ └── CollectionItem.svelte
└── ...
lib/components/
├── ui/ # shadcn-svelte (generic)
│ ├── button/
│ ├── card/
│ └── ...
└── docs/ # Documentation componentsProps Pattern
Components use the $props() rune with JSDoc for type safety:
<script>
/**
* @typedef {Object} Props
* @property {import('$lib/types').Beer} beer - The beer to display
* @property {boolean} [showActions=true] - Whether to show action buttons
* @property {(id: string) => void} [onSelect] - Selection callback
*/
/** @type {Props} */
let { beer, showActions = true, onSelect } = $props();
</script>Error Handling
API Error Responses
All API endpoints follow a consistent error format:
// Successful response
return json({
success: true,
data: result
});
// Error response
return json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid beer ID',
details: { field: 'beerId' }
}
}, { status: 400 });Error Boundaries
SvelteKit’s +error.svelte files catch and display errors:
<!-- src/routes/+error.svelte -->
<script>
import { page } from '$app/state';
</script>
<div class="error-page">
<h1>{page.status}</h1>
<p>{page.error?.message}</p>
</div>Security Considerations
Authentication Flow
Authorization Patterns
// Resource-level authorization
export async function GET({ params, locals }) {
const item = await getCollectionItem(params.id);
// Check ownership
if (item.user_id !== locals.user.id) {
throw error(403, 'Access denied');
}
return json(item);
}Performance Patterns
Database Query Optimization
// BAD: N+1 query problem
const beers = await sql`SELECT * FROM beers`;
for (const beer of beers) {
beer.brewery = await sql`
SELECT * FROM breweries WHERE id = ${beer.brewery_id}
`;
}
// GOOD: Single query with JOIN
const beers = await sql`
SELECT b.*, row_to_json(br.*) as brewery
FROM beers b
LEFT JOIN breweries br ON b.brewery_id = br.id
`;Lazy Loading
Large components load dynamically:
<script>
import { onMount } from 'svelte';
let AnalyticsDashboard;
onMount(async () => {
const module = await import('$components/analytics/AnalyticsDashboard.svelte');
AnalyticsDashboard = module.default;
});
</script>
{#if AnalyticsDashboard}
<AnalyticsDashboard />
{:else}
<div>Loading analytics...</div>
{/if}Next Steps
- Data Flow - Detailed request lifecycle
- State Management - Advanced state patterns
- Database - Schema and queries