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) |
localStorage, togglable from any asset card or detail pagestreaming pattern, showing skeleton cards while pendingsrc/
├── 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
npm install
npm run dev --open
App 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-format
GET /api/assetReturns 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
}
GET /api/asset/:idReturns full asset details.
| Param | Type | Default | Description |
|---|---|---|---|
currency |
string | USD |
Convert prices to this currency |
GET /api/asset/:id/price-historyReturns 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.
GET /api/categoryReturns all categories with asset counts.
./run.sh
The script builds the image and starts the container on port 8087. The default port can be overridden:
PORT=3000 ./run.sh
The Dockerfile uses a multi-stage build:
node:22-slim (Debian/glibc, required for native deps like lightningcss on ARM 32bit)node:22-alpine (lean, ~180MB final image; devDependencies stripped via npm 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 only
Stories 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 its own SvelteKit route with its own load function, rather than a URL param on the dashboard.limit + after cursor params. On client-side implement with IntersectionObserver a "sentinel" that intercepts the end of the list and triggers a new request.svelte-virtual-list) would reduce DOM node count and improve scroll performance.?sort=change_desc).svelte-i18n.src/lib/components/ into subdirectories by domain (charts/, layout/, asset/, ui/) as the component count grows.app.css into a dedicated tokens file for easier theming.