HyperExpress

repository·master·Indexed 24 days ago

https://github.com/kartikk221/hyper-express

A high-performance HTTP and WebSocket webserver for Node.js powered by uWebsockets.js. It provides high throughput and low latency with a simple-to-use snake_case API. Version 7.0.2 supports Node.js 22, 24, and 26 on macOS, Windows, and glibc-based Linux distributions. Features include a HostManager for multi-hostname SSL/TLS configurations via SNI, support for global and route-specific middlewares, and high-performance static file serving via LiveDirectory.

Tokens
24.5K
Snippets
23
Records
132
Agent score
84%

What's inside hyper-express

  1. Implement Middleware in HyperExpress v7

    master

    Middleware behavior in v7 follows these rules:

    • Synchronous Middleware: Must explicitly call next() to proceed.
    • Asynchronous Middleware: Any middleware that returns a thenable (Promise) will automatically advance the chain when the promise is fulfilled.
    • Error Handling: Invoking error handling occurs if a middleware throws, rejects, calls next(error), or fulfills with an Error object.
    • Completion Rules: Each middleware completes at most once. The first call to next() returns true; subsequent calls return false and do not advance the chain. Promise settlements occurring after next() is called are ignored.
    • Strict Mode: You can enable the server option strict_middleware: true to report duplicate completion attempts to the applicable scoped error handler while still preventing double advancement.
  2. Use global and route-specific middlewares

    master

    HyperExpress supports two levels of middleware:

    • Global Middleware: Registered via webserver.use(callback). These execute on every incoming request and run before any route-specific middlewares.
    • Route/Method Specific Middleware: Registered as an options object in route methods (e.g., .get(path, { middlewares: [...] }, callback)). Middlewares in the array are executed in the order specified.

    Middleware callbacks receive (request, response, next). Call next() to proceed to the next middleware or route handler.

    // Global middleware
    webserver.use((request, response, next) => {
        some_asynchronous_call((data) => {
            request.some_data = data;
            next();
        });
    });
    
    const specific_middleware1 = (request, response, next) => {
        console.log('route specific middleware 1 ran!');
        return next();
    };
    
    const specific_middleware2 = (request, response, next) => {
        console.log('route specific middleware 2 ran!');
        return next();
    };
    
    // Route specific middleware
    webserver.get('/', {
        middlewares: [specific_middleware1, specific_middleware2]
    }, (request, response) => {
        return response.send('Hello World');
    });
  3. Understand WebSocket upgrade and event lifetimes

    master

    WebSockets in HyperExpress have specific memory and lifecycle behaviors due to how uWebSockets.js handles upgrades and events:

    • Remote Address: During a WebSocket upgrade, the original HTTP socket data is destroyed. While getRemoteAddressAsText() on a native WebSocket might return an empty buffer, HyperExpress snapshots the request-entry IP and port and transfers them through the upgrade process so they are available in the Websocket wrapper.
    • Message Data: Inputs for messages, dropped messages, close reasons, pings, pongs, and subscription topics are treated as callback-lifetime native memory.
      • String and Buffer modes copy or consume data synchronously.
      • ArrayBufferSafe creates an owned copy.
      • Warning: The legacy ArrayBuffer mode is zero-copy and must not be retained after the synchronous listener returns.
    • Close Lifecycle: To prevent errors when the native core invokes close handlers synchronously, HyperExpress defensively clears its wrapper before close observers run.
  4. Understand the HyperExpress Response component

    master

    The Response component is an extended Writable stream that follows the official Node.js network specification. It provides compatibility with ExpressJS methods and properties, while also exposing underlying uWebsockets.js capabilities.

    Key characteristics:

    • It is a Writable stream, meaning you can use standard Node.js streaming patterns like .pipe().
    • It includes compatibility methods from ExpressJS.
    • It provides access to the underlying uWS.Response via the .raw property.
    • It supports Server-Sent Events (SSE) via the .sse property.
  5. How Routers provide modularity

    master

    A Router acts as a mini-app that holds route information independently of a Server. This allows you to group related routes into branches (e.g., an API version) and then mount that entire branch onto a master Server instance using webserver.use(). A single Router can even be reused across multiple Server instances.

    When you mount a router with a pattern like webserver.use('/api/v1', router), all routes defined within that router are automatically prefixed with that pattern. For example, a router route defined as /register becomes /api/v1/register on the server.

    const api_v1_router = new HyperExpress.Router();
    
    api_v1_router.post('/register', async (request, response) => {
        const { email, password, captcha } = await request.json();
        const id = await register_account(email, password, captcha);
        return response.json({ id });
    });
    
    // Mount the router to the server under the '/api/v1' prefix
    webserver.use('/api/v1', api_v1_router);
  6. Understand the HTTP request and response lifecycle

    master

    HyperExpress manages the lifetime of native uWebSockets.js objects to prevent memory errors. When working with HTTP requests and responses, keep the following rules in mind:

    • HttpRequest: The native HttpRequest object is only valid during the synchronous route or upgrade callback. HyperExpress copies the Method, URL, query, headers, and path parameters into a JavaScript Request object for safe use. Using Request.raw is an unsafe escape hatch that bypasses these protections.
    • HttpResponse: The native HttpResponse is valid until the response is completed via abort(), close(), end(), endWithoutBody(), a successful tryEnd(), or an upgrade(). HyperExpress tracks Response.completed to ensure no native methods are called after completion.
    • Peer/Proxy Information: Peer/proxy addresses and ports are captured at request entry and remain stable even after the response is completed.
    • Data Chunks (onDataV2): Native memory chunks are detached immediately after the callback. HyperExpress uses buffered/public-stream modes to copy data before retention, or parser mode to consume it synchronously.
    • onWritable: This callback only runs while the native response is active. HyperExpress manages a single guarded callback and clears it upon completion.
  7. How MultipartField works in HyperExpress

    master

    The MultipartField object is an abstraction provided by the request.multipart() handler to represent individual fields in a multipart form request. It distinguishes between text-type fields and file-type fields:

    • Text fields: Populate the value property.
    • File fields: Populate the file property.

    When using request.multipart(callback), the callback is invoked for every field found in the request. You can use the presence of field.file to determine if a field contains file data.

  8. How Server-Sent Events (SSE) work in HyperExpress

    master

    Server-Sent Events (SSE) allow a server to maintain an open HTTP connection and gradually push data to the client. In HyperExpress, this functionality is accessed via the Response.sse property.

    Important Constraints:

    • Once a SSEventStream is opened on a Response object, you cannot set the HTTP status or write any additional headers.
    • Opening an SSE stream automatically sets the following headers to comply with the v6 wire contract:
      • Content-Type: text/event-stream; charset=utf-8
      • Cache-Control: no-cache
      • Connection: keep-alive
      • X-Accel-Buffering: no (prevents buffering by reverse proxies like nginx).

    To use SSE, check if response.sse is available on the response object before attempting to open the stream.

  9. How to use middlewares in HyperExpress

    master

    HyperExpress implements a middleware API similar to ExpressJS, supporting both callback-based and async/promise-based iteration. You can attach middlewares to a Server or a Router using the .use() method.

    To trigger the next middleware in the chain, you must either call next() in a callback or resolve the promise in an async function. To trigger the global error handler, you must either pass an Error object to next(error) in a callback or return an Error object from an async function.

    // Callback-Based Iteration
    router.use('/api', (request, response, next) => {
        some_async_operation(request, response)
        .then(() => next())
        .catch((error) => next(error))
    });
    
    // Async/Promise-Based Iteration
    server.use(async (request, response) => {
        try {
            await some_async_operation();
        } catch (error) {
            return error; // Triggers global error handler
        }
    });
  10. Stream data using write() and drain()

    master

    For large datasets, use write() to implement chunked transfer. This method mimics Writable.write() and supports direct piping (e.g., readable.pipe(response)).

    Handling Backpressure with drain(): If write() returns false, the chunk was not fully sent due to backpressure. You must listen for the drain event to retry.

    Proper Retry Pattern:

    1. Listen for the drain event using response.drain(handler). The handler must be synchronous and receives an offset (Number).
    2. Use the offset to slice the failed chunk: chunk.slice(offset - response.write_offset).
    3. Retry the write() call with the sliced chunk.
    4. The drain handler may be called multiple times until the chunk is fully written.

    Note: You must call send() at the end of the chunked transfer to terminate it.

  11. Implement a Server-Sent Events (SSE) endpoint

    master

    To implement an SSE endpoint, verify response.sse exists, call .open() (or call .send() which opens it automatically), and manage the connection lifecycle by listening for the close event on the response object to clean up resources (like removing the stream from a broadcast pool).

    const crypto = require('crypto');
    const sse_streams = {};
    
    function broadcast_message(message) {
        Object.keys(sse_streams).forEach((id) => {
            sse_streams[id].send(message);
        });
    }
    
    webserver.get('/news/events', (request, response) => {
        if (response.sse) {
            // Open the stream
            response.sse.open();
            
            // Assign a unique ID and store for broadcasting
            response.sse.id = crypto.randomUUID();
            sse_streams[response.sse.id] = response.sse;
            
            // Cleanup on disconnection
            response.once('close', () => {
                delete sse_streams[response.sse.id];
            });
        } else {
            response.send('Server-Sent Events Not Supported!');
        }
    });
    const crypto = require('crypto');
    
    const sse_streams = {};
    function broadcast_message(message) {
        // Send the message to each connection in our connections object
        Object.keys(sse_streams).forEach((id) => {
            sse_streams[id].send(message);
        })
    }
    
    webserver.get('/news/events', (request, response) => {
        // You may perform some authentication here as this is just a normal HTTP GET request
        
        // Check to ensure that SSE if available for this request
        if (response.sse) {
            // Looks like we're all good, let's open the stream
            response.sse.open();
            // OR you may also send a message which will open the stream automatically
            response.sse.send('Some initial message');
            
            // Assign a unique identifier to this stream and store it in our broadcast pool
            response.sse.id = crypto.randomUUID();
            sse_streams[response.sse.id] = response.sse;
            
            // Bind a 'close' event handler to cleanup this connection once it disconnects
            response.once('close', () => {
                // Delete the stream from our broadcast pool
                delete sse_streams[response.sse.id]
            });
        } else {
            // End the response with some kind of error message as this request did not support SSE
            response.send('Server-Sent Events Not Supported!');
        }
    });
  12. Install HyperExpress via npm

    master

    HyperExpress v7 supports active even-numbered Node.js release lines 22, 24, and 26. It is a CommonJS-only package that preserves a snake_case API.

    Platform Support & Compatibility:

    • Supported: Tier 1 macOS, Windows, and glibc-based Tier 1 Linux distributions.
    • Unsupported: Alpine Linux (plain or with gcompat) is not supported because the native uWebSockets.js addon may terminate with SIGSEGV due to the use of musl instead of glibc.
    • Recommendation: Use Debian or Ubuntu-based Node.js images for reliable container deployments.
    npm i hyper-express