@d3vtool/ex-frame
Version:
This library enhances Express.js by providing a more organized structure for web API projects, along with improved control and error handling capabilities.
405 lines (313 loc) • 11 kB
Markdown
# 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
```