Action Handlers
In bun-router, "action handlers" are functions or classes that process HTTP requests and return responses. This page explains the different ways to define action handlers and how to use them effectively.
Types of Action Handlers
bun-router supports three types of action handlers:
- Inline Functions: Direct function handlers defined in your route declaration
- Action Classes: Classes with a
handle
method that process requests - String Paths: Strings that reference action classes to be loaded dynamically
Inline Function Handlers
The simplest way to define an action handler is as a function directly in your route declaration:
import { Router } from 'bun-router'
const router = new Router()
// Basic inline handler
router.get('/hello', (req) => {
return new Response('Hello, World!')
})
// Handler using request parameters
router.get('/users/{id}', (req) => {
const userId = req.params.id
return Response.json({ id: userId, name: `User ${userId}` })
})
// Async handler with database access
router.get('/products', async (req) => {
const products = await fetchProductsFromDatabase()
return Response.json(products)
})
Action Classes
For more complex handlers, you can create classes that implement the ActionHandlerClass
interface:
import type { ActionHandlerClass, EnhancedRequest } from 'bun-router'
import { Router } from 'bun-router'
// Define an action class
class UserController implements ActionHandlerClass {
async handle(req: EnhancedRequest): Promise<Response> {
const users = await this.fetchUsers()
return Response.json(users)
}
private async fetchUsers() {
// Implementation of user fetching logic
return [{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' }]
}
}
const router = new Router()
// Use the action class for a route
router.get('/users', UserController)
Using action classes has several benefits:
- Better organization for complex logic
- Ability to use dependency injection
- Reuse the same controller for multiple routes
- Separation of concerns
Dynamic Action Loading via String Paths
bun-router can dynamically load action handlers from string paths, which is particularly useful for organizing large applications:
import { Router } from 'bun-router'
const router = new Router()
// Load action from 'actions/HomeController'
router.get('/', 'HomeController')
// Load action from 'actions/users/show'
router.get('/users/{id}', 'Users/Show')
When you provide a string path, bun-router:
- Converts the path to a valid file path (e.g.,
Users/Show
becomesusers_show
) - Looks for the file in the
actions
directory - Loads the default export which should be an action class
- Instantiates the class and calls its
handle
method
Example action file (actions/users_show.ts
):
import type { ActionHandlerClass, EnhancedRequest } from 'bun-router'
import { UserService } from '../services/UserService'
export default class ShowUserAction implements ActionHandlerClass {
private userService = new UserService()
async handle(req: EnhancedRequest): Promise<Response> {
try {
const user = await this.userService.findById(req.params.id)
if (!user) {
return new Response('User not found', { status: 404 })
}
return Response.json(user)
}
catch (error) {
console.error('Error fetching user:', error)
return new Response('Internal Server Error', { status: 500 })
}
}
}
Using Action Handlers with Resources
Action handlers work especially well with resource routes:
import { Router } from 'bun-router'
const router = new Router()
// Define a complete resource with a controller path
router.resource('posts', 'PostsController')
This creates routes that map to specific methods on your controller:
HTTP Method | Path | Action Method | Action Path |
---|---|---|---|
GET | /posts | index() | PostsController/index |
GET | /posts/ | show() | PostsController/show |
POST | /posts | store() | PostsController/store |
PUT | /posts/ | update() | PostsController/update |
DELETE | /posts/ | destroy() | PostsController/destroy |
Action Handlers with Middleware
You can apply middleware to action handlers:
// Apply middleware to routes with specific action handlers
router.get('/admin/dashboard', 'Admin/DashboardController', {
middleware: [authMiddleware, adminMiddleware]
})
// Apply middleware to an entire resource
router.resource('users', 'UsersController', {
middleware: {
index: [authMiddleware, adminMiddleware],
show: [authMiddleware],
update: [authMiddleware, ownerMiddleware],
destroy: [authMiddleware, adminMiddleware]
}
})
Organizing Action Handlers
For larger applications, it's recommended to organize your action handlers using a consistent pattern:
src/
├── actions/
│ ├── home.ts
│ ├── auth/
│ │ ├── login.ts
│ │ ├── register.ts
│ │ └── logout.ts
│ ├── users/
│ │ ├── index.ts
│ │ ├── show.ts
│ │ ├── store.ts
│ │ ├── update.ts
│ │ └── destroy.ts
│ └── posts/
│ ├── index.ts
│ ├── show.ts
│ └── ...
└── ...
Single Action Classes vs. Controller Classes
bun-router supports both single-action classes and controller-style classes:
Single Action Pattern
Each file handles one specific action:
// actions/posts/show.ts
export default class ShowPostAction implements ActionHandlerClass {
async handle(req: EnhancedRequest): Promise<Response> {
// Logic to show a specific post
}
}
Controller Pattern
A single class handles multiple related actions:
// PostsController.ts
export default class PostsController {
async index(req: EnhancedRequest): Promise<Response> {
// Return list of posts
}
async show(req: EnhancedRequest): Promise<Response> {
// Show a specific post
}
async store(req: EnhancedRequest): Promise<Response> {
// Create a new post
}
// Other action methods...
}
Error Handling in Action Handlers
It's a good practice to handle errors properly in your action handlers:
router.get('/api/data', async (req) => {
try {
const data = await fetchData()
return Response.json(data)
}
catch (error) {
console.error('Error fetching data:', error)
// Return appropriate error response
return new Response(
JSON.stringify({ error: 'Failed to fetch data' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
)
}
})
Testing Action Handlers
Action handlers can be tested independently of the router:
import { expect, test } from 'bun:test'
import ShowUserAction from '../src/actions/users/show'
test('ShowUserAction returns a user when found', async () => {
// Setup
const action = new ShowUserAction()
const mockRequest = {
params: { id: '1' },
// Other request properties
} as EnhancedRequest
// Execute
const response = await action.handle(mockRequest)
const data = await response.json()
// Verify
expect(response.status).toBe(200)
expect(data.id).toBe('1')
expect(data.name).toBeDefined()
})
Best Practices
When working with action handlers:
- Use the Right Type for the Job: Choose between inline functions, classes, or string paths based on complexity
- Consistent Organization: Follow a consistent pattern for organizing your actions
- Keep Handlers Focused: Each handler should do one thing well
- Dependency Injection: Use constructor injection to make handlers testable
- Error Handling: Always handle errors and return appropriate responses
- Validation: Validate input before processing
- Logging: Include appropriate logging for debugging and monitoring
Next Steps
Now that you understand action handlers, check out these related topics:
- Route Parameters - Working with parameters in action handlers
- Middleware - Applying middleware to action handlers
- Resource Routes - Using actions with RESTful resources