localhost
GET

플러그인은 기능을 더 작은 부분으로 분리하는 패턴입니다. 웹 서버를 위한 재사용 가능한 컴포넌트를 만듭니다.
플러그인을 만들려면 별도의 인스턴스를 생성합니다.
import { Elysia } from 'elysia'
const plugin = new Elysia()
.decorate('plugin', 'hi')
.get('/plugin', ({ plugin }) => plugin)
const app = new Elysia()
.use(plugin)
.get('/', ({ plugin }) => plugin)
.listen(3000)Elysia.use에 인스턴스를 전달하여 플러그인을 사용할 수 있습니다.
GET
플러그인은 state, decorate와 같은 플러그인 인스턴스의 모든 속성을 상속하지만 기본적으로 격리되어 있어 플러그인 라이프사이클은 상속하지 않습니다.
Elysia는 타입 추론도 자동으로 처리합니다.
모든 Elysia 인스턴스는 플러그인이 될 수 있습니다.
로직을 별도의 Elysia 인스턴스로 분리하여 여러 인스턴스에서 재사용할 수 있습니다.
플러그인을 만들려면 별도의 파일에 인스턴스를 정의하기만 하면 됩니다:
// plugin.ts
import { Elysia } from 'elysia'
export const plugin = new Elysia()
.get('/plugin', () => 'hi')그런 다음 메인 파일로 인스턴스를 가져옵니다:
import { Elysia } from 'elysia'
import { plugin } from './plugin'
const app = new Elysia()
.use(plugin)
.listen(3000)Elysia 라이프사이클 메서드는 자체 인스턴스에만 캡슐화됩니다.
즉, 새 인스턴스를 생성하면 다른 인스턴스와 라이프사이클 메서드를 공유하지 않습니다.
import { Elysia } from 'elysia'
const profile = new Elysia()
.onBeforeHandle(({ cookie }) => {
throwIfNotSignIn(cookie)
})
.get('/profile', () => 'Hi there!')
const app = new Elysia()
.use(profile)
// ⚠️ 이 경로는 로그인 체크가 없습니다
.patch('/rename', ({ body }) => updateProfile(body))이 예제에서 isSignIn 체크는 profile에만 적용되고 app에는 적용되지 않습니다.
GET
URL 바에서 경로를 /rename으로 변경하고 결과를 확인해보세요
Elysia는 명시적으로 지정하지 않는 한 기본적으로 라이프사이클을 격리합니다. 이는 JavaScript의 export와 유사하며, 함수를 모듈 외부에서 사용할 수 있도록 하려면 내보내기가 필요합니다.
다른 인스턴스로 라이프사이클을 "내보내기"하려면 범위를 지정해야 합니다.
import { Elysia } from 'elysia'
const profile = new Elysia()
.onBeforeHandle(
{ as: 'global' },
({ cookie }) => {
throwIfNotSignIn(cookie)
}
)
.get('/profile', () => 'Hi there!')
const app = new Elysia()
.use(profile)
// 이 경로는 로그인 체크가 있습니다
.patch('/rename', ({ body }) => updateProfile(body))GET
라이프사이클을 "global"로 캐스팅하면 라이프사이클이 모든 인스턴스로 내보내집니다.
Elysia는 다음과 같은 3가지 수준의 범위를 가집니다:
범위 타입은 다음과 같습니다:
다음 예제를 사용하여 각 범위 타입이 수행하는 작업을 검토해보겠습니다:
import { Elysia } from 'elysia'
const child = new Elysia()
.get('/child', 'hi')
const current = new Elysia()
// ? 아래 표에 제공된 값 기반 값
.onBeforeHandle({ as: 'local' }, () => {
console.log('hi')
})
.use(child)
.get('/current', 'hi')
const parent = new Elysia()
.use(current)
.get('/parent', 'hi')
const main = new Elysia()
.use(parent)
.get('/main', 'hi')type 값을 변경하면 결과는 다음과 같습니다:
| type | child | current | parent | main |
|---|---|---|---|---|
| local | ✅ | ✅ | ❌ | ❌ |
| scoped | ✅ | ✅ | ✅ | ❌ |
| global | ✅ | ✅ | ✅ | ✅ |
기본적으로 플러그인은 자신과 하위 인스턴스에만 훅을 적용합니다.
훅이 플러그인에 등록된 경우, 플러그인을 상속하는 인스턴스는 훅과 스키마를 상속하지 않습니다.
import { Elysia } from 'elysia'
const plugin = new Elysia()
.onBeforeHandle(() => {
console.log('hi')
})
.get('/child', 'log hi')
const main = new Elysia()
.use(plugin)
.get('/parent', 'not log hi')훅을 전역으로 적용하려면 훅을 global로 지정해야 합니다.
import { Elysia } from 'elysia'
const plugin = new Elysia()
.onBeforeHandle(() => {
return 'hi'
})
.get('/child', 'child')
.as('scoped')
const main = new Elysia()
.use(plugin)
.get('/parent', 'parent')GET
플러그인을 더 유용하게 만들려면 구성을 통한 사용자 정의를 허용하는 것이 좋습니다.
플러그인의 동작을 변경할 수 있는 매개변수를 받는 함수를 만들어 재사용성을 높일 수 있습니다.
import { Elysia } from 'elysia'
const version = (version = 1) => new Elysia()
.get('/version', version)
const app = new Elysia()
.use(version(1))
.listen(3000)함수 콜백 대신 새 플러그인 인스턴스를 정의하는 것이 좋습니다.
함수 콜백을 사용하면 메인 인스턴스의 기존 속성에 액세스할 수 있습니다. 예를 들어 특정 경로나 스토어가 존재하는지 확인할 수 있지만 캡슐화와 범위를 올바르게 처리하기 어렵습니다.
함수 콜백을 정의하려면 Elysia를 매개변수로 받는 함수를 만듭니다.
import { Elysia } from 'elysia'
const plugin = (app: Elysia) => app
.state('counter', 0)
.get('/plugin', () => 'Hi')
const app = new Elysia()
.use(plugin)
.get('/counter', ({ store: { counter } }) => counter)
.listen(3000)GET
Elysia.use에 전달되면 함수 콜백은 일반 플러그인처럼 동작하지만 속성이 메인 인스턴스에 직접 할당됩니다.
TIP
함수 콜백과 인스턴스 생성 간의 성능 차이에 대해 걱정할 필요가 없습니다.
Elysia는 밀리초 만에 10,000개의 인스턴스를 만들 수 있으며, 새 Elysia 인스턴스는 함수 콜백보다 타입 추론 성능이 더 우수합니다.
기본적으로 Elysia는 모든 플러그인을 등록하고 타입 정의를 처리합니다.
일부 플러그인은 타입 추론을 제공하기 위해 여러 번 사용되어 초기 값이나 경로의 중복 설정을 초래할 수 있습니다.
Elysia는 name과 선택적 seeds를 사용하여 인스턴스를 구별하여 Elysia가 인스턴스 중복을 식별하는 데 도움을 줍니다:
import { Elysia } from 'elysia'
const plugin = <T extends string>(config: { prefix: T }) =>
new Elysia({
name: 'my-plugin',
seed: config,
})
.get(`${config.prefix}/hi`, () => 'Hi')
const app = new Elysia()
.use(
plugin({
prefix: '/v2'
})
)
.listen(3000)GET
Elysia는 name과 seed를 사용하여 체크섬을 생성하여 인스턴스가 이전에 등록되었는지 식별합니다. 등록되었다면 Elysia는 플러그인 등록을 건너뜁니다.
seed가 제공되지 않으면 Elysia는 name만 사용하여 인스턴스를 구별합니다. 즉, 여러 번 등록하더라도 플러그인은 한 번만 등록됩니다.
import { Elysia } from 'elysia'
const plugin = new Elysia({ name: 'plugin' })
const app = new Elysia()
.use(plugin)
.use(plugin)
.use(plugin)
.use(plugin)
.listen(3000)이를 통해 Elysia는 플러그인을 반복적으로 처리하는 대신 등록된 플러그인을 재사용하여 성능을 향상시킬 수 있습니다.
TIP
Seed는 문자열부터 복잡한 객체나 클래스까지 무엇이든 될 수 있습니다.
제공된 값이 클래스인 경우, Elysia는 .toString 메서드를 사용하여 체크섬을 생성합니다.
state/decorators가 있는 플러그인을 인스턴스에 적용하면 인스턴스가 타입 안전성을 얻습니다.
그러나 플러그인을 다른 인스턴스에 적용하지 않으면 타입을 추론할 수 없습니다.
import { Elysia } from 'elysia'
const child = new Elysia()
// ❌ 'a'가 누락됨
.get('/', ({ a }) => a)Property 'a' does not exist on type '{ body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends number | keyof StatusMap, const T = Code extends 100 | ... 59 more ... | 511 ? { ...; }[Code] : Code>(co...'.
const main = new Elysia()
.decorate('a', 'a')
.use(child)Elysia는 이를 해결하기 위해 Service Locator 패턴을 도입했습니다.
Elysia는 플러그인 체크섬을 조회하고 값을 가져오거나 새 값을 등록합니다. 플러그인에서 타입을 추론합니다.
따라서 Elysia가 서비스를 찾아 타입 안전성을 추가할 수 있도록 플러그인 참조를 제공해야 합니다.
import { Elysia } from 'elysia'
const setup = new Elysia({ name: 'setup' })
.decorate('a', 'a')
// 'setup' 없이는 타입이 누락됩니다
const error = new Elysia()
.get('/', ({ a }) => a)Property 'a' does not exist on type '{ body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends number | keyof StatusMap, const T = Code extends 100 | ... 59 more ... | 511 ? { ...; }[Code] : Code>(co...'.
// `setup`이 있으면 타입이 추론됩니다
const child = new Elysia()
.use(setup)
.get('/', ({ a }) => a)
const main = new Elysia()
.use(child)GET
Guard를 사용하면 여러 경로에 훅과 스키마를 한 번에 적용할 수 있습니다.
import { Elysia, t } from 'elysia'
new Elysia()
.guard(
{
body: t.Object({
username: t.String(),
password: t.String()
})
},
(app) =>
app
.post('/sign-up', ({ body }) => signUp(body))
.post('/sign-in', ({ body }) => signIn(body), {
beforeHandle: isUserExists
})
)
.get('/', 'hi')
.listen(3000)이 코드는 스키마를 하나씩 인라인으로 지정하는 대신 '/sign-in'과 '/sign-up' 모두에 body 유효성 검사를 적용하지만 '/'에는 적용하지 않습니다.
경로 유효성 검사를 다음과 같이 요약할 수 있습니다:
| Path | Has validation |
|---|---|
| /sign-up | ✅ |
| /sign-in | ✅ |
| / | ❌ |
Guard는 인라인 훅과 동일한 매개변수를 받습니다. 유일한 차이점은 범위 내의 여러 경로에 훅을 적용할 수 있다는 것입니다.
즉, 위의 코드는 다음과 같이 번역됩니다:
import { Elysia, t } from 'elysia'
new Elysia()
.post('/sign-up', ({ body }) => signUp(body), {
body: t.Object({
username: t.String(),
password: t.String()
})
})
.post('/sign-in', ({ body }) => body, {
beforeHandle: isUserExists,
body: t.Object({
username: t.String(),
password: t.String()
})
})
.get('/', () => 'hi')
.listen(3000)접두사가 있는 그룹을 사용하려면 그룹에 3개의 매개변수를 제공할 수 있습니다.
guard와 동일한 API를 두 번째 매개변수에 적용하여 group과 guard를 중첩하지 않습니다.
다음 예제를 고려하세요:
import { Elysia, t } from 'elysia'
new Elysia()
.group('/v1', (app) =>
app.guard(
{
body: t.Literal('Rikuhachima Aru')
},
(app) => app.post('/student', ({ body }) => body)
)
)
.listen(3000)중첩된 그룹 가드에서 group의 두 번째 매개변수에 guard 범위를 제공하여 group과 guard를 병합할 수 있습니다:
import { Elysia, t } from 'elysia'
new Elysia()
.group(
'/v1',
(app) => app.guard(
{
body: t.Literal('Rikuhachima Aru')
},
(app) => app.post('/student', ({ body }) => body)
)
)
.listen(3000)결과는 다음 구문이 됩니다:
import { Elysia, t } from 'elysia'
new Elysia()
.group(
'/v1',
{
body: t.Literal('Rikuhachima Aru')
},
(app) => app.post('/student', ({ body }) => body)
)
.listen(3000)POST
부모에 훅을 적용하려면 다음 중 하나를 사용할 수 있습니다:
모든 이벤트 리스너는 훅의 범위를 지정하는 as 매개변수를 받습니다.
import { Elysia } from 'elysia'
const plugin = new Elysia()
.derive({ as: 'scoped' }, () => {
return { hi: 'ok' }
})
.get('/child', ({ hi }) => hi)
const main = new Elysia()
.use(plugin)
// ✅ Hi를 이제 사용할 수 있습니다
.get('/parent', ({ hi }) => hi)그러나 이 방법은 단일 훅에만 적용되며 여러 훅에는 적합하지 않을 수 있습니다.
모든 이벤트 리스너는 훅의 범위를 지정하는 as 매개변수를 받습니다.
import { Elysia, t } from 'elysia'
const plugin = new Elysia()
.guard({
as: 'scoped',
response: t.String(),
beforeHandle() {
console.log('ok')
}
})
.get('/child', 'ok')
const main = new Elysia()
.use(plugin)
.get('/parent', 'hello')Guard를 사용하면 범위를 지정하면서 schema와 hook을 여러 경로에 한 번에 적용할 수 있습니다.
그러나 derive와 resolve 메서드는 지원하지 않습니다.
as는 현재 인스턴스의 모든 훅과 스키마 범위를 읽고 수정합니다.
import { Elysia } from 'elysia'
const plugin = new Elysia()
.derive(() => {
return { hi: 'ok' }
})
.get('/child', ({ hi }) => hi)
.as('scoped')
const main = new Elysia()
.use(plugin)
// ✅ Hi를 이제 사용할 수 있습니다
.get('/parent', ({ hi }) => hi)때때로 플러그인을 부모 인스턴스에도 다시 적용하고 싶지만 scoped 메커니즘에 의해 제한되어 1개의 부모에만 제한됩니다.
부모 인스턴스에 적용하려면 범위를 부모 인스턴스로 끌어올려야 하며, as가 이를 수행하는 완벽한 방법입니다.
즉, local 범위가 있고 부모 인스턴스에 적용하려면 as('scoped')를 사용하여 끌어올릴 수 있습니다.
import { Elysia, t } from 'elysia'
const plugin = new Elysia()
.guard({
response: t.String()
})
.onBeforeHandle(() => { console.log('called') })
.get('/ok', () => 'ok')
.get('/not-ok', () => 1)Argument of type '() => number' is not assignable to parameter of type 'InlineHandler<NoInfer<IntersectIfObjectSchema<{ body: unknown; headers: unknown; query: unknown; params: {}; cookie: unknown; response: { 200: string; }; }, {}>>, { decorator: {}; store: {}; derive: {}; resolve: {}; } & { ...; }, {}>'.
Type '() => number' is not assignable to type '(context: { body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends 200 | "OK", T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 59 mor...'.
Type 'number' is not assignable to type 'Response | MaybePromise<string | ElysiaCustomStatusResponse<200, string, 200>>'. .as('scoped')
const instance = new Elysia()
.use(plugin)
.get('/no-ok-parent', () => 2)Argument of type '() => number' is not assignable to parameter of type 'InlineHandler<NoInfer<IntersectIfObjectSchema<{ body: unknown; headers: unknown; query: unknown; params: {}; cookie: unknown; response: { 200: string; }; }, {} & {}>>, { decorator: {}; store: {}; derive: {}; resolve: {}; } & { ...; }, {}>'.
Type '() => number' is not assignable to type '(context: { body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends 200 | "OK", T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 59 mor...'.
Type 'number' is not assignable to type 'Response | MaybePromise<string | ElysiaCustomStatusResponse<200, string, 200>>'. .as('scoped')
const parent = new Elysia()
.use(instance)
// `scoped`가 부모로 끌어올려져서 이제 오류가 발생합니다
.get('/ok', () => 3)Argument of type '() => number' is not assignable to parameter of type 'InlineHandler<NoInfer<IntersectIfObjectSchema<{ body: unknown; headers: unknown; query: unknown; params: {}; cookie: unknown; response: { 200: string; }; }, {} & {}>>, { decorator: {}; store: {}; derive: {}; resolve: {}; } & { ...; }, {}>'.
Type '() => number' is not assignable to type '(context: { body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends 200 | "OK", T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 59 mor...'.
Type 'number' is not assignable to type 'Response | MaybePromise<string | ElysiaCustomStatusResponse<200, string, 200>>'.모듈은 기본적으로 즉시 로드됩니다.
Elysia는 서버가 시작되기 전에 모든 모듈이 등록되었는지 확인합니다.
그러나 일부 모듈은 계산 집약적이거나 차단될 수 있어 서버 시작이 느려질 수 있습니다.
이를 해결하기 위해 Elysia는 서버 시작을 차단하지 않는 비동기 플러그인을 제공할 수 있습니다.
지연 모듈은 서버가 시작된 후 등록할 수 있는 비동기 플러그인입니다.
// plugin.ts
import { Elysia, file } from 'elysia'
import { loadAllFiles } from './files'
export const loadStatic = async (app: Elysia) => {
const files = await loadAllFiles()
files.forEach((asset) => app
.get(asset, file(file))
)
return app
}그리고 메인 파일에서:
import { Elysia } from 'elysia'
import { loadStatic } from './plugin'
const app = new Elysia()
.use(loadStatic)비동기 플러그인과 마찬가지로 지연 로드 모듈은 서버가 시작된 후 등록됩니다.
지연 로드 모듈은 동기 또는 비동기 함수일 수 있으며, import로 모듈을 사용하는 한 모듈은 지연 로드됩니다.
import { Elysia } from 'elysia'
const app = new Elysia()
.use(import('./plugin'))모듈이 계산 집약적이거나 차단될 때 모듈 지연 로딩을 사용하는 것이 좋습니다.
서버가 시작되기 전에 모듈 등록을 보장하려면 지연 모듈에서 await를 사용할 수 있습니다.
테스트 환경에서는 await app.modules를 사용하여 지연 및 지연 로딩 모듈을 기다릴 수 있습니다.
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'
describe('Modules', () => {
it('inline async', async () => {
const app = new Elysia()
.use(async (app) =>
app.get('/async', () => 'async')
)
await app.modules
const res = await app
.handle(new Request('http://localhost/async'))
.then((r) => r.text())
expect(res).toBe('async')
})
})