Skip to content

docs: Improve Best Practices Guide for Elysia.js #1114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
106 changes: 106 additions & 0 deletions docs/best-practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# **Best Practices**

Elysia is a **pattern-agnostic framework**, allowing developers to choose the coding patterns that best suit their needs.

However, implementing the **MVC (Model-View-Controller) pattern** in Elysia can be challenging due to **difficulties in decoupling logic** and **handling types effectively**.

This guide outlines **best practices** for structuring an Elysia application while maintaining flexibility in adopting other coding styles.

---

## **Method Chaining**

Elysia code should always follow **method chaining** to maintain type integrity.

Since Elysia’s **type system is dynamic**, each method **returns a new type reference**. Using method chaining ensures proper **type inference**, preventing type-related issues.

✅ **Recommended Approach (Method Chaining):**
```typescript
import { Elysia } from 'elysia'

new Elysia()
.state('build', 1)
// The store is strictly typed
.get('/', ({ store: { build } }) => build)
.listen(3000)
```
Here, `.state('build', 1)` modifies the `ElysiaInstance` type, ensuring `build` is correctly inferred.

❌ **Avoid This (No Method Chaining):**
```typescript
import { Elysia } from 'elysia'

const app = new Elysia()
app.state('build', 1) // Type information is lost

app.get('/', ({ store: { build } }) => build) // ❌ Property 'build' does not exist
app.listen(3000)
```
### **Why?**
Without method chaining, Elysia **does not retain new types**, leading to incorrect type inference.

---

## **Controllers**

**One Elysia instance should act as one controller.**

Passing an entire `Context` object to a separate controller can cause issues:
- **Increased complexity** – Elysia’s types are dynamic and change based on plugins and method chaining.
- **Typing difficulties** – Elysia’s type system evolves with decorators and state.
- **Inconsistent type casting** – Manually casting types can break type consistency between definitions and runtime behavior.

❌ **Avoid Using a Separate Controller Class:**
```typescript
import { Elysia, type Context } from 'elysia'

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

// ❌ This adds unnecessary complexity
new Elysia()
.get('/', Controller.root)
```
### **Why?**
Using a full controller method **introduces an extra controller layer**, which complicates type inference.

✅ **Recommended Approach (Treat Elysia as the Controller):**
```typescript
import { Elysia } from 'elysia'
import { Service } from './service'

new Elysia()
.get('/', ({ stuff }) => Service.doStuff(stuff))
```
This approach maintains **type safety, simplicity, and clarity**.

---

## **Services**

A **service** in Elysia is a set of **utility/helper functions** that separate business logic from the main controller.

Elysia supports **two types of services**:
1. **Non-request dependent services** – Functions that do not require request properties.
2. **Request-dependent services** – Functions that rely on request-specific data.

✅ **Example of a Non-Request Dependent Service:**
```typescript
import { Elysia } from 'elysia'

abstract class Service {
static fibo(n: number): number {
return n < 2 ? n : Service.fibo(n - 1) + Service.fibo(n - 2)
}
}

new Elysia()
.get('/fibo', ({ body }) => Service.fibo(body), {
body: t.Numeric()
})
```
### **Why?**
If a service **does not need stored properties**, using an **abstract class with static methods** prevents unnecessary instantiation.