createServerApp
Creates the portable server application (pages, API, RPC, Socket, assets, sessions and middleware) from the build manifest and a module loader.
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#
// 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#
function createServerApp<Env = Readonly<Record<string, string | undefined>>>(
options: ServerAppOptions<Env>,
): Promise<ServerApp>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[]
}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>
}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#
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#
// 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#
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#
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.
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#
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.
// rerun meeko build and use the new manifest
const app = await createServerApp({ manifest, load, production: false });sessions that are not a SessionManager#
const runtime = () => ({ sessions: new MemorySessionStore() });TypeError: server/runtime.ts must return { sessions: SessionManager } or omit sessions: the store belongs inside the manager.
const runtime = defineServerRuntime(() => ({ sessions: new SessionManager({ store: new MemorySessionStore() }) }));Notes#
- Portable. Uses only Web APIs (
Request,Response, Web Crypto) andnode:async_hooks: the same application works on Node, Bun, Deno and Workers with their respective adapters. - Shutdown.
close()waits forwaitUntiltasks 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-Cookieand apublicCache-Controlis sent asprivate, no-storewithoutETag, so no shared cache stores the cookie. - CSP. Since
1.7, withmanifest.contentSecurityPolicy, each production response carries its policy: hashes on prerendered pages, a nonce in SSR anddefault-src 'none'on JSON. Not applied in development. - Errors. Request failures go to the runtime's
onError(or the console, with therequestId); in production, the browser never receives the stack trace.