HTTP Compatibility Guide
Build an HTTP application that is compatible with @ucdjs/fs-backend
This guide defines the standard for an HTTP application that should work with @ucdjs/fs-backend's built-in HTTP backend.
The goal is simple: if your service follows this contract, consumers can point HTTPFileSystemBackend at it and use the standard backend API without custom adapters.
Base Shape
The backend assumes a single baseUrl and issues requests relative to it:
GET {baseUrl}/{path}for files and directory listingsHEAD {baseUrl}/{path}for metadata and existence checks
Example:
import HTTPFileSystemBackend from "@ucdjs/fs-backend/backends/http";
const backend = HTTPFileSystemBackend({
baseUrl: new URL("https://api.example.com/files"),
});Path Rules
Your HTTP application should expose canonical backend paths:
- all paths start with
/ - directories end with
/ - files do not end with
/
Examples:
/16.0.0/ucd//16.0.0/ucd/UnicodeData.txt
GET Contract
File Reads
When a file path is requested with GET, return the raw file content.
200 OKwith the file body for existing files404 Not Foundfor missing files- other non-2xx responses are treated as backend errors
read() expects text and readBytes() expects binary-safe responses, so your application should return the real file bytes and an appropriate content type.
Directory Listings
When a directory path is requested with GET, return JSON.
Expected response shape:
[
{ "type": "file", "name": "UnicodeData.txt", "path": "/16.0.0/ucd/UnicodeData.txt" },
{ "type": "directory", "name": "emoji", "path": "/16.0.0/ucd/emoji/" }
]Rules:
- listings should be non-recursive
- directory entries may omit
children - extra fields like
lastModifiedare tolerated 404means the directory does not exist403should remain an error, not an empty directory
The HTTP backend handles recursive walking by following directory entries itself.
HEAD Contract
HEAD is the authoritative metadata endpoint for HTTP-compatible backends.
Required Header
X-UCD-Stat-Type:fileordirectory
Recommended Headers
X-UCD-Stat-Size: byte size for filesLast-Modified: modification timestamp, when availableContent-Type: content type for files
Example:
HTTP/1.1 200 OK
X-UCD-Stat-Type: file
X-UCD-Stat-Size: 12345
Last-Modified: Tue, 17 Mar 2026 10:00:00 GMT
Content-Type: text/plain; charset=utf-8For directories:
HTTP/1.1 200 OK
X-UCD-Stat-Type: directorySemantics Consumers Will See
exists()
exists() uses HEAD and is intentionally lossy:
200becomestrue404becomesfalse- callers should use
stat()when they need more than a boolean
stat()
stat() uses HEAD and expects your server to provide X-UCD-Stat-Type.
404becomesBackendFileNotFoundtypecomes fromX-UCD-Stat-TypesizeprefersX-UCD-Stat-Size
Minimal Compatibility Checklist
Your HTTP application is compatible if it does all of the following:
- Serves files from
GET. - Serves non-recursive directory JSON from
GET. - Uses canonical backend paths in directory responses.
- Returns
X-UCD-Stat-TypefromHEAD. - Returns
404for missing paths. - Does not turn authorization failures into fake empty directory listings.
Example Server Shape
The nicest way to structure this is with a small Request/Response helper.
That keeps the actual compatibility logic framework-agnostic, and both Hono and
H3 can call it directly.
function isNotFoundError(error: unknown): boolean {
return error instanceof Error && error.message === "Not Found";
}
async function handleFilesRequest(method: "GET" | "HEAD", path: string): Promise<Response> {
let stat;
try {
stat = await getStat(path);
} catch (error) {
if (isNotFoundError(error)) {
return new Response(null, { status: 404 });
}
throw error;
}
if (method === "HEAD") {
const headers = new Headers({
"X-UCD-Stat-Type": stat.type,
});
if (stat.type === "file") {
headers.set("X-UCD-Stat-Size", String(stat.size));
}
if (stat.mtime) {
headers.set("Last-Modified", stat.mtime.toUTCString());
}
return new Response(null, {
status: 200,
headers,
});
}
if (isDirectory(path)) {
return Response.json(await listDirectory(path));
}
return new Response(await readFile(path));
}Hono Adapter
import { Hono } from "hono";
const app = new Hono();
app.get("/files/*", (c) => {
const path = c.req.path.replace(/^\/files/, "") || "/";
return handleFilesRequest("GET", path);
});
app.head("/files/*", (c) => {
const path = c.req.path.replace(/^\/files/, "") || "/";
return handleFilesRequest("HEAD", path);
});H3 Adapter
import { getRequestURL, H3 } from "h3";
const app = new H3();
app.get("/files/**", (event) => {
const path = getRequestURL(event).pathname.replace(/^\/files/, "") || "/";
return handleFilesRequest("GET", path);
});
app.head("/files/**", (event) => {
const path = getRequestURL(event).pathname.replace(/^\/files/, "") || "/";
return handleFilesRequest("HEAD", path);
});