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

Initializing diagram...

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.

Initializing diagram...

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:

Initializing diagram...

Default artifact tree (build.outdir, usually .manic):

server.js

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 → 101

This 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

Initializing diagram...

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

PackagePurposeStatus
@manicjs/tailwindTailwind CSS v4✓ Stable
@manicjs/seorobots.txt, meta tags, Link headers✓ Stable
@manicjs/sitemapAuto-generate sitemap.xml✓ Stable
@manicjs/mcpModel Context Protocol endpoint✓ Stable
@manicjs/api-docsScalar API reference UI✓ Stable
@manicjs/mdxMarkdown + JSX✓ Stable
@manicjs/unocssUnoCSS✓ 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/db

Access in routes:

import { getEnv } from 'manicjs/env';

const apiUrl = getEnv('MANIC_PUBLIC_API_URL');  // Client
const dbUrl = getEnv('DATABASE_URL');           // Server

Never 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.

On this page