Architecture Overview
Deep dive into Manic's design, build pipeline, and core systems.
Architecture Overview
Manic is built on three core pillars: type-safe routing, zero-config build engine, and plugin-driven extensibility.
System Design
Build Pipeline
Development: manic dev
The CLI does not run oxlint when you save files — it loadConfig(), merges bunfig.toml, then Bun.spawn s bun --watch against ~manic.ts with plugin --preload flags and PORT / HOST / NETWORK env vars. Watching manic.config.* kills and respawns the child so plugins stay fresh.
Key files:
app/~routes.generated.ts— Route registry (generated — never edit)manic.config.ts— Plugins and framework options~manic.ts— Your server entry (you author this file)
See manic dev for flags.
Production: manic build
Exact ordering (plugins vs API graphs vs minify) is pinned in Build pipeline — higher-level diagram:
Default artifact tree (build.outdir, usually .manic):
Router (Client-Side)
Route Scoring: How Routes Are Matched
Manic uses a scoring algorithm to determine which route handles a URL:
// Example routes
app/routes/posts/new.tsx // Score: 200 (static + static)
app/routes/posts/[id].tsx // Score: 110 (static + dynamic)
app/routes/posts/[...slug].tsx // Score: 101 (static + catch-all)Scoring Rules:
- Static segment:
+100 - Dynamic segment:
+10 - Catch-all segment:
+1
URL Match Process:
/posts/new
├─ new.tsx → 200 ✓ (WINNER)
├─ [id].tsx → 110
└─ [...slug].tsx → 101
/posts/123
├─ new.tsx → No match
├─ [id].tsx → 110 ✓ (WINNER)
└─ [...slug].tsx → 101This ensures correct precedence without order dependency.
Code Splitting Strategy: Lazy Loading
Each route's component is lazy-loaded on first navigation:
// Generated in app/~routes.generated.ts
const routes = [
{
path: '/posts/[id]',
component: null, // Lazy
loader: () => import('./routes/posts/[id].tsx'),
},
];Prefetch Behavior:
When prefetch is true (default), Link calls preloadRoute(to) on hover/focus so the lazy chunk begins downloading before navigation (Router preloadRoute).
Benefits:
- Only ship JS needed for current page
- Chunks cached in memory after first load
- Smooth prefetch without janky loads
Client Navigation Flow
Hook Access:
import { useRouter, useQueryParams } from 'manicjs';
export default function PostPage() {
const { path, navigate, params } = useRouter();
const id = params.id;
const searchParams = useQueryParams();
const q = searchParams.get('q');
return (
<div>
<p>Post {id}</p>
<button type="button" onClick={() => navigate('/other')}>Go</button>
</div>
);
}API Routes (Server-Side)
File-Based API Routes
Each index.ts exports a Hono route handler:
// app/api/users/index.ts
import { Hono } from 'hono';
const app = new Hono();
app.get('/', async (c) => {
const users = await db.query('SELECT * FROM users');
return c.json(users);
});
export default app;Auto-Generated OpenAPI
Manic scans all API routes and generates an OpenAPI schema:
{
"openapi": "3.1.0",
"paths": {
"/api/users": {
"get": {
"operationId": "listUsers",
"responses": {
"200": {
"content": {
"application/json": {
"schema": { "type": "array" }
}
}
}
}
}
}
}
}Discovery: GET /.well-known/openapi.json
Use with tools like Scalar, Swagger, or postman for docs.
Type-Safe Client
Manic generates a type-safe RPC client from your API routes:
// Type-safe API client using Hono RPC
import type { AppType } from '../api/index';
import { hc } from 'hono/client';
const client = hc<AppType>('/api');
// Type-safe API calls
const users = await client.users.$get();
const user = await client.users[':id'].get({ param: { id: '123' } });Uses Hono's RPC for full type inference from your API routes.
Plugin System
Core Plugin API
import { createPlugin } from 'manicjs/config';
export function myPlugin(options = {}) {
return createPlugin({
name: 'my-plugin',
// Option 1: Static files (recommended)
staticFiles: [
{
path: '/robots.txt',
content: 'User-agent: *\nDisallow: /',
contentType: 'text/plain; charset=utf-8',
},
],
// Option 2: Server hooks (dev-only routes)
configureServer(ctx) {
ctx.addRoute('/health', () => new Response('ok'));
ctx.injectHtml('<meta name="my-plugin" content="true">');
ctx.addLinkHeader('<https://example.com>; rel="preconnect"');
},
// Option 3: Build hooks (emit & inject)
build(ctx) {
ctx.emitClientFile('__manifest.json', JSON.stringify({
routes: ctx.pageRoutes,
}));
ctx.injectHtml('<link rel="dns-prefetch" href="https://api.example.com">');
},
});
}Official Plugins
| Package | Purpose | Status |
|---|---|---|
@manicjs/tailwind | Tailwind CSS v4 | ✓ Stable |
@manicjs/seo | robots.txt, meta tags, Link headers | ✓ Stable |
@manicjs/sitemap | Auto-generate sitemap.xml | ✓ Stable |
@manicjs/mcp | Model Context Protocol endpoint | ✓ Stable |
@manicjs/api-docs | Scalar API reference UI | ✓ Stable |
@manicjs/mdx | Markdown + JSX | ✓ Stable |
@manicjs/unocss | UnoCSS | ✓ Stable |
Example config:
// manic.config.ts
import { defineConfig } from 'manicjs/config';
import { tailwind } from '@manicjs/tailwind';
import { seo } from '@manicjs/seo';
import { sitemap } from '@manicjs/sitemap';
export default defineConfig({
plugins: [
tailwind(),
seo({
hostname: 'https://example.com',
title: 'My App',
}),
sitemap({ hostname: 'https://example.com' }),
],
});Configuration
Framework Configuration
// manic.config.ts
import { defineConfig } from 'manicjs/config';
import { tailwind } from '@manicjs/tailwind';
export default defineConfig({
// App metadata
app: {
name: 'My App',
},
// Plugins & extensions
plugins: [
tailwind(),
// ... more plugins
],
// Build options
build: {
minify: true,
sourcemap: false,
},
// Server options
server: {
port: 6070,
},
});Managing Secrets
Create .env.local for development:
MANIC_PUBLIC_API_URL=http://localhost:6070/api
DATABASE_URL=postgres://user:pass@localhost/dbAccess in routes:
import { getEnv } from 'manicjs/env';
const apiUrl = getEnv('MANIC_PUBLIC_API_URL'); // Client
const dbUrl = getEnv('DATABASE_URL'); // ServerNever commit .env.local. Add to .gitignore.
Use MANIC_PUBLIC_ prefix for client-exposed variables. Server vars are never exposed.
Performance snapshot
Manic prioritizes fast dev startup, parallel minification, and per-route lazy graphs. Architectural reasoning + honest caveats live in Performance model; reproducible timings vs other frameworks are in Benchmarks.
Questions? See Troubleshooting or CLI Reference.