Concepts
Understand the backend, Runable engine, Vue application, generated files, and SSR lifecycle.
Runable connects three layers: your HTTP server, the framework engine, and your Vue application.
Three responsibilities
| Layer | Responsibility |
|---|---|
| Your backend | Listen on the network, run API routes, and handle application logic |
| Runable | Prepare the application, generate conventions, and produce the frontend response |
| Your Vue application | Define pages, components, layouts, and user interactions |
This separation lets you replace Express with Fastify or Hono without reorganizing Vue files.
The backend remains the entry point
Runable does not automatically start your application server. You create the server, then forward frontend requests to it.
// server.ts
import Express from "express";
import { express } from "runable/adapters/express";
const server = Express();
server.get("/api/orders", ordersController);
server.use(express());
server.listen(3000);You control the order. Place API routes before the Runable adapter so the backend handles them first.
Adapters for each backend
Each adapter initializes Runable once and translates framework objects for the rendering engine:
| Adapter | Environment |
|---|---|
express() | Express middleware |
fastify() | Fastify plugin |
hono() | Hono middleware |
koa() | Koa middleware |
RunableModule.register() | NestJS module on the Express platform |
adonis() | AdonisJS catch-all route handler |
bun() | fetch function for Bun.serve() |
deno() | fetch function for Deno.serve() |
Node adapters use Node request and response objects internally. Bun and Deno adapters use standard Request and Response objects.
Connect other backends
Always place the adapter after API routes or as the router's final fallback.
import Express from "express";
import { express } from "runable/adapters/express";
const app = Express();
app.use(express());
app.listen(3000);import Fastify from "fastify";
import { fastify } from "runable/adapters/fastify";
const app = Fastify();
await app.register(fastify());
await app.listen({ port: 3000 });import { Hono } from "hono";
import { hono } from "runable/adapters/hono";
const app = new Hono();
app.use("*", hono());
export default app;import Koa from "koa";
import { koa } from "runable/adapters/koa";
const app = new Koa();
app.use(koa());
app.listen(3000);import { Module } from "@nestjs/common";
import { RunableModule } from "runable/adapters/nestjs";
@Module({
imports: [RunableModule.register()],
})
export class AppModule {}import router from "@adonisjs/core/services/router";
import { adonis } from "runable/adapters/adonis";
router.any("*", adonis());import { bun } from "runable/adapters/bun";
Bun.serve({ port: 3000, fetch: bun() });import { deno } from "runable/adapters/deno";
Deno.serve({ port: 3000 }, deno());Conventions become generated code
At startup, Runable reads runable.config.ts, resolves paths, and configures several Vite plugins.
app/pages/ ──► Vue Router routes
app/layouts/ ──► layout registry
app/components/ ──► available components
app/composables/ ──► automatic imports
app/globals/ ──► auto-imported global functions and variables
app/middlewares/ ──► navigation guards
app/plugins/ ──► plugins installed in VueRequired declarations and virtual files are written to .app/. This directory is generated; do not use it for source code.
One Vue application per server render
For every SSR render, Runable creates a new Vue application, then installs the router, layouts, plugins, data manager, and Unhead.
This isolation prevents request-specific state from being shared with another user.
Request A ──► Vue App A ──► Cache A ──► HTML A
Request B ──► Vue App B ──► Cache B ──► HTML BDo not store user-specific data in a global module variable. Use state created in the application context.
The SSR lifecycle
When ssr is true, a request follows these steps:
useAsyncData() waits for non-lazy data;With ssr: false, Runable returns the client template without rendering components on the server.
Pages and metadata
A file in app/pages/ becomes a route:
app/pages/
├── index.vue → /
├── account.vue → /account
├── users/[id].vue → /users/:id
└── docs/[...slug].vue → /docs/:slug*definePageMeta() supplements file-name conventions:
<!-- app/pages/account.vue -->
<script setup lang="ts">
definePageMeta({
layout: "dashboard",
middleware: ["auth"],
});
</script>
<template>
<h1>My account</h1>
</template>Global middleware runs on every navigation. Named middleware is loaded when referenced by a page.
Plugins and modules
A plugin runs when the Vue application is created. It can provide values, register hooks, or install a client integration.
// app/plugins/api.ts
export default defineVuePlugin(() => {
return {
provide: {
apiBase: "/api",
},
};
});A module runs earlier, while configuration loads. It can add directories, plugins, or Vite options to an application.
| Extension | Execution time | Typical use |
|---|---|---|
| Plugin | Vue application creation | Injection, client SDK, runtime hooks |
| Module | Configuration loading | Reusable feature, generation, and configuration |
Development and production
In development, createRunableApp() returns a Vite instance in middleware mode. Your backend uses it for HMR and module transformation.
In production, createRunableApp() loads configuration without creating a Vite server. runable build must generate the expected files in .output/ before startup.
| Directory | Status | Content |
|---|---|---|
app/ | Source | Your Vue application |
public/ | Source | Static assets |
.app/ | Generated | Types, routes, and virtual registries |
.output/ | Generated | Production build artifacts |
Mental model
Remember this rule: your backend owns HTTP, Runable owns application assembly, and Vue owns the interface.
You can now browse the Structure section to understand every directory in a Runable project.
On this page