Skip to content

Hapi (Legacy)

Historical documentation of our hapi.js implementation patterns

Historical Practice

This document describes legacy patterns used in older Synapse Studios projects. These patterns are not recommended for new development. For modern backend development, see NestJS Implementation Guide.

Overview

Historically (prior to 2020), Synapse Studios built several Node.js APIs using the hapi framework. This document preserves our implementation patterns for maintaining these systems.

Architecture

Our hapi applications followed a 3-layer architecture organized by entity/domain:

api/src/
├── application/           # Entity-based organization
│   ├── user/
│   │   ├── user-model.js       # Bookshelf model
│   │   ├── user-routes.js      # Hapi route handlers
│   │   ├── user-service.js     # Business logic
│   │   └── user-access.js      # Access control (optional)
│   ├── order/
│   │   ├── order-model.js
│   │   ├── order-routes.js
│   │   └── order-service.js
│   └── [other entities]/
└── lib/                   # Shared utilities
    ├── bookshelf.js
    ├── joi.js
    └── [other libs]/

Key Characteristics

  1. Entity-based organization - Each entity had its own folder containing models, routes, and services (not organized by layer like controller/, model/, service/ folders)
  2. 3-layer separation - Models (data), Routes (HTTP), Services (business logic)
  3. Heavy ORM usage - Bookshelf.js for models with Knex for migrations
  4. Joi validation - Schema-based request validation
  5. Dependency injection - Using Electrolyte IoC container

Technology Stack

Core Dependencies

json
{
  "@hapi/hapi": "^20.2.0",        // Core framework
  "@hapi/boom": "^9.1.4",         // HTTP errors
  "@hapi/glue": "^8.0.0",         // Server composition
  "joi": "^17.7.0",               // Validation
  "bookshelf": "1.2.0",           // ORM
  "knex": "0.95.14",              // Query builder & migrations
  "hapi-auth-jwt2": "^10.3.0",   // JWT authentication
  "electrolyte": "^0.6.1"        // Dependency injection
}

Supporting Libraries

  • @hapi/vision - Template rendering
  • hapi-pino - Logging
  • pg - PostgreSQL driver

Layer Responsibilities

1. Models (Data Layer)

Bookshelf models defined database schema and relationships.

Key Patterns:

  • Factory function accepting bookshelf instance
  • Declarative relationship definitions
  • Built-in timestamp and soft delete support

2. Routes (HTTP Layer)

Route handlers defined HTTP endpoints and validation.

Key Patterns:

  • Array of route configuration objects exported from module
  • Factory function receiving dependencies (services, bookshelf)
  • Declarative validation with Joi
  • Hapi lifecycle hooks (ext.onPreHandler) for auth/access control
  • Boom for HTTP errors

3. Services (Business Logic Layer)

Services contained business logic and coordinated between models.

Key Patterns:

  • Class-based services
  • Constructor injection of dependencies
  • Coordinate multiple operations (file upload, DB save, search indexing)

Database Migrations

Knex migrations with timestamp-based naming:

Key Patterns:

  • UUIDs for primary keys
  • Automatic timestamps
  • Reversible migrations with up and down

Dependency Injection

Dependencies were managed using Electrolyte, an IoC container:

javascript
// src/application/supplier/supplier-service.js
exports = module.exports = function(bookshelf, fileService) {
  return new SupplierService(bookshelf, fileService);
};

exports['@require'] = [
  'lib/bookshelf',
  'application/file/file-service'
];
exports['@singleton'] = true;

A bootstrap plugin loaded all routes and registered them with the server using Electrolyte's dependency resolution.

Authentication & Authorization

JWT Authentication

JWT strategy configured with hapi-auth-jwt2:

javascript
server.auth.strategy("jwt", "jwt", {
  key: config.jwt_secret,
  validate: async (decoded, request) => {
    const user = await bookshelf.model("user")
      .forge({ id: decoded.user_id })
      .fetch();

    return {
      isValid: !!user,
      credentials: { user: user.toJSON() }
    };
  }
});

server.auth.default("jwt");

Request Validation

Joi Schemas

Validation was declarative:

javascript
validate: {
  payload: Joi.object({
    email: Joi.string().email().required(),
    firstName: Joi.string().max(100).required(),
    password: Joi.string().min(8).required(),
  })
}

Custom Async Validation

For database-dependent validation, a custom asyncValidation helper was used to check things like unique constraints.

Testing

Tests used @hapi/lab and @hapi/code:

javascript
const Lab = require("@hapi/lab");
const { expect } = require("@hapi/code");
const { describe, it } = (exports.lab = Lab.script());

describe("GET /suppliers/{id}", () => {
  it("returns supplier when found", async () => {
    const response = await server.inject({
      method: "GET",
      url: `/suppliers/${supplier.id}`,
      headers: { authorization: `Bearer ${token}` }
    });

    expect(response.statusCode).to.equal(200);
    expect(response.result.id).to.equal(supplier.id);
  });
});

Common Patterns

Eager Loading Relationships

javascript
const supplier = await bookshelf.model("supplier")
  .forge({ id })
  .fetch({
    withRelated: ["logo", "users", "priceLists"]
  });

Transactions

javascript
await bookshelf.transaction(async (trx) => {
  const user = await bookshelf.model("user")
    .forge()
    .save(userData, { transacting: trx });

  await bookshelf.model("userRole")
    .forge()
    .save({ user_id: user.id }, { transacting: trx });
});

File Uploads

Routes configured multipart parsing:

javascript
payload: {
  output: "stream",
  maxBytes: 10485760,
  parse: true,
  allow: "multipart/form-data",
  multipart: true
}

Additional Resources

Documentation


This document captures our historical hapi patterns for reference. For new projects, see our current backend framework guide.