Hapi HTTP Server Framework

repository·master·Indexed 12 days ago

https://github.com/hapijs/hapi

A simple, secure, and scalable web framework for Node.js designed for building powerful applications with minimal overhead. Version 21.4.10 includes features for server-side caching via Catbox, load monitoring, custom MIME type overrides, and a robust plugin system.

Tokens
45.6K
Snippets
118
Records
174
Agent score
92%

What's inside Hapi

  1. Access Hapi documentation and resources

    master

    For tutorials, comprehensive API documentation, and support, visit the official hapi.dev Developer Portal.

    Key resources available on the portal include:

    • Documentation and API: Detailed guides for using the framework.
    • Version status: Information regarding builds, dependencies, supported Node.js versions, licenses, and End of Life (EOL) schedules.
    • Changelog: History of updates and changes.
    • Project policies: Governance and contribution guidelines.
    • Support: Channels for getting help with your Hapi applications.
  2. Use path parameters in routes

    master

    Hapi allows you to define dynamic segments in your route paths using curly braces {}. These values are accessible via request.params.

    Parameter Types:

    • Standard Parameter: '/book/{id}/cover' matches '/book/123/cover', where request.params.id is '123'.
    • Optional Parameter: Adding a '?' suffix makes a parameter optional. '/book/{id?}' matches '/book/' with request.params.id as an empty string ''.
    • Multi-segment Wildcard: Using * allows matching multiple segments.
      • '/person/{name*2}' matches exactly two segments (e.g., '/person/john/doe').
      • '/path/{param*}' matches any number of segments (must be the last segment in the path).

    Constraints:

    • Parameter names may only contain letters, numbers, and underscores (e.g., /{file_name} is valid; /{file-name} is invalid).
    • Each segment can only contain one named parameter.
    const Hapi = require('@hapi/hapi');
    const server = Hapi.server({ port: 80 });
    
    const getAlbum = function (request, h) {
        return 'You asked for ' + 
               (request.params.song ? request.params.song + ' from ' : '') + 
               request.params.album;
    };
    
    server.route({
        path: '/{album}/{song?}',
        method: 'GET',
        handler: getAlbum
    });
    
    // Multi-segment example
    server.route({
        path: '/person/{name*2}', // Matches '/person/john/doe'
        method: 'GET',
        handler: (request, h) => {
            const nameParts = request.params.name.split('/');
            return { first: nameParts[0], last: nameParts[1] };
        }
    });
  3. Handle multipart/form-data payloads

    master

    To process multipart requests, use the route.options.payload.multipart option.

    • Set to true to enable processing using the output value.
    • Set to an object with output: 'annotated' to wrap each part in an object containing headers (including filename) and the payload.

    Note on Memory: If using output: 'stream', multipart field values are text while files are streams. To avoid loading large multipart payloads into memory, set parse: false and use a streaming parser like pez.

  4. Configure server-side caching with Catbox

    master

    Hapi uses catbox for caching. The server.options.cache setting defines the storage containers available to methods and plugins.

    By default, a memory-based cache is provided via @hapi/catbox-memory. You can configure multiple caches by providing an array of configuration objects. Each object requires either an engine or a provider.

    Key configuration options for a cache object:

    • name: A unique identifier for the cache. If omitted, it becomes the default cache.
    • provider: A class or constructor (e.g., require('@hapi/catbox-redis')) used to create the client.
    • provider.options.partition: A string used to isolate cached data. Defaults to 'hapi-cache'.
    • shared: If true, allows multiple cache users to share the same segment. Defaults to false.
  5. Understand route matching order and specificity

    master

    Hapi uses a deterministic routing algorithm. The router iterates through the routing table and executes the first and only matching route. Matching is based on the combination of the request path and the HTTP verb.

    Specificity Hierarchy (Highest to Lowest):

    1. String Literals: No path parameters.
    2. Mixed Parameters: e.g., '/a{p}b'.
    3. Parameters: e.g., '/{p}'.
    4. Wildcards: e.g., '/{p*}'.

    Key Behaviors:

    • The order in which routes are added does not matter; specificity determines the match.
    • Query strings are excluded from routing logic.
    • Mixed parameters are slower because they require regular expression iteration and cannot be hashed.
  6. Handle server events with `server.events`

    master

    The server.events object is a podium public interface used to interact with server events. You can subscribe to events using on() or once(), and emit application events using emit().

    Key event types include:

    'log' Event

    Emits internal server events and application events logged via server.log(). The handler signature is function(event, tags).

    • event.channel: 'internal' for framework events, 'app' for server.log() events.
    • event.tags: An object where each event.tag is a key and the value is true.

    'cachePolicy' Event

    Emitted when a cache policy is created via server.cache() or a cached server.method(). The handler signature is function(cachePolicy, cache, segment).

    'request' Event

    Emits internal request events and application events logged via request.log(). The handler signature is function(request, event, tags).

    • event.channel: 'app' (from request.log()), 'error' (on 500 status codes), or 'internal' (framework generated).
    // Example: Listening for request errors
    server.events.on('request', (request, event, tags) => {
    
    if (tags.error) {
            console.log(`Request ${event.request} error: ${event.error ? event.error.message : 'unknown'}`);
        }
    });
    
    // Example: Listening to a specific channel
    server.events.on({ name: 'request', channels: 'error' }, (request, event, tags) => {
    
    console.log(`Request ${event.request} failed`);
    });
  7. Use Module Augmentation for Global Types

    master

    Use TypeScript's declare module to extend hapi's interfaces globally when a type applies to every route in your application. This is the preferred method for types that define a consistent baseline across the entire server.

    Common interfaces for augmentation include:

    • UserCredentials: Shape of request.auth.credentials.user
    • AppCredentials: Shape of request.auth.credentials.app
    • RequestApplicationState: Shape of request.app
    • ServerApplicationState: Shape of server.app
    • RouteOptionsApp: Shape of route.options.app
    • ServerMethods: Typed server methods
    • Request: Request decorations
    • ResponseToolkit: Toolkit decorations
    • Server: Server decorations
    • PluginProperties: Typed server.plugins
    • PluginsStates: Typed request.plugins
  8. Distinguish between server.settings.app and server.app

    master

    When configuring or managing state, understand the difference between these two locations:

    1. server.settings.app: Used to store static configuration values. This is populated via server.options.app and is intended to be immutable during the server lifecycle.
    2. server.app: Used for storing run-time state. This is the object intended for data that changes as the application runs.
  9. Understand the Hapi request lifecycle

    master

    Every incoming request in Hapi passes through a strictly ordered sequence of steps. Understanding this order is critical for implementing extensions, authentication, and validation correctly.

    The complete lifecycle sequence:

    1. onRequest: Always called if extensions exist. Can be used for URL/method rewrites via request.setUrl() and request.setMethod().
    2. Route lookup: Based on path and method. Skips to onPreResponse if no route is found.
    3. Cookies processing: Based on route state options.
    4. onPreAuth: Called regardless of whether authentication is performed.
    5. Authentication: Based on route auth options.
    6. Payload processing: Based on route payload options.
    7. Payload authentication: Based on route auth options.
    8. onCredentials: Called only if authentication is performed.
    9. Authorization: Based on route auth.access options.
    10. onPostAuth: Called regardless of authentication.
    11. Headers validation: Based on validate.headers.
    12. Path parameters validation: Based on validate.params.
    13. Query validation: Based on validate.query.
    14. Payload validation: Based on validate.payload.
    15. State validation: Based on validate.state.
    16. onPreHandler
    17. Pre-handler methods: Based on route pre options.
    18. Route handler: Executes the route handler.
    19. onPostHandler: Allows modifying request.response (but not reassigning it).
    20. Response validation: Based on validate.response.
    21. onPreResponse: Always called unless the request is aborted. Allows modifying the response.
    22. Response transmission: May emit a 'request' event on the 'error' channel.
    23. Finalize request: Emits 'response' event.
    24. onPostResponse: Executed after response is sent. Note: If performing IO here, defer it to another tick to avoid blocking.
  10. Understand server realms and plugin sandboxing

    master

    The server.realm object provides sandboxed server settings specific to a plugin or an authentication strategy. When a plugin is registered, it receives a server object with a unique realm container. This ensures that settings (like route prefixes or file paths) applied within a plugin do not leak to the global server or other plugins.

    Key components of server.realm:

    • modifiers.route: Contains route preferences like prefix and vhost. A prefix applied here will automatically be prepended to any routes added via server.route() within this realm.
    • plugin: The name of the active plugin.
    • pluginOptions: The options passed during registration.
    • settings: Overrides for settings like files.relativeTo or bind.
    • plugins: A dictionary of plugin-specific state that can be shared among activities within the same realm.

    Note: server.realm is read-only, except for the plugins property, which plugins can manipulate to share state.

    exports.register = function (server, options) {
        // Access the route prefix configured for this plugin's realm
        console.log(server.realm.modifiers.route.prefix);
    };
  11. How plugins work in Hapi

    master

    Plugins allow you to organize application logic by splitting server functionality into smaller, modular components. Each plugin can interact with the server via the standard server interface. A key feature of plugins is sandboxing: certain properties (like server.realm) are scoped to the plugin, meaning changes made to a sandboxed property in one plugin do not affect other plugins.

    To create a plugin, you must provide a register function that accepts the server object and an options object. The server object provided to the plugin includes a plugin-specific server.realm for sandboxing.

    const plugin = {
        name: 'test',
        version: '1.0.0',
        register: function (server, options) {
    
            server.route({
                method: 'GET',
                path: '/test',
                handler: function (request, h) {
    
                    return 'ok';
                }
            });
        }
    };
  12. Understand hapi TypeScript support patterns

    master

    hapi provides built-in TypeScript definitions (.d.ts) and uses two complementary patterns for typing:

    1. Module augmentation: Used to declare global types that apply to every route (e.g., UserCredentials, ServerApplicationState).
    2. Generic refs: Used to pass per-route type overrides via ServerRoute<Refs>, Request<Refs>, and Lifecycle.Method<Refs>.

    You can use both together: module augmentation sets the baseline, while generic refs narrow types for specific routes.

    import { server as createServer, ServerRoute, Request, ResponseToolkit } from '@hapi/hapi';
    
    interface AppSpace {
        startedAt: number;
    }
    
    // Use generic to type server.app
    const server = createServer<AppSpace>({ port: 3000 });
    server.app.startedAt = Date.now();
    
    // Use generic to type request.params
    const route: ServerRoute<{ Params: { id: string } }> = {
        method: 'GET',
        path: '/users/{id}',
        handler: (request, h) => {
            const id: string = request.params.id;
            return { id };
        }
    };
    
    server.route(route);