← Back to DevBytes

Elysia from Beginner to Expert: A Learning Path

Introduction: What is Elysia?

Elysia is a high-performance HTTP framework built on top of Bun, designed with a developer-first philosophy. It leverages Bun's native speed and provides a declarative, chainable API that feels familiar to developers coming from Express or Fastify, yet introduces powerful concepts like automatic type generation, strict validation, and built-in support for OpenAPI documentation through its Eden companion library.

Unlike traditional Node.js frameworks, Elysia is built from the ground up for the Bun runtime. This means it takes full advantage of Bun's blazing-fast HTTP server, native TypeScript execution, and built-in utilities. The result is a framework capable of handling over 100,000 requests per second on modest hardware, while maintaining a clean and expressive developer experience.

The core philosophy of Elysia revolves around three principles: simplicity (the API should be intuitive), performance (every microsecond counts), and type safety (your routes should generate their own documentation and client libraries automatically). This tutorial will guide you from your first "Hello World" endpoint all the way to building complex, production-grade systems with custom plugins, advanced middleware, and the Eden full-stack integration.

Chapter 1: Getting Started — The Beginner Phase

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

1.1 Prerequisites and Installation

Before writing any Elysia code, you need Bun installed on your system. Bun serves as both the runtime and the package manager. Install it via the official command:

curl -fsSL https://bun.sh/install | bash

Once Bun is installed, create a new project directory and initialize it. Elysia can be added with a single command using Bun's package manager, which is significantly faster than npm:

bun create elysia my-elysia-app
cd my-elysia-app
bun run dev

Alternatively, if you prefer to set up manually:

bun init
bun add elysia

The scaffolded project gives you a minimal src/index.ts file with a working server. The dev script uses Bun's hot-reloading capabilities, so changes are reflected instantly without manual restarts.

1.2 Your First Route

Open src/index.ts and examine the default structure. An Elysia application begins by creating an instance and defining routes using method-specific functions like .get(), .post(), .put(), and .delete(). Here is the simplest possible server:

import { Elysia } from 'elysia';

const app = new Elysia()
  .get('/', () => 'Hello from Elysia!')
  .listen(3000);

console.log(`Server running at ${app.server?.hostname}:${app.server?.port}`);

The .get() method takes a path string and a handler function. The handler receives a Context object containing request details, but for simple responses you can return a string, object, or even a Response directly. Elysia automatically serializes objects to JSON and sets appropriate headers.

1.3 Understanding the Context Object

The handler function receives more than just a path — it receives a rich Context object. This object contains params for path parameters, query for query string values, body for parsed request bodies, and headers for incoming headers. Here is an example demonstrating parameterized routes and query access:

import { Elysia } from 'elysia';

const app = new Elysia()
  .get('/user/:id', ({ params, query }) => {
    const userId = params.id;
    const includeDetails = query.details === 'true';
    
    const user = {
      id: userId,
      name: 'John Doe',
      email: 'john@example.com'
    };
    
    if (includeDetails) {
      return { ...user, createdAt: '2024-01-15', role: 'admin' };
    }
    
    return user;
  })
  .listen(3000);

Path parameters are extracted from the URL pattern using the colon syntax (:id). The params object is automatically typed — Elysia infers that params.id is a string. Query parameters are accessed via query, and they are also automatically parsed from the URL.

1.4 Handling Different HTTP Methods

A RESTful API requires support for multiple HTTP verbs. Elysia provides chainable methods for each common verb. Here is a complete CRUD outline for a notes resource:

import { Elysia, t } from 'elysia';

// In-memory storage for demonstration
const notes: { id: number; title: string; content: string }[] = [];
let nextId = 1;

const app = new Elysia()
  // GET all notes
  .get('/notes', () => notes)
  
  // GET a single note by ID
  .get('/notes/:id', ({ params, set }) => {
    const note = notes.find(n => n.id === parseInt(params.id));
    if (!note) {
      set.status = 404;
      return { error: 'Note not found' };
    }
    return note;
  })
  
  // POST create a new note
  .post('/notes', ({ body, set }) => {
    const { title, content } = body as { title: string; content: string };
    const newNote = { id: nextId++, title, content };
    notes.push(newNote);
    set.status = 201;
    return newNote;
  })
  
  // PUT update a note
  .put('/notes/:id', ({ params, body, set }) => {
    const note = notes.find(n => n.id === parseInt(params.id));
    if (!note) {
      set.status = 404;
      return { error: 'Note not found' };
    }
    const { title, content } = body as { title: string; content: string };
    note.title = title;
    note.content = content;
    return note;
  })
  
  // DELETE a note
  .delete('/notes/:id', ({ params, set }) => {
    const index = notes.findIndex(n => n.id === parseInt(params.id));
    if (index === -1) {
      set.status = 404;
      return { error: 'Note not found' };
    }
    notes.splice(index, 1);
    set.status = 204;
    return null;
  })
  
  .listen(3000);

Notice the set property on the context. It allows you to modify the response status code, headers, and other response properties before they are sent to the client. This gives you fine-grained control over the HTTP response without needing to construct a Response object manually.

1.5 Grouping Routes for Organization

As your application grows, organizing routes becomes essential. Elysia provides a .group() method that prefixes all routes within the group and allows you to apply middleware or configurations scoped to that group:

import { Elysia } from 'elysia';

const app = new Elysia()
  .group('/api/v1', app => app
    .group('/users', users => users
      .get('/', () => ['user1', 'user2'])
      .get('/:id', ({ params }) => ({ id: params.id }))
    )
    .group('/products', products => products
      .get('/', () => ['product1', 'product2'])
      .get('/:id', ({ params }) => ({ id: params.id }))
    )
  )
  .listen(3000);

The nested groups create clean URL prefixes: /api/v1/users and /api/v1/products. Each group receives its own Elysia instance, allowing you to apply group-specific plugins, guards, or lifecycle hooks. This pattern scales beautifully as your API surface expands.

Chapter 2: Intermediate Concepts — Validation and Type Safety

2.1 Schema Validation with the Built-in Type System

One of Elysia's most powerful features is its built-in runtime validation powered by the @elysiajs/type module (often imported as t). This system allows you to define schemas for request bodies, query parameters, headers, and path parameters, and Elysia will automatically validate incoming requests against these schemas. Invalid requests receive a descriptive error response automatically.

Here is a user registration endpoint with full body validation:

import { Elysia, t } from 'elysia';

const app = new Elysia()
  .post('/register', ({ body }) => {
    // At this point, body is already validated
    // and typed as { username: string; email: string; password: string; age: number }
    return {
      message: `User ${body.username} registered successfully`,
      user: { username: body.username, email: body.email, age: body.age }
    };
  }, {
    body: t.Object({
      username: t.String({ minLength: 3, maxLength: 30 }),
      email: t.String({ format: 'email' }),
      password: t.String({ minLength: 8 }),
      age: t.Number({ minimum: 13, maximum: 120 })
    })
  })
  .listen(3000);

The t.Object, t.String, and t.Number functions create schema definitions that Elysia uses both for runtime validation and for TypeScript type inference. If a client sends a body that doesn't match the schema — for example, an age of 5 or a username shorter than 3 characters — Elysia automatically responds with a 400 status and a detailed error object indicating exactly which fields failed validation.

2.2 Validating Query Parameters and Path Parameters

Validation extends beyond request bodies. You can validate query strings and path parameters with the same schema system, giving you end-to-end type safety across all inputs:

import { Elysia, t } from 'elysia';

const app = new Elysia()
  .get('/search', ({ query }) => {
    // query.q is guaranteed to be a non-empty string
    // query.limit is a number between 1 and 100
    return {
      results: [`Result for: ${query.q}`],
      limit: query.limit
    };
  }, {
    query: t.Object({
      q: t.String({ minLength: 1 }),
      limit: t.Number({ minimum: 1, maximum: 100, default: 10 }),
      offset: t.Optional(t.Number({ minimum: 0, default: 0 }))
    })
  })
  
  .get('/posts/:slug', ({ params }) => {
    return { slug: params.slug, viewCount: 42 };
  }, {
    params: t.Object({
      slug: t.String({ pattern: '^[a-z0-9-]+$' })
    })
  })
  
  .listen(3000);

The t.Optional wrapper marks a field as not required, and the default property supplies a fallback value when the field is missing. The pattern constraint on the slug parameter uses a regular expression to ensure only lowercase alphanumeric characters and hyphens are accepted, preventing path traversal or injection attempts.

2.3 Response Validation and Type Inference

Elysia can also validate responses, ensuring that your server never accidentally leaks fields or sends malformed data to clients. Combined with TypeScript, this creates a fully type-safe contract between your server and its consumers:

import { Elysia, t } from 'elysia';

const app = new Elysia()
  .get('/user/:id', ({ params }) => {
    // Imagine this comes from a database
    const userFromDb = {
      id: parseInt(params.id),
      username: 'alice',
      email: 'alice@example.com',
      internalNotes: 'sensitive data',
      passwordHash: 'abc123def456'
    };
    
    // Only return what the schema allows
    return {
      id: userFromDb.id,
      username: userFromDb.username,
      email: userFromDb.email
    };
  }, {
    params: t.Object({
      id: t.String({ pattern: '^[0-9]+$' })
    }),
    response: t.Object({
      id: t.Number(),
      username: t.String(),
      email: t.String({ format: 'email' })
    })
  })
  .listen(3000);

With response validation enabled, if your handler accidentally returns extra fields or fields of the wrong type, Elysia will catch this during development and either strip the extra fields or throw an error (depending on configuration). This guarantees that your API contract remains precise.

2.4 Error Handling and Custom Error Responses

Elysia provides structured error handling. The .onError() hook allows you to intercept errors globally or per-group and return custom error responses:

import { Elysia } from 'elysia';

const app = new Elysia()
  .onError(({ code, error, set }) => {
    // Log the error internally
    console.error(`[${code}] ${error.message}`);
    
    if (code === 'NOT_FOUND') {
      set.status = 404;
      return {
        error: 'Resource not found',
        path: set.headers.get('x-request-path') || 'unknown'
      };
    }
    
    if (code === 'VALIDATION') {
      set.status = 400;
      return {
        error: 'Validation failed',
        details: error.message
      };
    }
    
    // Default for unhandled errors
    set.status = 500;
    return {
      error: 'Internal server error',
      reference: crypto.randomUUID()
    };
  })
  
  .get('/example', () => {
    throw new Error('Something went wrong!');
  })
  
  .listen(3000);

The code parameter is a string identifier like 'NOT_FOUND', 'VALIDATION', or 'UNKNOWN'. By matching on these codes, you can provide user-friendly error responses while maintaining detailed internal logging. The set object allows you to override the status code even within the error handler.

Chapter 3: Advanced Patterns — Middleware, Hooks, and Plugins

3.1 Understanding Lifecycle Hooks

Elysia exposes a rich lifecycle that allows you to hook into various stages of request processing. The main hooks are:

Here is a practical example that implements request timing, authentication, and response logging using hooks:

import { Elysia } from 'elysia';

const app = new Elysia()
  // Track request timing
  .onRequest(({ request }) => {
    const startTime = Date.now();
    request.headers.set('x-request-start', String(startTime));
  })
  
  // Authentication check before handler
  .onBeforeHandle(({ headers, set }) => {
    const authToken = headers.authorization;
    if (!authToken || !authToken.startsWith('Bearer ')) {
      set.status = 401;
      return { error: 'Missing or invalid authorization token' };
    }
    // In a real app, validate the token here
  })
  
  // Log after response is sent
  .onAfterResponse(({ request, response }) => {
    const startTime = parseInt(request.headers.get('x-request-start') || '0');
    const duration = Date.now() - startTime;
    console.log(`${request.method} ${request.url} - ${response?.status} - ${duration}ms`);
  })
  
  .get('/secure-data', () => {
    return { secret: 'This is protected data', timestamp: Date.now() };
  })
  
  .listen(3000);

Hooks can return a value. If an onBeforeHandle hook returns a value, that value becomes the response and the main handler is never called. This is perfect for implementing authentication guards, rate limiters, or caching layers.

3.2 Creating Reusable Middleware with the derive() Pattern

The .derive() method is a unique Elysia feature that allows you to inject computed values into the context that are then available to all downstream handlers. Unlike traditional middleware that mutates a shared object, derive() creates new context properties through a pure function:

import { Elysia } from 'elysia';

// Create a derived state plugin
const authPlugin = (app: Elysia) => 
  app.derive(({ headers }) => {
    const token = headers.authorization?.replace('Bearer ', '');
    
    // Simulate token validation
    if (token === 'valid-token') {
      return {
        currentUser: {
          id: 42,
          username: 'authenticated_user',
          role: 'admin'
        }
      };
    }
    
    return {
      currentUser: null
    };
  });

const app = new Elysia()
  .use(authPlugin)
  
  .get('/profile', ({ currentUser, set }) => {
    if (!currentUser) {
      set.status = 401;
      return { error: 'Authentication required' };
    }
    
    return {
      message: `Welcome back, ${currentUser.username}!`,
      role: currentUser.role
    };
  })
  
  .get('/public', ({ currentUser }) => {
    return {
      authenticated: currentUser !== null,
      message: 'This is public data'
    };
  })
  
  .listen(3000);

The derive() method returns an object that gets merged into the context. TypeScript automatically infers the new properties, so currentUser appears in IntelliSense suggestions for all downstream handlers. This pattern scales elegantly: you can chain multiple derive() calls to build up a rich context layer by layer.

3.3 Building Custom Plugins

Plugins in Elysia are simply functions that receive an Elysia instance and return one (usually with modifications). This functional approach makes plugins composable, testable, and easy to share. Here is a complete plugin that implements rate limiting with an in-memory store:

import { Elysia } from 'elysia';

interface RateLimitOptions {
  windowMs: number;
  maxRequests: number;
}

const rateLimiter = (options: RateLimitOptions) => {
  const store = new Map();
  
  // Clean up expired entries every minute
  setInterval(() => {
    const now = Date.now();
    for (const [key, entry] of store) {
      if (entry.resetAt <= now) {
        store.delete(key);
      }
    }
  }, 60000);
  
  return (app: Elysia) =>
    app.derive(({ request, set }) => {
      const ip = request.headers.get('x-forwarded-for') || 'unknown';
      const now = Date.now();
      const entry = store.get(ip);
      
      if (!entry || entry.resetAt <= now) {
        store.set(ip, { count: 1, resetAt: now + options.windowMs });
        return { rateLimitRemaining: options.maxRequests - 1 };
      }
      
      entry.count++;
      
      if (entry.count > options.maxRequests) {
        set.status = 429;
        set.headers['Retry-After'] = String(Math.ceil((entry.resetAt - now) / 1000));
        return { error: 'Too many requests', retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
      }
      
      return { rateLimitRemaining: options.maxRequests - entry.count };
    });
};

const app = new Elysia()
  .use(rateLimiter({ windowMs: 60000, maxRequests: 100 }))
  
  .get('/', ({ rateLimitRemaining }) => {
    return {
      message: 'Hello!',
      remainingRequests: rateLimitRemaining
    };
  })
  
  .listen(3000);

This plugin demonstrates several advanced patterns: it uses closure to maintain private state (the store map), it accepts configuration options, and it uses derive() to inject rate limit information into the context. The plugin can be published as a standalone package and reused across multiple projects.

3.4 Guards and Conditional Middleware

Elysia provides a .guard() method that applies middleware or hooks conditionally, only to routes defined within its scope. This is more efficient than applying global middleware and checking conditions inside each handler:

import { Elysia, t } from 'elysia';

const app = new Elysia()
  // Public routes — no authentication required
  .get('/health', () => ({ status: 'ok' }))
  .get('/docs', () => ({ version: '1.0.0' }))
  
  // Guarded admin routes
  .guard({
    beforeHandle: ({ headers, set }) => {
      const adminKey = headers.authorization?.replace('Bearer ', '');
      if (adminKey !== 'admin-secret-token') {
        set.status = 403;
        return { error: 'Admin access required' };
      }
    }
  }, admin => admin
    .get('/admin/dashboard', () => ({
      metrics: { users: 1500, revenue: 42000 }
    }))
    .get('/admin/logs', () => ({
      logs: ['user_signup', 'payment_received', 'error_occurred']
    }))
  )
  
  .listen(3000);

The guard applies the beforeHandle hook only to routes inside the admin group. Public routes like /health remain unaffected. Guards can also include validation schemas, making them a powerful tool for segmenting your API by concern.

Chapter 4: The Eden Full-Stack Integration

4.1 What is Eden?

Eden is Elysia's companion library for creating end-to-end type-safe full-stack applications. It generates a fully typed client from your Elysia server's schema definitions, eliminating the need for manual API documentation or separate type definitions. When your server changes, the client types automatically update — no code generation step required.

Install Eden alongside Elysia:

bun add elysia @elysiajs/eden

4.2 Creating a Type-Safe Server with Eden Contracts

To use Eden effectively, you define your API contracts using Elysia's schema system. Eden then extracts these types and provides them to the client. Here is a server set up for Eden integration:

import { Elysia, t } from 'elysia';
import { eden } from '@elysiajs/eden';

const app = new Elysia()
  .use(eden())
  
  .get('/users', () => {
    return [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' }
    ];
  }, {
    response: t.Array(t.Object({
      id: t.Number(),
      name: t.String(),
      email: t.String()
    }))
  })
  
  .post('/users', ({ body }) => {
    return { id: 3, ...body };
  }, {
    body: t.Object({
      name: t.String(),
      email: t.String({ format: 'email' })
    }),
    response: t.Object({
      id: t.Number(),
      name: t.String(),
      email: t.String()
    })
  })
  
  .listen(3000);

// Export the type for the client
export type App = typeof app;

4.3 Building the Client with Automatic Type Inference

On the client side (which can be a separate Bun or Node.js project, or even a browser frontend), you import the server's type and create a fully typed client. The client's methods mirror your server routes exactly:

// client.ts — can run in a separate project
import { edenTreaty } from '@elysiajs/eden';
import type { App } from './server'; // Import just the type, not the server code

const client = edenTreaty('http://localhost:3000');

// TypeScript knows the shape of the response
const users = await client.users.get();
// users.data is typed as Array<{ id: number; name: string; email: string }>
console.log(users.data?.[0]?.name); // Fully typed access

// Creating a user — TypeScript validates the body
const newUser = await client.users.post({
  name: 'Charlie',
  email: 'charlie@example.com'
});
// newUser.data is typed as { id: number; name: string; email: string }
console.log(newUser.data?.id);

The magic here is that edenTreaty uses TypeScript's advanced type inference to map your server's route structure into a proxy object. When you call client.users.get(), TypeScript knows the exact response type, query parameter types, and even the HTTP method. There is no code generation, no OpenAPI spec to maintain separately — the server is the source of truth.

4.4 Handling Errors on the Client

Eden clients return a discriminated union response that separates success and error cases, making error handling explicit and type-safe:

import { edenTreaty } from '@elysiajs/eden';
import type { App } from './server';

const client = edenTreaty('http://localhost:3000');

async function fetchUsers() {
  const result = await client.users.get();
  
  if (result.error) {
    // result.error is typed based on your server's error responses
    console.error(`Failed to fetch users: ${result.error.message}`);
    console.error(`Status code: ${result.error.status}`);
    return [];
  }
  
  // TypeScript narrows the type — result.data is now available
  return result.data;
}

async function createUser(name: string, email: string) {
  const result = await client.users.post({ name, email });
  
  if (result.error) {
    if (result.error.status === 409) {
      console.error('User already exists');
    } else {
      console.error(`Unexpected error: ${result.error.message}`);
    }
    return null;
  }
  
  return result.data;
}

The discriminated union pattern forces you to handle error cases before accessing the data, preventing runtime crashes due to unhandled error responses. This is a significant improvement over traditional fetch-based approaches where error handling is often an afterthought.

Chapter 5: Expert-Level Techniques

5.1 Custom Lifecycle Event Extensions

Elysia allows you to extend the framework itself by adding new lifecycle events or modifying existing ones. This is done through the .on() method for custom events and through deep plugin composition. Here is an example that adds a custom caching layer by hooking into the lifecycle:

import { Elysia } from 'elysia';

interface CacheEntry {
  value: unknown;
  expiresAt: number;
}

const cachingPlugin = (ttlMs: number) => {
  const cache = new Map();
  
  // Clean expired entries periodically
  setInterval(() => {
    const now = Date.now();
    for (const [key, entry] of cache) {
      if (entry.expiresAt <= now) cache.delete(key);
    }
  }, Math.min(ttlMs, 30000));
  
  return (app: Elysia) =>
    app
      .onBeforeHandle(({ request, set }) => {
        const cacheKey = `${request.method}:${request.url}`;
        const entry = cache.get(cacheKey);
        
        if (entry && entry.expiresAt > Date.now()) {
          // Short-circuit: return cached response immediately
          set.headers['X-Cache'] = 'HIT';
          return entry.value;
        }
        
        set.headers['X-Cache'] = 'MISS';
        // Store the cache key for use after the handler
        request.headers.set('x-cache-key', cacheKey);
      })
      .onAfterHandle(({ request, response }) => {
        const cacheKey = request.headers.get('x-cache-key');
        if (cacheKey && response) {
          cache.set(cacheKey, {
            value: response,
            expiresAt: Date.now() + ttlMs
          });
        }
      });
};

const app = new Elysia()
  .use(cachingPlugin(10000)) // 10-second cache
  
  .get('/slow-endpoint', () => {
    // Simulate a slow operation
    const start = Date.now();
    while (Date.now() - start < 100) {} // Artificial delay
    return { computed: 'expensive result', timestamp: Date.now() };
  })
  
  .listen(3000);

This caching plugin demonstrates expert-level lifecycle manipulation. It uses onBeforeHandle to intercept requests and potentially short-circuit the handler entirely, and onAfterHandle to store responses. The plugin communicates between hooks using request headers as a temporary channel — a pattern that works because headers are mutable within the request lifecycle.

5.2 Dynamic Route Injection and Meta-Programming

Advanced use cases may require generating routes dynamically based on configuration files, database schemas, or external service registries. Elysia's chainable API and the ability to pass instances between functions enables meta-programming patterns:

import { Elysia, t } from 'elysia';

// Imagine this comes from a database schema or configuration
const resourceDefinitions = [
  { name: 'articles', fields: { title: 'string', content: 'string', published: 'boolean' } },
  { name: 'comments', fields: { text: 'string', articleId: 'number', approved: 'boolean' } },
  { name: 'tags', fields: { name: 'string', color: 'string' } }
];

function buildCrudRoutes(resources: typeof resourceDefinitions) {
  return (app: Elysia) => {
    let result = app;
    
    for (const resource of resources) {
      const fieldSchema = t.Object(
        Object.fromEntries(
          Object.entries(resource.fields).map(([key, type]) => {
            if (type === 'string') return [key, t.String()];
            if (type === 'number') return [key, t.Number()];
            if (type === 'boolean') return [key, t.Boolean()];
            return [key, t.String()];
          })
        )
      );
      
      // In-memory store per resource
      const store: Record[] = [];
      let idCounter = 1;
      
      result = result
        .group(`/${resource.name}`, group => group
          .get('/', () => store, {
            response: t.Array(fieldSchema)
          })
          .post('/', ({ body }) => {
            const record = { id: idCounter++, ...body as object };
            store.push(record);
            return record;
          }, {
            body: fieldSchema,
            response: fieldSchema
          })
          .get('/:id', ({ params, set }) => {
            const record = store.find(r => r.id === parseInt(params.id));
            if (!record) { set.status = 404; return { error: 'Not found' }; }
            return record;
          })
          .put('/:id', ({ params, body, set }) => {
            const index = store.findIndex(r => r.id === parseInt(params.id));
            if (index === -1) { set.status = 404; return { error: 'Not found' }; }
            store[index] = { ...store[index], ...body as object };
            return store[index];
          })
          .delete('/:id', ({ params, set }) => {
            const index = store.findIndex(r => r.id === parseInt(params.id));
            if (index === -1) { set.status = 404; return { error: 'Not found' }; }
            store.splice(index, 1);
            set.status = 204;
            return null;
          })
        );
    }
    
    return result;
  };
}

const app = new Elysia()
  .use(buildCrudRoutes(resourceDefinitions))
  .listen(3000);

This code dynamically generates full CRUD endpoints for each resource definition, complete with validation schemas derived from the field definitions. The pattern can be extended to generate routes from OpenAPI specs, database introspection, or even gRPC service definitions. Each generated route group is fully validated and typed.

5.3 Server-Sent Events and Streaming Responses

For real-time applications, Elysia supports streaming responses natively. You can return a ReadableStream or use generators to produce server-sent events (SSE):

import { Elysia } from 'elysia';

const app = new Elysia()
  .get('/events', function* () {
    let eventId = 0;
    
    // Send initial connection event
    yield {
      event: 'connected',
      data: { message: 'SSE connection established', timestamp: Date.now() }
    };
    
    // Keep sending events indefinitely
    while (true) {
      eventId++;
      yield {
        event: 'update',
        data: { 
          id: eventId, 
          value: Math.random() * 100, 
          timestamp: Date.now() 
        }
      };
      
      // Wait 1 second between events
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }, {

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles