@hapi/inert

Serves static files and directories in hapi.js applications with caching, range requests, and path-confinement security.

Library
npm
v7.1.2
236stars
BSD 3-Clause License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
53/100Fair
Development Activity28
Maintenance28
Community76
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
68/100Good
Architecture82
Code Quality90
Innovation45
Learning Curve55

@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

  • file route handler — serve a single static file, or a function-resolved path, from any route.
  • directory route 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-Modified caching headers and byte-range (HTTP 206) support for large or streamed files.
  • Pre-compressed asset lookup — automatically serves a .gz sibling file when the client supports content-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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search