A financial assets watchlist SPA built with SvelteKit 2, Svelte 5, TypeScript, and Tailwind CSS 4. Browse stocks, ETFs, cryptocurrencies, and commodities, track favourites in a persistent watchlist, and inspect price history via interactive candlestick charts.
View Demo
Built as a technical interview project — and my first hands-on experience with Svelte and SvelteKit. Structure and choices may not be best practices, though I tried to follow the SvelteKit conventions as closely as possible.
| Layer | Choice |
|---|---|
| Framework | SvelteKit 2 + Svelte 5 (runes) |
| Language | TypeScript |
| Styling | Tailwind CSS 4 |
| Charts | lightweight-charts |
| Testing | Vitest (unit + component) |
| Component explorer | Storybook 10 + addon-svelte-csf |
| Adapter | @sveltejs/adapter-node |
| Container | Docker (multi-stage) |
- Dashboard — paginated asset grid with live search (debounced), category filter, and currency selector
- Asset detail — full asset info page with an interactive OHLC candlestick chart (90D / 52W / 24M intervals)
- Watchlist — client-side favourites list persisted to
localStorage, togglable from any asset card or detail page - Currency conversion — switch display currency (USD, EUR, GBP, JPY, CHF) via URL param; all prices converted on the API side
- Streaming data — categories load synchronously; the slower asset list streams in via SvelteKit's
streamingpattern, showing skeleton cards while pending - Error handling — granular error boundaries per route with retry actions
- Navigation backdrop — loading overlay on page changes + loading bar on top of the page
- Accessibility — semantic HTML, ARIA labels, skip-to-content link, keyboard-navigable components
src/
├── lib/
│ ├── assets/ # Static assets (logo, favicon)
│ ├── components/ # Reusable UI components
│ │ ├── *.svelte
│ │ └── *.stories.svelte # Co-located Storybook stories
│ ├── data/
│ │ ├── *.ts # Mock dataset
│ ├── stores/
│ │ └── *.svelte.ts # Rune stores (+ localStorage persistance)
│ ├── types/
│ │ └── *.ts # Shared TypeScript types
│ └── utils/
│ └── *.ts # Helper functions
└── routes/
├── +layout.svelte # App shell
├── +*.svelte # Svelte pages
├── +*.ts # Pages load functions
├── +error.svelte # Global error boundary
├── api/ # Api routes
│ └── */
│ ├── +*.ts
│ └── [id]/ # Nested api routes
│ └── +*.ts
└── */
└── [id]/
├── +*.svelte # Nested routes
├── +*.ts # Nested pages load functions
└── +error.svelte # Nested specific error boundary
- Node.js 22+
- npm (or yarn if you prefer)
npm install
npm run dev --openApp available at http://localhost:5173.
npm run build # Production build (outputs to build/)
npm run preview # Preview production build locally
npm run test # Run all unit tests
npm run test:unit # Same, explicit
npm run storybook # Storybook dev server on :6006
npm run build-storybook
npm run check # svelte-check + TypeScript
npm run lint # Prettier + ESLint
npm run format # Auto-formatReturns a paginated, filtered list of asset summaries.
| Param | Type | Default | Description |
|---|---|---|---|
q |
string | — | Search by name or symbol |
category |
string | — | Filter by category id |
currency |
string | USD |
Convert prices to this currency |
Response
{
"data": [ AssetSummary ],
"total": 35
}Returns full asset details.
| Param | Type | Default | Description |
|---|---|---|---|
currency |
string | USD |
Convert prices to this currency |
Returns OHLC candlestick data for the chart.
| Param | Type | Default | Description |
|---|---|---|---|
interval |
1D | 1W | 1M |
1D |
Candle interval |
currency |
string | USD |
Convert prices |
Returns 90 candles for 1D, 52 for 1W, 24 for 1M.
Returns all categories with asset counts.
./run.shThe script builds the image and starts the container on port 8087. The default port can be overridden:
PORT=3000 ./run.shThe Dockerfile uses a multi-stage build:
- Builder —
node:22-slim(Debian/glibc, required for native deps likelightningcsson ARM 32bit) - Runner —
node:22-alpine(lean, ~180MB final image; devDependencies stripped vianpm prune)
Tests are co-located with source files and split into two Vitest projects:
| Project | Scope | Environment |
|---|---|---|
server |
API route handlers, utility functions | Node |
storybook |
Storybook stories as component tests | Browser |
npm run test # all tests
npx vitest run --project=server # server tests only
npx vitest run --project=storybook # story tests onlyStories are co-located next to their components (*.stories.svelte). Each story uses @storybook/addon-svelte-csf and includes play() interaction tests where relevant.
npm run storybook # http://localhost:6006- Watchlist as a dedicated route —
/watchlistas its own SvelteKit route with its own load function, rather than a URL param on the dashboard.
- Pagination — with a cursor + infinite scroll solution, add API supports
limit+aftercursor params. On client-side implement withIntersectionObservera "sentinel" that intercepts the end of the list and triggers a new request. - Virtual scroller — If the list became very large (thousands of items rendered simultaneously), a virtual list (e.g.
svelte-virtual-list) would reduce DOM node count and improve scroll performance.
- Sorting — allow sorting the asset grid by price, change %, market cap, or volume, via URL param (
?sort=change_desc). - Localization — implement a language switch handled by a i18n library, like
svelte-i18n.
- Component organisation — split
src/lib/components/into subdirectories by domain (charts/,layout/,asset/,ui/) as the component count grows. - Design tokens — extract CSS custom properties from
app.cssinto a dedicated tokens file for easier theming.