Build Single Page Applications around your server stack. Build in vanilla TypeScript, no framework lock-in, no supply-chain vulnerabilities.
Switchback is ~300 lines meant to be vendored. Audit the codebase before you finish your first cup of coffee, customize it, happy trails.
Here are some pairings you can spin up in a container to try out.
- Zig - Backend owns form routing and state, frontend delivers app-like UX
- Erlang - Server-side rendering with HTML morphing and stateful back-end
- Go - Real-time visualization of a concurrent worker pool processing CPU-intensive tasks
- Rust - Track bulk photos as they're swiftly processed in parallel
- OCaml - Workflow states with compiler-enforced safety
- C - Pure C99 multi-threaded server with optimistic updates
- C# - ASP.NET minimal APIs with typed data contracts
- Deno - Modern TypeScript runtime with type sharing between server and client
- PHP - Persistent navigation with backend-controlled routing in vanilla PHP
- Assembly - x86-64 assembly (because why not?)
Your server owns routing and returns JSON for async requests, HTML for initial page loads. The client intercepts navigation and renders components.
- Link interception (make
<a>tags async) - Form interception (make forms async)
- History management (back/forward buttons work)
- Progress indicators (XHR option for download progress)
- Scroll restoration (remembers scroll position)
- Partial reloads (only refresh specific props)
- Error handling (server errors, validation errors)
- SSR support (morph server-rendered HTML into the DOM)
Pre-built bundles (no build step required):
Download ESM or UMD from releases
<script type="module">
import { newSwitchback } from './switchback.esm.js';
</script>TypeScript source:
Download source from releases and import directly
Vendor with git:
git submodule add https://github.com/ge3224/switchback vendor/switchbackSwitchback coordinates between your server and client. The server detects async requests and returns JSON. The client intercepts links/forms and handles navigation.
// Rust example using Axum web framework
// Check for X-Switchback header
let is_switchback = req.headers().get("X-Switchback").is_some();
// Build page response
let page = json!({
"component": "Home",
"props": {
"stats": {
"users": user_count
}
},
"url": "/"
});
// Return JSON for Switchback, HTML otherwise
if is_switchback {
Json(page).into_response()
} else {
Html(render_html(&page)).into_response()
}This pattern can be implemented easily in your favorite language. Check the demos for examples in Go, Rust, C, OCaml, Erlang, and more.
Define your page components and initialize Switchback:
import { newSwitchback } from 'switchback';
// Simple DOM helper
function h(tag, props, ...children) {
const el = document.createElement(tag);
Object.assign(el, props);
children.flat().forEach(child => {
el.appendChild(
typeof child === 'string'
? document.createTextNode(child)
: child
);
});
return el;
}
// Define page components
const pages = {
'Home': (props) => h('div', {},
h('h1', {}, 'Form Demo'),
h('p', {}, `Submissions: ${props.stats.submissions}`)
),
'Success': (props) => h('div', {},
h('h1', {}, 'Success!'),
h('p', {}, `Submitted: ${props.submitted.name}`)
),
};
// Initialize
newSwitchback({
resolve: (name) => pages[name],
setup: ({ el, App, props }) => {
el.innerHTML = '';
el.appendChild(App(props));
},
initialPage: window.initialPage,
});Add data-swbk to links and forms:
<!-- Links navigate without page reload -->
<a href="/users" data-swbk>View Users</a>
<!-- Forms submit asynchronously -->
<form action="/" method="post" data-swbk>
<input name="name" type="text" required>
<input name="email" type="email" required>
<button type="submit">Submit</button>
</form>That's it. Your server returns JSON for X-Switchback requests, your client renders components, and Switchback handles the rest.
See the demos for complete working examples.
Switchback draws from Inertia.js philosophy while staying framework-independent and highly auditable.
MIT