Skip to main content

Architecture

EdgeMaster is built with a layered architecture specifically designed for edge computing environments. This guide explores the internal design, request flow, and key architectural decisions.


Design Philosophy

EdgeMaster follows these core principles:

1. Zero Dependencies

  • No runtime dependencies for minimal bundle size (~14 KB)
  • Only platform APIs (Web APIs) for maximum compatibility
  • Dev dependencies only for testing and building

2. Edge-First Design

  • Optimized for Cloudflare Workers' execution model
  • Fast cold starts (<1ms)
  • Minimal memory footprint
  • CPU-time optimized operations

3. Type Safety First

  • Full TypeScript support with strict typing
  • Generic types for extensibility
  • Complete IntelliSense support

4. Composability

  • Small, focused components that work together
  • Reusable tasks, interceptors, and matchers
  • Functional composition patterns

5. Production Ready

  • Built-in authentication, rate limiting, caching
  • Error handling and recovery
  • Observability hooks

System Architecture

EdgeMaster uses a layered architecture with clear separation of concerns:

┌─────────────────────────────────────────┐
│ Application Layer │
│ (Your Routes & Business Logic) │
└───────────────┬─────────────────────────┘

┌───────────────▼─────────────────────────┐
│ EdgeController │
│ (Routing Engine & Orchestrator) │
└───────────────┬─────────────────────────┘

┌───────┴────────┐
│ │
┌───────▼─────┐ ┌──────▼────────┐
│ Interceptors│ │ Route Matcher │
│ (Middleware)│ │ (Patterns) │
└───────┬─────┘ └──────┬────────┘
│ │
│ ┌───────▼─────┐
│ │RouteHandler │
│ └───────┬─────┘
│ │
│ ┌───────▼─────┐
│ │ Tasks │
│ │ (Execution) │
│ └─────────────┘

┌───────▼─────────────────────────────────┐
│ Helper Layer │
│ (Request/Response/Context Helpers) │
└─────────────────────────────────────────┘

Core Components

1. EdgeController

Location: src/EdgeController.ts:11

The EdgeController is the heart of EdgeMaster - it's the main orchestrator that manages routing, interceptors, and request lifecycle.

Responsibilities

  • Route Management: Add, organize, and match routes
  • Request Orchestration: Coordinate request/response flow
  • Interceptor Management: Execute middleware pipeline
  • Error Handling: Global error and 404 handling
  • Context Management: Maintain request context and state

Key Methods

class EdgeController {
// Route management
addRoute(matcher: IMatcher, handler: IRouteHandler, priority?: number): EdgeController
GET(path: string, handler: IRouteHandler): EdgeController
POST(path: string, handler: IRouteHandler): EdgeController
PUT(path: string, handler: IRouteHandler): EdgeController
DELETE(path: string, handler: IRouteHandler): EdgeController

// Interceptors (middleware)
addInterceptor(interceptor: IInterceptor): EdgeController

// Error handlers
onNotFound(handler: (ctx: ContextWithReq) => Promise<Response>): EdgeController
onError(handler: (error: Error, ctx: Context) => Promise<Response>): EdgeController

// Route grouping
group(prefix: string, callback: (controller: EdgeController) => void): EdgeController

// Request handling
handleRequest(args: RequestHandlerArgs): Promise<Response>
}

Internal State

class EdgeController {
private _reqInterceptors: IRequestInterceptor[] // Request middleware
private _resInterceptors: IResponseInterceptor[] // Response middleware
private _routes: Route[] // Registered routes
private _notFoundHandler?: Function // Custom 404 handler
private _errorHandler?: Function // Custom error handler
}

Request Lifecycle

The request flows through multiple stages in EdgeMaster:

┌─────────────────────────────────────────────┐
│ 1. Incoming Request │
│ fetch(request, env, ctx) │
└──────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────┐
│ 2. Context Initialization │
│ • Create Context object │
│ • Initialize state Map() │
│ • Set req, env, ctx │
└──────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────┐
│ 3. Request Interceptors (Sequential) │
│ • JWT/API Key Auth │
│ • Cache Check (may short-circuit) │
│ • Rate Limiting (may short-circuit) │
│ • Request Logging │
│ • Custom interceptors │
└──────────────┬──────────────────────────────┘

┌─────────┴──────────┐
│ Responder Set? │
│ (Short-circuit) │
└────┬──────────┬────┘
│ Yes │ No
│ │
▼ ▼
┌────────┐ ┌─────────────────────────────┐
│ Return │ │ 4. Route Matching │
│Response│ │ • Priority-based matching │
└────────┘ │ • Custom matchers │
│ • Pattern matching │
└──────────┬──────────────────┘

┌─────────┴──────────┐
│ Route Found? │
└────┬─────────┬─────┘
│ Yes │ No
│ │
▼ ▼
┌─────────┐ ┌────────────────┐
│Execute │ │ 404 Handler │
│Handler │ │ (or default) │
└────┬────┘ └────┬───────────┘
│ │
└─────┬─────┘


┌─────────────────────────────┐
│ 5. RouteHandler Execution │
│ • Execute task chain │
│ • Conditional execution │
│ • Task composition │
└──────────┬──────────────────┘


┌─────────────────────────────┐
│ 6. Task Chain Execution │
│ • when() condition check │
│ • do() main logic │
│ • doThen() post-processing│
└──────────┬──────────────────┘


┌─────────────────────────────┐
│ 7. Response Generated │
└──────────┬──────────────────┘


┌─────────────────────────────┐
│ 8. Response Interceptors │
│ • Cache Store │
│ • CORS Headers │
│ • Response Logging │
│ • Custom interceptors │
└──────────┬──────────────────┘


┌─────────────────────────────┐
│ 9. Final Response │
│ Return to Worker │
└─────────────────────────────┘

Detailed Flow

Stage 1-2: Initialization

export default {
fetch: (request: Request, env: any, ctx: ExecutionContext) => {
return app.handleRequest({
req: request,
env: env,
ctx: ctx
});
}
};

Stage 3: Request Interceptors

// Sequential execution, each interceptor can:
// 1. Modify the request
// 2. Set a responder to short-circuit
// 3. Add data to context.state

for (const interceptor of this._reqInterceptors) {
req = await interceptor.intercept({ ...ctx, reqCtx: { req } });
}

Stage 4-5: Route Matching & Handler Execution

// Find first matching route (priority-based)
const route = this._routes.find(r => r.matcher(ctx.req));

if (route) {
return route.routeHandler.execute(ctx);
}

Stage 6: Task Execution

// Tasks execute in sequence with conditional logic
for (const task of tasks) {
if (!task.when || await task.when(ctx)) {
const result = await task.do(ctx);
if (task.doThen) await task.doThen(result, ctx);
}
}

Stage 8: Response Interceptors

// Transform the response
for (const interceptor of this._resInterceptors) {
res = await interceptor.intercept({ ...ctx, res });
}

Interceptor Pattern (Middleware)

Interceptors implement the Chain of Responsibility pattern with two types:

Request Interceptors

Execute before route handling:

interface IRequestInterceptor {
type: InterceptorType.Request;
intercept(ctx: Context): Promise<Request>;
responder?: (ctx: Context) => Promise<Response>; // Short-circuit
}

Use Cases:

  • Authentication (JWT, API keys)
  • Rate limiting
  • Cache checking
  • Request logging
  • Request validation

Response Interceptors

Execute after route handling:

interface IResponseInterceptor {
type: InterceptorType.Response;
intercept(ctx: ContextWithRes): Promise<Response>;
}

Use Cases:

  • CORS headers
  • Cache storage
  • Response logging
  • Security headers
  • Response compression

Short-Circuiting

Request interceptors can short-circuit the pipeline by setting a responder:

{
intercept: async (ctx) => ctx.req,
responder: async (ctx) => {
// Return early (e.g., from cache or rate limit)
return new Response('Rate limited', { status: 429 });
}
}

Route Matching System

EdgeMaster uses a priority-based matching system:

Matcher Interface

type IMatcher = (req: Request) => boolean;

Built-in Matchers

// HTTP method matching
httpMethod('GET')

// Path matching
pathStartWith('/api')
urlStartWith('https://example.com/api')

// Pattern matching (with parameters)
routePattern('/users/:id')

// Logical combinators
and(httpMethod('POST'), pathStartWith('/api'))
or(pathStartWith('/v1'), pathStartWith('/v2'))

Priority System

Routes are sorted by priority (higher first):

app.addRoute(matcher, handler, 100);  // High priority
app.addRoute(matcher, handler, 50); // Medium priority
app.addRoute(matcher, handler, 0); // Default priority

Example:

// Specific route (high priority)
app.addRoute(
and(httpMethod('GET'), routePattern('/users/me')),
meHandler,
100
);

// General route (low priority)
app.addRoute(
and(httpMethod('GET'), routePattern('/users/:id')),
userHandler,
0
);

Task-Based Execution

Tasks are the atomic units of work in EdgeMaster, implementing a Strategy Pattern:

interface ITask {
when?: (ctx: ContextWithReq) => Promise<boolean>; // Condition
do: (ctx: ContextWithReq) => Promise<Response>; // Main logic
doThen?: (res: Response, ctx: ContextWithReq) => Promise<void>; // Post-process
}

Task Composition

// Single task
new Task({
do: async () => json({ message: 'Hello' })
});

// Conditional task
new Task({
when: async ({ req }) => req.headers.get('x-api-key') === 'valid',
do: async () => json({ authorized: true })
});

// Task with post-processing
new Task({
do: async ({ req }) => {
const user = await getUser(req);
return json({ user });
},
doThen: async (res, ctx) => {
// Log or perform side effects
console.log('User fetched:', await res.json());
}
});

Multiple Tasks per Route

app.GET('/api/process', new RouteHandler(
new Task({
do: async ({ req }) => {
// Validate request
const isValid = await validate(req);
if (!isValid) return badRequest('Invalid input');
}
}),
new Task({
do: async ({ req }) => {
// Process request
const result = await process(req);
return json({ result });
}
})
));

Context & State Management

Context Object

The context is passed through the entire request lifecycle:

interface Context {
reqCtx: { req: Request };
env?: any; // Cloudflare bindings
ctx?: ExecutionContext; // Workers execution context
state: Map<string, any>; // Shared state
}

State Management

Share data between interceptors and handlers:

// In interceptor
setState(ctx, 'user', { id: 123, role: 'admin' });

// In handler
app.GET('/api/me', new RouteHandler(new Task({
do: async (ctx) => {
const user = getState(ctx, 'user');
return json({ user });
}
})));

Helper Functions:

  • setState(ctx, key, value) - Set state
  • getState(ctx, key) - Get state
  • hasState(ctx, key) - Check state
  • deleteState(ctx, key) - Remove state
  • clearState(ctx) - Clear all state

Design Patterns

EdgeMaster uses several well-established design patterns:

1. Chain of Responsibility

  • Interceptors process requests/responses in sequence
  • Each can pass, modify, or short-circuit the chain

2. Strategy Pattern

  • Tasks encapsulate different execution strategies
  • Matchers implement different matching strategies
  • Interceptors implement different middleware strategies

3. Builder/Fluent Interface

  • Method chaining for route definition
  • app.GET().POST().PUT()

4. Factory Pattern

  • Helper functions create configured interceptors
  • corsInterceptor(), jwtInterceptor(), etc.

5. Template Method

  • Task's when/do/doThen structure
  • Interceptor's intercept method

6. Dependency Injection

  • Context passed to all components
  • Environment and bindings injected

Performance Characteristics

Bundle Size

  • Core: ~14 KB minified
  • With interceptors: ~20 KB minified
  • Zero dependencies: No external code

Execution Speed

  • Cold start: <1ms
  • Route matching: O(n) with early exit
  • Interceptor overhead: ~0.1ms per interceptor
  • Context creation: ~0.05ms

Memory Usage

  • Base memory: ~2 MB
  • Per request: ~100 KB
  • Context state: Dynamic (Map-based)

Scalability

  • Routes: Tested up to 1000+ routes
  • Interceptors: Recommended <10 for optimal performance
  • Concurrent requests: Limited only by Workers platform

Edge Computing Optimizations

1. No Node.js APIs

  • Only Web APIs for edge compatibility
  • No fs, path, buffer, etc.

2. Minimal Allocations

  • Reuse objects where possible
  • Efficient context passing

3. Async/Await Throughout

  • Non-blocking operations
  • Optimized for Workers async model

4. CPU Time Optimization

  • Early exit conditions
  • Lazy evaluation
  • Minimal synchronous computation

5. Memory Efficiency

  • Small core footprint
  • No global state
  • Garbage collector friendly

Comparison with Traditional Frameworks

vs Express.js

FeatureEdgeMasterExpress
TargetEdge/CloudflareNode.js
Bundle Size14 KB200 KB+
Dependencies0Many
Cold Start<1ms100-500ms
MiddlewareInterceptorsMiddleware
RoutingPriority-basedOrder-based
TypeScriptNativeVia @types

vs Hono

FeatureEdgeMasterHono
ArchitectureTask-basedHandler-based
Complexity HandlingBuilt for scaleSimple apps
State ManagementContext.state MapContext variables
Built-in FeaturesAuth, cache, rate limitBasic routing
InterceptorsRequest/Response splitSingle middleware

Extensibility

Custom Matchers

const customMatcher: IMatcher = (req: Request) => {
return req.headers.get('x-custom') === 'value';
};

app.addRoute(customMatcher, handler);

Custom Interceptors

const customInterceptor: IRequestInterceptor = {
type: InterceptorType.Request,
intercept: async (ctx) => {
// Modify request
const req = new Request(ctx.reqCtx.req);
req.headers.set('x-processed', 'true');
return req;
}
};

app.addInterceptor(customInterceptor);

Custom Tasks

class ValidationTask implements ITask {
constructor(private schema: Schema) {}

async do(ctx: ContextWithReq): Promise<Response> {
const body = await parseJSON(ctx.req);
if (!this.schema.validate(body)) {
return badRequest('Validation failed');
}
return undefined; // Continue to next task
}
}

Best Practices

1. Keep Interceptors Lightweight

// ✅ Good: Fast interceptor
const auth = jwtInterceptor({ secret: env.JWT_SECRET });

// ❌ Bad: Heavy computation in interceptor
const heavy = {
intercept: async (ctx) => {
await heavyComputation(); // Slows down all requests
return ctx.req;
}
};

2. Use Priority for Route Specificity

// More specific routes get higher priority
app.addRoute(exactMatcher, handler, 100);
app.addRoute(wildcardMatcher, handler, 0);

3. Leverage Context State

// Share data between interceptors and handlers
setState(ctx, 'requestId', generateId());
setState(ctx, 'user', authenticatedUser);

4. Handle Errors Gracefully

app.onError(async (error, ctx) => {
console.error('Error:', error);
return json({ error: error.message }, { status: 500 });
});

Next Steps

Now that you understand the architecture:


Architecture matters! Understanding EdgeMaster's design helps you build better edge applications.