Skip to content

s00d/bun-vite-boilerplate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

14 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿž Bun + Vue 3 + Drizzle + Elysia Fullstack Template

A full-featured TypeScript boilerplate using Bun, Vue 3 (SSR + SPA), Drizzle ORM, ElysiaJS, and a built-in WebSocket server. Designed without Express or Node.js, this project utilizes Bun's native HTTP server, Vite, and Elysia for modern high-performance development.

Ideal for building isomorphic applications with authentication, real-time features, SSR/SSG, and scalable backend architecture.


๐Ÿš€ Features

  • โœ… Bun-native backend using Elysia.js
  • โœ… Fullstack TypeScript (Bun + Vue 3)
  • โœ… SSR + SPA hybrid with Vite
  • โœ… Static Generation (SSG) support
  • โœ… Pinia + SSR hydration
  • โœ… Drizzle ORM (SQLite, MySQL, PostgreSQL)
  • โœ… Session + API key authentication
  • โœ… Native WebSocket server (with Elysia)
  • โœ… Clean architecture: MVC + Router + Middleware
  • โœ… Unified configuration and composables
  • โœ… CSRF protection, CORS, CSP
  • โœ… i18n with dynamic translation loading
    • ๐ŸŒ Auto-detect locale from route
    • ๐Ÿ“ฆ Load only necessary translation namespace per page
    • ๐Ÿง  SSR/SSG compatible
    • ๐Ÿงฉ Backend integration via i18next-fs-backend and Elysia route

๐Ÿ“ Project Structure

data/                         # SQLite DB file (default: mydb.sqlite)
dist/                         # Production build (client + server bundles)
scripts/                      # Generation, build, and utility scripts
tests/                        # Unit, integration, and load tests
public/                       # Static public assets (e.g., logo, icons)
.env                          # Environment variable definitions
index.html                    # HTML template for SSR rendering
vite.config.ts                # Vite build configuration

config/                       # Configuration layer
โ”œโ”€โ”€ ssg.config.ts             # Static routes for pre-rendering
โ”œโ”€โ”€ ws.config.ts              # WebSocket ping/pong settings
โ””โ”€โ”€ security.config.ts        # CORS, CSP, cookie settings
โ””โ”€โ”€ i18n.config.ts            # i18n config

src/
โ”œโ”€โ”€ client/                   # Vue 3 SPA (SSR + hydration)
โ”‚   โ”œโ”€โ”€ pages/                # File-based routing (SPA/SSR)
โ”‚   โ”œโ”€โ”€ composables/          # Vue composables
โ”‚   โ”œโ”€โ”€ store/                # Pinia stores
โ”‚   โ”œโ”€โ”€ App.vue               # Main layout
โ”‚   โ”œโ”€โ”€ main.ts               # App factory
โ”‚   โ”œโ”€โ”€ router.ts             # Vue Router config
โ”‚   โ”œโ”€โ”€ entry-client.ts       # Client SPA bootstrap
โ”‚   โ”œโ”€โ”€ entry-server.ts       # SSR rendering entry
โ”‚   โ”œโ”€โ”€ entry-static-client.ts# Static client hydration
โ”‚   โ”œโ”€โ”€ env.d.ts              # Type declarations
โ”‚   โ””โ”€โ”€ vite-env.d.ts         # Vite typings
โ”‚
โ”œโ”€โ”€ server/                   # Bun HTTP + WebSocket + Elysia
โ”‚   โ”œโ”€โ”€ db/                   # Drizzle ORM DB init
โ”‚   โ”œโ”€โ”€ models/               # Drizzle schemas
โ”‚   โ”œโ”€โ”€ middleware/           # CSRF, auth, validation
โ”‚   โ”œโ”€โ”€ controllers/          # Route handlers (business logic)
โ”‚   โ”œโ”€โ”€ routes/               # API + SSR + WS routes
โ”‚   โ”œโ”€โ”€ utils/                # preload, static walker
โ”‚   โ””โ”€โ”€ index.ts              # Server entrypoint (Bun + Elysia)
โ”‚
โ”œโ”€โ”€ shared/                   # Cross-layer utils
โ”‚   โ”œโ”€โ”€ axios.ts              # Axios with SSR support
โ”‚   โ”œโ”€โ”€ env.ts                # PUBLIC_ environment reader
โ”‚   โ””โ”€โ”€ globalCookieJar.ts    # Server cookie holder

Create .env:

PORT=8888
HOST=localhost
DB_FILE_NAME=data/mydb.sqlite
PUBLIC_API_URL=http://localhost:8888
PUBLIC_WS_URL=ws://localhost:8888/ws

Start in dev:

bun run dev

Build and serve:

bun run build
bun run start

โš™๏ธ How It Works

SSR/SPA Hybrid

  • Server renders HTML via entry-server.ts
  • State is hydrated client-side using window.__pinia
  • Preload links injected into <head> for performance

Authentication

  • Session: Set-Cookie: sessionId
  • Alternative: Authorization: Bearer <apiKey>
  • Example routes:
    • POST /api/guest/login
    • POST /api/guest/register
    • GET /api/profile
    • POST /api/logout

Data Fetching

  • Via global axios client @/shared/axios.ts
  • SSR requests auto-include cookies from globalCookieJar
  • baseURL is PUBLIC_API_URL

WebSocket

  • Connect via ws://localhost:8888/ws
  • Use useWebSocket() composable on the client
  • Broadcast server messages with broadcast()

โœ๏ธ Adding...

A Page

Create a .vue file in src/client/pages/ and register it in router.ts:

{ path: '/dashboard', component: () => import('./pages/Dashboard.vue') }

A Model

Add it to src/server/models, then export from schema.ts:

export const posts = sqliteTable('posts', { ... });

A Controller

Create a handler in controllers/:

export async function dashboardController({ body, request, set }: Context<{ body: PostBody }>) {
  return { ok: true };
}

An API Route

Add to routes/guest.ts or routes/protected.ts:

routes['/api/dashboard'] = { GET: dashboardController };
export const protectedRoutes = new Elysia({ prefix: "/api" })
//...
.post("/api/dashboard", dashboardController)

A Generator Script

Use CLI to scaffold code:

bun run scripts/gen.ts controller MyController
bun run scripts/gen.ts route MyRoute guest
bun run scripts/gen.ts route MyRoute protected
bun run scripts/gen.ts model MyModel
bun run scripts/gen.ts middleware MyMiddleware

๐Ÿ“ฆ Scripts

bun run dev       # Dev mode with Vite
bun run build     # Build frontend and SSR bundle
bun run generate  # Generate frontend and bundle
bun run start     # Start production server

๐Ÿง  State Management

  • Pinia (useUserStore, etc.)
  • Auto-hydration in entry-client.ts
  • Server sets window.__pinia on render

๐Ÿฉฑ Database

  • Powered by Drizzle ORM
  • SQLite by default (.env: DB_FILE_NAME)
  • Tables:
    • users: email, passwordHash, apiKey
    • sessions: id, userId, expiresAt

๐Ÿ—ผ Clean Architecture

  • controllers/: business logic
  • middleware/: validation/auth
  • routes/: route mapping
  • models/: Drizzle schema
  • shared/: universal utils (axios, env, cookies)


๐Ÿ” CSRF Protection

The template includes built-in CSRF protection:

  • A secure CSRF token is generated on each SSR HTML response and stored in a cookie.
  • The client automatically attaches the token via the x-csrf-token header on requests.
  • Server middleware validates the token by comparing it to the cookie.
  • Only affects POST, PUT, PATCH, and DELETE methods.
  • The logic is implemented in src/server/middleware/csrf.ts and used in the main router.

This helps prevent cross-site request forgery attacks in authenticated requests.


๐Ÿ—„๏ธ Database Support

This template uses Drizzle ORM, which supports multiple SQL dialects including:

  • SQLite (default, easy to start with)
  • MySQL
  • PostgreSQL
  • MariaDB

To switch databases:

  1. Update .env:
# For MySQL:
DB_URL=mysql://user:pass@host:port/dbname
  1. Change the Drizzle adapter in src/server/db/init.ts:

Replace:

import { drizzle } from 'drizzle-orm/bun-sqlite';
import { Database } from 'bun:sqlite';

With (example for MySQL):

import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';

And update initDb():

const connection = await mysql.createConnection(process.env.DB_URL!);
db = drizzle(connection, { schema });
  1. Adapt models in src/server/models/ using the corresponding dialectโ€™s schema utilities:
  • drizzle-orm/sqlite-core โ†’ drizzle-orm/mysql-core or pg-core
  1. Install required dependencies:
bun add mysql2 drizzle-orm

Once this is done, the rest of the application logic remains the same โ€” only the schema and adapter change depending on your database engine.


๐Ÿท๏ธ Meta Tags (SEO + Social)

This template supports dynamic <title>, <meta name="description">, and Open Graph tags both on the server (SSR) and client using @vueuse/head.

๐Ÿ”ง How to Add Meta Tags to a Page

Import and call useHead() inside your <script setup> block:

<!-- src/client/pages/HomePage.vue -->
<script setup lang="ts">
import { useHead } from "@vueuse/head";

useHead({
  title: "Home Page - My App",
  meta: [
    {
      name: "description",
      content: "Welcome to the home page of our app.",
    },
    {
      property: "og:title",
      content: "My App Home",
    },
  ],
});
</script>

โœ… Works with SSR

During server-side rendering, the generated tags are serialized and injected into the <head> of the HTML response. This ensures search engines and social media platforms can read correct metadata on first load.

No extra configuration is needed โ€” SSR automatically includes the result of useHead() in the response.


๐ŸŒ Internationalization (i18n)

Localization is powered by vue-i18n, integrated with SSR + SPA support and automatic translation loading based on the current route.


๐Ÿ“Œ Overview

  • Translations are organized by locale and namespace, e.g.
    lang/en/home.json, lang/ru/profile.json
  • The namespace is automatically derived from the route:
    • /en/profile โ†’ namespace: profile
    • /auth/login โ†’ namespace: auth
    • / โ†’ namespace: home (default)

๐Ÿ” On the client, translations are automatically loaded based on the current page name. You don't need to manually specify a namespace โ€” it's inferred from the URL.


๐Ÿง  Usage in Components

import { useI18n } from "vue-i18n";
const { t } = useI18n();

t("home.title"); // => "Home" (en), "ะ“ะปะฐะฒะฝะฐั" (ru)

๐Ÿ“ฆ Key Format

  • All translation keys use the format namespace.key
  • Translations are fetched dynamically based on the current route

๐ŸŒ Switching Locale

The <LocaleSwitcher /> component changes the current language via redirect:

<LocaleSwitcher />
  • The default locale (en) has no prefix
  • Other locales use a prefixed path (/ru/...)

โš™๏ธ Adding Translations

โž• Adding a New Namespace

When creating a new translation file (e.g. lang/en/dashboard.json, lang/ru/dashboard.json):

Make sure to add the new namespace in config/i18n.config.ts:

namespaces: ["common", "auth", "profile", "meta", "home", "dashboard"],

โž• Adding a New Locale

When adding a new locale (e.g. de):

Update the i18n config to include it:

preload: ["en", "ru", "de"],
supportedLngs: ["en", "ru", "de"],

Ensure the corresponding translation files exist:

lang/de/home.json
lang/de/common.json
...

๐Ÿ–ฅ๏ธ Using i18n on the Backend

To use a translation inside a backend controller or handler, use the t() function provided in the context. You must reference translations using the namespace:key format.

โœ… Example

export function healthController({ t }: { t: TFunction }) {
  return {
    message: t("meta:health"),
  };
}

This will return a translated string from lang/{locale}/meta.json under the key "health".


Let me know if you'd like to add an example of pluralization or interpolation too.

โœ… Summary

  • Use t("namespace.key") for all translation calls
  • Namespace is resolved automatically from the route
  • Translations are loaded dynamically as needed
  • Update config/i18n.config.ts when adding new locales or namespaces


Load Testing Results

๐Ÿ“Š Latency Overview

This section summarizes the performance of critical endpoints under load.

๐Ÿ“Š Load Testing Results

This document presents performance metrics and session analysis based on synthetic load testing using artillery and bun.


๐Ÿ” Login API (/api/guest/login)

  • Connections: 50
  • Test Duration: 10 seconds
  • Total Requests: 191
  • 2xx Responses: 141
  • Average Latency: 2910 ms
  • p99 Latency: 4249 ms
  • Max Latency: 6944 ms
  • Throughput: 3.27 kB/s

๐Ÿ“ˆ Latency Distribution

Percentile Latency (ms)
2.5% 291
50% 3462
97.5% 4179
99% 4249

๐Ÿ•’ Session Length (ms)

Metric Value
Min 96.7
Max 5487.8
Mean 2714.2
Median 2780
p95 4770.6
p99 5378.9

๐Ÿ  Homepage (/)

  • Load Profile: 50 โ†’ 500 RPS (ramp-up over 30 seconds)
  • Test Duration: 30 seconds
  • Total Requests: 8250
  • 2xx Responses: 4501
  • Timeout Errors (ETIMEDOUT): 3749
  • Average Latency: 1 ms
  • p99 Latency: 4 ms
  • Max Latency: 7 ms
  • Throughput: ~7.5 MB in 30s

๐Ÿ“ˆ Latency Distribution

Percentile Latency (ms)
2.5% 1
50% 1
97.5% 2
99% 4

๐Ÿ•’ Session Length (ms)

Metric Value
Min 1.3
Max 219.1
Mean 5.7
Median 2.3
p95 5.4
p99 127.8

๐ŸŽฏ RPC / Profile Load (CSRF โ†’ Login โ†’ Profile)

  • Connections: 50
  • Total Requests: 600
  • 2xx Responses: 600
  • Average Latency: 903 ms
  • p99 Latency: 3605 ms

๐Ÿ•’ Session Length (ms)

Metric Value
Min 96.7
Max 5487.8
Mean 2714.2
Median 2780
p95 4770.6
p99 5378.9

๐Ÿ“ฆ Notes

  • All tests were performed on http://localhost:8888.
  • Bun server memory usage peaked at 327 MB, with negligible CPU load.

๐Ÿงฎ Session Duration Stats

Extracted from artillery and custom loadtest logs:

Metric Value (seconds)
Average 2714.2
Median 2780
95th Percentile 4770.6
99th Percentile 5378.9
Max 5487.8

Most user sessions range between 2.5โ€“5 minutes. A few sessions exceed 20 minutes, indicating background usage or idle tabs.


๐Ÿš€ Performance Without Frontend or WebSocket

To isolate backend performance, we executed a high-load benchmark targeting the /meta/info endpoint without involving frontend rendering, WebSocket communication, or additional system load (e.g., SSR or SQLite queries).

๐Ÿ“Œ Test Configuration:

  • Command: bun run scripts/metatest.ts
  • Connections: 1000
  • Duration: 10 seconds
  • Workers: 1

๐Ÿ“ˆ Results:

  • Average Latency: 72.28 ms
  • p99 Latency: 142 ms
  • Max Latency: 269 ms
  • Avg Throughput: ~49,686 req/sec, ~19.3 MB/s
  • Total Processed: ~501,000 requests in 10 seconds

โœ… Conclusion

Given the SQLite backend and SSR integration, the Bun server delivers:

  • High performance for static and API requests.
  • Consistent latency within a few hundred milliseconds for chained operations (e.g., CSRF โ†’ Login โ†’ Profile).
  • Excellent RPS handling in raw conditions without frontend or I/O overhead.

These results confirm that the backend powered by Bun is highly capable under load, especially when isolated from rendering or database operations.


๐Ÿ—๏ธ Static Generation (SSG)

This template supports static pre-rendering (SSG) for selected routes.

โš™๏ธ Setup

  1. Define routes to generate in ssg.config.ts:

    export const staticRoutes = ["/", "/about", "/auth/login"];
  2. Run the generation script:

    bun run generate

    This will:

  • Build the project using vite.config.ts
  • Render all configured routes to HTML
  • Save files to dist/static
  1. Serve the app:
    bun run start

๐Ÿšฆ How it works

  • When a user requests a page:
    • If a pre-rendered HTML file exists in dist/static, it is served instantly.
    • Otherwise, the page is rendered via SSR on demand.
  • This ensures fast load for common pages, while keeping SSR flexibility.

๐Ÿ’ก Notes

  • You can mix SSG and SSR freely.
  • Useful for marketing, landing, auth, and public pages.
  • Static files are generated once and served without re-computation.

Last updated: April 18, 2025


๐ŸฆŠ Powered By

  • Bun โ€” fast all-in-one JS runtime
  • Vue 3 โ€” reactive UI framework
  • Elysia.js โ€” ultra-fast server framework
  • Drizzle ORM โ€” typesafe SQL
  • Pinia โ€” Vue store with SSR support
  • Vite โ€” dev/build tool

๐Ÿ“œ License

MIT ยฉ s00d


Pull requests and contributions welcome.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published