Skip to content

Vue + Moost (SSR) ​

Build a fullstack Vue + Moost application with server-side rendering, in-process API calls, and zero-config HMR.

Scaffold a project ​

bash
npm create moost -- --ssr

Or with a project name:

bash
npm create moost my-app -- --ssr

The scaffolder asks whether to enable SSR (server-side rendering) or run as SPA only. Both modes share the same project structure — the difference is a single line in vite.config.ts.

Project structure ​

my-app/
├── src/
│   ├── controllers/
│   │   └── api.controller.ts    # Moost API routes
│   ├── pages/
│   │   ├── Home.vue             # Landing page
│   │   └── About.vue            # About page
│   ├── main.ts                  # Moost server entry
│   ├── app.ts                   # Vue app factory
│   ├── entry-client.ts          # Client hydration
│   ├── entry-server.ts          # SSR render function
│   └── router.ts                # Vue Router config
├── public/                       # Static assets
├── index.html                    # HTML template
├── vite.config.ts                # Vite + Moost config
└── tsconfig.json

No server.ts needed — the plugin handles dev serving and auto-generates a production server during build.

How it works ​

vite.config.ts ​

ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { moostVite } from '@moostjs/vite'

export default defineConfig({
  server: { port: 3000 },
  plugins: [
    vue(),
    moostVite({
      entry: '/src/main.ts',
      middleware: true,
      prefix: '/api',
      ssrEntry: '/src/entry-server.ts', // remove for SPA-only
    }),
  ],
})

The moostVite plugin runs in middleware mode — Moost handles /api/* routes, everything else falls through to Vite.

API routes ​

ts
// src/controllers/api.controller.ts
import { Controller, Param } from 'moost'
import { Get } from '@moostjs/event-http'

@Controller('api')
export class ApiController {
  @Get('hello/:name')
  hello(@Param('name') name: string) {
    return { message: `Hello, ${name}!`, timestamp: Date.now() }
  }
}

SSR data fetching ​

With local fetch enabled (default), fetch('/api/...') calls Moost handlers in-process during SSR — no HTTP round-trip:

vue
<script setup lang="ts">
import { ref, onServerPrefetch, onMounted, useSSRContext } from 'vue'

const ssrState = import.meta.env.SSR ? useSSRContext()! : (window as any).__SSR_STATE__ || {}
// Restore server-fetched data on the client so hydration matches the server HTML
const data = ref(ssrState.data ?? null)

// Server: in-process fetch via Moost (no network)
onServerPrefetch(async () => {
  const res = await fetch('/api/hello/SSR')
  data.value = await res.json()
  ssrState.state = { ...ssrState.state, data: data.value }
})

// Client fallback for SPA mode (no transferred state)
onMounted(async () => {
  if (!data.value) {
    const res = await fetch('/api/hello/SSR')
    data.value = await res.json()
  }
})
</script>

Initializing the ref from the transferred state is what makes the window.__SSR_STATE__ transfer effective — without it the client hydrates with null, mismatches the server-rendered HTML, and refetches on every load.

SSR vs SPA ​

The only difference between SSR and SPA mode is the ssrEntry option in vite.config.ts:

SSRSPA
ssrEntry'/src/entry-server.ts'not set
First paintServer-rendered HTMLEmpty shell, client renders
SEOFull content in initial HTMLRequires client-side hydration
Data fetchingonServerPrefetch runs on serveronMounted runs on client

To switch, add or remove ssrEntry in your moostVite() options.

Running ​

Development ​

bash
npm run dev

Runs vite — opens at http://localhost:3000. Vue pages have HMR, Moost controllers hot-reload without restart. The plugin handles SSR rendering in dev automatically.

Production build ​

bash
npm run build

Produces client assets, SSR bundle, and server bundle to dist/ in a single pass.

Production start ​

bash
npm run start

Runs node dist/server/server.js — production server with static file serving, API routes, and SSR rendering (or SPA fallback).

Custom server entry ​

If you need custom middleware in production (compression, auth, logging), see the Custom Server Entry section in the Vite Plugin docs.

Released under the MIT License.