Skip to main content
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
timeout
number
Default timeout in milliseconds for all request scopes

Plugin Architecture

1

Root Scope Creation

The plugin creates a root scope and attaches it to server.rootScope
2

Request Decoration

Decorates the request object with scope and rootScope properties
3

Extension Points

Uses onRequest to create request scopes and onPostResponse for cleanup
4

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.
Always use request.scope.task() for operations tied to a single request.
Access server.rootScope for application-level tasks that outlive requests.
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

Build docs developers (and LLMs) love