Quick Start
Create a Runable application with two pages, a layout, an API route, and server-rendered data.
Build a small application that combines automatic routing, a shared layout, and SSR data loading.
This page starts from the Express project created in Installation.
Add an API route
Replace server.ts with this example:
// server.ts
import Express from "express";
import { express } from "runable/adapters/express";
const server = Express();
server.get("/api/projects", (_req, res) => {
res.json([
{ id: 1, name: "Documentation" },
{ id: 2, name: "Dashboard" },
]);
});
server.use(express());
server.listen(3000, () => {
console.log("http://localhost:3000");
});Your API remains a regular Express route. Runable does not move it into the frontend.
Create a layout
<!-- app/layouts/default.vue -->
<template>
<div>
<header>
<strong>My application</strong>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/projects">Projects</RouterLink>
</nav>
</header>
<main>
<slot />
</main>
</div>
</template>The default.vue layout wraps pages that do not explicitly request another layout.
Create the home page
<!-- app/pages/index.vue -->
<template>
<section>
<h1>Welcome</h1>
<p>The Express backend and Vue application live in the same project.</p>
</section>
</template>app/pages/index.vue automatically maps to /.
Load data
Create a second page:
<!-- app/pages/projects.vue -->
<script setup lang="ts">
type Project = {
id: number;
name: string;
};
const { data: projects, pending, error, refresh } = await useAsyncData(
"projects",
async (signal) => {
const response = await fetch("http://localhost:3000/api/projects", {
signal,
});
if (!response.ok) {
throw new Error("Unable to load projects");
}
return response.json() as Promise<Project[]>;
},
);
</script>
<template>
<section>
<h1>Projects</h1>
<p v-if="pending">Loading…</p>
<p v-else-if="error">{{ error.message }}</p>
<ul v-else>
<li v-for="project in projects" :key="project.id">
{{ project.name }}
</li>
</ul>
<button type="button" @click="refresh">Refresh</button>
</section>
</template>useAsyncData() runs the fetch during server rendering. Runable embeds the result in the HTML and restores the cache on the client, so the browser does not immediately repeat the request during hydration.
The projects key identifies the cache entry and deduplicates simultaneous calls. Use a different key when request parameters change.
Observe automatic routing
Your directory now contains two routes:
app/pages/
├── index.vue → /
└── projects.vue → /projectsAdd a file to app/pages/ to create a route. There is no route table to maintain.
What you just used
| Need | Runable solution |
|---|---|
| Display several screens | Routing based on app/pages/ |
| Share navigation | default.vue layout |
| Keep application routes | /api/projects route in Express |
| Preload data during SSR | useAsyncData() |
| Avoid a second fetch on mount | Cache serialization and hydration |
Compare this model with Nuxt in Runable vs Nuxt.
On this page