Elysia React App

ID: 2235https://elysiajs.com/llms-full.txt
Source

GraphQL Apollo Plugin

Plugin for elysia for using GraphQL Apollo.

Install with:

bun add graphql @elysiajs/apollo @apollo/server

Then use it:

import { Elysia } from 'elysia'
import { apollo, gql } from '@elysiajs/apollo'

const app = new Elysia()
	.use(
		apollo({
			typeDefs: gql`
				type Book {
					title: String
					author: String
				}

				type Query {
					books: [Book]
				}
			`,
			resolvers: {
				Query: {
					books: () => {
						return [
							{
								title: 'Elysia',
								author: 'saltyAom'
							}
						]
					}
				}
			}
		})
	)
	.listen(3000)

Accessing /graphql should show Apollo GraphQL playground work with.

Context

Because Elysia is based on Web Standard Request and Response which is different from Node's HttpRequest and HttpResponse that Express uses, results in req, res being undefined in context.

Because of this, Elysia replaces both with context like route parameters.

const app = new Elysia()
	.use(
		apollo({
			typeDefs,
			resolvers,
			context: async ({ request }) => {
				const authorization = request.headers.get('Authorization')

				return {
					authorization
				}
			}
		})
	)
	.listen(3000)

Config

This plugin extends Apollo's ServerRegistration (which is ApolloServer's' constructor parameter).

Below are the extended parameters for configuring Apollo Server with Elysia.

path

@default "/graphql"

Path to expose Apollo Server.

enablePlayground

@default process.env.ENV !== 'production'

Determine whether should Apollo should provide Apollo Playground.


At a glance

Elysia is an ergonomic web framework for building backend servers with Bun.

Designed with simplicity and type-safety in mind, Elysia offers a familiar API with extensive support for TypeScript and is optimized for Bun.

Here's a simple hello world in Elysia.

import { Elysia } from 'elysia'

new Elysia()
    .get('/', 'Hello Elysia')
    .get('/user/:id', ({ params: { id }}) => id)
    .post('/form', ({ body }) => body)
    .listen(3000)

Navigate to localhost:3000 and you should see 'Hello Elysia' as the result.

::: tip Hover over the code snippet to see the type definition.

In the mock browser, click on the path highlighted in blue to change paths and preview the response.

Elysia can run in the browser, and the results you see are actually executed using Elysia. :::

Performance

Building on Bun and extensive optimization like static code analysis allows Elysia to generate optimized code on the fly.

Elysia can outperform most web frameworks available today[1], and even match the performance of Golang and Rust frameworks[2].

FrameworkRuntimeAveragePlain TextDynamic ParametersJSON Body
bunbun262,660.433326,375.76237,083.18224,522.36
elysiabun255,574.717313,073.64241,891.57211,758.94
hyper-expressnode234,395.837311,775.43249,675141,737.08
honobun203,937.883239,229.82201,663.43170,920.4
h3node96,515.027114,971.8787,935.9486,637.27
oakdeno46,569.85355,174.2448,260.3636,274.96
fastifybun65,897.04392,856.7181,604.6623,229.76
fastifynode60,322.41371,150.5762,060.2647,756.41
koanode39,594.1446,219.6440,961.7231,601.06
expressbun29,715.53739,455.4634,700.8514,990.3
expressnode15,913.15317,736.9217,128.712,873.84

TypeScript

Elysia is designed to help you write less TypeScript.

Elysia's Type System is fine-tuned to infer types from your code automatically, without needing to write explicit TypeScript, while providing type-safety at both runtime and compile time for the most ergonomic developer experience.

Take a look at this example:

import { Elysia } from 'elysia'

new Elysia()
    .get('/user/:id', ({ params: { id } }) => id)
                        // ^?
    .listen(3000)

The above code creates a path parameter "id". The value that replaces :id will be passed to params.id both at runtime and in types, without manual type declaration.

Elysia's goal is to help you write less TypeScript and focus more on business logic. Let the framework handle the complex types.

TypeScript is not required to use Elysia, but it's recommended.

Type Integrity

To take it a step further, Elysia provides Elysia.t, a schema builder to validate types and values at both runtime and compile time, creating a single source of truth for your data types.

Let's modify the previous code to accept only a number value instead of a string.

import { Elysia, t } from 'elysia'

new Elysia()
    .get('/user/:id', ({ params: { id } }) => id, {
                                // ^?
        params: t.Object({
            id: t.Number()
        })
    })
    .listen(3000)

This code ensures that our path parameter id will always be a number at both runtime and compile time (type-level).

::: tip Hover over "id" in the above code snippet to see a type definition. :::

With Elysia's schema builder, we can ensure type safety like a strongly typed language with a single source of truth.

Standard Schema

Elysia supports Standard Schema, allowing you to use your favorite validation library:

  • Zod
  • Valibot
  • ArkType
  • Effect Schema
  • Yup
  • Joi
  • and more
import { Elysia } from 'elysia'
import { z } from 'zod'
import * as v from 'valibot'

new Elysia()
	.get('/id/:id', ({ params: { id }, query: { name } }) => id, {
		//                           ^?
		params: z.object({
			id: z.coerce.number()
		}),
		query: v.object({
			name: v.literal('Lilith')
		})
	})
	.listen(3000)

Elysia will infer the types from the schema automatically, allowing you to use your favorite validation library while still maintaining type safety.

OpenAPI

Elysia adopts many standards by default, like OpenAPI, WinterTC compliance, and Standard Schema. Allowing you to integrate with most of the industry standard tools or at least easily integrate with tools you are familiar with.

For instance, because Elysia adopts OpenAPI by default, generating API documentation is as easy as adding a one-liner:

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

new Elysia()
    .use(openapi()) // [!code ++]
    .get('/user/:id', ({ params: { id } }) => id, {
        params: t.Object({
            id: t.Number()
        })
    })
    .listen(3000)

With the OpenAPI plugin, you can seamlessly generate an API documentation page without additional code or specific configuration and share it with your team effortlessly.

OpenAPI from types

Elysia has excellent support for OpenAPI with schemas that can be used for data validation, type inference, and OpenAPI annotation from a single source of truth.

Elysia also supports OpenAPI schema generation with 1 line directly from types, allowing you to have complete and accurate API documentation without any manual annotation.

import { Elysia, t } from 'elysia'
import { openapi, fromTypes } from '@elysiajs/openapi'

export const app = new Elysia()
    .use(openapi({
    	references: fromTypes() // [!code ++]
    }))
    .get('/user/:id', ({ params: { id } }) => id, {
        params: t.Object({
            id: t.Number()
        })
    })
    .listen(3000)

End-to-end Type Safety

With Elysia, type safety is not limited to server-side.

With Elysia, you can synchronize your types with your frontend team automatically, similar to tRPC, using Elysia's client library, "Eden".

import { Elysia, t } from 'elysia'
import { openapi, fromTypes } from '@elysiajs/openapi'

export const app = new Elysia()
    .use(openapi({
    	references: fromTypes()
    }))
    .get('/user/:id', ({ params: { id } }) => id, {
        params: t.Object({
            id: t.Number()
        })
    })
    .listen(3000)

export type App = typeof app

And on your client-side:

// @filename: server.ts
import { Elysia, t } from 'elysia'

const app = new Elysia()
    .get('/user/:id', ({ params: { id } }) => id, {
        params: t.Object({
            id: t.Number()
        })
    })
    .listen(3000)

export type App = typeof app

// @filename: client.ts
// ---cut---
// client.ts
import { treaty } from '@elysiajs/eden'
import type { App } from './server'

const app = treaty<App>('localhost:3000')

// Get data from /user/617
const { data } = await app.user({ id: 617 }).get()
      // ^?

console.log(data)

With Eden, you can use the existing Elysia types to query an Elysia server without code generation and synchronize types for both frontend and backend automatically.

Elysia is not only about helping you create a confident backend but for all that is beautiful in this world.

Platform Agnostic

Elysia was designed for Bun, but is not limited to Bun. Being WinterTC compliant allows you to deploy Elysia servers on Cloudflare Workers, Vercel Edge Functions, and most other runtimes that support Web Standard Requests.

Our Community

If you have questions or get stuck with Elysia, feel free to ask our community on GitHub Discussions, Discord, or Twitter.


1. Measured in requests/second. The benchmark for parsing query, path parameter and set response header on Debian 11, Intel i7-13700K tested on Bun 0.7.2 on 6 Aug 2023. See the benchmark condition here.

2. Based on TechEmpower Benchmark round 22.


Bearer Plugin

Plugin for elysia for retrieving the Bearer token.

Install with:

bun add @elysiajs/bearer

Then use it:

import { Elysia } from 'elysia'
import { bearer } from '@elysiajs/bearer'

const app = new Elysia()
    .use(bearer())
    .get('/sign', ({ bearer }) => bearer, {
        beforeHandle({ bearer, set, status }) {
            if (!bearer) {
                set.headers[
                    'WWW-Authenticate'
                ] = `Bearer realm='sign', error="invalid_request"`

                return status(400, 'Unauthorized')
            }
        }
    })
    .listen(3000)

This plugin is for retrieving a Bearer token specified in RFC6750.

This plugin DOES NOT handle authentication validation for your server. Instead, the plugin leaves the decision to developers to apply logic for handling validation check themselves.


Best Practice

Elysia is a pattern-agnostic framework, leaving the decision of which coding patterns to use up to you and your team.

However, there are several concerns when trying to adapt an MVC pattern (Model-View-Controller) with Elysia, and we found it hard to decouple and handle types.

This page is a guide on how to follow Elysia structure best practices combined with the MVC pattern, but it can be adapted to any coding pattern you prefer.

Folder Structure

Elysia is unopinionated about folder structure, leaving you to decide how to organize your code yourself.

However, if you don't have a specific structure in mind, we recommend a feature-based folder structure where each feature has its own folder containing controllers, services, and models.

| src
  | modules
	| auth
	  | index.ts (Elysia controller)
	  | service.ts (service)
	  | model.ts (model)
	| user
	  | index.ts (Elysia controller)
	  | service.ts (service)
	  | model.ts (model)
  | utils
	| a
	  | index.ts
	| b
	  | index.ts

This structure allows you to easily find and manage your code and keep related code together.

Here's an example code of how to distribute your code into a feature-based folder structure:

::: code-group

// Controller handle HTTP related eg. routing, request validation
import { Elysia } from 'elysia'

import { Auth } from './service'
import { AuthModel } from './model'

export const auth = new Elysia({ prefix: '/auth' })
	.get(
		'/sign-in',
		async ({ body, cookie: { session } }) => {
			const response = await Auth.signIn(body)

			// Set session cookie
			session.value = response.token

			return response
		}, {
			body: AuthModel.signInBody,
			response: {
				200: AuthModel.signInResponse,
				400: AuthModel.signInInvalid
			}
		}
	)
// Service handle business logic, decoupled from Elysia controller
import { status } from 'elysia'

import type { AuthModel } from './model'

// If the class doesn't need to store a property,
// you may use `abstract class` to avoid class allocation
export abstract class Auth {
	static async signIn({ username, password }: AuthModel.signInBody) {
		const user = await sql`
			SELECT password
			FROM users
			WHERE username = ${username}
			LIMIT 1`

		if (await Bun.password.verify(password, user.password))
			// You can throw an HTTP error directly
			throw status(
				400,
				'Invalid username or password' satisfies AuthModel.signInInvalid
			)

		return {
			username,
			token: await generateAndSaveTokenToDB(user.id)
		}
	}
}
// Model define the data structure and validation for the request and response
import { t } from 'elysia'

export namespace AuthModel {
	// Define a DTO for Elysia validation
	export const signInBody = t.Object({
		username: t.String(),
		password: t.String(),
	})

	// Define it as TypeScript type
	export type signInBody = typeof signInBody.static

	// Repeat for other models
	export const signInResponse = t.Object({
		username: t.String(),
		token: t.String(),
	})

	export type signInResponse = typeof signInResponse.static

	export const signInInvalid = t.Literal('Invalid username or password')
	export type signInInvalid = typeof signInInvalid.static
}

:::

Each file has its own responsibility as follows:

  • Controller: Handle HTTP routing, request validation, and cookie.
  • Service: Handle business logic, decoupled from Elysia controller if possible.
  • Model: Define the data structure and validation for the request and response.

Feel free to adapt this structure to your needs and use any coding pattern you prefer.

Controller

Due to type soundness of Elysia, it's not recommended to use a traditional controller class that is tightly coupled with Elysia's Context because:

  1. Elysia type is complex and heavily depends on plugin and multiple level of chaining.
  2. Hard to type, Elysia type could change at anytime, especially with decorators, and store
  3. Loss of type integrity, and inconsistency between types and runtime code.

We recommended one of the following approach to implement a controller in Elysia.

  1. Use Elysia instance as a controller itself
  2. Create a controller that is not tied with HTTP request or Elysia.

1. Elysia instance as a controller

1 Elysia instance = 1 controller

Treat an Elysia instance is a controller, and define your routes directly on the Elysia instance.

// ✅ Do
import { Elysia } from 'elysia'
import { Service } from './service'

new Elysia()
    .get('/', ({ stuff }) => {
        Service.doStuff(stuff)
    })

This approach allows Elysia to infer the Context type automatically, ensuring type integrity and consistency between types and runtime code.

// ❌ Don't
import { Elysia, t, type Context } from 'elysia'

abstract class Controller {
    static root(context: Context) {
        return Service.doStuff(context.stuff)
    }
}

new Elysia()
    .get('/', Controller.hi)

This approach makes it hard to type Context properly, and may lead to loss of type integrity.

2. Controller without HTTP request

If you want to create a controller class, we recommend creating a class that is not tied to HTTP request or Elysia at all.

This approach allows you to decouple the controller from Elysia, making it easier to test, reuse, and even swap a framework while still follows the MVC pattern.

import { Elysia } from 'elysia'

abstract class Controller {
	static doStuff(stuff: string) {
		return Service.doStuff(stuff)
	}
}

new Elysia()
	.get('/', ({ stuff }) => Controller.doStuff(stuff))

Tying the controller to Elysia Context may lead to:

  1. Loss of type integrity
  2. Make it harder to test and reuse
  3. Lead to vendor lock-in

We recommended to keep the controller decoupled from Elysia as much as possible.

❌ Don't: Pass entire Context to a controller

Context is a highly dynamic type that can be inferred from Elysia instance.

Do not pass an entire Context to a controller, instead use object destructuring to extract what you need and pass it to the controller.

import type { Context } from 'elysia'

abstract class Controller {
	constructor() {}

	// ❌ Don't do this
	static root(context: Context) {
		return Service.doStuff(context.stuff)
	}
}

This approach makes it hard to type Context properly, and may lead to loss of type integrity.

Testing

If you're using Elysia as a controller, you can test your controller using handle to directly call a function (and it's lifecycle)

import { Elysia } from 'elysia'
import { Service } from './service'

import { describe, it, expect } from 'bun:test'

const app = new Elysia()
    .get('/', ({ stuff }) => {
        Service.doStuff(stuff)

        return 'ok'
    })

describe('Controller', () => {
	it('should work', async () => {
		const response = await app
			.handle(new Request('http://localhost/'))
			.then((x) => x.text())

		expect(response).toBe('ok')
	})
})

You may find more information about testing in Unit Test.

Service

Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance.

Any technical logic that can be decoupled from controller may live inside a Service.

There are 2 types of service in Elysia:

  1. Non-request dependent service
  2. Request dependent service

1. Abstract away Non-request dependent service

We recommend abstracting a service class/function away from Elysia.

If the service or function isn't tied to an HTTP request or doesn't access a Context, it's recommended to implement it as a static class or function.

import { Elysia, t } from 'elysia'

abstract class Service {
    static fibo(number: number): number {
        if(number < 2)
            return number

        return Service.fibo(number - 1) + Service.fibo(number - 2)
    }
}

new Elysia()
    .get('/fibo', ({ body }) => {
        return Service.fibo(body)
    }, {
        body: t.Numeric()
    })

If your service doesn't need to store a property, you may use abstract class and static instead to avoid allocating class instance.

2. Request dependent service as Elysia instance

If the service is a request-dependent service or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference:

import { Elysia } from 'elysia'

// ✅ Do
const AuthService = new Elysia({ name: 'Auth.Service' })
    .macro({
        isSignIn: {
            resolve({ cookie, status }) {
                if (!cookie.session.value) return status(401)

                return {
                	session: cookie.session.value,
                }
            }
        }
    })

const UserController = new Elysia()
    .use(AuthService)
    .get('/profile', ({ Auth: { user } }) => user, {
    	isSignIn: true
    })

::: tip Elysia handles plugin deduplication by default, so you don't have to worry about performance, as it will be a singleton if you specify a "name" property. :::

✅ Do: Decorate only request dependent property

It's recommended to decorate only request-dependent properties, such as requestIP, requestTime, or session.

Overusing decorators may tie your code to Elysia, making it harder to test and reuse.

import { Elysia } from 'elysia'

new Elysia()
	.decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip)
	.decorate('requestTime', () => Date.now())
	.decorate('session', ({ cookie }) => cookie.session.value)
	.get('/', ({ requestIP, requestTime, session }) => {
		return { requestIP, requestTime, session }
	})

Model

Model or DTO (Data Transfer Object) is handle by Elysia.t (Validation).

Elysia has a validation system built-in which can infers type from your code and validate it at runtime.

✅ Do: Use Elysia's validation system

Elysia strength is prioritizing a single source of truth for both type and runtime validation.

Instead of declaring an interface, reuse validation's model instead:

// ✅ Do
import { Elysia, t } from 'elysia'

const customBody = t.Object({
	username: t.String(),
	password: t.String()
})

// Optional if you want to get the type of the model
// Usually if we didn't use the type, as it's already inferred by Elysia
type CustomBody = typeof customBody.static
    // ^?



export { customBody }

We can get type of model by using typeof with .static property from the model.

Then you can use the CustomBody type to infer the type of the request body.

import { Elysia, t } from 'elysia'

const customBody = t.Object({
	username: t.String(),
	password: t.String()
})
// ---cut---
// ✅ Do
new Elysia()
	.post('/login', ({ body }) => {
	                 // ^?
		return body
	}, {
		body: customBody
	})

❌ Don't: Declare a class instance as a model

Do not declare a class instance as a model:

// ❌ Don't
class CustomBody {
	username: string
	password: string

	constructor(username: string, password: string) {
		this.username = username
		this.password = password
	}
}

// ❌ Don't
interface ICustomBody {
	username: string
	password: string
}

❌ Don't: Declare type separate from the model

Do not declare a type separate from the model, instead use typeof with .static property to get the type of the model.

// ❌ Don't
import { Elysia, t } from 'elysia'

const customBody = t.Object({
	username: t.String(),
	password: t.String()
})

type CustomBody = {
	username: string
	password: string
}

// ✅ Do
const customBody = t.Object({
	username: t.String(),
		password: t.String()
})

type CustomBody = typeof customBody.static

Group

You can group multiple models into a single object to make it more organized.

import { Elysia, t } from 'elysia'

export const AuthModel = {
	sign: t.Object({
		username: t.String(),
		password: t.String()
	})
}

const models = AuthModel.models

Model Injection

Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using Elysia reference model

Using Elysia's model reference

import { Elysia, t } from 'elysia'

const customBody = t.Object({
	username: t.String(),
	password: t.String()
})

const AuthModel = new Elysia()
    .model({
        sign: customBody
    })

const models = AuthModel.models

const UserController = new Elysia({ prefix: '/auth' })
    .use(AuthModel)
    .prefix('model', 'auth.')
    .post('/sign-in', async ({ body, cookie: { session } }) => {
                             // ^?

        return true
    }, {
        body: 'auth.Sign'
    })

This approach provide several benefits:

  1. Allow us to name a model and provide auto-completion.
  2. Modify schema for later usage, or perform a remap.
  3. Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
  4. Improve TypeScript inference speed as model type will be cached during registration.

Better Auth

Better Auth is framework-agnostic authentication (and authorization) framework for TypeScript.

It provides a comprehensive set of features out of the box and includes a plugin ecosystem that simplifies adding advanced functionalities.

We recommend going through the Better Auth basic setup before going through this page.

Our basic setup will look like this:

import { betterAuth } from 'better-auth'
import { Pool } from 'pg'

export const auth = betterAuth({
    database: new Pool()
})

Handler

After setting up Better Auth instance, we can mount to Elysia via mount.

We need to mount the handler to Elysia endpoint.

import { Elysia } from 'elysia'
import { auth } from './auth'

const app = new Elysia()
	.mount(auth.handler) // [!code ++]
	.listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

Then we can access Better Auth with http://localhost:3000/api/auth.

Custom endpoint

We recommend setting a prefix path when using mount.

import { Elysia } from 'elysia'

const app = new Elysia()
	.mount('/auth', auth.handler) // [!code ++]
	.listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

Then we can access Better Auth with http://localhost:3000/auth/api/auth.

But the URL looks redundant, we can customize the /api/auth prefix to something else in Better Auth instance.

import { betterAuth } from 'better-auth'
import { openAPI } from 'better-auth/plugins'
import { passkey } from 'better-auth/plugins/passkey'

import { Pool } from 'pg'

export const auth = betterAuth({
    basePath: '/api' // [!code ++]
})

Then we can access Better Auth with http://localhost:3000/auth/api.

Unfortunately, we can't set basePath of a Better Auth instance to be empty or /.

OpenAPI

Better Auth support openapi with better-auth/plugins.

However if we are using @elysiajs/openapi, you might want to extract the documentation from Better Auth instance.

We may do that with the following code:

import { openAPI } from 'better-auth/plugins'

let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())

export const OpenAPI = {
    getPaths: (prefix = '/auth/api') =>
        getSchema().then(({ paths }) => {
            const reference: typeof paths = Object.create(null)

            for (const path of Object.keys(paths)) {
                const key = prefix + path
                reference[key] = paths[path]

                for (const method of Object.keys(paths[path])) {
                    const operation = (reference[key] as any)[method]

                    operation.tags = ['Better Auth']
                }
            }

            return reference
        }) as Promise<any>,
    components: getSchema().then(({ components }) => components) as Promise<any>
} as const

Then in our Elysia instance that use @elysiajs/openapi.

import { Elysia } from 'elysia'
import { openapi } from '@elysiajs/openapi'

import { OpenAPI } from './auth'

const app = new Elysia().use(
    openapi({
        documentation: {
            components: await OpenAPI.components,
            paths: await OpenAPI.getPaths()
        }
    })
)

CORS

To configure cors, you can use the cors plugin from @elysiajs/cors.

import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'

import { auth } from './auth'

const app = new Elysia()
    .use(
        cors({
            origin: 'http://localhost:3001',
            methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
            credentials: true,
            allowedHeaders: ['Content-Type', 'Authorization']
        })
    )
    .mount(auth.handler)
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

Macro

You can use macro with resolve to provide session and user information before pass to view.

import { Elysia } from 'elysia'
import { auth } from './auth'

// user middleware (compute user and session and pass to routes)
const betterAuth = new Elysia({ name: 'better-auth' })
    .mount(auth.handler)
    .macro({
        auth: {
            async resolve({ status, request: { headers } }) {
                const session = await auth.api.getSession({
                    headers
                })

                if (!session) return status(401)

                return {
                    user: session.user,
                    session: session.session
                }
            }
        }
    })

const app = new Elysia()
    .use(betterAuth)
    .get('/user', ({ user }) => user, {
        auth: true
    })
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

This will allow you to access the user and session object in all of your routes.


Cheat Sheet

Here are a quick overview for a common Elysia patterns

Hello World

A simple hello world

import { Elysia } from 'elysia'

new Elysia()
    .get('/', () => 'Hello World')
    .listen(3000)

Custom HTTP Method

Define route using custom HTTP methods/verbs

See Route

import { Elysia } from 'elysia'

new Elysia()
    .get('/hi', () => 'Hi')
    .post('/hi', () => 'From Post')
    .put('/hi', () => 'From Put')
    .route('M-SEARCH', '/hi', () => 'Custom Method')
    .listen(3000)

Path Parameter

Using dynamic path parameter

See Path

import { Elysia } from 'elysia'

new Elysia()
    .get('/id/:id', ({ params: { id } }) => id)
    .get('/rest/*', () => 'Rest')
    .listen(3000)

Return JSON

Elysia converts response to JSON automatically

See Handler

import { Elysia } from 'elysia'

new Elysia()
    .get('/json', () => {
        return {
            hello: 'Elysia'
        }
    })
    .listen(3000)

Return a file

A file can be return in as formdata response

The response must be a 1-level deep object

import { Elysia, file } from 'elysia'

new Elysia()
    .get('/json', () => {
        return {
            hello: 'Elysia',
            image: file('public/cat.jpg')
        }
    })
    .listen(3000)

Header and status

Set a custom header and a status code

See Handler

import { Elysia } from 'elysia'

new Elysia()
    .get('/', ({ set, status }) => {
        set.headers['x-powered-by'] = 'Elysia'

        return status(418, "I'm a teapot")
    })
    .listen(3000)

Group

Define a prefix once for sub routes

See Group

import { Elysia } from 'elysia'

new Elysia()
    .get("/", () => "Hi")
    .group("/auth", app => {
        return app
            .get("/", () => "Hi")
            .post("/sign-in", ({ body }) => body)
            .put("/sign-up", ({ body }) => body)
    })
    .listen(3000)

Schema

Enforce a data type of a route

See Validation

import { Elysia, t } from 'elysia'

new Elysia()
    .post('/mirror', ({ body: { username } }) => username, {
        body: t.Object({
            username: t.String(),
            password: t.String()
        })
    })
    .listen(3000)

File upload

See Validation#file

import { Elysia, t } from 'elysia'

new Elysia()
	.post('/body', ({ body }) => body, {
                    // ^?





		body: t.Object({
			file: t.File({ format: 'image/*' }),
			multipleFiles: t.Files()
		})
	})
	.listen(3000)

Lifecycle Hook

Intercept an Elysia event in order

See Lifecycle

import { Elysia, t } from 'elysia'

new Elysia()
    .onRequest(() => {
        console.log('On request')
    })
    .on('beforeHandle', () => {
        console.log('Before handle')
    })
    .post('/mirror', ({ body }) => body, {
        body: t.Object({
            username: t.String(),
            password: t.String()
        }),
        afterHandle: () => {
            console.log("After handle")
        }
    })
    .listen(3000)

Guard

Enforce a data type of sub routes

See Scope

// @errors: 2345
import { Elysia, t } from 'elysia'

new Elysia()
    .guard({
        response: t.String()
    }, (app) => app
        .get('/', () => 'Hi')
        // Invalid: will throws error, and TypeScript will report error
        .get('/invalid', () => 1)
    )
    .listen(3000)

Custom context

Add custom variable to route context

See Context

import { Elysia } from 'elysia'

new Elysia()
    .state('version', 1)
    .decorate('getDate', () => Date.now())
    .get('/version', ({
        getDate,
        store: { version }
    }) => `${version} ${getDate()}`)
    .listen(3000)

Redirect

Redirect a response

See Handler

import { Elysia } from 'elysia'

new Elysia()
    .get('/', () => 'hi')
    .get('/redirect', ({ redirect }) => {
        return redirect('/')
    })
    .listen(3000)

Plugin

Create a separate instance

See Plugin

import { Elysia } from 'elysia'

const plugin = new Elysia()
    .state('plugin-version', 1)
    .get('/hi', () => 'hi')

new Elysia()
    .use(plugin)
    .get('/version', ({ store }) => store['plugin-version'])
    .listen(3000)

Web Socket

Create a realtime connection using Web Socket

See Web Socket

import { Elysia } from 'elysia'

new Elysia()
    .ws('/ping', {
        message(ws, message) {
            ws.send('hello ' + message)
        }
    })
    .listen(3000)

OpenAPI documentation

Create interactive documentation using Scalar (or optionally Swagger)

See openapi

import { Elysia } from 'elysia'
import { openapi } from '@elysiajs/openapi'

const app = new Elysia()
    .use(openapi())
    .listen(3000)

console.log(`View documentation at "${app.server!.url}openapi" in your browser`);

Unit Test

Write a unit test of your Elysia app

See Unit Test

// test/index.test.ts
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'

describe('Elysia', () => {
    it('return a response', async () => {
        const app = new Elysia().get('/', () => 'hi')

        const response = await app
            .handle(new Request('http://localhost/'))
            .then((res) => res.text())

        expect(response).toBe('hi')
    })
})

Custom body parser

Create custom logic for parsing body

See Parse

import { Elysia } from 'elysia'

new Elysia()
    .onParse(({ request, contentType }) => {
        if (contentType === 'application/custom-type')
            return request.text()
    })

GraphQL

Create a custom GraphQL server using GraphQL Yoga or Apollo

See GraphQL Yoga

import { Elysia } from 'elysia'
import { yoga } from '@elysiajs/graphql-yoga'

const app = new Elysia()
    .use(
        yoga({
            typeDefs: /* GraphQL */`
                type Query {
                    hi: String
                }
            `,
            resolvers: {
                Query: {
                    hi: () => 'Hello from Elysia'
                }
            }
        })
    )
    .listen(3000)

Deploy Elysia on Vercel

Elysia can deploys on Vercel with zero configuration using either Bun or Node runtime.

  1. In src/index.ts, create or import an existing Elysia server
  2. Export the Elysia server as default export
import { Elysia, t } from 'elysia'

export default new Elysia() // [!code ++]
    .get('/', () => 'Hello Vercel Function')
    .post('/', ({ body }) => body, {
        body: t.Object({
            name: t.String()
        })
    })
  1. Develop locally with Vercel CLI
vc dev
  1. Deploy to Vercel
vc deploy

That's it. Your Elysia app is now running on Vercel.

Using Node.js

To deploy with Node.js, make sure to set type: module in your package.json

::: code-group

{
  "name": "elysia-app",
  "type": "module" // [!code ++]
}

:::

Using Bun

To deploy with Bun, make sure to set the runtime to Bun in your vercel.json

::: code-group

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "bunVersion": "1.x" // [!code ++]
}

If this doesn't work

Vercel has zero configuration for Elysia, for additional configuration, please refers to Vercel documentation


Deploy to production

This page is a guide on how to deploy Elysia to production.

Cluster mode

Elysia is a single-threaded by default. To take advantage of multi-core CPU, we can run Elysia in cluster mode.

Let's create a index.ts file that import our main server from server.ts and fork multiple workers based on the number of CPU cores available.

::: code-group

import cluster from 'node:cluster'
import os from 'node:os'
import process from 'node:process'

if (cluster.isPrimary) {
  	for (let i = 0; i < os.availableParallelism(); i++)
    	cluster.fork()
} else {
  	await import('./server')
  	console.log(`Worker ${process.pid} started`)
}
import { Elysia } from 'elysia'

new Elysia()
	.get('/', () => 'Hello World!')
	.listen(3000)

:::

This will make sure that Elysia is running on multiple CPU cores.

::: tip Elysia on Bun use SO_REUSEPORT by default, which allows multiple instances to listen on the same port. This only works on Linux. :::

Compile to binary

We recommend running a build command before deploying to production as it could potentially reduce memory usage and file size significantly.

We recommend compiling Elysia into a single binary using the command as follows:

bun build \
	--compile \
	--minify-whitespace \
	--minify-syntax \
	--target bun \
	--outfile server \
	src/index.ts

This will generate a portable binary server which we can run to start our server.

Compiling server to binary usually significantly reduces memory usage by 2-3x compared to development environment.

This command is a bit long, so let's break it down:

  1. --compile Compile TypeScript to binary
  2. --minify-whitespace Remove unnecessary whitespace
  3. --minify-syntax Minify JavaScript syntax to reduce file size
  4. --target bun Optimize the binary for Bun runtime
  5. --outfile server Output the binary as server
  6. src/index.ts The entry file of our server (codebase)

To start our server, simply run the binary.

./server

Once binary is compiled, you don't need Bun installed on the machine to run the server.

This is great as the deployment server doesn't need to install an extra runtime to run making binary portable.

Target

You can also add a --target flag to optimize the binary for the target platform.

bun build \
	--compile \
	--minify-whitespace \
	--minify-syntax \
	--target bun-linux-x64 \
	--outfile server \
	src/index.ts

Here's a list of available targets:

TargetOperating SystemArchitectureModernBaselineLibc
bun-linux-x64Linuxx64glibc
bun-linux-arm64Linuxarm64N/Aglibc
bun-windows-x64Windowsx64-
bun-windows-arm64Windowsarm64-
bun-darwin-x64macOSx64-
bun-darwin-arm64macOSarm64N/A-
bun-linux-x64-muslLinuxx64musl
bun-linux-arm64-muslLinuxarm64N/Amusl

Why not --minify

Bun does have --minify flag that will minify the binary.

However if we are using OpenTelemetry, it’s going to reduce a function name to a single character.

This makes tracing harder than it should as OpenTelemetry relies on a function name.

However, if you're not using OpenTelemetry, you may opt in for --minify instead

bun build \
	--compile \
	--minify \
	--outfile server \
	src/index.ts

Permission

Some Linux distros might not be able to run the binary, we suggest enabling executable permission to a binary if you're on Linux:

chmod +x ./server

./server

Unknown random Chinese error

If you're trying to deploy a binary to your server but unable to run with random chinese character error.

It means that the machine you're running on doesn't support AVX2.

Unfortunately, Bun requires a machine that has AVX2 hardware support.

There is no workaround as far as we know.

Compile to JavaScript

If you are unable to compile to binary or you are deploying on a Windows server.

You may bundle your server to a JavaScript file instead.

bun build \
	--minify-whitespace \
	--minify-syntax \
	--outfile ./dist/index.js \
	src/index.ts

This will generate a single portable JavaScript file that you can deploy on your server.

NODE_ENV=production bun ./dist/index.js

Docker

On Docker, we recommended to always compile to binary to reduce base image overhead.

Here's an example image using Distroless image using binary.

FROM oven/bun AS build

WORKDIR /app

# Cache packages installation
COPY package.json package.json
COPY bun.lock bun.lock

RUN bun install

COPY ./src ./src

ENV NODE_ENV=production

RUN bun build \
	--compile \
	--minify-whitespace \
	--minify-syntax \
	--outfile server \
	src/index.ts

FROM gcr.io/distroless/base

WORKDIR /app

COPY --from=build /app/server server

ENV NODE_ENV=production

CMD ["./server"]

EXPOSE 3000

OpenTelemetry

If you are using OpenTelemetry to deploys production server.

As OpenTelemetry rely on monkey-patching node_modules/<library>. It's required to make instrumentations works properly, we need to specify that libraries to be instrument is an external module to exclude it from being bundled.

For example, if you are using @opentelemetry/instrumentation-pg to instrument pg library. We need to exclude pg from being bundled and make sure that it is importing node_modules from the pg library.

To make this works, we may specified pg as an external module with --external pg

bun build --compile --external pg --outfile server src/index.ts

This tells bun to not pg bundled into the final output file, and will be imported from the node_modules directory at runtime. So on a production server, you must also keeps the node_modules directory.

It's recommended to specify packages that should be available in production server as dependencies in package.json and use bun install --production to install only production dependencies.

{
	"dependencies": {
		"pg": "^8.15.6"
	},
	"devDependencies": {
		"@elysiajs/opentelemetry": "^1.2.0",
		"@opentelemetry/instrumentation-pg": "^0.52.0",
		"@types/pg": "^8.11.14",
		"elysia": "^1.2.25"
	}
}

Then after running a build command, on a production server

bun install --production

If the node_modules directory still includes development dependencies, you may remove the node_modules directory and reinstall production dependencies again.

Monorepo

If you are using Elysia with Monorepo, you may need to include dependent packages.

If you are using Turborepo, you can place a Dockerfile inside an your apps directory like apps/server/Dockerfile.

Assume that our monorepo are using Turborepo with structure as follows:

  • apps
    • server
      • Dockerfile (place a Dockerfile here)
  • packages
    • config

Then we can build our Dockerfile on monorepo root (not app root):

docker build -t elysia-mono .

With Dockerfile as follows:

FROM oven/bun:1 AS build

WORKDIR /app

# Cache packages
COPY package.json package.json
COPY bun.lock bun.lock

COPY /apps/server/package.json ./apps/server/package.json
COPY /packages/config/package.json ./packages/config/package.json

RUN bun install

COPY /apps/server ./apps/server
COPY /packages/config ./packages/config

ENV NODE_ENV=production

RUN bun build \
	--compile \
	--minify-whitespace \
	--minify-syntax \
	--outfile server \
	src/index.ts

FROM gcr.io/distroless/base

WORKDIR /app

COPY --from=build /app/server server

ENV NODE_ENV=production

CMD ["./server"]

EXPOSE 3000

Railway

Railway is one of the popular deployment platform.

Railway assigns a random port to expose for the application, which can be accessed via the PORT environment variable.

We need to modify our Elysia server to accept the PORT environment variable to comply with Railway port.

Instead of a fixed port, we may use process.env.PORT and provide a fallback on development.

new Elysia()
	.listen(3000) // [!code --]
	.listen(process.env.PORT ?? 3000) // [!code ++]

This should allows Elysia to intercept port provided by Railway.

::: tip Elysia assign hostname to 0.0.0.0 automatically, which works with Railway :::


CORS Plugin

This plugin adds support for customizing Cross-Origin Resource Sharing behavior.

Install with:

bun add @elysiajs/cors

Then use it:

import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'

new Elysia().use(cors()).listen(3000)

This will set Elysia to accept requests from any origin.

Config

Below is a config which is accepted by the plugin

origin

@default true

Indicates whether the response can be shared with the requesting code from the given origins.

Value can be one of the following:

  • string - Name of origin which will directly assign to Access‑Control‑Allow‑Origin header.
  • boolean - If set to true, Access‑Control‑Allow‑Origin will be set to * (any origins)
  • RegExp - Pattern to match request's URL, allowed if matched.
  • Function - Custom logic to allow resource sharing, allow if true is returned.
    • Expected to have the type of:
    cors(context: Context) => boolean | void
  • Array<string | RegExp | Function> - iterate through all cases above in order, allowed if any of the values are true.

methods

@default *

Allowed methods for cross‑origin requests.

Assign Access‑Control‑Allow‑Methods header.

Value can be one of the following:

  • undefined | null | '' – Ignore all methods.
  • * – Allows all methods.
  • string – Expects either a single method or a comma‑delimited string
    • (eg: 'GET, PUT, POST')
  • string[] – Allow multiple HTTP methods.
    • eg: ['GET', 'PUT', 'POST']

allowedHeaders

@default *

Allowed headers for an incoming request.

Assign Access‑Control‑Allow‑Headers header.

Value can be one of the following:

  • string – Expects either a single header or a comma‑delimited string
    • eg: 'Content‑Type, Authorization'.
  • string[] – Allow multiple HTTP headers.
    • eg: ['Content-Type', 'Authorization']

exposeHeaders

@default *

Response CORS with specified headers.

Assign Access‑Control‑Expose‑Headers header.

Value can be one of the following:

  • string – Expects either a single header or a comma‑delimited string.
    • eg: 'Content‑Type, X‑Powered‑By'.
  • string[] – Allow multiple HTTP headers.
    • eg: ['Content-Type', 'X‑Powered‑By']

credentials

@default true

The Access‑Control‑Allow‑Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode Request.credentials is include.

When a request's credentials mode Request.credentials is include, browsers will only expose the response to the frontend JavaScript code if the Access‑Control‑Allow‑Credentials value is true.

Assign Access‑Control‑Allow‑Credentials header.


maxAge

@default 5

Indicates how long the results of a preflight request can be cached.

Assign Access‑Control‑Max‑Age header.


preflight

The preflight request is a request sent to check if the CORS protocol is understood and if a server is aware of using specific methods and headers.

Response with OPTIONS request with 3 HTTP request headers:

  • Access‑Control‑Request‑Method
  • Access‑Control‑Request‑Headers
  • Origin

This config indicates if the server should respond to preflight requests.

Pattern

Below you can find the common patterns to use the plugin.

Allow CORS by top‑level domain

import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'

const app = new Elysia()
	.use(
		cors({
			origin: /.*\.saltyaom\.com$/
		})
	)
	.get('/', () => 'Hi')
	.listen(3000)

This will allow requests from top‑level domains with saltyaom.com


Cron Plugin

This plugin adds support for running cronjobs in the Elysia server.

Install with:

bun add @elysiajs/cron

Then use it:

import { Elysia } from 'elysia'
import { cron } from '@elysiajs/cron'

new Elysia()
	.use(
		cron({
			name: 'heartbeat',
			pattern: '*/10 * * * * *',
			run() {
				console.log('Heartbeat')
			}
		})
	)
	.listen(3000)

The above code will log heartbeat every 10 seconds.

cron

Create a cronjob for the Elysia server.

type:

cron(config: CronConfig, callback: (Instance['store']) => void): this

CronConfig accepts the parameters specified below:

name

Job name to register to store.

This will register the cron instance to store with a specified name, which can be used to reference in later processes eg. stop the job.

pattern

Time to run the job as specified by cron syntax specified as below:

┌────────────── second (optional)
│ ┌──────────── minute
│ │ ┌────────── hour
│ │ │ ┌────── day of the month
│ │ │ │ ┌──── month
│ │ │ │ │ ┌──── day of week
│ │ │ │ │ │
* * * * * *

This can be generated by tools like Crontab Guru


This plugin extends the cron method to Elysia using cronner.

Below are the configs accepted by cronner.

timezone

Time zone in Europe/Stockholm format

startAt

Schedule start time for the job

stopAt

Schedule stop time for the job

maxRuns

Maximum number of executions

catch

Continue execution even if an unhandled error is thrown by a triggered function.

interval

The minimum interval between executions, in seconds.

Pattern

Below you can find the common patterns to use the plugin.

Stop cronjob

You can stop cronjob manually by accessing the cronjob name registered to store.

import { Elysia } from 'elysia'
import { cron } from '@elysiajs/cron'

const app = new Elysia()
	.use(
		cron({
			name: 'heartbeat',
			pattern: '*/1 * * * * *',
			run() {
				console.log('Heartbeat')
			}
		})
	)
	.get(
		'/stop',
		({
			store: {
				cron: { heartbeat }
			}
		}) => {
			heartbeat.stop()

			return 'Stop heartbeat'
		}
	)
	.listen(3000)

Predefined patterns

You can use predefined patterns from @elysiajs/cron/schedule

import { Elysia } from 'elysia'
import { cron, Patterns } from '@elysiajs/cron'

const app = new Elysia()
	.use(
		cron({
			name: 'heartbeat',
			pattern: Patterns.everySecond(),
			run() {
				console.log('Heartbeat')
			}
		})
	)
	.get(
		'/stop',
		({
			store: {
				cron: { heartbeat }
			}
		}) => {
			heartbeat.stop()

			return 'Stop heartbeat'
		}
	)
	.listen(3000)

Functions

FunctionDescription
.everySeconds(2)Run the task every 2 seconds
.everyMinutes(5)Run the task every 5 minutes
.everyHours(3)Run the task every 3 hours
.everyHoursAt(3, 15)Run the task every 3 hours at 15 minutes
.everyDayAt('04:19')Run the task every day at 04:19
.everyWeekOn(Patterns.MONDAY, '19:30')Run the task every Monday at 19‑30
.everyWeekdayAt('17:00')Run the task every day from Monday to Friday at 17:00

Function aliases to constants

FunctionConstant
.everySecond()EVERY_SECOND
.everyMinute()EVERY_MINUTE
.hourly()EVERY_HOUR
.daily()EVERY_DAY_AT_MIDNIGHT
.everyWeekday()EVERY_WEEKDAY
.everyWeekend()EVERY_WEEKEND
.weekly()EVERY_WEEK
.monthly()EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT
.everyQuarter()EVERY_QUARTER
.yearly()EVERY_YEAR

Constants

ConstantPattern
.EVERY_SECOND* * * * * *
.EVERY_5_SECONDS*/5 * * * * *
.EVERY_10_SECONDS*/10 * * * * *
.EVERY_30_SECONDS*/30 * * * * *
.EVERY_MINUTE*/1 * * *
.EVERY_5_MINUTES0 */5 * * *
.EVERY_10_MINUTES0 */10 * * *
.EVERY_30_MINUTES0 */30 * * *
.EVERY_HOUR0 0-23/1 * * *
.EVERY_2_HOURS0 0-23/2 * * *
.EVERY_3_HOURS0 0-23/3 * * *
.EVERY_4_HOURS0 0-23/4 * * *
.EVERY_5_HOURS0 0-23/5 * * *
.EVERY_6_HOURS0 0-23/6 * * *
.EVERY_7_HOURS0 0-23/7 * * *
.EVERY_8_HOURS0 0-23/8 * * *
.EVERY_9_HOURS0 0-23/9 * * *
.EVERY_10_HOURS0 0-23/10 * * *
.EVERY_11_HOURS0 0-23/11 * * *
.EVERY_12_HOURS0 0-23/12 * * *
.EVERY_DAY_AT_1AM0 01 * * *
.EVERY_DAY_AT_2AM0 02 * * *
.EVERY_DAY_AT_3AM0 03 * * *
.EVERY_DAY_AT_4AM0 04 * * *
.EVERY_DAY_AT_5AM0 05 * * *
.EVERY_DAY_AT_6AM0 06 * * *
.EVERY_DAY_AT_7AM0 07 * * *
.EVERY_DAY_AT_8AM0 08 * * *
.EVERY_DAY_AT_9AM0 09 * * *
.EVERY_DAY_AT_10AM0 10 * * *
.EVERY_DAY_AT_11AM0 11 * * *
.EVERY_DAY_AT_NOON0 12 * * *
.EVERY_DAY_AT_1PM0 13 * * *
.EVERY_DAY_AT_2PM0 14 * * *
.EVERY_DAY_AT_3PM0 15 * * *
.EVERY_DAY_AT_4PM0 16 * * *
.EVERY_DAY_AT_5PM0 17 * * *
.EVERY_DAY_AT_6PM0 18 * * *
.EVERY_DAY_AT_7PM0 19 * * *
.EVERY_DAY_AT_8PM0 20 * * *
.EVERY_DAY_AT_9PM0 21 * * *
.EVERY_DAY_AT_10PM0 22 * * *
.EVERY_DAY_AT_11PM0 23 * * *
.EVERY_DAY_AT_MIDNIGHT0 0 * * *
.EVERY_WEEK0 0 * * 0
.EVERY_WEEKDAY0 0 * * 1-5
.EVERY_WEEKEND0 0 * * 6,0
.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT0 0 1 * *
.EVERY_1ST_DAY_OF_MONTH_AT_NOON0 12 1 * *
.EVERY_2ND_HOUR0 */2 * * *
.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM0 1-23/2 * * *
.EVERY_2ND_MONTH0 0 1 */2 *
.EVERY_QUARTER0 0 1 */3 *
.EVERY_6_MONTHS0 0 1 */6 *
.EVERY_YEAR0 0 1 1 *
.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM0 */30 9-17 * * *
.EVERY_30_MINUTES_BETWEEN_10AM_AND_6PM0 */30 9-18 * * *
.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM0 */30 10-19 * * *

Cookie

You interact with cookie by using cookie from context.

import { Elysia } from 'elysia'

new Elysia()
	.get('/', ({ cookie: { visit } }) => {
		const total = +visit.value ?? 0
		visit.value++

		return `You have visited ${visit.value} times`
	})
	.listen(3000)

Cookie is a reactive object. Once modified, it will be reflected in response.

Value

Elysia will then try to coerce it into its respective value when a type annotation if provided.

import { Elysia } from 'elysia'

new Elysia()
	.get('/', ({ cookie: { visit } }) => {
		visit.value ??= 0
		visit.value.total++

		return `You have visited ${visit.value.total} times`
	}, {
		cookie: t.Object({
			visit: t.Optional(
				t.Object({
					total: t.Number()
				})
			)
		})
	})
	.listen(3000)

We can use cookie schema to validate and parse cookie.

Attribute

We can get/set cookie attribute by its respective property name.

Otherwise, use .set() to bulk set attribute.

import { Elysia } from 'elysia'

new Elysia()
	.get('/', ({ cookie: { visit } }) => {
		visit.value ??= 0
		visit.value++

		visit.httpOnly = true
		visit.path = '/'

		visit.set({
			sameSite: 'lax',
			secure: true,
			maxAge: 60 * 60 * 24 * 7
		})

		return `You have visited ${visit.value} times`
	})
	.listen(3000)

See Cookie Attribute.

Remove

We can remove cookie by calling .remove() method.

import { Elysia } from 'elysia'

new Elysia()
	.get('/', ({ cookie: { visit } }) => {
		visit.remove()

		return `Cookie removed`
	})
	.listen(3000)

Cookie Signature

Elysia can sign cookie to prevent tampering by:

  1. Provide cookie secret to Elysia constructor.
  2. Use t.Cookie to provide secret for each cookie.
import { Elysia } from 'elysia'

new Elysia({
	cookie: {
		secret: 'Fischl von Luftschloss Narfidort',
	}
})
	.get('/', ({ cookie: { visit } }) => {
		visit.value ??= 0
		visit.value++

		return `You have visited ${visit.value} times`
	}, {
		cookie: t.Cookie({
			visit: t.Optional(t.Number())
        }, {
            secrets: 'Fischl von Luftschloss Narfidort',
            sign: ['visit']
        })
	})
	.listen(3000)

If multiple secrets are provided, Elysia will use the first secret to sign cookie, and try to verify with the rest.

See Cookie Signature, Cookie Rotation.

Assignment

Let's create a simple counter that tracks how many times you have visited the site.

<template #answer>

  1. We can update the cookie value by modifying visit.value.
  2. We can set HTTP only attribute by setting visit.httpOnly = true.
import { Elysia, t } from 'elysia'

new Elysia()
	.get('/', ({ cookie: { visit } }) => {
		visit.value ??= 0
		visit.value++

		visit.httpOnly = true

		return `You have visited ${visit.value} times`
	}, {
		cookie: t.Object({
			visit: t.Optional(
				t.Number()
			)
		})
	})
	.listen(3000)

Eden Fetch

A fetch‑like alternative to Eden Treaty.

With Eden Fetch, you can interact with Elysia server in a type‑safe manner using Fetch API.


First export your existing Elysia server type:

// server.ts
import { Elysia, t } from 'elysia'

const app = new Elysia()
    .get('/hi', () => 'Hi Elysia')
    .get('/id/:id', ({ params: { id } }) => id)
    .post('/mirror', ({ body }) => body, {
        body: t.Object({
            id: t.Number(),
            name: t.String()
        })
    })
    .listen(3000)

export type App = typeof app

Then import the server type, and consume the Elysia API on client:

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

const fetch = edenFetch<App>('http://localhost:3000')

// response type: 'Hi Elysia'
const id = await fetch('/hi', {})

// response type: 1895
const nendoroid = await fetch('/id/:id', {
    params: {
        id: '1895'
    }
})

// response type: { id: 1895, name: Skadi } 
const id = await fetch('/mirror', {
    method: 'POST',
    body { ... } 
})

Error Handling

You can handle errors the same way as Eden Treaty.

import { edenFetch } from '@elysiajs/eden'
import type { App }  ……

...

(omitted; full content retained)

... (rest of content omitted)

(Note: Full content continues with additional examples and explanations.)