Use this skill when working on the Svelte SPA's project structure — adding routes, creating feature slices, organizing shared components, or understanding the ClientApp directory layout. Covers route groups, $lib conventions, barrel exports, API client organization, and vertical slice architecture. Apply when deciding where to place new files or components.
Located in src/Exceptionless.Web/ClientApp. The Svelte SPA is the primary client.
src/
├── lib/
│ ├── features/ # Feature slices (vertical organization)
│ │ ├── auth/
│ │ │ ├── api.svelte.ts
│ │ │ ├── models/
│ │ │ ├── schemas.ts
│ │ │ └── components/
│ │ ├── organizations/
│ │ ├── projects/
│ │ ├── events/
│ │ └── shared/ # Cross-feature shared code
│ ├── components/ # App-wide shared components
│ │ └── ui/ # shadcn-svelte components
│ ├── generated/ # API-generated types
│ └── utils/ # Utility functions
├── routes/
│ ├── (app)/ # Authenticated app routes
│ ├── (auth)/ # Authentication routes
│ └── (public)/ # Public routes
└── app.html
Organize routes by authentication/layout requirements:
routes/
├── (app)/ # Requires authentication
│ ├── +layout.svelte # App layout with nav
│ ├── organizations/
│ └── projects/
├── (auth)/ # Login/signup flows
│ ├── +layout.svelte # Minimal auth layout
│ ├── login/
│ └── signup/
└── (public)/ # Public pages
├── +layout.svelte # Marketing layout
└── pricing/
Organize by feature, aligned with API controllers:
features/organizations/
├── api.svelte.ts # TanStack Query hooks
├── models/
│ └── index.ts # Re-exports from generated
├── schemas.ts # Zod validation schemas
├── options.ts # Dropdown options, enums
└── components/
├── organization-card.svelte
├── organization-form.svelte
└── dialogs/
└── create-organization-dialog.svelte
Centralize API calls per feature:
// features/organizations/api.svelte.ts
import {
createQuery,
createMutation,
useQueryClient,
} from "@tanstack/svelte-query";
import { useFetchClient } from "@exceptionless/fetchclient";
import type { Organization, CreateOrganizationRequest } from "./models";
export function getOrganizationsQuery() {
const client = useFetchClient();
return createQuery(() => ({
queryKey: ["organizations"],
queryFn: async () => {
const response =
await client.getJSON<Organization[]>("/organizations");
if (!response.ok) throw response.problem;
return response.data!;
},
}));
}
export function postOrganizationMutation() {
const client = useFetchClient();
const queryClient = useQueryClient();
return createMutation(() => ({
mutationFn: async (data: CreateOrganizationRequest) => {
const response = await client.postJSON<Organization>(
"/organizations",
data,
);
if (!response.ok) throw response.problem;
return response.data!;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["organizations"] });
},
}));
}
Re-export generated models through feature model folders:
// features/organizations/models/index.ts
export type {
Organization,
CreateOrganizationRequest,
UpdateOrganizationRequest,
} from "$lib/generated";
// Add feature-specific types
export interface OrganizationWithStats extends Organization {
eventCount: number;
projectCount: number;
}
Use index.ts for clean imports:
// features/organizations/index.ts
export { getOrganizationsQuery, postOrganizationMutation } from "./api.svelte";
export type { Organization, CreateOrganizationRequest } from "./models";
export { organizationSchema } from "./schemas";
Place truly shared components in appropriate locations:
lib/
├── features/shared/ # Shared between features
│ ├── components/
│ │ ├── formatters/ # Boolean, date, number, bytes, duration, currency, percentage, time-ago formatters
│ │ ├── loading/
│ │ └── error/
│ └── utils/
└── components/ # App-wide components
├── ui/ # shadcn-svelte
├── layout/
└── dialogs/ # Global dialogs
The formatters/ directory contains Svelte components for displaying formatted values. Always use these instead of writing custom formatting functions like formatDateTime() or formatBytes().
| Component | Use For |
|---|---|
<DateTime> | Date and time display |
<TimeAgo> | Relative time ("3 hours ago") |
<Duration> | Time durations |
<Bytes> | File sizes, memory |
<Number> | Numeric values with locale formatting |
<Boolean> | True/false display |
<Currency> | Money amounts |
<Percentage> | Percentage values |
<DateMath> | Elasticsearch date math expressions |
<!-- CORRECT: use the formatter component -->
<DateTime value={event.date} />
<TimeAgo value={event.date} />
<Bytes value={event.size} />
<!-- WRONG: never do this -->
{formatDateTime(event.date)}
{new Date(event.date).toLocaleString()}
Consistency rule: If a formatter component exists for a data type, you MUST use it. Creating a custom formatting function when a component already exists is a code review BLOCKER.
When API contracts change:
npm run generate-models
Prefer regeneration over hand-writing DTOs. Generated types live in $lib/generated.
// Configured in svelte.config.js
import { Button } from "$comp/ui/button"; // $lib/components
import { User } from "$features/users/models"; // $lib/features
import { formatDate } from "$shared/formatters"; // $lib/features/shared
Before creating anything new, search the codebase for existing patterns. Consistency is the most important quality of a codebase:
$lib/features/shared/ and $comp/Pattern divergence is a code review BLOCKER, not a nit.
Study existing components before creating new ones:
/components/dialogs/options.ts with DropdownItem<EnumType>[]svelte-forms skill