Building an Extensible HTTP Response Plugin System with TypeScript
Building an Extensible HTTP Response Plugin System with Bun
In modern web development, having a flexible and maintainable way to modify HTTP responses is crucial. In this blog post, we'll explore how to build a plugin system that allows you to easily extend and modify HTTP responses in a Bun application using TypeScript.
Understanding the Plugin Architecture
Our plugin system consists of four main components:
- Plugin Interface
- Plugin Implementation
- Plugin Loader
- Plugin Executor
Let's dive into each component and understand how they work together.
Folder Structure
๐ฆroot โโโ ๐plugins โ โโโ ๐add-server-info.ts โ โโโ ๐add-timestamp.ts โโโ ๐types โ โโโ ๐response.ts โโโ ๐utils โ โโโ ๐plugin-loader.ts โ โโโ ๐response-plugin-execute.ts โโโ ๐index.ts
The Plugin Interface
First, we define a ResponsePlugin
interface that all plugins must implement:
1// types/response.ts 2interface ResponsePlugin { 3 name: string; 4 onLoad(): void; 5 execute<T>(data: T): Promise<T>; 6} 7
This interface ensures that each plugin has:
- A unique name for identification
- An
onLoad
method for initialization - An
execute
method that processes the response data
Creating Plugins
Let's look at two example plugins that demonstrate different use cases:
1. Server Info Plugin
1// plugins/add-server-info.ts 2const AddServerInfo: ResponsePlugin = { 3 name: "add-server-info-plugin", 4 onLoad() { 5 console.log(`${this.name} has been loaded`); 6 }, 7 async execute(data) { 8 if (typeof data === "object") { 9 (data as any).serverInfo = { 10 name: "Axole's sever", 11 version: "v1.0.0", 12 os: "Windows 11", 13 }; 14 } 15 return data; 16 }, 17}; 18
This plugin adds server information to the response object, useful for debugging or client-side feature detection.
2. Timestamp Plugin
1// plugins/add-timestamp.ts 2const AddTimestampPlugin: ResponsePlugin = { 3 name: "add-timestamp-plugin", 4 onLoad() { 5 console.log(`${this.name} has been loaded`); 6 }, 7 async execute<T>(data: T): Promise<T> { 8 (data as any).timestamp = new Date().toISOString(); 9 return data; 10 }, 11}; 12
This plugin adds a timestamp to each response, which can be useful for caching or tracking purposes.
The Plugin Loader
The plugin loader is responsible for dynamically loading plugins from the filesystem:
1// utils/plugin-loader.ts 2export const loadPlugins = async (): Promise<ResponsePlugin[]> => { 3 const plugins: ResponsePlugin[] = []; 4 const pluginsFolder = path.join(process.cwd(), "plugins"); 5 const pluginsFiles_ = await readdir(pluginsFolder); 6 7 const validFiles = pluginsFiles_.filter( 8 (f) => f.endsWith(".ts") || f.endsWith(".js") 9 ); 10 11 for (const file of validFiles) { 12 const plugin = await validatePlugin(path.join(pluginsFolder, file)); 13 plugins.push(plugin); 14 } 15 16 return plugins; 17}; 18
Validator
1// utils/plugin-loader.ts 2const validatePlugin = async (pluginPath: string) => { 3 const pluginModule = await import(pluginPath); 4 const plugin: ResponsePlugin = pluginModule.default; 5 try { 6 if (!("name" in plugin)) throw new Error("Plugin name not defined."); 7 if (!("onLoad" in plugin)) throw new Error("Plugin onLoad not defined."); 8 if (!("execute" in plugin)) throw new Error("Plugin execute not defined."); 9 10 plugin.onLoad(); 11 12 return plugin; 13 } catch (e) { 14 console.error( 15 `Error loading plugin ${plugin?.name} on file: ${pluginPath}: `, 16 (e as Error).message 17 ); 18 process.exit(1); 19 } 20}; 21
The loader:
- Reads the plugins directory
- Filters for TypeScript and JavaScript files
- Validates each plugin
- Returns an array of loaded plugins
Plugin Execution
The execution of plugins is handled by a simple but powerful executor:
1// utils/response-plugin-execute.ts 2const executeResponsePlugins = async <T>(data: T): Promise<T> => { 3 for (const plugin of plugins) { 4 data = await plugin.execute(data); 5 } 6 return data; 7}; 8
This executor:
- Takes any type of data as input
- Runs each plugin's execute method sequentially
- Returns the modified data
Usage with Bun
1// index.ts 2import { executeResponsePlugins } from "./utils/response-plugin-execute"; 3 4Bun.serve({ 5 routes: { 6 "/api/greet/:name/:surname": { 7 GET: async (req) => { 8 // Obviously would validate these 9 const data = { 10 name: req.params.name, 11 surname: req.params.surname, 12 }; 13 14 const transformedData = await executeResponsePlugins(data); 15 return Response.json(transformedData); 16 }, 17 }, 18 }, 19}); 20
Benefits of This Architecture
- Modularity: Each plugin is a self-contained unit that can be easily added or removed
- Type Safety: TypeScript ensures plugins implement the correct interface
- Flexibility: Plugins can modify any type of response data
- Dynamic Loading: Plugins are loaded automatically from the plugins directory
- Easy to Extend: Adding new plugins is as simple as creating a new file in the plugins directory
Best Practices
When implementing this plugin system, consider these best practices:
- Plugin Naming: Use descriptive names that indicate the plugin's purpose
- Error Handling: Always validate plugin data and handle errors gracefully
- Plugin Order: Consider the order of plugin execution, as it can affect the final result
- Type Safety: Use TypeScript generics to ensure type safety throughout the plugin chain
- Documentation: Document each plugin's purpose and any side effects it may have
Conclusion
This plugin system provides a solid foundation for building extensible HTTP applications in Bun. It's flexible enough to handle various use cases while maintaining code organization and type safety.
You can extend this system further by:
- Adding plugin configuration options
- Implementing plugin dependencies
- Adding plugin lifecycle hooks
- Creating plugin groups or categories
The complete code for this plugin system is available in the repository, and you can use it as a starting point for your own projects.
Additional Resources
Happy coding! ๐
Subscribe to my newsletter
Get notified when I publish new articles. No spam, just quality content.
We won't send you spam. Unsubscribe at any time. Powered by Kit