Skip to main content
The Koa adapter provides middleware-based integration of go-go-scope with Koa’s async/await-first architecture.

Installation

npm install @go-go-scope/adapter-koa go-go-scope koa

Quick Start

import Koa from 'koa'
import { koaGoGoScope } from '@go-go-scope/adapter-koa'

const app = new Koa()

// Apply middleware
app.use(koaGoGoScope({
  name: 'koa-api',
  timeout: 30000 // Optional: default timeout for all requests
}))

app.use(async (ctx) => {
  const scope = ctx.state.scope
  
  const [err, user] = await scope.task(
    () => fetchUser(ctx.params.id),
    { retry: 'exponential', timeout: 5000 }
  )

  if (err) {
    ctx.status = 500
    ctx.body = { error: err.message }
    return
  }
  ctx.body = user
})

const server = app.listen(3000)

// Graceful shutdown
process.on('SIGTERM', async () => {
  await closeKoaScope()
  server.close()
})

Configuration Options

name
string
default:"'koa-app'"
Name for the root application scope
timeout
number
Default timeout in milliseconds for all request scopes
onError
(error: Error, ctx: Context) => void
Custom error handler for scope disposal errors

Middleware Architecture

1

Root Scope Creation

A singleton root scope is created on first middleware invocation
2

State Injection

Request scope is stored in ctx.state.scope and root in ctx.state.rootScope
3

Request Processing

Each request gets a unique child scope from the root
4

Cleanup in Finally

Request scopes are disposed in a finally block after middleware chain completes

Helper Functions

getScope(ctx)

Retrieves the request-scoped scope from the Koa context:
import { getScope } from '@go-go-scope/adapter-koa'

app.use(async (ctx) => {
  const scope = getScope(ctx)
  const [err, data] = await scope.task(() => fetchData())
  ctx.body = data
})

getRootScope(ctx)

Retrieves the root application scope:
import { getRootScope } from '@go-go-scope/adapter-koa'

app.use(async (ctx) => {
  const rootScope = getRootScope(ctx)
  // Access application-level scope
  ctx.body = { status: 'ok' }
})

closeKoaScope()

Gracefully disposes the root scope:
import { closeKoaScope } from '@go-go-scope/adapter-koa'

process.on('SIGTERM', async () => {
  await closeKoaScope()
  server.close()
})

Usage Examples

REST API with Router

import Koa from 'koa'
import Router from '@koa/router'
import { koaGoGoScope } from '@go-go-scope/adapter-koa'

const app = new Koa()
const router = new Router()

app.use(koaGoGoScope())

router.get('/posts/:id', async (ctx) => {
  const scope = ctx.state.scope
  const [err, post] = await scope.task(
    () => db.posts.findById(ctx.params.id)
  )

  if (err) {
    ctx.status = 404
    ctx.body = { error: 'Post not found' }
    return
  }
  ctx.body = post
})

app.use(router.routes())
app.listen(3000)

Parallel Data Fetching

import Koa from 'koa'
import { koaGoGoScope } from '@go-go-scope/adapter-koa'

const app = new Koa()
app.use(koaGoGoScope())

app.use(async (ctx) => {
  if (ctx.path !== '/dashboard') return
  
  const scope = ctx.state.scope
  const userId = ctx.query.userId

  const [err, results] = await scope.parallel([
    () => fetchProfile(userId),
    () => fetchPosts(userId),
    () => fetchFollowers(userId)
  ])

  if (err) {
    ctx.status = 500
    ctx.body = { error: 'Failed to load dashboard' }
    return
  }

  ctx.body = {
    profile: results[0],
    posts: results[1],
    followers: results[2]
  }
})

Custom Error Handler

import Koa from 'koa'
import { koaGoGoScope } from '@go-go-scope/adapter-koa'

const app = new Koa()

app.use(koaGoGoScope({
  name: 'error-aware-api',
  onError: (error, ctx) => {
    console.error('Scope error:', error.message, 'Path:', ctx.path)
    // Could send to error tracking service
  }
}))

app.use(async (ctx) => {
  const scope = ctx.state.scope
  const [err, data] = await scope.task(
    () => fetchData(),
    { timeout: 5000 }
  )

  if (err) {
    ctx.status = 500
    ctx.body = { error: err.message }
    return
  }
  
  ctx.body = data
})

Streaming Response

import Koa from 'koa'
import { koaGoGoScope } from '@go-go-scope/adapter-koa'
import { PassThrough } from 'stream'

const app = new Koa()
app.use(koaGoGoScope())

app.use(async (ctx) => {
  if (ctx.path !== '/stream-logs') return
  
  const scope = ctx.state.scope
  const channel = scope.channel({ buffer: 100 })

  // Producer
  scope.task(async ({ signal }) => {
    for (let i = 0; i < 100; i++) {
      if (signal.aborted) break
      await channel.send(`Log line ${i}\n`)
      await new Promise(r => setTimeout(r, 100))
    }
    channel.close()
  })

  // Stream to response
  ctx.type = 'text/plain'
  const stream = new PassThrough()
  ctx.body = stream

  for await (const line of channel) {
    stream.write(line)
  }
  stream.end()
})

Middleware Composition

import Koa from 'koa'
import bodyParser from 'koa-bodyparser'
import cors from '@koa/cors'
import { koaGoGoScope } from '@go-go-scope/adapter-koa'

const app = new Koa()

// Apply middleware in order
app.use(cors())
app.use(bodyParser())
app.use(koaGoGoScope({ name: 'api' }))

app.use(async (ctx) => {
  // Scope is available after koaGoGoScope middleware
  const scope = ctx.state.scope
  const [err, data] = await scope.task(() => fetchData())
  
  ctx.body = data
})

Circuit Breaker

import Koa from 'koa'
import { koaGoGoScope } from '@go-go-scope/adapter-koa'
import { CircuitBreaker } from 'go-go-scope'

const breaker = new CircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30000
})

const app = new Koa()
app.use(koaGoGoScope())

app.use(async (ctx) => {
  if (ctx.path !== '/external') return
  
  const scope = ctx.state.scope
  
  const [err, response] = await scope.task(async ({ signal }) => {
    return breaker.execute(() =>
      fetch('https://external-api.com/data', { signal })
    )
  })

  if (err) {
    if (breaker.state === 'open') {
      ctx.status = 503
      ctx.body = { error: 'Service unavailable' }
      return
    }
    ctx.status = 500
    ctx.body = { error: err.message }
    return
  }

  ctx.body = await response.json()
})

Type Augmentation

The adapter augments Koa’s DefaultState interface:
declare module 'koa' {
  interface DefaultState {
    scope: Scope<Record<string, unknown>>
    rootScope: Scope<Record<string, unknown>>
  }
}
This provides full type safety:
app.use(async (ctx) => {
  // TypeScript knows ctx.state.scope is Scope
  const scope = ctx.state.scope
  const [err, data] = await scope.task(() => fetchData())
  ctx.body = data
})

Error Handling

The middleware handles errors during scope disposal:
finally {
  await requestScope[Symbol.asyncDispose]().catch((err) => {
    if (onError) {
      onError(err as Error, ctx)
    }
  })
}

Graceful Shutdown

import { closeKoaScope } from '@go-go-scope/adapter-koa'

const server = app.listen(3000)

const shutdown = async () => {
  console.log('Shutting down gracefully...')
  
  // Dispose root scope (cancels all tasks)
  await closeKoaScope()
  
  // Close HTTP server
  server.close(() => {
    console.log('Server closed')
    process.exit(0)
  })
}

process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)

Best Practices

Always access scopes via ctx.state.scope or use the helper function getScope(ctx).
Register koaGoGoScope() before route handlers to ensure scopes are available.
Set appropriate HTTP status codes when tasks fail.
Always call closeKoaScope() before closing the HTTP server.

Express Adapter

Express middleware integration

Hapi Adapter

Hapi plugin integration

Fastify Adapter

Fastify plugin integration

Core API

Core go-go-scope concepts

Build docs developers (and LLMs) love