Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 103 additions & 9 deletions docs/essential/best-practice.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ head:
content: Elysia is a pattern agnostic framework, we leave the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handle types. This page is a guide to use Elysia with MVC pattern.
---

<script setup>
import Tab from '../components/fern/tab.vue'
</script>

# Best Practice

Elysia is a pattern-agnostic framework, leaving the decision of which coding patterns to use up to you and your team.
Expand Down Expand Up @@ -85,10 +89,9 @@ 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) {
// Group related functions - see Service section for alternative patterns
export const Auth = {
async signIn({ username, password }: AuthModel.signInBody) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix hard tab indentation to use spaces.

The static analysis tool flagged hard tabs (Tab character) on lines 94, 289-291. Markdown linting standards require spaces for indentation. Replace tabs with spaces to maintain consistency with markdown best practices.

Apply the following diff to fix hard tabs on line 94 (Auth object literal):

~// Group related functions - see Service section for alternative patterns
~export const Auth = {
~	async signIn({ username, password }: AuthModel.signInBody) {
+// Group related functions - see Service section for alternative patterns
+export const Auth = {
+	async signIn({ username, password }: AuthModel.signInBody) {

Apply the following diff to fix hard tabs on lines 289-291 (Tab component props):

~<Tab
~	id="service-pattern"
~	:names="['Plain Object', 'Namespace', 'Abstract Class']"
~	:tabs="['object', 'namespace', 'class']"
~>
+<Tab
+    id="service-pattern"
+    :names="['Plain Object', 'Namespace', 'Abstract Class']"
+    :tabs="['object', 'namespace', 'class']"
+>

As per static analysis tools, use consistent indentation throughout the file.

Also applies to: 289-291

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🤖 Prompt for AI Agents
In docs/essential/best-practice.md around lines 94 and 289-291, there are hard
tab characters used for indentation; replace the tab(s) on line 94 (inside the
Auth object literal) and the tabs on lines 289-291 (Tab component props) with
spaces to match the repository's markdown indentation style (e.g., 2 spaces per
indent), ensure no other tabs remain in the file, and re-run markdown linting to
confirm the issue is resolved.

const user = await sql`
SELECT password
FROM users
Expand Down Expand Up @@ -211,6 +214,10 @@ new Elysia()
.get('/', ({ stuff }) => Controller.doStuff(stuff))
```

::: tip
This example uses an abstract class, but you can also use plain objects, module exports, or namespaces. See [Service patterns](#1-abstract-away-non-request-dependent-service) for alternatives.
:::

Tying the controller to Elysia Context may lead to:
1. Loss of type integrity
2. Make it harder to test and reuse
Expand Down Expand Up @@ -280,16 +287,99 @@ There are 2 types of service in Elysia:

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.
If the service or function isn't tied to an HTTP request or doesn't access a `Context`, you can group related functions using several patterns depending on your preference:

<Tab
id="service-pattern"
:names="['Plain Object', 'Module Exports', 'Namespace', 'Abstract Class']"
:tabs="['object', 'module', 'namespace', 'class']"
>

<template v-slot:object>

The simplest approach is using a plain object. This is idiomatic JavaScript and easy to understand:

```typescript
import { Elysia, t } from 'elysia'

const Service = {
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()
})
```

</template>

<template v-slot:module>

Export functions directly from a module and import with `* as`. This is the most ES module-native approach and enables tree-shaking:

```typescript
// service.ts
export function fibo(number: number): number {
if (number < 2) return number
return fibo(number - 1) + fibo(number - 2)
}
```

```typescript
// index.ts
import { Elysia, t } from 'elysia'
import * as Service from './service'

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

</template>

<template v-slot:namespace>

TypeScript namespaces can hold both values and types with the same name, which is useful if you want to co-locate types with your service:

```typescript
import { Elysia, t } from 'elysia'

namespace Service {
export function 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()
})
```

</template>

<template v-slot:class>

If you're coming from Java or C#, you may prefer using an abstract class with static methods:

```typescript
import { Elysia, t } from 'elysia'

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

if (number < 2) return number
return Service.fibo(number - 1) + Service.fibo(number - 2)
}
}
Expand All @@ -302,7 +392,11 @@ new Elysia()
})
```

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

</Tab>

All four patterns produce the same runtime behavior. Choose based on your team's familiarity and whether you need to co-locate types.

### 2. Request dependent service as Elysia instance

Expand Down
4 changes: 4 additions & 0 deletions docs/key-concept.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,10 @@ const app = new Elysia()
})
```

::: tip
This example uses an abstract class, but you can also use plain objects, module exports, or namespaces. See [Best Practice: Service patterns](/essential/best-practice.html#1-abstract-away-non-request-dependent-service) for alternatives.
:::

See [Best practice: MVC Controller](/essential/best-practice.html#controller).

### TypeScript
Expand Down