Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

createServerApp

Creates the portable server application (pages, API, RPC, Socket, assets, sessions and middleware) from the build manifest and a module loader.

advanced API Since 1.0.0 @meekojs/meeko/server Source code
ts
import { createServerApp } from "@meekojs/meeko/server";

createServerApp is Meeko's server application: it takes the manifest fixed by the build (routes, endpoints, channels, middleware and assets), a loader that returns each module by its key and the runtime from server/runtime.ts, and returns an object with fetch(request) that handles everything: server middleware, public files, prerendered pages, SSR, navigation payloads, API, RPC and the Socket hub. It calls the runtime factory once and, if it does not declare sessions, creates the default sessions.

The entry points generated by meeko build for Node, Bun, Deno and Cloudflare use it. Use it directly to host the application on your own host or test it without a network by calling app.fetch.

Usage#

ts
// server/host.ts, bundled with Vite and the meeko() plugin
import { createServerApp } from "@meekojs/meeko/server";
import { serveNode } from "@meekojs/meeko/node";
import { appName, load, loadRedis, manifest, plugins, runtime } from "virtual:meeko-server-app";

const app = await createServerApp({ manifest, load, runtime, plugins, appName, loadRedis, production: true });
const server = await serveNode(app, { port: 3000, handleSignals: true });
console.log(server.url);

virtual:meeko-server-app is the module that the Vite plugin generates with the project's manifest and loaders; it is also what .meeko/generated/entry-server.ts imports.

Type#

ts
function createServerApp<Env = Readonly<Record<string, string | undefined>>>(
  options: ServerAppOptions<Env>,
): Promise<ServerApp>

ServerAppOptions

ts
interface ServerAppOptions<Env = Readonly<Record<string, string | undefined>>> {
	readonly manifest: AppManifest
	readonly load: AppModuleLoader
	readonly runtime?: ServerRuntimeDefinition<Env, any> | undefined
	readonly production: boolean
	readonly appName?: string | undefined
	readonly loadRedis?: (() => Promise<unknown>) | undefined
	readonly platform?: ServerPlatform<Env>
	readonly assets?: AssetReader
	readonly prerender?: PrerenderSource
	readonly document?: DocumentOptions
	readonly publicOrigin?: string
	readonly prerendering?: boolean
	readonly root?: string | undefined
	readonly defaultSocketPubSub?: SocketPubSub
	readonly plugins?: readonly PluginRegistration[]
}

ServerApp

ts
interface ServerApp {
	fetch(request: Request, platform?: ServerPlatform): Promise<Response>
	readonly socket?: ServerAppSocket
	readonly manifest: AppManifest
	readonly publicOrigin?: string
	reportError(error: unknown, requestId: string, info?: ServerErrorInfo): void
	close(): Promise<void>
}

AppManifest

ts
interface AppManifest {
  readonly version: 1;
  readonly routes: readonly RouteRecord[];
  readonly errorPage?: ErrorPageRecord | undefined;
  readonly server: ServerManifest;
  readonly routeMiddleware: readonly string[];
  readonly serverMiddleware: readonly string[];
  readonly serverHandlers?: readonly { readonly route: string; readonly file: string }[];
  readonly client: ClientAssets;
  readonly assets?: AssetManifest;
  readonly document?: DocumentConfig | undefined;
  readonly contentSecurityPolicy?: ContentSecurityPolicy | undefined;
  readonly headers?: ServerHeaders | undefined;
  readonly publicFileHeaders?:
    Readonly<Record<string, Readonly<Record<string, string>>>> | undefined;
  readonly buildId?: string | undefined;
}

Options#

Option Type Default Description
manifest AppManifest required Routes, endpoints, channels, middleware, client assets and, since 1.7, contentSecurityPolicy, headers and buildId. Must have version: 1.
load (key) => Promise<unknown> required Returns the module for each manifest key (pages, sidecars, API, RPC, channels, middleware). The server never reads the disk on its own.
production boolean required Requires persistent session storage, hides error details and enables page caching and CSP.
runtime default export from server/runtime.ts none The defineServerRuntime factory: services, sessions, Socket, onError, close…
platform ServerPlatform { name: "node", env: {} } Runtime name, environment and waitUntil; passed to the factory and each request without its own platform.
assets AssetReader none Reader for the build's public files, such as createNodeAssetReader. Without it, only the application responds.
prerender PrerenderSource none Prerendered pages, served from their files without a loader or rendering.
document DocumentOptions none Trusted HTML for <head> and the end of <body>, and the host's nonce.
publicOrigin origin runtime.publicOrigin Public origin used to check Origin. Only http: or https:.
plugins PluginRegistration[] none Plugins from app/plugins and modules, executed on every page request.
appName, loadRedis none The name from package.json, for the default Redis prefix, and the import of the redis package for MEEKO_REDIS_URL.
prerendering boolean false Enabled by meeko build during prerendering: in-memory sessions even in production.
defaultSocketPubSub SocketPubSub LocalSocketPubSub Socket broker when the runtime does not declare one.

Return value#

A promise of ServerApp:

Member Description
fetch(request, platform?) Handles a request and always resolves with a Response. A URL without a route or an error page returns plain-text 404 Not Found.
socket { hub, pathnamePrefix, maxMessageBytes?, pubsub?, sessionCookieName? } if the application has channels; used by the adapter for the upgrade.
manifest The received manifest.
publicOrigin The resolved public origin, or undefined.
reportError(error, requestId, info?) Sends a transport failure to the same observer as request failures.
close() Waits for waitUntil tasks and closes sessions and runtime resources.

Rejects with TypeError if the manifest has a different version (Unsupported Meeko application manifest; rebuild the application), the loader is missing (createServerApp requires a module loader), publicOrigin is not HTTP(S), or the runtime returns something other than an object or sessions that are not a SessionManager; and with Error in production without persistent storage.

Examples#

A minimal application without a network#

ts
import { createServerApp, defineApiRoute } from "@meekojs/meeko/server";
import type { AppManifest, Schema } from "@meekojs/meeko/server";

const health: Schema<{ ok: boolean }> = {
  "~standard": { version: 1, validate: (value) => ({ value: value as { ok: boolean } }) },
};
const modules: Record<string, unknown> = {
  "/server/api/health.get.ts": { default: defineApiRoute({ response: health, handler: () => ({ ok: true }) }) },
};
const manifest: AppManifest = {
  version: 1,
  routes: [],
  routeMiddleware: [],
  serverMiddleware: [],
  server: {
    api: [{ kind: "api", file: "/server/api/health.get.ts", pathname: "/api/health", method: "GET", clientKey: "health" }],
    rpc: [],
    socket: [],
  },
  client: { entry: "/assets/entry.js", css: [], preload: [] },
};

const app = await createServerApp({ manifest, load: async (key) => modules[key], production: false });
const response = await app.fetch(new Request("http://localhost/api/health"));
console.log(response.status, await response.json()); // 200 { ok: true }
await app.close();

A POST /api/health returns 405 with Allow: GET, and /otra returns 404 Not Found.

Test the Node build without opening a port#

ts
// tests/smoke.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createNodeApp } from "../.meeko/output/node/index.mjs";

test("the home page responds", async () => {
  const app = await createNodeApp();
  const response = await app.fetch(new Request("http://localhost/", { headers: { accept: "text/html" } }));
  assert.equal(response.status, 200);
  await app.close();
});

The build's createNodeApp calls createServerApp with the build's manifest, assets and prerendered pages. Since it runs in production, it needs MEEKO_REDIS_URL or sessions in server/runtime.ts.

A host with its own nonce#

ts
const app = await createServerApp({
  manifest, load, runtime, production: true,
  document: {
    nonce: (request) => request.headers.get("x-csp-nonce") ?? undefined,
    bodyEnd: ({ nonce }) => `<script nonce="${nonce ?? ""}" src="/analytics.js" defer></script>`,
  },
});

With server.contentSecurityPolicy (since 1.7), Meeko generates a nonce per response itself; DocumentOptions.nonce is only needed when the host sets the CSP.

Common mistakes#

Production without persistent sessions#

Incorrectts
const app = await createServerApp({ manifest, load, production: true });

Rejects with Production requires a persistent session store: set MEEKO_REDIS_URL (with the redis package installed), or return { sessions } with a persistent SessionStore from server/runtime.ts.

Correctts
const app = await createServerApp({ manifest, load, runtime, production: true, platform: { name: "node", env: process.env } });

With MEEKO_REDIS_URL in platform.env (and loadRedis), or with sessions in the runtime.

A manifest from a different Meeko version#

Incorrectts
import manifest from "./old-build/manifest.json";
await createServerApp({ manifest, load, production: false });

TypeError: Unsupported Meeko application manifest; rebuild the application. The manifest and runtime must come from the same @meekojs/meeko.

Correctts
// rerun meeko build and use the new manifest
const app = await createServerApp({ manifest, load, production: false });

sessions that are not a SessionManager#

Incorrectts
const runtime = () => ({ sessions: new MemorySessionStore() });

TypeError: server/runtime.ts must return { sessions: SessionManager } or omit sessions: the store belongs inside the manager.

Correctts
const runtime = defineServerRuntime(() => ({ sessions: new SessionManager({ store: new MemorySessionStore() }) }));

Notes#

  • Portable. Uses only Web APIs (Request, Response, Web Crypto) and node:async_hooks: the same application works on Node, Bun, Deno and Workers with their respective adapters.
  • Shutdown. close() waits for waitUntil tasks and closes the runtime; it does not close the HTTP server or Socket connections, which belong to the adapter. serveNode({ handleSignals: true }) handles everything in order.
  • Cookies and caches. Any response with Set-Cookie and a public Cache-Control is sent as private, no-store without ETag, so no shared cache stores the cookie.
  • CSP. Since 1.7, with manifest.contentSecurityPolicy, each production response carries its policy: hashes on prerendered pages, a nonce in SSR and default-src 'none' on JSON. Not applied in development.
  • Errors. Request failures go to the runtime's onError (or the console, with the requestId); in production, the browser never receives the stack trace.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/advanced/createserverapp.md