# Hono and Elysia: Bun Framework Integration Guide

Integrate bunqueue with Hono and Elysia frameworks. Queue jobs from HTTP handlers, middleware patterns, and graceful shutdown in Bun apps.

Canonical: https://bunqueue.dev/blog/framework-integrations/

---

import { Aside } from '@astrojs/starlight/components';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">blog · integrations</span>
  <h1 class="bq-hero-h1 bq-bench-h1">Hono and Elysia, queued <em>cleanly.</em></h1>
  <p class="bq-hero-sub">Hono and Elysia are the most popular web frameworks for Bun, and bunqueue is built for the same ecosystem. Queue jobs from HTTP handlers, wire up middleware, and shut down gracefully.</p>
</div>

## Hono Integration

### Basic Setup

```typescript
import { Hono } from 'hono';
import { Queue, Worker } from 'bunqueue/client';

// Create queue and worker
const emailQueue = new Queue<{ to: string; subject: string }>('emails', {
  embedded: true,
});

const worker = new Worker('emails', async (job) => {
  await sendEmail(job.data.to, job.data.subject);
  return { sent: true };
}, { embedded: true, concurrency: 5 });

// Create Hono app
const app = new Hono();

app.post('/api/send-email', async (c) => {
  const { to, subject } = await c.req.json();

  const job = await emailQueue.add('send', { to, subject });

  return c.json({ jobId: job.id, status: 'queued' });
});

app.get('/api/job/:id', async (c) => {
  const job = await emailQueue.getJob(c.req.param('id'));

  if (!job) return c.json({ error: 'Not found' }, 404);

  return c.json({
    id: job.id,
    state: await job.getState(),
    progress: job.progress,
    data: job.data,
  });
});

export default app;
```

### Middleware Pattern

Create a reusable middleware that injects the queue into the context:

```typescript
import { createMiddleware } from 'hono/factory';
import { Queue } from 'bunqueue/client';

// Queue factory middleware
export function withQueue<T>(name: string) {
  const queue = new Queue<T>(name, { embedded: true });

  return createMiddleware(async (c, next) => {
    c.set('queue', queue);
    await next();
  });
}

// Usage
const app = new Hono();

app.use('/api/orders/*', withQueue<OrderData>('orders'));

app.post('/api/orders', async (c) => {
  const queue = c.get('queue') as Queue<OrderData>;
  const order = await c.req.json();

  const job = await queue.add('process', order, {
    priority: order.express ? 10 : 1,
    attempts: 3,
  });

  return c.json({ orderId: job.id });
});
```

## Elysia Integration

### Basic Setup

```typescript
import { Elysia } from 'elysia';
import { Queue, Worker } from 'bunqueue/client';

const taskQueue = new Queue<{ type: string; payload: unknown }>('tasks', {
  embedded: true,
});

const worker = new Worker('tasks', async (job) => {
  switch (job.data.type) {
    case 'resize-image':
      return await resizeImage(job.data.payload);
    case 'generate-pdf':
      return await generatePdf(job.data.payload);
    default:
      throw new Error(`Unknown task type: ${job.data.type}`);
  }
}, { embedded: true, concurrency: 3 });

const app = new Elysia()
  .post('/tasks', async ({ body }) => {
    const job = await taskQueue.add(body.type, body);
    return { jobId: job.id, status: 'queued' };
  })
  .get('/tasks/:id', async ({ params }) => {
    const job = await taskQueue.getJob(params.id);
    if (!job) return { error: 'Not found' };
    return {
      id: job.id,
      state: await job.getState(),
      progress: job.progress,
    };
  })
  .get('/tasks/counts', async () => {
    return await taskQueue.getJobCountsAsync();
  })
  .listen(3000);
```

### Plugin Pattern

Encapsulate the queue as an Elysia plugin:

```typescript
import { Elysia } from 'elysia';
import { Queue, Worker } from 'bunqueue/client';

export function queuePlugin<T>(name: string) {
  const queue = new Queue<T>(name, { embedded: true });

  return new Elysia({ name: `queue-${name}` })
    .decorate('queue', queue)
    .get(`/queues/${name}/counts`, async ({ queue }) => {
      return await queue.getJobCountsAsync();
    })
    .get(`/queues/${name}/health`, async ({ queue }) => {
      const counts = await queue.getJobCountsAsync();
      return {
        name,
        ...counts,
        healthy: counts.active < 1000,
      };
    });
}

// Usage
const app = new Elysia()
  .use(queuePlugin<EmailData>('emails'))
  .use(queuePlugin<OrderData>('orders'))
  .listen(3000);
```

## Graceful Shutdown

Both frameworks need proper shutdown handling to ensure jobs are completed:

```typescript
const queue = new Queue('tasks', { embedded: true });
const worker = new Worker('tasks', processor, {
  embedded: true,
  concurrency: 5,
});

// Handle shutdown signals
async function gracefulShutdown() {
  console.log('Shutting down...');

  // 1. Stop accepting new HTTP requests
  server.stop();

  // 2. Close worker (finishes active jobs)
  await worker.close();

  // 3. Disconnect queue (flushes buffers)
  await queue.disconnect();

  process.exit(0);
}

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
```

<Aside type="tip">
  Always close the worker before the queue. The worker needs an active queue connection to send final ACK messages for completed jobs.
</Aside>

## TCP Mode for Microservices

If your web app and workers run as separate processes, use TCP mode:

```typescript
// Web server (process 1)
const queue = new Queue('tasks', {
  connection: { host: 'bunqueue-server', port: 6789 },
});

// Worker (process 2)
const worker = new Worker('tasks', processor, {
  connection: { host: 'bunqueue-server', port: 6789 },
  concurrency: 10,
});
```

This separates concerns: the web server only adds jobs, and dedicated worker processes handle processing. bunqueue's connection pooling and auto-batching make this efficient even under high load.

## Queue Groups for Multi-Queue Apps

Organize related queues with `QueueGroup`:

```typescript
import { QueueGroup } from 'bunqueue/client';

const billing = new QueueGroup('billing');

const invoices = billing.getQueue<InvoiceData>('invoices', { embedded: true });
const payments = billing.getQueue<PaymentData>('payments', { embedded: true });
const refunds = billing.getQueue<RefundData>('refunds', { embedded: true });

// Queue names are prefixed: "billing:invoices", "billing:payments", etc.

// Pause all billing queues at once (e.g., during maintenance).
// Group-wide control (pauseAll/resumeAll/drainAll) operates on the
// embedded engine, so it applies to embedded-mode queues.
billing.pauseAll();
billing.resumeAll();
```