├── .vscode └── extensions.json ├── src ├── vite-env.d.ts ├── main.ts ├── server │ ├── test_data.ts │ ├── index.ts │ └── trpc.ts ├── lib │ ├── trpc.ts │ └── Counter.svelte ├── App.svelte ├── app.css └── assets │ └── svelte.svg ├── tsconfig.node.json ├── tsconfig.server.json ├── svelte.config.js ├── playwright.config.ts ├── tests └── example.test.ts ├── vite.config.ts ├── index.html ├── e2e └── example.spec.ts ├── .gitignore ├── tsconfig.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── public ├── logo_trpc.svg └── vite.svg ├── package.json └── README.md /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.css' 2 | import App from './App.svelte' 3 | 4 | const app = new App({ 5 | target: document.getElementById('app') as HTMLElement, 6 | }) 7 | 8 | export default app 9 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "target": "ES2020", 6 | "sourceMap": false, 7 | "outDir": "dist", 8 | }, 9 | "include": ["src/server/**/*"], 10 | } -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: "npm run build && npm run start", 6 | port: 5000, 7 | }, 8 | testDir: "e2e", 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /src/server/test_data.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | name: string; 4 | } 5 | 6 | export const userList: User[] = [ 7 | { 8 | id: "1", 9 | name: "Vite + Svelte + tRPC", 10 | }, 11 | ]; 12 | 13 | export let counter = 0; 14 | 15 | export const increaseCounter = () => { 16 | counter += 1 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; 2 | import superjson from "superjson"; 3 | 4 | import type { AppRouter } from "../server/trpc"; 5 | 6 | export const client = createTRPCProxyClient({ 7 | links: [httpBatchLink({ url: "/trpc" })], 8 | transformer: superjson, 9 | }); 10 | -------------------------------------------------------------------------------- /tests/example.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { counter, increaseCounter } from "../src/server/test_data"; 3 | 4 | it("Counter is zero by default", () => { 5 | expect(counter).eq(0); 6 | }); 7 | 8 | it("Counter increments by one", () => { 9 | increaseCounter(); 10 | expect(counter).eq(1); 11 | }); 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | server: { 7 | proxy: { 8 | "/trpc": "http://localhost:5000/", 9 | }, 10 | }, 11 | build: { outDir: "dist/public" }, 12 | plugins: [svelte()], 13 | test: { dir: "tests" }, 14 | }); 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Svelte + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /e2e/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('test button is working', async ({ page }) => { 4 | await page.goto('http://localhost:5000/'); 5 | const countButton = page.getByRole('button', { name: 'count is 0' }); 6 | await countButton.click(); 7 | 8 | const countButtonNew = page.getByRole('button', { name: 'count is 1' }); 9 | await expect(countButtonNew).toHaveText('count is 1') 10 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | /test-results/ 26 | /playwright-report/ 27 | /playwright/.cache/ 28 | -------------------------------------------------------------------------------- /src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | groups: 13 | production-dependencies: 14 | dependency-type: "production" 15 | development-dependencies: 16 | dependency-type: "development" 17 | 18 | - package-ecosystem: "github-actions" 19 | directory: "/" 20 | schedule: 21 | interval: "monthly" 22 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | 4 | import express from "express"; 5 | import * as trpcExpress from "@trpc/server/adapters/express"; 6 | import sirv from "sirv"; 7 | import compress from "compression"; 8 | 9 | import { appRouter, createContext } from "./trpc.js"; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | const app = express(); 14 | const assets = sirv("public", { 15 | maxAge: 31536000, 16 | immutable: true, 17 | }); 18 | 19 | app.use( 20 | "/trpc", 21 | trpcExpress.createExpressMiddleware({ 22 | router: appRouter, 23 | createContext, 24 | }) 25 | ); 26 | 27 | app.use(express.static(path.join(__dirname, "public"))); 28 | app.use(compress(), assets); 29 | 30 | const { PORT = 5000 } = process.env; 31 | 32 | app.listen(PORT, () => { 33 | console.log(); 34 | console.log( 35 | `tRPC running at \x1b[36mhttp://localhost:\x1b[1m${PORT}/\x1b[0m` 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [ 18.x, 20.x, 21.x, 22.x ] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v5 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v5 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: "npm" 28 | 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | 32 | - name: Lint 33 | run: npm run check 34 | 35 | - name: Run unit tests 36 | run: | 37 | npm run test 38 | 39 | - name: Run E2E tests 40 | run: | 41 | npx playwright install 42 | npm run e2e 43 | -------------------------------------------------------------------------------- /public/logo_trpc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | {#if name} 32 |

Hello {name}!

33 | {/if} 34 | 35 |
36 | 37 |
38 | 39 |

40 | Click on the Vite, Svelte and tRPC logos to learn more 41 |

42 |
43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-svelte-trpc", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev:client": "vite", 8 | "dev:server": "tsx watch src/server", 9 | "dev": "concurrently --kill-others \"npm run dev:client\" \"npm run dev:server\"", 10 | "build:client": "vite build", 11 | "build:server": "tsc --project tsconfig.server.json", 12 | "build": "npm run build:client && npm run build:server", 13 | "start": "node dist", 14 | "preview": "vite preview", 15 | "check": "svelte-check --tsconfig ./tsconfig.json", 16 | "e2e": "playwright test", 17 | "test": "vitest" 18 | }, 19 | "devDependencies": { 20 | "@playwright/test": "^1.47.2", 21 | "@sveltejs/vite-plugin-svelte": "^3.1.2", 22 | "@tsconfig/svelte": "^5.0.4", 23 | "@types/compression": "^1.7.5", 24 | "@types/express": "^5.0.0", 25 | "@types/node": "^22.7.4", 26 | "concurrently": "^9.0.1", 27 | "svelte": "^4.2.19", 28 | "svelte-check": "^4.0.4", 29 | "tslib": "^2.7.0", 30 | "tsx": "^4.19.1", 31 | "typescript": "^5.6.2", 32 | "vite": "^5.4.20", 33 | "vitest": "^2.1.9" 34 | }, 35 | "dependencies": { 36 | "@trpc/client": "^10.45.2", 37 | "@trpc/server": "^10.7.0", 38 | "compression": "^1.8.1", 39 | "express": "^4.21.2", 40 | "sirv": "^3.0.1", 41 | "superjson": "^2.2.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | .card { 41 | padding: 2em; 42 | } 43 | 44 | #app { 45 | max-width: 1280px; 46 | margin: 0 auto; 47 | padding: 2rem; 48 | text-align: center; 49 | } 50 | 51 | button { 52 | border-radius: 8px; 53 | border: 1px solid transparent; 54 | padding: 0.6em 1.2em; 55 | font-size: 1em; 56 | font-weight: 500; 57 | font-family: inherit; 58 | background-color: #1a1a1a; 59 | cursor: pointer; 60 | transition: border-color 0.25s; 61 | } 62 | button:hover { 63 | border-color: #646cff; 64 | } 65 | button:focus, 66 | button:focus-visible { 67 | outline: 4px auto -webkit-focus-ring-color; 68 | } 69 | 70 | @media (prefers-color-scheme: light) { 71 | :root { 72 | color: #213547; 73 | background-color: #ffffff; 74 | } 75 | a:hover { 76 | color: #747bff; 77 | } 78 | button { 79 | background-color: #f9f9f9; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC, type inferAsyncReturnType } from "@trpc/server"; 2 | import type * as trpcExpress from "@trpc/server/adapters/express"; 3 | import superjson from "superjson"; 4 | 5 | import { userList, counter, increaseCounter } from "./test_data.js"; 6 | 7 | // created for each request 8 | export const createContext = (_options: trpcExpress.CreateExpressContextOptions) => ({}); // no context 9 | 10 | type Context = inferAsyncReturnType; 11 | 12 | export const t = initTRPC.context().create({ transformer: superjson }); 13 | 14 | export const appRouter = t.router({ 15 | userById: t.procedure 16 | // The input is unknown at this time. 17 | // A client could have sent us anything 18 | // so we won't assume a certain data type. 19 | .input((val: unknown) => { 20 | // If the value is of type string, return it. 21 | // TypeScript now knows that this value is a string. 22 | if (typeof val === "string") return val; 23 | 24 | // Uh oh, looks like that input wasn't a string. 25 | // We will throw an error instead of running the procedure. 26 | throw new Error(`Invalid input: ${typeof val}`); 27 | }) 28 | .query((req) => { 29 | const { input } = req; 30 | 31 | const user = userList.find((u) => u.id === input); 32 | 33 | return user; 34 | }), 35 | 36 | getCounter: t.procedure 37 | .query(() => { 38 | return counter 39 | }), 40 | 41 | increaseCounter: t.procedure 42 | .mutation(() => { 43 | increaseCounter() 44 | return counter 45 | }) 46 | }); 47 | 48 | // export type definition of API 49 | export type AppRouter = typeof appRouter; 50 | -------------------------------------------------------------------------------- /src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/mishankov/vite-svelte-trpc/actions/workflows/ci.yml/badge.svg)](https://github.com/mishankov/vite-svelte-trpc/actions/workflows/ci.yml) 2 | [![MadeWithSvelte.com shield](https://madewithsvelte.com/storage/repo-shields/4184-shield.svg)](https://madewithsvelte.com/p/vite-svelte-trpc-template/shield-link) 3 | 4 | # Vite + Svelte + tRPC 5 | 6 | This template should help get you started developing with [Vite](https://vitejs.dev/), [Svelte](https://svelte.dev/) and [tRPC](https://trpc.io/) combo 7 | 8 | ## Features 9 | 10 | - HMR for client code with Vite and automatic build and restart for server with tcx on file changes 11 | - [Vitest](https://vitest.dev/) for unit tests 12 | - [Playwright](https://playwright.dev/) for E2E tests 13 | - [GitHub Actions](https://github.com/features/actions) workflow with build, lint and test steps 14 | - [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot) setup to keep dependencies updated 15 | - [Express](https://expressjs.com/) and [sirv](https://github.com/lukeed/sirv) to serve SPA and handle tRPC API calls 16 | 17 | ## How to work with this template? 18 | 19 | ### Installation 20 | 21 | #### Using `degit` 22 | 23 | ```bash 24 | npx degit github:mishankov/vite-svelte-trpc my-app 25 | ``` 26 | 27 | #### Using GitHub template 28 | 29 | Create repo from this template with "Use this template" button above and clone it 30 | 31 | ### Install dependencies 32 | 33 | ```bash 34 | npm i 35 | ``` 36 | 37 | ### Start server and client 38 | 39 | ```bash 40 | npm run dev 41 | ``` 42 | 43 | For HMR support use http://localhost:5173/ link by default. tRPC endpoint is http://localhost:5000/ 44 | 45 | You can also start client and server separately. Use `npm run dev:server` to start server and `npm run dev:client` to start client 46 | 47 | ### Testing 48 | 49 | Unit test are living in `tests` directory. To execute them, run: 50 | 51 | ```bash 52 | npm run test 53 | ``` 54 | 55 | E2E UI tests are living in `e2e` directory. To execute them, run: 56 | 57 | ```bash 58 | npm run e2e 59 | ``` 60 | 61 | ### Create and run production build 62 | 63 | ```bash 64 | npm run build 65 | npm run start 66 | ``` 67 | 68 | ## Important points of file structure 69 | 70 | - `dist` - client and server build directory 71 | - `e2e` - E2E tests 72 | - `tests` - unit tests 73 | - `src` 74 | - `lib` 75 | - `trpc.ts` - tRPC client setup to use in browser code 76 | - `server` 77 | - `index.ts` - setup for ExpressJS server which combines static files serving and tRPC server 78 | - `trpc.ts` - setup for tRPC server 79 | - `test_data.ts` - test data to showcase basic capabilities of this setup 80 | 81 | ## Recommended IDE Setup 82 | 83 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 84 | --------------------------------------------------------------------------------