Create the foundation
Node.js runs the creator. npm retrieves the versioned RCMV package and installs the project. TypeScript makes every boundary visible before the application runs.
npx @rcmv/rcmv create hello-rcmv
cd hello-rcmv
npm install
RCMVSee the exact application produced by the default installer.
Open the install demoNode.js runs the creator. npm retrieves the versioned RCMV package and installs the project. TypeScript makes every boundary visible before the application runs.
npx @rcmv/rcmv create hello-rcmv
cd hello-rcmv
npm installThe generated folders are the map, and their order is the architecture: the Route receives the request, the Controller coordinates the work, the Model owns dependable data, and the View presents the outcome.
src/
├── routes/
├── controllers/
├── models/
└── views/RCMV begins at the Route. It declares which request enters the feature and explicitly connects that request to its Controller and View without absorbing either responsibility.
import { defineRoute } from '@rcmv/rcmv'
import { showGreeting } from '../controllers/greeting.controller'
import { renderGreeting } from '../views/greeting.view'
export const greetingRoute = defineRoute({
method: 'GET', path: '/hello',
controller: showGreeting, view: renderGreeting,
})The Controller reads the user intention, calls the Model, and chooses a semantic outcome. It knows nothing about HTML, colors, or deployment.
import { controller } from '@rcmv/rcmv'
import { readGreeting } from '../models/greeting.model'
export async function showGreeting({ url }) {
const result = await readGreeting(url.searchParams.get('name') ?? 'world')
return result.status === 'found'
? controller.ready(result.data)
: controller.unavailable(false)
}The Model owns the greeting data and its contract. Zod ensures the same dependable shape reaches every consumer, regardless of where the data may come from later.
import { model } from '@rcmv/rcmv'
import { z } from 'zod'
const greeting = z.object({ message: z.string() })
export async function readGreeting(name: string) {
const data = greeting.parse({ message: `Hello, ${name}!` })
return model.found(data)
}React defines the DOM component, React DOM turns it into HTML, Lucide supplies the icon, and Tailwind provides consistent visual guardrails. The View depends only on the Controller outcome.
import { CircleCheck } from 'lucide-react'
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
export function renderGreeting(outcome) {
const page = createElement('main', { className: 'grid min-h-screen place-items-center' },
createElement(CircleCheck),
createElement('h1', null, outcome.data.message))
return new Response(renderToStaticMarkup(page), {
headers: { 'content-type': 'text/html; charset=utf-8' },
})
}Register the route once. Vite builds the View assets and provides the fast development loop; Wrangler runs the same request through the Cloudflare Workers runtime.
import { createApp } from '@rcmv/rcmv'
import { greetingRoute } from './routes/greeting.route'
export default createApp({ routes: [greetingRoute] })
// Then run:
npm run devOpen the URL below. The Worker receives a standards-based Request; Route selects the work; Controller coordinates; Model validates; View renders a standards-based Response.
http://localhost:8787/hello?name=Martin
Request → Route → Controller → Model
↓
Response ← View ← ready outcomeType-check before release, build the production assets, then let Wrangler publish the Worker. Cloudflare runs the application at the edge without changing any MVC code.
npm run check
npm run build
npm run deploy