DEV Community

Egeo Minotti
Egeo Minotti

Posted on

flashQ + Elysia & Hono.js: Background Jobs for Modern Bun Apps

Elysia and Hono.js are two of the fastest TypeScript frameworks out there. Combine them with flashQ and you get a stack capable of handling millions of background jobs with sub-millisecond API response times.

Why This Stack?

Elysia Hono.js flashQ
Performance ~2.5M req/sec ~1.5M req/sec ~1.9M jobs/sec
Runtime Bun-native Multi-runtime Rust-powered
Type Safety End-to-end Full TypeScript Typed SDK

Your API responds instantly. Heavy work happens in the background.


Elysia Integration

Setup

bun create elysia flashq-elysia
cd flashq-elysia
bun add flashq
Enter fullscreen mode Exit fullscreen mode

Queue Configuration

// src/queue.ts
import { FlashQ } from 'flashq';

let client: FlashQ | null = null;

export async function getClient(): Promise<FlashQ> {
  if (!client) {
    client = new FlashQ({
      host: process.env.FLASHQ_HOST || 'localhost',
      port: parseInt(process.env.FLASHQ_PORT || '6789'),
      token: process.env.FLASHQ_TOKEN,
    });
    await client.connect();
  }
  return client;
}

export const QUEUES = {
  EMAIL: 'email',
  AI_PROCESSING: 'ai-processing',
} as const;
Enter fullscreen mode Exit fullscreen mode

Elysia Plugin

// src/plugins/flashq.ts
import { Elysia } from 'elysia';
import { getClient, QUEUES } from '../queue';

export const flashqPlugin = new Elysia({ name: 'flashq' })
  .decorate('queue', {
    async push<T>(queue: string, data: T, options?: any) {
      const client = await getClient();
      return client.push(queue, data, options);
    },
    async getJob(jobId: string) {
      const client = await getClient();
      return client.getJob(jobId);
    },
    async waitForResult(jobId: string, timeout = 30000) {
      const client = await getClient();
      return client.finished(jobId, timeout);
    },
    QUEUES,
  });
Enter fullscreen mode Exit fullscreen mode

API Routes

// src/index.ts
import { Elysia, t } from 'elysia';
import { flashqPlugin } from './plugins/flashq';

const app = new Elysia()
  .use(flashqPlugin)

  .post('/api/email', async ({ body, queue }) => {
    const job = await queue.push(queue.QUEUES.EMAIL, body, {
      attempts: 5,
      backoff: 5000,
    });
    return { success: true, jobId: job.id };
  }, {
    body: t.Object({
      to: t.String({ format: 'email' }),
      subject: t.String({ minLength: 1 }),
      template: t.String(),
      data: t.Record(t.String(), t.Any()),
    }),
  })

  .post('/api/generate', async ({ body, query, queue }) => {
    const job = await queue.push(queue.QUEUES.AI_PROCESSING, body, {
      priority: body.priority || 5,
      timeout: 120000,
    });

    // Sync mode: wait for result
    if (query.sync === 'true') {
      const result = await queue.waitForResult(job.id, 60000);
      return { success: true, result };
    }

    return { success: true, jobId: job.id };
  }, {
    body: t.Object({
      prompt: t.String({ minLength: 1 }),
      model: t.Optional(t.Union([
        t.Literal('gpt-4'),
        t.Literal('claude-3'),
      ])),
      userId: t.String(),
    }),
  })

  .get('/api/jobs/:id', async ({ params, queue }) => {
    const job = await queue.getJob(params.id);
    return job || { error: 'Job not found' };
  })

  .listen(3000);
Enter fullscreen mode Exit fullscreen mode

Hono.js Integration

Setup

bun create hono@latest flashq-hono
cd flashq-hono
bun add flashq zod @hono/zod-validator
Enter fullscreen mode Exit fullscreen mode

Queue Middleware

// src/middleware/queue.ts
import { createMiddleware } from 'hono/factory';
import { FlashQ } from 'flashq';

let client: FlashQ | null = null;

async function getClient(): Promise<FlashQ> {
  if (!client) {
    client = new FlashQ({
      host: process.env.FLASHQ_HOST || 'localhost',
      port: parseInt(process.env.FLASHQ_PORT || '6789'),
    });
    await client.connect();
  }
  return client;
}

export const QUEUES = { EMAIL: 'email', AI: 'ai-processing' } as const;

export const queueMiddleware = createMiddleware(async (c, next) => {
  const flashq = await getClient();
  c.set('queue', {
    push: (name, data, options) => flashq.push(name, data, options),
    getJob: (id) => flashq.getJob(id),
    finished: (id, timeout) => flashq.finished(id, timeout),
  });
  await next();
});
Enter fullscreen mode Exit fullscreen mode

Routes

// src/index.ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { queueMiddleware, QUEUES } from './middleware/queue';

const app = new Hono();
app.use('/api/*', queueMiddleware);

const emailSchema = z.object({
  to: z.string().email(),
  subject: z.string().min(1),
  template: z.string(),
  data: z.record(z.any()),
});

app.post('/api/email', zValidator('json', emailSchema), async (c) => {
  const body = c.req.valid('json');
  const queue = c.get('queue');

  const job = await queue.push(QUEUES.EMAIL, body, {
    attempts: 5,
    backoff: 5000,
  });

  return c.json({ success: true, jobId: job.id });
});

app.get('/api/jobs/:id', async (c) => {
  const queue = c.get('queue');
  const job = await queue.getJob(c.req.param('id'));
  return job ? c.json(job) : c.json({ error: 'Not found' }, 404);
});

export default { port: 3000, fetch: app.fetch };
Enter fullscreen mode Exit fullscreen mode

Worker (Shared)

Works with both Elysia and Hono:

// worker/index.ts
import { Worker } from 'flashq';
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

const emailWorker = new Worker('email', async (job) => {
  const { to, subject, template, data } = job.data;
  const html = renderTemplate(template, data);

  const result = await resend.emails.send({
    from: 'noreply@yourdomain.com',
    to,
    subject,
    html,
  });

  return { emailId: result.id };
}, {
  connection: {
    host: process.env.FLASHQ_HOST || 'localhost',
    port: parseInt(process.env.FLASHQ_PORT || '6789'),
  },
  concurrency: 10,
});

emailWorker.on('completed', (job) => {
  console.log(`✓ Job ${job.id} completed`);
});

emailWorker.on('failed', (job, error) => {
  console.error(`✗ Job ${job.id} failed: ${error.message}`);
});
Enter fullscreen mode Exit fullscreen mode

Advanced: Job Workflows

Chain jobs with dependencies:

app.post('/api/pipeline', async (c) => {
  const client = await getClient();

  // Step 1: Extract
  const extractJob = await client.push('extract', { documentUrl: body.url });

  // Step 2: Summarize (waits for extract)
  const summarizeJob = await client.push('summarize', {
    sourceJobId: extractJob.id,
  }, {
    depends_on: [extractJob.id],
  });

  // Step 3: Embed (waits for extract)
  const embedJob = await client.push('embed', {
    sourceJobId: extractJob.id,
  }, {
    depends_on: [extractJob.id],
  });

  return c.json({
    jobs: {
      extract: extractJob.id,
      summarize: summarizeJob.id,
      embed: embedJob.id,
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

Docker Compose

version: '3.8'

services:
  flashq:
    image: ghcr.io/egeominotti/flashq:latest
    ports:
      - "6789:6789"
    environment:
      - DATABASE_URL=postgres://flashq:flashq@postgres:5432/flashq
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=flashq
      - POSTGRES_PASSWORD=flashq
      - POSTGRES_DB=flashq

  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - FLASHQ_HOST=flashq
      - FLASHQ_PORT=6789
    depends_on:
      - flashq

  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    environment:
      - FLASHQ_HOST=flashq
    depends_on:
      - flashq
    deploy:
      replicas: 3
Enter fullscreen mode Exit fullscreen mode

TL;DR

  • Elysia: Bun-native, end-to-end type safety with t schemas
  • Hono.js: Multi-runtime (Bun, Node, Cloudflare Workers), Zod validation
  • flashQ: 1.9M jobs/sec, BullMQ-compatible API, Rust-powered

Both frameworks integrate seamlessly. Pick Elysia for pure Bun projects, Hono for portability.


Links:

Top comments (0)