Extending Jetpath: Plugins
Plugins are the primary way to extend Jetpath’s core functionality, promote code reuse, and encapsulate complex or shared logic, such as authentication, database interactions, file handling, logging, or connections to third-party services.
What are Plugins?
Think of plugins as self-contained modules that can:
- Initialize Resources: Set up database connections, configure API clients, read configuration, etc., when the application starts.
- Expose Functionality: Add new methods and properties to the
ctx.plugins
object, making them easily accessible within your middleware and route handlers. - Manage Dependencies: Encapsulate dependencies needed by the plugin’s functionality (as discussed in Dependency Injection).
Using Plugins
Integrating existing plugins (whether official Jetpath plugins or community-created ones) is straightforward.
1. Installation
Install the plugin package using your preferred package manager:
# Example installing an official file upload plugin
npm install @jetpath/plugin-busboy
# or
bun add @jetpath/plugin-busboy
# or add via import map/URL for Deno
2. Registration
Instantiate the plugin (if necessary, passing configuration options) and register it with your Jetpath application instance using app.use()
. Registration typically happens in your main server file (server.ts
).
// server.ts
import { Jetpath } from "jetpath";
// Assuming jetbusboy is the exported plugin factory/instance
import { jetbusboy } from "@jetpath/plugin-busboy";
import { createAuthPlugin } from "./plugins/authPlugin"; // Your custom auth plugin
const app = new Jetpath({ source: "./src" });
// Instantiate and register plugins
// Official plugin for multipart/form-data handling
app.use(jetbusboy);
// Custom authentication plugin (example)
const authPlugin = createAuthPlugin({ /* options like JWT secret */ });
app.use(authPlugin);
app.listen();
[cite: Registration pattern shown in tests/app.jet.ts]
Important: Plugins are generally executed/initialized in the order they are registered with app.use()
.
3. Accessing Plugin Functionality
Once registered, the methods and properties returned by the plugin’s executor
function become available on the ctx.plugins
object within middleware and route handlers.
// In a route handler or middleware
import type { JetRoute, JetMiddleware } from "jetpath";
// Import types exposed by plugins if available
import type { JetBusBoyType } from "@jetpath/plugin-busboy";
import type { AuthPluginAPI } from "./plugins/authPlugin";
// Use generics to type ctx.plugins
type HandlerPlugins = [JetBusBoyType, AuthPluginAPI];
export const POST_upload: JetRoute<{}, HandlerPlugins> = async (ctx) => {
// Access file upload functionality from jetbusboy plugin
const formData = await ctx.plugins.formData(ctx);
const image = formData.image;
// … process image …
ctx.<span class="hljs-title function_">send</span>({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Upload processed"</span> });
};
export const GET_profile: JetRoute<{}, HandlerPlugins> = (ctx) => {
// Access auth functionality from authPlugin
const authResult = ctx.plugins.verifyAuth(ctx); // Example method name
if (!authResult.authenticated) {
ctx.send("Not authenticated", 402);
}
ctx.send({ user: authResult.user });
};
[cite: Usage pattern ctx.plugins.methodName()
shown in tests/app.jet.ts]
Creating Plugins
Creating your own plugins allows you to structure reusable logic cleanly.
The executor
Function
- Purpose: This function is the core of your plugin. It’s executed when
app.use(yourPluginInstance)
is called. - Initialization: Use the
executor
to perform any setup required by your plugin (e.g., establish database connections, initialize SDKs, read configuration). It can beasync
if needed. - Return Value: The
executor
must return an object. The properties and methods of this returned object are merged into thectx.plugins
object, forming the public API of your plugin. - Dependency Scope: Variables defined outside the returned object but within the
executor
’s scope (or the factory function’s scope, likeprefix
anddbClient
in the examples) act as private state or encapsulated dependencies for your plugin’s public methods.
Example: Simplified Auth Plugin Structure
This mirrors the authPlugin
structure seen in tests/app.jet.ts
.
import { JetPlugin } from "jetpath";
import type { JetContext } from "jetpath";
// Define the API exposed by this plugin
export interface AuthPluginAPI {
verifyAuth: (ctx: JetContext) => { authenticated: boolean; user?: any; message?: string };
isAdmin: (ctx: JetContext) => boolean;
}
// Define configuration options
interface AuthPluginOptions {
jwtSecret: string;
adminApiKey: string;
}
export function createAuthPlugin(options: AuthPluginOptions): JetPlugin {
// Dependencies are configured here and accessible within the executor's returned methods
const JWT_SECRET = options.jwtSecret;
const ADMIN_API_KEY = options.adminApiKey;
// In-memory store or DB connection could be initialized here
return ({
executor(): AuthPluginAPI {
// Return the methods that handlers will call via ctx.plugins
return {
<span class="hljs-title function_">verifyAuth</span>(<span class="hljs-params"><span class="hljs-attr">this</span>: <span class="hljs-title class_">JetContext</span></span>) {
<span class="hljs-comment">// ? the this here is the ctx of the api request, hence you have access to the entire context;</span>
<span class="hljs-keyword">const</span> authHeader = <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">"authorization"</span>);
<span class="hljs-comment">// ... logic to validate token using JWT_SECRET ...</span>
<span class="hljs-keyword">if</span> (<span class="hljs-comment">/* valid token */</span>) {
<span class="hljs-comment">// const user = findUserFromToken(...);</span>
<span class="hljs-keyword">return</span> { <span class="hljs-attr">authenticated</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">user</span>: { <span class="hljs-attr">id</span>: <span class="hljs-string">'...'</span>, <span class="hljs-attr">role</span>: <span class="hljs-string">'...'</span> } };
}
<span class="hljs-keyword">return</span> { <span class="hljs-attr">authenticated</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">"Invalid token"</span> };
},
<span class="hljs-title function_">isAdmin</span>(<span class="hljs-params"><span class="hljs-attr">this</span>: <span class="hljs-title class_">JetContext</span></span>) {
<span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">"x-admin-key"</span>) === <span class="hljs-variable constant_">ADMIN_API_KEY</span>) {
<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
<span class="hljs-keyword">const</span> auth = <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">verifyAuth</span>(ctx); <span class="hljs-comment">// Can call other plugin methods</span>
<span class="hljs-keyword">return</span> auth.<span class="hljs-property">authenticated</span> && auth.<span class="hljs-property">user</span>?.<span class="hljs-property">role</span> === <span class="hljs-string">"admin"</span>;
}
};
}
});
}
[cite: Based on authPlugin
structure in tests/app.jet.ts]
Plugin Lifecycle
- Instantiation: You create an instance of your plugin, potentially passing configuration options.
- Registration: You call
app.use(pluginInstance)
. - Execution: The plugin’s
executor
function runs during theapp.use()
call. Any asynchronous operations within theexecutor
should complete before the server starts fully listening or handling requests (depending on Jetpath’s internal handling, usuallyapp.listen
awaits plugin initialization implicitly or explicitly). - Runtime: The methods returned by the
executor
are available onctx.plugins
for every incoming request handled after the plugin was registered.
Best Practices
- Single Responsibility: Design plugins to handle a specific concern (authentication, database access, specific API client).
- Clear API: Define a clear and well-typed interface for the functionality your plugin exposes.
- Configuration: Allow configuration via options passed during instantiation rather than relying solely on global environment variables within the plugin.
- Asynchronous Initialization: Handle connections and other async setup correctly within the
executor
usingasync/await
. - Documentation: Document your plugin’s configuration options and the methods it provides on
ctx.plugins
.
Next Steps
- Understand how plugin methods are accessed via the Context (
ctx
) Object.