Flink plugin that makes it possible to override default request handlers and handle specific requests manually
npm install @flink-app/generic-request-pluginA lightweight Flink plugin that allows you to register custom request handlers with direct access to Express request and response objects. This plugin is useful when you need to bypass Flink's standard handler system and implement custom request handling logic.
- Register custom Express route handlers
- Direct access to Express req, res, and Flink app objects
- Support for all HTTP methods (GET, POST, PUT, DELETE)
- Optional authentication and permission validation
- Bypass Flink's standard validation and response handling
- Useful for webhooks, custom integrations, and special endpoints
``bash`
npm install @flink-app/generic-request-plugin
`typescript
import { FlinkApp } from "@flink-app/flink";
import { genericRequestPlugin, HttpMethod } from "@flink-app/generic-request-plugin";
function start() {
new FlinkApp
name: "My app",
plugins: [
genericRequestPlugin({
path: "/custom-endpoint",
method: HttpMethod.get,
handler: (req, res, app) => {
res.setHeader("Content-Type", "text/html");
res.end("Hello world!");
},
}),
],
}).start();
}
`
- path (required): The URL path for the endpoint (e.g., /webhook, /custom)method
- (required): HTTP method for the requesthandler
- (required): Function that handles the requestpermissions
- (optional): Permission(s) required to access this route (string or array of strings)
The plugin supports the following HTTP methods via the HttpMethod enum:
- HttpMethod.get - GET requestsHttpMethod.post
- - POST requestsHttpMethod.put
- - PUT requestsHttpMethod.delete
- - DELETE requests
The handler function receives three parameters:
`typescript`
handler: (req, res, app) => void
- req - Express Request objectres
- - Express Response objectapp
- - FlinkApp instance (provides access to context, repos, etc.)
`typescript`
genericRequestPlugin({
path: "/hello",
method: HttpMethod.get,
handler: (req, res, app) => {
res.json({ message: "Hello from Flink!" });
},
});
`typescript`
genericRequestPlugin({
path: "/webhook",
method: HttpMethod.post,
handler: (req, res, app) => {
const payload = req.body;
console.log("Received webhook:", payload);
res.status(200).json({ received: true });
},
});
`typescript`
genericRequestPlugin({
path: "/custom-data",
method: HttpMethod.get,
handler: async (req, res, app) => {
try {
const data = await app.ctx.repos.myRepo.findAll();
res.json({ data });
} catch (error) {
res.status(500).json({ error: "Failed to fetch data" });
}
},
});
`typescript`
genericRequestPlugin({
path: "/search",
method: HttpMethod.get,
handler: (req, res, app) => {
const query = req.query.q;
const results = performSearch(query);
res.json({ results });
},
});
`typescript
genericRequestPlugin({
path: "/user/:id",
method: HttpMethod.get,
handler: async (req, res, app) => {
const userId = req.params.id;
const user = await app.ctx.repos.userRepo.findById(userId);
if (!user) {
res.status(404).json({ error: "User not found" });
return;
}
res.json({ user });
},
});
`
`typescript
genericRequestPlugin({
path: "/status",
method: HttpMethod.get,
handler: (req, res, app) => {
const html =
All systems operational
;
res.setHeader("Content-Type", "text/html");
res.end(html);
},
});
`$3
`typescript
genericRequestPlugin({
path: "/webhook/stripe",
method: HttpMethod.post,
handler: async (req, res, app) => {
const signature = req.headers["stripe-signature"];
const payload = req.body; // Verify webhook signature
if (!verifyStripeSignature(payload, signature)) {
res.status(401).json({ error: "Invalid signature" });
return;
}
// Process webhook
await app.ctx.repos.orderRepo.updatePaymentStatus(payload.data);
res.status(200).json({ received: true });
},
});
`$3
`typescript
genericRequestPlugin({
path: "/download/:fileId",
method: HttpMethod.get,
handler: async (req, res, app) => {
const fileId = req.params.fileId;
const file = await app.ctx.repos.fileRepo.findById(fileId); if (!file) {
res.status(404).json({ error: "File not found" });
return;
}
res.setHeader("Content-Type", file.mimeType);
res.setHeader("Content-Disposition",
attachment; filename="${file.name}");
res.end(file.buffer);
},
});
`$3
If you need full control over authentication, you can implement it manually in your handler:
`typescript
genericRequestPlugin({
path: "/admin/action",
method: HttpMethod.post,
handler: async (req, res, app) => {
const apiKey = req.headers["x-api-key"]; // Custom authentication logic
if (apiKey !== process.env.ADMIN_API_KEY) {
res.status(401).json({ error: "Unauthorized" });
return;
}
// Perform admin action
const result = await performAdminAction(req.body);
res.json({ success: true, result });
},
});
`Authentication and Permissions
Starting with version 0.12.1-alpha.46, the generic request plugin supports automatic authentication and permission validation, just like standard Flink handlers.
$3
You must configure an auth plugin (such as
@flink-app/jwt-auth-plugin) in your FlinkApp:`typescript
import { FlinkApp } from "@flink-app/flink";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { genericRequestPlugin, HttpMethod } from "@flink-app/generic-request-plugin";const app = new FlinkApp({
name: "My app",
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
return {
id: user._id,
username: user.username,
roles: user.roles,
};
},
rolePermissions: {
admin: ["read", "write", "delete", "admin:webhooks"],
user: ["read", "write"],
},
}),
plugins: [
// Your generic request plugins here
],
});
`$3
Use the
permissions option to require authentication:`typescript
genericRequestPlugin({
path: "/webhook/admin",
method: HttpMethod.post,
permissions: "*", // Require any authenticated user
handler: (req, res, app) => {
// req.user is automatically populated with authenticated user
console.log(Webhook called by: ${req.user.username});
res.json({ success: true });
},
});
`$3
Require a specific permission:
`typescript
genericRequestPlugin({
path: "/admin/action",
method: HttpMethod.post,
permissions: "admin:webhooks", // User must have this permission
handler: (req, res, app) => {
// Only users with "admin:webhooks" permission can access
const result = performAdminAction(req.body);
res.json({ result });
},
});
`$3
Require multiple permissions (user must have ALL):
`typescript
genericRequestPlugin({
path: "/sensitive/data",
method: HttpMethod.get,
permissions: ["read", "admin:sensitive"], // User needs BOTH permissions
handler: (req, res, app) => {
const data = app.ctx.repos.sensitiveRepo.findAll();
res.json({ data });
},
});
`$3
When permissions are validated, the authenticated user is available via
req.user:`typescript
genericRequestPlugin({
path: "/webhook/process",
method: HttpMethod.post,
permissions: "webhook:process",
handler: async (req, res, app) => {
// Access authenticated user properties
const userId = req.user.id;
const username = req.user.username;
const roles = req.user.roles; // Use user info in your logic
await app.ctx.repos.auditRepo.create({
userId,
action: "webhook_processed",
payload: req.body,
});
res.json({ success: true });
},
});
`$3
The permission validation works identically to standard Flink handlers:
1. Token extraction: The auth plugin extracts the JWT token (default:
Authorization: Bearer header)
2. Token validation: Token is decoded and validated
3. Permission check: User's roles are checked against required permissions
4. User attachment: If successful, user is attached to req.user
5. Handler execution: Your handler is only called if authentication succeeds
6. Auto 401: If authentication fails, a 401 response is returned automatically$3
The auth plugin's
tokenExtractor option works with generic request handlers:`typescript
// In your FlinkApp setup
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => { / ... / },
rolePermissions: { / ... / },
tokenExtractor: (req) => {
// Allow query param tokens for webhook routes
if (req.path?.startsWith('/webhook/')) {
return req.query?.token as string || null;
}
return undefined; // Use default Bearer token
},
}),// Your generic request handler
genericRequestPlugin({
path: "/webhook/external",
method: HttpMethod.post,
permissions: "webhook:external",
handler: (req, res, app) => {
// Token can come from query param for this route
res.json({ success: true });
},
});
`$3
The auth plugin's
checkPermissions callback works with generic request handlers:`typescript
// In your FlinkApp setup
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await ctx.repos.userRepo.getById(tokenData.userId);
const permissions = await ctx.repos.permissionRepo.getUserPermissions(user._id);
return {
id: user._id,
username: user.username,
permissions, // Attach DB permissions
};
},
rolePermissions: {},
checkPermissions: async (user, routePermissions) => {
// Custom permission logic from database
return routePermissions.every(perm => user.permissions?.includes(perm));
},
}),// Your generic request handler
genericRequestPlugin({
path: "/custom/action",
method: HttpMethod.post,
permissions: ["custom:action", "premium:feature"],
handler: (req, res, app) => {
// Permissions validated against database
res.json({ success: true });
},
});
`$3
If authentication fails, the plugin automatically returns:
`json
{
"status": 401,
"error": {
"title": "Unauthorized",
"detail": "Authentication required or insufficient permissions"
}
}
`$3
If you don't specify
permissions, the route is public (no authentication required):`typescript
genericRequestPlugin({
path: "/public/webhook",
method: HttpMethod.post,
// No permissions = public route
handler: (req, res, app) => {
// Anyone can access this
res.json({ received: true });
},
});
`Multiple Endpoints
You can register multiple generic request handlers by including multiple plugin instances:
`typescript
new FlinkApp({
name: "My app",
plugins: [
genericRequestPlugin({
path: "/webhook/stripe",
method: HttpMethod.post,
handler: stripeWebhookHandler,
}),
genericRequestPlugin({
path: "/webhook/github",
method: HttpMethod.post,
handler: githubWebhookHandler,
}),
genericRequestPlugin({
path: "/status",
method: HttpMethod.get,
handler: statusHandler,
}),
],
}).start();
`When to Use This Plugin
Use the generic request plugin when you need to:
- Handle webhooks from external services
- Return non-JSON responses (HTML, XML, plain text, binary files)
- Bypass Flink's request validation
- Access low-level Express request/response objects
- Implement server-sent events (SSE)
- Handle file uploads/downloads with custom logic
- Integrate with third-party services requiring specific response formats
- Require authentication but need custom response handling
When NOT to Use This Plugin
For standard API endpoints, use Flink's built-in handler system instead:
- Standard CRUD operations - Use
FlinkHttpHandler with GET/POST/PUT/DELETE
- Endpoints requiring validation - Use Flink's schema validation
- Type-safe endpoints - Use Flink's typed handlers
- Auto-documented endpoints - Use standard handlers with the API docs pluginAccess to FlinkApp Context
The handler receives the full FlinkApp instance, giving you access to:
`typescript
handler: (req, res, app) => {
// Access repositories
const data = await app.ctx.repos.myRepo.findAll(); // Access plugins
const stripeAPI = app.ctx.plugins.stripePlugin.stripeAPI;
// Access database
const collection = app.db.collection("myCollection");
// Access any custom context properties
const config = app.ctx.config;
}
`Error Handling
Always implement proper error handling in your custom handlers:
`typescript
genericRequestPlugin({
path: "/custom",
method: HttpMethod.post,
handler: async (req, res, app) => {
try {
// Your logic here
const result = await performOperation(req.body);
res.json({ success: true, result });
} catch (error) {
console.error("Error in custom handler:", error);
res.status(500).json({
error: "Internal server error",
message: error.message
});
}
},
});
`TypeScript Support
The plugin includes TypeScript definitions. For better type safety, you can type your handlers:
`typescript
import { Request, Response } from "express";
import { FlinkApp } from "@flink-app/flink";const myHandler = async (
req: Request,
res: Response,
app: FlinkApp
) => {
// Fully typed handler
const userId: string = req.params.id;
const user = await app.ctx.repos.userRepo.findById(userId);
res.json({ user });
};
genericRequestPlugin({
path: "/user/:id",
method: HttpMethod.get,
handler: myHandler,
});
`Security Considerations
- Always validate and sanitize input data
- Use the
permissions option for routes requiring authentication
- For manual authentication, implement it carefully and consistently
- Be careful with direct database access
- Validate webhook signatures for external integrations
- Use HTTPS in production
- Rate limit sensitive endpoints
- Never expose sensitive information in error messages
- Remember: without permissions` set, your route is publicly accessibleMIT