Skip to main content
The Express adapter provides middleware-based integration of go-go-scope with Express applications.

Installation

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

Quick Start

import express from 'express'
import { goGoScope } from '@go-go-scope/adapter-express'

const app = express()

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

app.get('/users/:id', async (req, res) => {
  // Access request-scoped scope
  const [err, user] = await req.scope.task(
    () => fetchUser(req.params.id),
    { retry: 'exponential', timeout: 5000 }
  )

  if (err) {
    return res.status(500).json({ error: err.message })
  }
  res.json(user)
})

const server = app.listen(3000)

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

Configuration Options

name
string
default:"'express-app'"
Name for the root application scope
timeout
number
Default timeout in milliseconds for all request scopes

Middleware Architecture

1

Root Scope Creation

The middleware creates a single root scope, accessible via app.scope
2

Request Scope Creation

For each request, a child scope is created with the root as parent, accessible via req.scope
3

Response Cleanup

Request scopes are disposed on both finish and close events to handle streaming responses
4

Manual Shutdown

Call closeScope(app) during graceful shutdown to dispose the root scope

Usage Examples

Basic CRUD Operations

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

  if (err) {
    return res.status(404).json({ error: 'Post not found' })
  }
  res.json(post)
})

Parallel Operations

app.get('/aggregated-data', async (req, res) => {
  const userId = req.query.userId

  const [err, results] = await req.scope.parallel([
    () => fetchUserProfile(userId),
    () => fetchUserPosts(userId),
    () => fetchUserFollowers(userId)
  ])

  if (err) {
    return res.status(500).json({ error: err.message })
  }

  res.json({
    profile: results[0],
    posts: results[1],
    followers: results[2]
  })
})

Streaming Responses

app.get('/stream-logs', async (req, res) => {
  const channel = req.scope.channel({ buffer: 50 })

  // Producer: tail log file
  req.scope.task(async ({ signal }) => {
    for await (const line of tailLogFile(signal)) {
      await channel.send(line)
    }
    channel.close()
  })

  res.setHeader('Content-Type', 'text/plain')
  res.setHeader('Transfer-Encoding', 'chunked')

  // Consumer: stream to response
  for await (const line of channel) {
    res.write(line + '\n')
  }
  
  res.end()
})

Error Handling Middleware

import { goGoScope, closeScope } from '@go-go-scope/adapter-express'

app.use(goGoScope(app, { name: 'my-api' }))

// Custom error handler
app.use((err, req, res, next) => {
  // Scope errors are automatically propagated
  if (err.name === 'TimeoutError') {
    return res.status(504).json({ error: 'Request timeout' })
  }
  if (err.name === 'AbortError') {
    return res.status(499).json({ error: 'Request cancelled' })
  }
  res.status(500).json({ error: 'Internal server error' })
})

Background Tasks

const app = express()
app.use(goGoScope(app, { name: 'worker-api' }))

// Start background job using root scope
app.scope.task(async ({ signal }) => {
  while (!signal.aborted) {
    await processQueue()
    await new Promise(r => setTimeout(r, 1000))
  }
})

app.listen(3000)

Graceful Shutdown

The adapter provides a closeScope helper for graceful shutdown:
import { closeScope } from '@go-go-scope/adapter-express'

const server = app.listen(3000)

process.on('SIGTERM', async () => {
  console.log('Shutting down gracefully...')
  
  // Dispose root scope (cancels all tasks)
  await closeScope(app)
  
  // Close HTTP server
  server.close(() => {
    console.log('Server closed')
    process.exit(0)
  })
})

Type Augmentation

The adapter automatically augments Express types:
declare global {
  namespace Express {
    interface Request {
      scope: Scope
    }
    interface Application {
      scope: Scope
    }
  }
}

Request ID Generation

Each request scope has a unique name:
// Format: request-{timestamp}-{random}
const scopeName = `request-${Date.now()}-${Math.random().toString(36).slice(2)}`
For custom request IDs, use a request ID middleware before the scope middleware:
import { v4 as uuidv4 } from 'uuid'

app.use((req, res, next) => {
  req.id = uuidv4()
  next()
})

app.use(goGoScope(app))

Response Lifecycle

The middleware handles both finish and close events:
// Normal response completion
res.on('finish', () => {
  req.scope[Symbol.asyncDispose]().catch(() => {})
})

Best Practices

Register the go-go-scope middleware before route handlers to ensure scopes are available.
Check error values in result tuples and return appropriate HTTP status codes.
Leverage channels for streaming responses to handle backpressure automatically.
Always call closeScope(app) before closing the HTTP server.

Fastify Adapter

Plugin-based Fastify integration

Koa Adapter

Koa middleware integration

NestJS Adapter

Dependency injection for NestJS

Core API

Core go-go-scope concepts

Build docs developers (and LLMs) love