# ExFrame

**ExFrame** is a lightweight library that streamlines web API development with Express.js by offering improved organization, error handling, middleware management, and enhanced Dependency Injection (DI) capabilities. It simplifies controller-based routing, boosts maintainability, and promotes best practices for scalable applications. By tightly integrating with Express.js, ExFrame ensures cleaner code and better separation of concerns.

---

## 📌 Installation  
```sh
npm install @d3vtool/ex-frame
```

---

- **Contents**
    - [**Getting Started**](#-getting-started)
    - [**Dependency Injection**](#-dependency-injection)
    - [**Ex-Frame Utilities**](#ex-frame-utilities)

---

## 🚀 Getting Started  

### 1️⃣ Setting Up

### ✅ Prerequisites  
Before using ExFrame, ensure the following:  
1. TypeScript is being used.  
2. The following options are enabled in `tsconfig.json`:  
   ```json
   {
     "compilerOptions": {
       "experimentalDecorators": true,
       "emitDecoratorMetadata": true
     }
   }
   ```

---

### ✅ Basic Setup  
Create an entry point (`index.ts`) and initialize the framework:  
```typescript
import { ExFrame } from "@d3vtool/ex-frame";
import express from "express";
import { UsersController } from "./src/controllers/UsersController";

const app = express();
const frame = new ExFrame(app);

frame.controller(UsersController); // Registers the controller

frame.listen(3000, () => {
    console.log("Server is running on port 3000");
});
```

---

## ✅ Dependency Injection

Create an entry point (`index.ts`), initialize the framework, and configure the Dependency Injection (DI) system:  

```typescript
import express, { type Request, type Response } from "express";
import { ExFrame, Get, InjectScoped } from "@d3vtool/ex-frame"; 

const app = express();
const frame = new ExFrame(app);

// (test-service.ts) Define Services with different lifetimes
class TestService {
    doesWork() {
        console.log("it works in test service...");
    }
}
frame.addTransient(TestService); // Adds TestService as a Transient service

// (user-service.ts)
class UserService {
    getUser(id: string) {
        console.log(`fetching data for ${id}...`);        
    }
}
frame.addScoped(UserService); // Adds UserService as a Scoped service

// (db-service.ts)
class DBService {
    static query(stmt: string) {
        console.log(`query ${stmt}...`);        
    }
}
frame.addSingleton(DBService); // Adds DBService as a Singleton service


// Controller with Dependency Injection
class UserController {
    @Get("/")
    @InjectScoped() // scoped lifetime
    static async getUser(
        // [ the variable name should be same as configuring Class name. ]
        userService: UserService,
        req: Request,
        res: Response
    ) {
        const user =  await userService.getUser(req.params.id);
        res.send({
            status: "success",
            user
        });
    }
}
frame.controller(UserController); // Registers the controller

// Start the server
frame.listen(3000, () => {
    console.log("Server is running on port 3000");
});
```

### 🛠️ Key Concepts:  
- **Transient Service:** A new instance of `TestService` is created each time it is requested.
- **Scoped Service:** A single instance of `UserService` is shared within the request's scope.
- **Singleton Service:** `DBService` is a shared instance across the entire application.
  
### 🔄 Dependency Injection:
- The `@InjectTransient()` decorator automatically injects `TestService` as a transient dependency.
- The `@InjectScoped()` decorator automatically injects `UserService` as a transient dependency.
- The `@InjectSingleton()` decorator automatically injects `DBService` as a transient dependency.
- The `frame.addTransient()`, `frame.addScoped()`, and `frame.addSingleton()` methods configure the DI container with the appropriate service lifetime.

---

## 📌 Controllers and Decorators  

### 2️⃣ Creating a Controller  

1. A controller defines the logic for handling incoming requests. Decorators help in structuring routes, managing middleware, and handling errors.

2. Controller methods must be declared as static.

### ✅ Example  
```typescript
export class UsersController {
    @Get("/")
    @Middlewares(authMiddleware)
    static getAll(req: Request, res: Response) {
        res.send("Hello");
    }
}
```
---

### 3️⃣ **@ParentRoute** (Class Decorator)  

Defines a parent route for all methods inside a controller.  

```typescript
@ParentRoute("/users") // applied to this controller only
export class UsersController {
    // All methods inside this class will be prefixed with "/users"
}
```
📌 **Notes**  
- All methods inside the class will be grouped under `/users`.  
- Methods inside the controller must be **static**.  

---

### 4️⃣ **@ErrorHandler** (Class | Method Decorator)  

Handles errors within a specific route or acts as a fallback if an explicit error handler is not set.  

```typescript
/* 
 It will also act as a fallback if some method throws error 
 and you didn't declare '@ErrorHandler' over that method, 
 then this will catch it. 
*/
@ErrorHandler(controllerErrorHandler) // applied to this controller only
export class UsersController {
    @Get("/:id")
    @ErrorHandler(routeErrorHandler) // Can be applied to specific methods
    static getUser(req: Request, res: Response) {
        throw new Error("Something went wrong");
    }
}
```
📌 **Notes**  
- If an error occurs inside `getUser`, the method-level `@ErrorHandler` will handle it.  
- If no method-level error handler is defined, the class-level handler will take over.  

---

### 5️⃣ **@Middlewares** (Class | Method Decorator)  

Registers middleware functions for a specific route.  

```typescript
// this will run for before any controller method execute.
@Middlewares(userControllerMiddleware)
export class UsersController {
    @Get("/:id")
    @Middlewares(authMiddleware) // Middleware for this specific route
    static getUser(req: Request, res: Response) {
        res.send("User details");
    }
}
```
📌 **Notes**  
- Middleware functions are executed before the route handler.  
- Multiple middleware functions can be applied.  

---

### 6️⃣ **Route Method Decorators (@Get, @Post, etc.)**  

Defines routes for handling different HTTP methods.  

```typescript
@Get("/:id")
static getUser(req: Request, res: Response) {
    res.send("User details");
}
```
📌 **Notes**  
- Decorators like `@Get`, `@Post`, `@Put`, and `@Delete` associate methods with specific HTTP methods.  

---

### 7️⃣ **Static Methods in Controllers**  

All controller methods must be static.  

```typescript
export class UsersController {
    @Get("/:id")
    static getUser(req: Request, res: Response) {
        res.send("User details");
    }
}
```
📌 **Notes**  
- Static methods ensure proper route registration and execution.

---

## **Ex-Frame Utilities**  

### ✅ `@Memoize`  
Caches the result of a function call, preventing unnecessary recomputation.  

#### Example:  
```typescript
import { Memoize } from "@d3vtool/ex-frame";

class UserService {
    @Memoize()
    static getUser(id: number) {
        console.log("Fetching user from DB...");
        return { id, name: "John Doe" };
    }
}

console.log(UserService.getUser(1)); // Fetches from DB
console.log(UserService.getUser(1)); // Returns cached result
```

---

### ✅ `@Cache(ttl: number)`  
Caches the result of a function for a specified time-to-live (TTL).  

#### Example:  
```typescript
import { Cache } from "@d3vtool/ex-frame";

class DataService {
    @Cache(5000) // Cache for 5 seconds
    static fetchData() {
        console.log("Fetching data...");
        return "data";
    }
}

console.log(DataService.fetchData()); // Fetches new data
setTimeout(() => console.log(DataService.fetchData()), 2000); // Returns cached data
setTimeout(() => console.log(DataService.fetchData()), 6000); // Fetches fresh data
```

---

### ✅ `@OnError(fn: (error: unknown) => void)`  
Catches errors from a function and handles them using the provided error handler.  

#### Example:  
```typescript
import { OnError } from "@d3vtool/ex-frame";

function logError(error: unknown) {
    console.error("Caught error:", error);
}

class ExampleService {
    @OnError(logError)
    static riskyOperation() {
        throw new Error("Something went wrong!");
    }
}

ExampleService.riskyOperation(); // Logs error instead of crashing
```

---

### ✅ `@Pipe<T>(fn: (arg: T) => void)`  
Passes the return value of a function to the provided callback.  

#### Example:  
```typescript
import { Pipe } from "@d3vtool/ex-frame";

function logOutput(data: number) {
    console.log("Processed data:", data);
}

class UsersController {
    @Pipe<number>(logOutput)
    static getUserId(username: string) {
        return 1001;
    }
}

UsersController.getUserId("Alice"); // Logs: "Processed data: 1001"
```

---

### ✅ `@MeasureTime(fn?: (time: number) => void)`  
Measures execution time and optionally reports it, else it will log to console.

#### Example:  
```typescript
import { MeasureTime } from "@d3vtool/ex-frame";

function reportTime(ms: number) {
    console.log(`Execution time: ${ms}ms`);
}

class PerformanceService {
    @MeasureTime(reportTime)
    static compute() {
        for (let i = 0; i < 1e6; i++); // Simulate work
    }
}

PerformanceService.compute(); // Logs execution time
```

---

### ✅ `@Throttle(limit: number)`  
Limits function execution to at most once per specified time interval.  

#### Example:  
```typescript
import { Throttle } from "@d3vtool/ex-frame";

class ClickHandler {
    @Throttle(2000) // Allow execution every 2 seconds
    static handleClick() {
        console.log("Button clicked!");
    }
}

// Simulate rapid clicks
ClickHandler.handleClick();
ClickHandler.handleClick(); // Ignored
setTimeout(() => ClickHandler.handleClick(), 2500); // Executed
```

---

### ✅ `@Debounce(delay: number, fn?: (data: T) => void)`  
Ensures that a function is executed only after a specified delay, preventing excessive calls.  

#### Example:  
```typescript
import { Debounce } from "@d3vtool/ex-frame";

function logSearch(data: string) {
    console.log("Searching for:", data);
}

class SearchBar {
    @Debounce(500, logSearch) // Wait 500ms before execution
    static search(query: string) {
        return query;
    }
}

// Simulate rapid typing
SearchBar.search("A");
SearchBar.search("Ap");
SearchBar.search("App"); // Executes only once after 500ms
```