OpenAPI
Elysia has first-class support and follows OpenAPI schema by default.
Elysia can automatically generate an API documentation page by using an OpenAPI plugin.
To generate the Swagger page, install the plugin:
bun add @elysiajs/openapi
And register the plugin to the server:
import { Elysia } from 'elysia'
import { openapi } from '@elysiajs/openapi'
new Elysia()
.use(openapi())
By default, Elysia uses OpenAPI V3 schema and Scalar UI
For OpenAPI plugin configuration, see the OpenAPI plugin page.
OpenAPI from types
By default, Elysia relies on runtime schema to generate OpenAPI documentation.
However, you can also generate OpenAPI documentation from types by using a generator from OpenAPI plugin as follows:
Specify the root file of your project (usually
src/index.ts
), and export an instanceImport a generator and provide a file path from project root to type generator
import { Elysia, t } from 'elysia'
import { openapi } from '@elysiajs/openapi'
import { fromTypes } from '@elysiajs/openapi/gen'
export const app = new Elysia()
.use(
openapi({
references: fromTypes('src/index.ts')
})
)
.get('/', { test: 'hello' as const })
.post('/json', ({ body, status }) => body, {
body: t.Object({
hello: t.String()
})
})
.listen(3000)
Elysia will attempt to generate OpenAPI documentation by reading the type of an exported instance to generate OpenAPI documentation.
This will co-exists with the runtime schema, and the runtime schema will take precedence over the type definition.
Production
In production environment, it's likely that you might compile Elysia to a single executable with Bun or bundle into a single JavaScript file.
It's recommended that you should pre-generate the declaration file (.d.ts) to provide type declaration to the generator.
import { Elysia, t } from 'elysia'
import { openapi } from '@elysiajs/openapi'
import { fromTypes } from '@elysiajs/openapi/gen'
const app = new Elysia()
.use(
openapi({
references: fromTypes(
process.env.NODE_ENV === 'production'
? 'dist/index.d.ts'
: 'src/index.ts'
)
})
)
Having issues with type generation?
Caveats: Root path
As it's unreliable to guess to root of the project, it's recommended to provide the path to the project root to allow generator to run correctly, especially when using monorepo.
import { Elysia, t } from 'elysia'
import { openapi } from '@elysiajs/openapi'
import { fromTypes } from '@elysiajs/openapi/gen'
export const app = new Elysia()
.use(
openapi({
references: fromTypes('src/index.ts', {
projectRoot: path.join('..', import.meta.dir)
})
})
)
.get('/', { test: 'hello' as const })
.post('/json', ({ body, status }) => body, {
body: t.Object({
hello: t.String()
})
})
.listen(3000)
Custom tsconfig.json
If you have multiple tsconfig.json
files, it's important that you must specify a correct tsconfig.json
file to be used for type generation.
import { Elysia, t } from 'elysia'
import { openapi } from '@elysiajs/openapi'
import { fromTypes } from '@elysiajs/openapi/gen'
export const app = new Elysia()
.use(
openapi({
references: fromTypes('src/index.ts', {
// This is reference from root of the project
tsconfigPath: 'tsconfig.dts.json'
})
})
)
.get('/', { test: 'hello' as const })
.post('/json', ({ body, status }) => body, {
body: t.Object({
hello: t.String()
})
})
.listen(3000)
Describing route
We can add route information by providing a schema type.
However, sometimes defining only a type does not make it clear what the route might do. You can use detail fields to explicitly describe the route.
import { Elysia, t } from 'elysia'
import { openapi } from '@elysiajs/openapi'
new Elysia()
.use(openapi())
.post(
'/sign-in',
({ body }) => body, {
body: t.Object(
{
username: t.String(),
password: t.String({
minLength: 8,
description: 'User password (at least 8 characters)'
})
},
{
description: 'Expected a username and password'
}
),
detail: {
summary: 'Sign in the user',
tags: ['authentication']
}
})
The detail fields follows an OpenAPI V3 definition with auto-completion and type-safety by default.
Detail is then passed to OpenAPI to put the description to OpenAPI route.
Response headers
We can add a response headers by wrapping a schema with withHeader
:
import { Elysia, t } from 'elysia'
import { openapi, withHeader } from '@elysiajs/openapi'
new Elysia()
.use(openapi())
.get(
'/thing',
({ body, set }) => {
set.headers['x-powered-by'] = 'Elysia'
return body
},
{
response: withHeader(
t.Literal('Hi'),
{
'x-powered-by': t.Literal('Elysia')
}
)
}
)
Note that withHeader
is an annotation only, and does not enforce or validate the actual response headers. You need to set the headers manually.
Hide route
You can hide the route from the Swagger page by setting detail.hide
to true
import { Elysia, t } from 'elysia'
import { openapi } from '@elysiajs/openapi'
new Elysia()
.use(openapi())
.post(
'/sign-in',
({ body }) => body,
{
body: t.Object(
{
username: t.String(),
password: t.String()
},
{
description: 'Expected a username and password'
}
),
detail: {
hide: true
}
}
)
Tags
Elysia can separate the endpoints into groups by using the Swaggers tag system
Firstly define the available tags in the swagger config object
new Elysia().use(
openapi({
documentation: {
tags: [
{ name: 'App', description: 'General endpoints' },
{ name: 'Auth', description: 'Authentication endpoints' }
]
}
})
)
Then use the details property of the endpoint configuration section to assign that endpoint to the group
new Elysia()
.get('/', () => 'Hello Elysia', {
detail: {
tags: ['App']
}
})
.group('/auth', (app) =>
app.post(
'/sign-up',
({ body }) =>
db.user.create({
data: body,
select: {
id: true,
username: true
}
}),
{
detail: {
tags: ['Auth']
}
}
)
)
Which will produce a swagger page like the following
Tags group
Elysia may accept tags to add an entire instance or group of routes to a specific tag.
import { Elysia, t } from 'elysia'
new Elysia({
tags: ['user']
})
.get('/user', 'user')
.get('/admin', 'admin')
Models
By using reference model, Elysia will handle the schema generation automatically.
By separating models into a dedicated section and linked by reference.
new Elysia()
.model({
User: t.Object({
id: t.Number(),
username: t.String()
})
})
.get('/user', () => ({ id: 1, username: 'saltyaom' }), {
response: {
200: 'User'
},
detail: {
tags: ['User']
}
})
Guard
Alternatively, Elysia may accept guards to add an entire instance or group of routes to a specific guard.
import { Elysia, t } from 'elysia'
new Elysia()
.guard({
detail: {
description: 'Require user to be logged in'
}
})
.get('/user', 'user')
.get('/admin', 'admin')
Change OpenAPI Endpoint
You can change the OpenAPI endpoint by setting path in the plugin config.
import { Elysia } from 'elysia'
import { openapi } from '@elysiajs/openapi'
new Elysia()
.use(
openapi({
path: '/v2/openapi'
})
)
.listen(3000)
Customize OpenAPI info
We can customize the OpenAPI information by setting documentation.info in the plugin config.
import { Elysia } from 'elysia'
import { openapi } from '@elysiajs/openapi'
new Elysia()
.use(
openapi({
documentation: {
info: {
title: 'Elysia Documentation',
version: '1.0.0'
}
}
})
)
.listen(3000)
This can be useful for
- adding a title
- settings an API version
- adding a description explaining what our API is about
- explaining what tags are available, what each tag means
Security Configuration
To secure your API endpoints, you can define security schemes in the Swagger configuration. The example below demonstrates how to use Bearer Authentication (JWT) to protect your endpoints:
new Elysia().use(
openapi({
documentation: {
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
}
})
)
export const addressController = new Elysia({
prefix: '/address',
detail: {
tags: ['Address'],
security: [
{
bearerAuth: []
}
]
}
})
This will ensures that all endpoints under the /address
prefix require a valid JWT token for access.