Server rendering
Every page renders to real HTML on the server through a typed loader() — fast first paint, real SEO, no client round-trip.
export async function loader() {
return { posts: await db.select().from(schema.posts) }
}
Apex is to Alpine what Next.js is to React — file routing, server rendering, and islands. Then it does what no other framework does: every typed API route is also an MCP tool your AI can call. No extra library.
The anatomy of an Apex app
Click through the tree — every file is the actual code Apex scaffolds. Folders nest with proper indent guides, so pages/blog/[slug] sits two levels deep, not flat.
PS C:\dev\my-app> apex dev v0.1.20 · The full-stack, AI-native meta-framework for Alpine.js ✓ Dev server ready ➜ Local http://localhost:3000/ ➜ MCP http://localhost:3000/mcp
The same code, running on sample data — no database. Posts a JSON body and prepends the result.
File routing, server rendering, islands, components, a typed data layer, and AI-native APIs — the essentials, out of the box.
Every page renders to real HTML on the server through a typed loader() — fast first paint, real SEO, no client round-trip.
export async function loader() {
return { posts: await db.select().from(schema.posts) }
}
Ship almost no JavaScript. Hydrate only what's interactive, exactly when you choose.
<div x-data="counter()" client:visible>…</div>
// client:load | idle | visible | none
Single-file .alpine components with props and scoped styles — reused across pages.
<Counter start="5" :label="'Likes'" />
Your pages/ folder is your route map — nested folders and dynamic segments included.
pages/index.alpine → /
pages/blog/[slug].alpine → /blog/:slug
One defineResource gives you CRUD over Drizzle — SQLite, Postgres, Turso or Neon.
export default defineResource('posts', {
db, table: schema.posts,
insert: { title: z.string() },
})
The part no one else has: every typed route is also a live MCP tool your AI can call — no extra library.
export default defineApexRoute({
input: { a: z.number(), b: z.number() },
mcp: true, // → REST endpoint + MCP tool
handler: ({ input }) => ({ sum: input.a + input.b }),
})
server/api/features.ts — the whole backend of the demo
// One definition → REST endpoints AND MCP tools
// (features_list / get / create / update / delete). The UI
// below and an AI assistant call the SAME handlers.
export default defineResource('features', {
db,
table: schema.features,
insert: { name: z.string(), tag: z.string().optional() },
})
pages/index.alpine — the list is rendered from the DB on the server
<script server lang="ts">
import { db, schema } from '../db/index.js'
export async function loader() {
const features = await db.select().from(schema.features)
return { features } // → Alpine x-data, in the first HTML
}
</script>
The browser's "Add" and the "Simulate an AI" button both POST /api/features — the
exact endpoint exposed as the MCP tool features_create. One
defineResource, and the UI and an assistant operate on the same rows.
See the Data guide →
Three pillars
Static-first pages. Islands hydrate only when they need to — many pages ship zero framework JS until you scroll.
<!-- wakes on scroll, not on load -->
<section x-data="{ n: 0 }" client:visible>
<button @click="n++" x-text="n"></button>
</section>
File-based routing, server-side rendering, and a component you already know how to write. On Node — no PHP required.
// pages/blog/[slug].alpine
<script server>
export function loader({ params }) {
return { post: getPost(params.slug) }
}
</script>
Write a route once. It's a REST endpoint and an MCP tool. Point Claude at /mcp and it can call your app.
// server/api/add.ts
export default defineApexRoute({
description: 'Add two numbers',
input: { a: z.number(), b: z.number() },
mcp: true,
handler: ({ input }) => ({ sum: input.a + input.b }),
})
The unfair advantage
Because every route carries a strict, typed schema, exposing it to an AI is automatic. The framework carries the MCP transport — you install nothing extra.
server/api/add.ts
POST /api/add → { "sum": 5 }
tools/call add → { "sum": 5 }
Quickstart
# install the CLI once, then use `apex` everywhere
npm i -g @apex-stack/core
apex new my-app # scaffold + install deps + git init
cd my-app
apex dev # http://localhost:3000
apex dev --islands # static-first islands mode
apex new installs everything and sets up git for you; the app ships a server-rendered
page (pages/index.alpine) and an API route that is also an MCP tool
(server/api/hello.ts). Don't want a global install? Run
npm create apexjs@latest my-app and use npm run dev — same result, local CLI.
Early, but real
Apex JS is on npm under @apex-stack/*. SSR + hydration, islands, file routing,
components, AI-native APIs, a multi-database data layer, and a full production build (static
or Node server) are all proven and covered by tests. Jobs/queues and Nitro deploy presets
are next — see the roadmap.