The Hapi adapter provides plugin-based integration of go-go-scope with Hapi’s extension system.
Installation
npm install @go-go-scope/adapter-hapi go-go-scope @hapi/hapi
Quick Start
import Hapi from '@hapi/hapi'
import { hapiGoGoScope } from '@go-go-scope/adapter-hapi'
const server = Hapi.server({
port: 3000,
host: 'localhost'
})
// Register plugin
await server.register({
plugin: hapiGoGoScope,
options: {
name: 'hapi-api',
timeout: 30000 // Optional: default timeout for all requests
}
})
server.route({
method: 'GET',
path: '/users/{id}',
handler: async (request) => {
const [err, user] = await request.scope.task(
() => fetchUser(request.params.id),
{ retry: 'exponential', timeout: 5000 }
)
if (err) {
return { error: err.message }
}
return user
}
})
await server.start()
console.log('Server running on %s', server.info.uri)
Configuration Options
name
string
default:"'hapi-app'"
Name for the root application scope
Default timeout in milliseconds for all request scopes
Plugin Architecture
Root Scope Creation
The plugin creates a root scope and attaches it to server.rootScope
Request Decoration
Decorates the request object with scope and rootScope properties
Extension Points
Uses onRequest to create request scopes and onPostResponse for cleanup
Server Lifecycle
Disposes the root scope in the onPostStop extension
Helper Functions
getScope(request)
Retrieves the request-scoped scope from the Hapi request:
import { getScope } from '@go-go-scope/adapter-hapi'
server.route({
method: 'GET',
path: '/data',
handler: async (request) => {
const scope = getScope(request)
const [err, data] = await scope.task(() => fetchData())
return data
}
})
getRootScope(server)
Retrieves the root application scope from the Hapi server:
import { getRootScope } from '@go-go-scope/adapter-hapi'
const rootScope = getRootScope(server)
await rootScope.task(() => startBackgroundJob())
closeHapiScope(server)
Gracefully disposes the root scope:
import { closeHapiScope } from '@go-go-scope/adapter-hapi'
process.on('SIGTERM', async () => {
await closeHapiScope(server)
await server.stop()
})
Usage Examples
REST API Routes
import Hapi from '@hapi/hapi'
import { hapiGoGoScope } from '@go-go-scope/adapter-hapi'
const server = Hapi.server({ port: 3000 })
await server.register({ plugin: hapiGoGoScope })
server.route({
method: 'GET',
path: '/posts/{id}',
handler: async (request, h) => {
const [err, post] = await request.scope.task(
() => db.posts.findById(request.params.id)
)
if (err) {
return h.response({ error: 'Post not found' }).code(404)
}
return post
}
})
Parallel Operations
server.route({
method: 'GET',
path: '/dashboard/{userId}',
handler: async (request) => {
const [err, results] = await request.scope.parallel([
() => fetchProfile(request.params.userId),
() => fetchPosts(request.params.userId),
() => fetchFollowers(request.params.userId)
])
if (err) {
return { error: 'Failed to load dashboard' }
}
return {
profile: results[0],
posts: results[1],
followers: results[2]
}
}
})
Server-Sent Events
import Hapi from '@hapi/hapi'
import { hapiGoGoScope } from '@go-go-scope/adapter-hapi'
const server = Hapi.server({ port: 3000 })
await server.register({ plugin: hapiGoGoScope })
server.route({
method: 'GET',
path: '/events',
handler: async (request, h) => {
const channel = request.scope.channel({ buffer: 100 })
// Producer
request.scope.task(async ({ signal }) => {
for (let i = 0; i < 50; i++) {
if (signal.aborted) break
await channel.send({ event: 'update', data: i })
await new Promise(r => setTimeout(r, 1000))
}
channel.close()
})
// Stream to response
const stream = new ReadableStream({
async start(controller) {
for await (const message of channel) {
controller.enqueue(
`data: ${JSON.stringify(message)}\n\n`
)
}
controller.close()
}
})
return h.response(stream)
.type('text/event-stream')
.header('Cache-Control', 'no-cache')
.header('Connection', 'keep-alive')
}
})
Circuit Breaker
import { CircuitBreaker } from 'go-go-scope'
import { hapiGoGoScope } from '@go-go-scope/adapter-hapi'
const breaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000
})
const server = Hapi.server({ port: 3000 })
await server.register({ plugin: hapiGoGoScope })
server.route({
method: 'GET',
path: '/external',
handler: async (request, h) => {
const [err, response] = await request.scope.task(
async ({ signal }) => {
return breaker.execute(() =>
fetch('https://external-api.com/data', { signal })
)
}
)
if (err) {
if (breaker.state === 'open') {
return h.response({ error: 'Service unavailable' }).code(503)
}
return h.response({ error: err.message }).code(500)
}
return await response.json()
}
})
Plugin Composition
import Hapi from '@hapi/hapi'
import Inert from '@hapi/inert'
import Vision from '@hapi/vision'
import { hapiGoGoScope } from '@go-go-scope/adapter-hapi'
const server = Hapi.server({ port: 3000 })
// Register multiple plugins
await server.register([
{ plugin: Inert },
{ plugin: Vision },
{
plugin: hapiGoGoScope,
options: { name: 'my-api', timeout: 15000 }
}
])
server.route({
method: 'GET',
path: '/data',
handler: async (request) => {
const [err, data] = await request.scope.task(() => fetchData())
if (err) return { error: err.message }
return data
}
})
Background Jobs
import { hapiGoGoScope, getRootScope } from '@go-go-scope/adapter-hapi'
const server = Hapi.server({ port: 3000 })
await server.register({ plugin: hapiGoGoScope })
// Start background job using root scope
const rootScope = getRootScope(server)
rootScope.task(async ({ signal }) => {
while (!signal.aborted) {
await processQueue()
await new Promise(r => setTimeout(r, 5000))
}
})
await server.start()
Request Lifecycle Extensions
import Hapi from '@hapi/hapi'
import { hapiGoGoScope } from '@go-go-scope/adapter-hapi'
const server = Hapi.server({ port: 3000 })
await server.register({ plugin: hapiGoGoScope })
// Add custom extension after scope creation
server.ext('onPreHandler', (request, h) => {
// request.scope is available here
console.log('Request scope:', request.scope)
return h.continue
})
server.route({
method: 'GET',
path: '/test',
handler: async (request) => {
return { message: 'Scope available in extensions' }
}
})
Type Augmentation
The adapter augments Hapi’s types:
declare module '@hapi/hapi' {
interface Request {
scope: Scope<Record<string, unknown>>
rootScope: Scope<Record<string, unknown>>
}
interface Server {
rootScope: Scope<Record<string, unknown>>
}
}
This provides full type safety:
server.route({
method: 'GET',
path: '/typed',
handler: async (request) => {
// TypeScript knows request.scope is Scope
const [err, data] = await request.scope.task(() => fetchData())
return data
}
})
Extension Points
The plugin integrates with Hapi’s extension system:
// Request scope is created in onRequest
server.ext('onRequest', (request, h) => {
request.scope = scope({
parent: rootScope,
name: `request-${request.id}`
})
return h.continue
})
Graceful Shutdown
import { closeHapiScope } from '@go-go-scope/adapter-hapi'
const shutdown = async () => {
console.log('Shutting down gracefully...')
// Dispose root scope (cancels all tasks)
await closeHapiScope(server)
// Stop Hapi server
await server.stop({ timeout: 10000 })
console.log('Server stopped')
process.exit(0)
}
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
Best Practices
Register the plugin before defining routes to ensure scopes are available in all handlers.
Use request scope for request-specific tasks
Always use request.scope.task() for operations tied to a single request.
Use root scope for background jobs
Access server.rootScope for application-level tasks that outlive requests.
Implement graceful shutdown
Always call closeHapiScope(server) before stopping the server.
Fastify Adapter
Fastify plugin integration
Koa Adapter
Koa middleware integration
Express Adapter
Express middleware integration
Core API
Core go-go-scope concepts