/**
* Supported HTTP methods for routing.
*/
export enum METHOD {
GET = "GET",
POST = "POST",
PUT = "PUT",
PATCH = "PATCH",
DELETE = "DELETE",
}
/**
* The standard request object passed to route handlers.
* Contains parsed URL components, headers, and body.
*/
type HttpRequest = {
/** HTTP method (e.g., "GET", "POST") */
method: string;
/** The matched route pattern (e.g., "/users/:id") */
route: string;
/** The full URL path */
path: string;
/** Extracted path parameters (e.g., { id: "123" }) */
params: Record<string, string>;
/** Query string parameters (e.g., { sort: "asc" }) */
query: Record<string, string>;
/** Request headers */
headers: Record<string, string>;
/** Parsed request body */
body: any;
/** Raw request string (if applicable) */
raw_string: string | null;
};
/**
* Windmill HTTP event object, extending HttpRequest with trigger metadata.
*/
type HttpEvent = HttpRequest & {
/** Always "http" for HTTP triggers */
kind: "http";
/** The path used to trigger the script */
trigger_path: string;
};
/**
* A lightweight, chainable router for Windmill HTTP scripts.
* Supports method chaining, custom error handling, and automatic response formatting.
*/
export class Router {
private routes: Record<string, Record<string, (req: HttpRequest) => any>> = {};
/**
* Preprocessor hook for Windmill.
* Validates the event kind and prepares the request object.
* @param event - The incoming Windmill event
* @returns An object containing the normalized request
*/
preprocessor = async function (event: HttpEvent) {
if (event.kind === "http") {
return { req: event };
}
throw new Error(`Unsupported trigger kind: ${event.kind}`);
};
/**
* Normalizes HTTP method strings to uppercase for consistent routing.
*/
private normalizeMethod(method: string): string {
return method.toUpperCase();
}
/**
* Registers a GET route.
* @param route - The URL path pattern (e.g., "/users")
* @param handler - The function to execute when the route is matched
*/
get(route: string, handler: (req: HttpRequest) => any) {
return this.addRoute(METHOD.GET, route, handler);
}
/**
* Registers a POST route.
* @param route - The URL path pattern
* @param handler - The function to execute
*/
post(route: string, handler: (req: HttpRequest) => any) {
return this.addRoute(METHOD.POST, route, handler);
}
/**
* Registers a PUT route.
* @param route - The URL path pattern
* @param handler - The function to execute
*/
put(route: string, handler: (req: HttpRequest) => any) {
return this.addRoute(METHOD.PUT, route, handler);
}
/**
* Registers a PATCH route.
* @param route - The URL path pattern
* @param handler - The function to execute
*/
patch(route: string, handler: (req: HttpRequest) => any) {
return this.addRoute(METHOD.PATCH, route, handler);
}
/**
* Registers a DELETE route.
* @param route - The URL path pattern
* @param handler - The function to execute
*/
delete(route: string, handler: (req: HttpRequest) => any) {
return this.addRoute(METHOD.DELETE, route, handler);
}
/**
* Internal method to register a route.
* @param method - The HTTP method
* @param route - The path pattern
* @param handler - The handler function
* @returns The Router instance for chaining
*/
private addRoute(method: METHOD, route: string, handler: (req: HttpRequest) => any) {
if (!this.routes[method]) {
this.routes[method] = {};
}
this.routes[method][route] = handler;
return this;
}
/**
* Executes the matched handler or throws an appropriate HttpError.
* @param req - The request object
* @returns The handler's return value
* @throws HttpError if route or method is not found
*/
private executeHandler(req: HttpRequest): any {
const method = this.normalizeMethod(req.method);
const route = req.route;
if (!this.routes[method]) {
throw HttpError.MethodNotAllowed(`Method ${method} not allowed`);
}
const handler = this.routes[method][route];
if (!handler) {
throw HttpError.NotFound(`Route ${route} not found`);
}
return handler(req);
}
/**
* Main entry point for handling requests.
* Wraps execution in try-catch to standardize error responses.
* @param req - The request object
* @returns A formatted WindmillResponse object
*/
handle(req: HttpRequest): any {
try {
return this.executeHandler(req);
} catch (error) {
console.error("Router Error:", error);
if (!(error instanceof HttpError)) {
// Wrap unexpected errors in a 500 HttpError
throw HttpError.InternalError(undefined, error);
}
// Format HttpError for Windmill
return {
windmill_status_code: error.status,
result: {
statusCode: error.status,
error: error.message
}
};
}
}
}
/**
* Custom error class for HTTP status codes.
* Extends the native Error class with a status code and optional cause.
*/
export class HttpError extends Error {
status: number;
/**
* Creates a new HttpError.
* @param status - HTTP status code (e.g., 404, 500)
* @param message - Error message
* @param cause - The original error that caused this one (preserves stack trace)
*/
constructor(status: number, message: string, cause?: any) {
super(message);
this.status = status;
this.name = 'HttpError';
// Manual assignment for runtime compatibility in serverless environments
if (cause) {
(this as any).cause = cause;
}
}
static BadRequest(message = 'Bad Request', cause?: any) {
return new HttpError(400, message, cause);
}
static Unauthorized(message = 'Unauthorized', cause?: any) {
return new HttpError(401, message, cause);
}
static Forbidden(message = 'Forbidden', cause?: any) {
return new HttpError(403, message, cause);
}
static NotFound(message = 'Not found', cause?: any) {
return new HttpError(404, message, cause);
}
static MethodNotAllowed(message = 'Method not allowed', cause?: any) {
return new HttpError(405, message, cause);
}
static UnprocessableContent(message = 'Unprocessable Content', cause?: any) {
return new HttpError(422, message, cause);
}
static InternalError(message = 'Internal server error', cause?: any) {
return new HttpError(500, message, cause);
}
}
/**
* Standard Windmill HTTP response structure.
*/
type WindmillResponse = {
windmill_status_code: number;
windmill_headers?: Record<string, string>;
result: any;
};
/**
* Helper class to construct HTTP responses with convenient status codes.
* Automatically formats output for Windmill.
*/
export class HttpResponse {
public status: number;
public data: any;
public headers?: Record<string, string>;
constructor(data: any, status: number = 200, headers?: Record<string, string>) {
this.data = data;
this.status = status;
this.headers = headers;
}
/**
* Returns a 200 OK response.
*/
static Ok(data: any, headers?: Record<string, string>) {
return new HttpResponse(data, 200, headers).toWindmillResponse();
}
/**
* Returns a 201 Created response.
*/
static Created(data: any, headers?: Record<string, string>) {
return new HttpResponse(data, 201, headers).toWindmillResponse();
}
/**
* Returns a 202 Accepted response.
*/
static Accepted(data: any, headers?: Record<string, string>) {
return new HttpResponse(data, 202, headers).toWindmillResponse();
}
/**
* Returns a 204 No Content response.
*/
static NoContent(headers?: Record<string, string>) {
return new HttpResponse(null, 204, headers).toWindmillResponse();
}
private toWindmillResponse(): WindmillResponse {
return {
windmill_status_code: this.status,
windmill_headers: this.headers,
result: this.data
};
}
}
// --- Usage Example ---
const router = new Router();
router
.get("router-test", (req) => {
return HttpResponse.Ok({ message: "GET TEST" });
})
.post("router-test", (req) => {
if (true) {
throw HttpError.Forbidden();
}
return HttpResponse.NoContent();
});
// # REQUEST MIDDLEWARE
export async function preprocessor(event: HttpEvent) {
return await router.preprocessor(event);
}
// # MAIN ENTRY POINT
export async function main(req: HttpRequest) {
return router.handle(req);
}Submitted by as900 11 days ago