@hapi/inert
Serves static files and directories in hapi.js applications with caching, range requests, and path-confinement security.
Repository Health
Technical Analysis
@hapi/inert is the official hapi.js plugin for serving static files and directories over HTTP. It registers two route handlers — file and directory — plus an h.file() toolkit method, giving hapi servers everything needed to act as a static asset or media server without reaching for separate static-file middleware.
Under the hood it streams files directly from the filesystem using Node’s fs module, computes ETag and Last-Modified headers for cache validation, supports HTTP range requests for partial content (video/audio seeking, resumable downloads), and can serve pre-compressed .gz variants when the client accepts gzip. Path resolution is confined to a configured root directory by default, preventing path traversal outside the intended folder, and every filesystem error is translated into the appropriate Boom HTTP error (404, 403) rather than leaking raw Node errors.
What You Get
fileroute handler — serve a single static file, or a function-resolved path, from any route.directoryroute handler — serve an entire folder tree with multi-path fallback, index-file lookup, and optional auto-generated directory listings.h.file()toolkit method — return a file response manually from inside any custom handler, with full control over headers.- Built-in
ETag/Last-Modifiedcaching headers and byte-range (HTTP 206) support for large or streamed files. - Pre-compressed asset lookup — automatically serves a
.gzsibling file when the client supportscontent-encoding: gzip.
Common Use Cases
- Serving a single-page app’s built assets (HTML/CSS/JS) directly from a hapi server without a separate CDN or static host.
- Building a lightweight static file or media server, including partial-content video/audio streaming via range requests.
- Serving user-uploaded files (images, documents) with correct content-disposition headers, confined to a safe root directory.
- Adding a custom 404 fallback page or a directory index/listing to an existing hapi API server.
Under The Hood
Architecture
The plugin registers file and directory route handlers plus an h.file() toolkit decorator in lib/index.js. lib/file.js resolves the requested path with a confine safety check, wraps it in a source object (path/settings/stat/file), and returns request.generateResponse() wired to hapi’s response lifecycle hooks — prepare (opens and stats the file, sets content-type/length/etag/last-modified headers), marshal (called at transmission time, creates the read stream and optionally swaps in a pre-compressed variant), and close (releases the file descriptor). lib/directory.js builds on File.load, trying each configured base path in order, falling back to index files, then to a generated HTML listing, distinguishing 404 vs. 403/EISDIR failures via @hapi/bounce. lib/fs.js wraps raw Node fs calls (open/close/fstat/readdir/createReadStream) in a small File class that translates errno codes into typed Boom errors. lib/etag.js layers a shared LRU cache keyed by path+inode+size+mtime, computing SHA-1 hashes lazily and deduplicating concurrent requests for the same file via a pending-promise map. The layering — registration, handler orchestration, raw fs abstraction, caching as a cross-cutting concern — is clean and consistently typed with Boom/Bounce across every layer.
Tech Stack
Plain CommonJS JavaScript with hand-maintained TypeScript declarations (lib/index.d.ts), no build step — lib/ ships as-is. Zero non-hapi runtime dependencies beyond @hapi/ammo (HTTP range-header parsing), @hapi/boom (typed HTTP errors), @hapi/bounce (error-type filtering/rethrow), @hapi/hoek (utilities), @hapi/validate (Joi-based options schemas), and lru-cache for the ETag cache. Tests run on @hapi/lab and @hapi/code, hapi’s own test/assertion stack, and CI is a shared reusable GitHub Actions workflow (hapijs/.github ci-plugin) enforcing a minimum Node 14 and hapi 20.
Code Quality
The test/ directory (file.js, directory.js, security.js, esm.js, plus fixture folders) totals roughly 2,600 lines against about 500 lines of library code, and package.json’s test script runs lab -t 100, meaning 100% coverage is enforced by CI rather than aspirational. Error handling is explicit and typed throughout via Boom for HTTP-facing errors and Bounce for rethrow-by-type filtering (e.g. Bounce.ignore(err, 'boom')), with no silent swallowing observed. Naming follows consistent hapi house style (an internals namespace, minimal OOP outside the small Fs.File class), and linting runs through the shared @hapi/eslint-plugin config.
What Makes It Unique
Inert does not attempt to be a novel general-purpose static-file server — it deliberately does one thing, serving files and directories inside hapi’s own request/response lifecycle, integrating tightly via hapi’s prepare/marshal/close response hooks and a custom 'file' response variety rather than reimplementing what Express’s static middleware or serve-static already do differently. Its real strengths are conventional but carefully executed: safe path confinement against traversal, concurrency-safe ETag hashing that deduplicates in-flight hash computations for the same file, and pluggable pre-compressed-asset lookup.