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].
| Framework | Runtime | Average | Plain Text | Dynamic Parameters | JSON Body |
|---|---|---|---|---|---|
| bun | bun | 262,660.433 | 326,375.76 | 237,083.18 | 224,522.36 |
| elysia | bun | 255,574.717 | 313,073.64 | 241,891.57 | 211,758.94 |
| hyper-express | node | 234,395.837 | 311,775.43 | 249,675 | 141,737.08 |
| hono | bun | 203,937.883 | 239,229.82 | 201,663.43 | 170,920.4 |
| h3 | node | 96,515.027 | 114,971.87 | 87,935.94 | 86,637.27 |
| oak | deno | 46,569.853 | 55,174.24 | 48,260.36 | 36,274.96 |
| fastify | bun | 65,897.043 | 92,856.71 | 81,604.66 | 23,229.76 |
| fastify | node | 60,322.413 | 71,150.57 | 62,060.26 | 47,756.41 |
| koa | node | 39,594.14 | 46,219.64 | 40,961.72 | 31,601.06 |
| express | bun | 29,715.537 | 39,455.46 | 34,700.85 | 14,990.3 |
| express | node | 15,913.153 | 17,736.92 | 17,128.7 | 12,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:
- Elysia type is complex and heavily depends on plugin and multiple level of chaining.
- Hard to type, Elysia type could change at anytime, especially with decorators, and store
- Loss of type integrity, and inconsistency between types and runtime code.
We recommended one of the following approach to implement a controller in Elysia.
- Use Elysia instance as a controller itself
- 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:
- Loss of type integrity
- Make it harder to test and reuse
- 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:
- Non-request dependent service
- 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:
- Allow us to name a model and provide auto-completion.
- Modify schema for later usage, or perform a remap.
- Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
- 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.
- In src/index.ts, create or import an existing Elysia server
- 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()
})
})
- Develop locally with Vercel CLI
vc dev
- 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:
- --compile Compile TypeScript to binary
- --minify-whitespace Remove unnecessary whitespace
- --minify-syntax Minify JavaScript syntax to reduce file size
- --target bun Optimize the binary for Bun runtime
- --outfile server Output the binary as
server - 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:
| Target | Operating System | Architecture | Modern | Baseline | Libc |
|---|---|---|---|---|---|
| bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc |
| bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc |
| bun-windows-x64 | Windows | x64 | ✅ | ✅ | - |
| bun-windows-arm64 | Windows | arm64 | ❌ | ❌ | - |
| bun-darwin-x64 | macOS | x64 | ✅ | ✅ | - |
| bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | - |
| bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl |
| bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl |
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)
- server
- 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
trueis 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')
- (eg:
- string[] – Allow multiple HTTP methods.
- eg:
['GET', 'PUT', 'POST']
- eg:
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'.
- eg:
- string[] – Allow multiple HTTP headers.
- eg:
['Content-Type', 'Authorization']
- eg:
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'.
- eg:
- string[] – Allow multiple HTTP headers.
- eg:
['Content-Type', 'X‑Powered‑By']
- eg:
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
| Function | Description |
|---|---|
.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
| Function | Constant |
|---|---|
.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
| Constant | Pattern |
|---|---|
.EVERY_SECOND | * * * * * * |
.EVERY_5_SECONDS | */5 * * * * * |
.EVERY_10_SECONDS | */10 * * * * * |
.EVERY_30_SECONDS | */30 * * * * * |
.EVERY_MINUTE | */1 * * * |
.EVERY_5_MINUTES | 0 */5 * * * |
.EVERY_10_MINUTES | 0 */10 * * * |
.EVERY_30_MINUTES | 0 */30 * * * |
.EVERY_HOUR | 0 0-23/1 * * * |
.EVERY_2_HOURS | 0 0-23/2 * * * |
.EVERY_3_HOURS | 0 0-23/3 * * * |
.EVERY_4_HOURS | 0 0-23/4 * * * |
.EVERY_5_HOURS | 0 0-23/5 * * * |
.EVERY_6_HOURS | 0 0-23/6 * * * |
.EVERY_7_HOURS | 0 0-23/7 * * * |
.EVERY_8_HOURS | 0 0-23/8 * * * |
.EVERY_9_HOURS | 0 0-23/9 * * * |
.EVERY_10_HOURS | 0 0-23/10 * * * |
.EVERY_11_HOURS | 0 0-23/11 * * * |
.EVERY_12_HOURS | 0 0-23/12 * * * |
.EVERY_DAY_AT_1AM | 0 01 * * * |
.EVERY_DAY_AT_2AM | 0 02 * * * |
.EVERY_DAY_AT_3AM | 0 03 * * * |
.EVERY_DAY_AT_4AM | 0 04 * * * |
.EVERY_DAY_AT_5AM | 0 05 * * * |
.EVERY_DAY_AT_6AM | 0 06 * * * |
.EVERY_DAY_AT_7AM | 0 07 * * * |
.EVERY_DAY_AT_8AM | 0 08 * * * |
.EVERY_DAY_AT_9AM | 0 09 * * * |
.EVERY_DAY_AT_10AM | 0 10 * * * |
.EVERY_DAY_AT_11AM | 0 11 * * * |
.EVERY_DAY_AT_NOON | 0 12 * * * |
.EVERY_DAY_AT_1PM | 0 13 * * * |
.EVERY_DAY_AT_2PM | 0 14 * * * |
.EVERY_DAY_AT_3PM | 0 15 * * * |
.EVERY_DAY_AT_4PM | 0 16 * * * |
.EVERY_DAY_AT_5PM | 0 17 * * * |
.EVERY_DAY_AT_6PM | 0 18 * * * |
.EVERY_DAY_AT_7PM | 0 19 * * * |
.EVERY_DAY_AT_8PM | 0 20 * * * |
.EVERY_DAY_AT_9PM | 0 21 * * * |
.EVERY_DAY_AT_10PM | 0 22 * * * |
.EVERY_DAY_AT_11PM | 0 23 * * * |
.EVERY_DAY_AT_MIDNIGHT | 0 0 * * * |
.EVERY_WEEK | 0 0 * * 0 |
.EVERY_WEEKDAY | 0 0 * * 1-5 |
.EVERY_WEEKEND | 0 0 * * 6,0 |
.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT | 0 0 1 * * |
.EVERY_1ST_DAY_OF_MONTH_AT_NOON | 0 12 1 * * |
.EVERY_2ND_HOUR | 0 */2 * * * |
.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM | 0 1-23/2 * * * |
.EVERY_2ND_MONTH | 0 0 1 */2 * |
.EVERY_QUARTER | 0 0 1 */3 * |
.EVERY_6_MONTHS | 0 0 1 */6 * |
.EVERY_YEAR | 0 0 1 1 * |
.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM | 0 */30 9-17 * * * |
.EVERY_30_MINUTES_BETWEEN_10AM_AND_6PM | 0 */30 9-18 * * * |
.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM | 0 */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:
- Provide cookie secret to Elysia constructor.
- Use
t.Cookieto 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>
- We can update the cookie value by modifying
visit.value. - 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.)