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.
npm install @d3vtool/ex-frame
Before using ExFrame, ensure the following:
- TypeScript is being used.
- The following options are enabled in
tsconfig.json
:{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
Create an entry point (index.ts
) and initialize the framework:
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");
});
Create an entry point (index.ts
), initialize the framework, and configure the Dependency Injection (DI) system:
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");
});
-
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.
- The
@InjectTransient()
decorator automatically injectsTestService
as a transient dependency. - The
@InjectScoped()
decorator automatically injectsUserService
as a transient dependency. - The
@InjectSingleton()
decorator automatically injectsDBService
as a transient dependency. - The
frame.addTransient()
,frame.addScoped()
, andframe.addSingleton()
methods configure the DI container with the appropriate service lifetime.
-
A controller defines the logic for handling incoming requests. Decorators help in structuring routes, managing middleware, and handling errors.
-
Controller methods must be declared as static.
export class UsersController {
@Get("/")
@Middlewares(authMiddleware)
static getAll(req: Request, res: Response) {
res.send("Hello");
}
}
Defines a parent route for all methods inside a controller.
@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.
Handles errors within a specific route or acts as a fallback if an explicit error handler is not set.
/*
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.
Registers middleware functions for a specific route.
// 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.
Defines routes for handling different HTTP methods.
@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.
All controller methods must be static.
export class UsersController {
@Get("/:id")
static getUser(req: Request, res: Response) {
res.send("User details");
}
}
📌 Notes
- Static methods ensure proper route registration and execution.
Caches the result of a function call, preventing unnecessary recomputation.
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
Caches the result of a function for a specified time-to-live (TTL).
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
Catches errors from a function and handles them using the provided error handler.
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
Passes the return value of a function to the provided callback.
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"
Measures execution time and optionally reports it, else it will log to console.
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
Limits function execution to at most once per specified time interval.
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
Ensures that a function is executed only after a specified delay, preventing excessive calls.
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