Appearance
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
- Entity-based organization - Each entity had its own folder containing models, routes, and services (not organized by layer like controller/, model/, service/ folders)
- 3-layer separation - Models (data), Routes (HTTP), Services (business logic)
- Heavy ORM usage - Bookshelf.js for models with Knex for migrations
- Joi validation - Schema-based request validation
- 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 renderinghapi-pino- Loggingpg- PostgreSQL driver
Layer Responsibilities
1. Models (Data Layer)
Bookshelf models defined database schema and relationships.
Key Patterns:
- Factory function accepting
bookshelfinstance - 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
upanddown
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.