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

createNodeAssetReader

Serves only build files listed in the asset manifest from disk on Node, using streaming and Meeko's ETag, HEAD, 304 and cache policy.

adapter Since 1.0.0 @meekojs/meeko/node Source code
ts
import { createNodeAssetReader } from "@meekojs/meeko/node";

createNodeAssetReader({ root, assets }) is the Node adapter's public file reader: the application asks it about each pathname and, if it is listed in assets (the manifest written by meeko build), it responds by streaming the file from root. Any other path returns null and is never read from disk: there is no way to request ../package.json, %2e%2e/… or build files that are not public.

Each response carries content-type, ETag, content-length and the cache policy shared by all adapters; HEAD and a matching If-None-Match (304) do not open the file.

Usage#

ts
// .meeko/output/node/index.mjs (simplified output generated by meeko build)
import { fileURLToPath } from "node:url";
import { createServerApp } from "@meekojs/meeko/server";
import { createNodeAssetReader } from "@meekojs/meeko/node";

const root = fileURLToPath(new URL("./client/", import.meta.url));
const app = await createServerApp({
  manifest, load, runtime, production: true,
  assets: createNodeAssetReader({ root, assets: manifest.assets ?? {} }),
});

Type#

ts
function createNodeAssetReader(options: NodeAssetReaderOptions): AssetReader

NodeAssetReaderOptions

ts
interface NodeAssetReaderOptions {
	readonly root: string
	readonly assets: AssetManifest
}

AssetReader

ts
interface AssetReader {
	get(pathname: string, request: Request): Promise<Response | null>
}

AssetManifest

ts
type AssetManifest = Readonly<Record<string, AssetRecord>>

Parameters#

Option Type Description
root string Folder containing the manifest's files: the client build output, client/.
assets AssetManifest Public URL paths with their type, etag, size and immutable.

The keys of assets are paths as serialized by the WHATWG URL: public/[email protected] is /[email protected], a b.txt is /a%20b.txt.

Return value#

An AssetReader with get(pathname, request):

Request Response
Path not in assets, or method other than GET or HEAD null: the request continues to the application.
HEAD 200 without a body, with headers.
If-None-Match equals the ETag 304 without a body.
Vite-hashed file (immutable) 200 with cache-control: public, max-age=31536000, immutable.
Everything else (public/, including public/assets/) 200 with cache-control: public, max-age=0, must-revalidate.

Examples#

Checking with curl#

sh
curl -sI http://127.0.0.1:4173/robots.txt
# HTTP/1.1 200 OK
# content-type: text/plain; charset=utf-8
# etag: "…"
# cache-control: public, max-age=0, must-revalidate

curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:4173/../package.json   # 404

Your own reader for a calculated file#

createServerApp accepts any AssetReader. To serve a file that is not in the build, compose a reader:

ts
import type { AssetReader } from "@meekojs/meeko/server";

const files = createNodeAssetReader({ root, assets: manifest.assets ?? {} });
const assets: AssetReader = {
  async get(pathname, request) {
    if (pathname === "/version.txt") return new Response(process.env.GIT_SHA ?? "dev", { headers: { "content-type": "text/plain" } });
    return files.get(pathname, request);
  },
};

Files behind a CDN#

texto
/assets/*   → cache-control: public, max-age=31536000, immutable  (hash in the name: the CDN caches them for a year)
/favicon.svg → cache-control: public, max-age=0, must-revalidate   (the CDN revalidates with ETag)

Common mistakes#

Copying files to client/ after the build#

Incorrecttexto
cp extra/terms.pdf .meeko/output/node/client/terms.pdf

It is not in the manifest: /terms.pdf is never served, and the request reaches the application (a 404). Put it in public/ and build again.

Correcttexto
public/terms.pdf  →  npm run build  →  /terms.pdf

Reading disk files manually in a route#

Incorrectts
// server/api/files/[name].get.ts
handler: async ({ params }) => new Response(await readFile(`./uploads/${params.name}`)),

params.name could be ..%2f.env: path traversal. The asset reader only serves manifest keys; for user files, validate the name with a schema and resolve the path within its folder.

Correctts
params: v.object({ name: v.pipe(v.string(), v.regex(/^[\w-]+\.pdf$/)) }),
handler: async ({ params }) => new Response(await readFile(join(uploads, params.name))),

Notes#

  • Security. The path is looked up in the manifest without decoding, so no form of traversal (.., %2e%2e, %2f, %5c) matches; the conformance suite tests this on Node, Bun, Deno and Workers.
  • Before middleware. Public files are served before server/middleware; server.headers (since 1.7) does apply to them, except headers they already carry, such as Cache-Control.
  • Streaming. The file opens only for a 200 with a body and is sent with createReadStream; if the client leaves, the stream is canceled.
  • Other runtimes. Bun, Deno and Workers have their own readers with the same behavior: createBunAssetReader, createDenoAssetReader and createCloudflareAssetReader.

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/adapters/createnodeassetreader.md