Handlers
Organize HTTP request handling with Velocity handlers and Context-based handlers.
Handlers in Velocity handle HTTP requests and responses, providing a clean way to organize your application logic following the MVC pattern. Handlers use Context-based functions that receive a *router.Context and return an error.
Quick Start
Creating and using handlers in Velocity:
// internal/handlers/user_handler.go
package handlers
import (
"myapp/internal/models"
"github.com/velocitykode/velocity/pkg/router"
"github.com/velocitykode/velocity/pkg/view"
)
type UserHandler struct{}
func NewUserHandler() *UserHandler {
return &UserHandler{}
}
func (c *UserHandler) Index(ctx *router.Context) error {
users, err := models.User{}.All()
if err != nil {
return ctx.JSON(500, map[string]string{"error": "Failed to load users"})
}
view.Render(ctx.Response, ctx.Request, "users/index", view.Props{
"users": users,
})
return nil
}
func (c *UserHandler) Show(ctx *router.Context) error {
id := ctx.Param("id")
user, err := models.User{}.Find(id)
if err != nil {
return ctx.JSON(404, map[string]string{"error": "User not found"})
}
view.Render(ctx.Response, ctx.Request, "users/show", view.Props{
"user": user,
})
return nil
}Handler Structure
Basic Handler
package handlers
import (
"myapp/internal/models"
"github.com/velocitykode/velocity/pkg/auth"
"github.com/velocitykode/velocity/pkg/router"
"github.com/velocitykode/velocity/pkg/view"
)
type PostHandler struct{}
func NewPostHandler() *PostHandler {
return &PostHandler{}
}
// Show all posts
func (c *PostHandler) Index(ctx *router.Context) error {
posts, err := models.Post{}.With("User").OrderBy("created_at", "DESC").Get()
if err != nil {
return ctx.JSON(500, map[string]string{"error": "Failed to load posts"})
}
view.Render(ctx.Response, ctx.Request, "posts/index", view.Props{
"posts": posts,
})
return nil
}
// Show single post
func (c *PostHandler) Show(ctx *router.Context) error {
id := ctx.Param("id")
post, err := models.Post{}.With("User", "Comments.User").Find(id)
if err != nil {
return ctx.JSON(404, map[string]string{"error": "Post not found"})
}
view.Render(ctx.Response, ctx.Request, "posts/show", view.Props{
"post": post,
})
return nil
}
// Show create form
func (c *PostHandler) Create(ctx *router.Context) error {
view.Render(ctx.Response, ctx.Request, "posts/create", view.Props{})
return nil
}
// Store new post
func (c *PostHandler) Store(ctx *router.Context) error {
// Bind and validate request
var input struct {
Title string `json:"title"`
Body string `json:"body"`
}
if err := ctx.Bind(&input); err != nil {
return ctx.JSON(400, map[string]string{"error": "Invalid input"})
}
// Get authenticated user
user := auth.User(ctx.Request).(*models.User)
// Create post
post, err := models.Post{}.Create(map[string]any{
"title": input.Title,
"body": input.Body,
"user_id": user.ID,
})
if err != nil {
return ctx.JSON(500, map[string]string{"error": "Failed to create post"})
}
view.Location(ctx.Response, ctx.Request, fmt.Sprintf("/posts/%d", post.ID))
return nil
}
// Show edit form
func (c *PostHandler) Edit(ctx *router.Context) error {
id := ctx.Param("id")
post, err := models.Post{}.Find(id)
if err != nil {
return ctx.JSON(404, map[string]string{"error": "Post not found"})
}
view.Render(ctx.Response, ctx.Request, "posts/edit", view.Props{
"post": post,
})
return nil
}
// Update post
func (c *PostHandler) Update(ctx *router.Context) error {
id := ctx.Param("id")
post, err := models.Post{}.Find(id)
if err != nil {
return ctx.JSON(404, map[string]string{"error": "Post not found"})
}
var input struct {
Title string `json:"title"`
Body string `json:"body"`
}
if err := ctx.Bind(&input); err != nil {
return ctx.JSON(400, map[string]string{"error": "Invalid input"})
}
post.Update(map[string]any{
"title": input.Title,
"body": input.Body,
})
view.Location(ctx.Response, ctx.Request, fmt.Sprintf("/posts/%d", post.ID))
return nil
}
// Delete post
func (c *PostHandler) Destroy(ctx *router.Context) error {
id := ctx.Param("id")
post, err := models.Post{}.Find(id)
if err != nil {
return ctx.JSON(404, map[string]string{"error": "Post not found"})
}
if err := post.Delete(); err != nil {
return ctx.JSON(500, map[string]string{"error": "Failed to delete post"})
}
view.Location(ctx.Response, ctx.Request, "/posts")
return nil
}Base Handler
Create a base handler with common functionality:
// internal/handlers/base_handler.go
package handlers
import (
"github.com/velocitykode/velocity/pkg/router"
"github.com/velocitykode/velocity/pkg/view"
"github.com/velocitykode/velocity/pkg/auth"
"myapp/internal/models"
)
type BaseHandler struct{}
// JSON response helper
func (c *BaseHandler) JSON(ctx *router.Context, data interface{}, status ...int) error {
statusCode := 200
if len(status) > 0 {
statusCode = status[0]
}
return ctx.JSON(statusCode, data)
}
// Error response helper
func (c *BaseHandler) Error(ctx *router.Context, message string, status ...int) error {
statusCode := 500
if len(status) > 0 {
statusCode = status[0]
}
return ctx.JSON(statusCode, map[string]string{
"error": message,
})
}
// Not found response
func (c *BaseHandler) NotFound(ctx *router.Context) error {
return c.Error(ctx, "Resource not found", 404)
}
// Forbidden response
func (c *BaseHandler) Forbidden(ctx *router.Context) error {
return c.Error(ctx, "Access forbidden", 403)
}
// Unauthorized response
func (c *BaseHandler) Unauthorized(ctx *router.Context) error {
return c.Error(ctx, "Authentication required", 401)
}
// Redirect helper
func (c *BaseHandler) Redirect(ctx *router.Context, url string, status ...int) error {
statusCode := 302
if len(status) > 0 {
statusCode = status[0]
}
return ctx.Redirect(url, statusCode)
}
// Render with validation errors
func (c *BaseHandler) WithErrors(ctx *router.Context, errors map[string][]string) {
view.WithErrors(ctx.Response, errors)
}
// Authorization helper
func (c *BaseHandler) authorize(ctx *router.Context, action string, resource interface{}) bool {
user := auth.User(ctx.Request)
if user == nil {
return false
}
// Implement your authorization logic here
// For example, check if user can perform action on resource
return true
}
// Get authenticated user
func (c *BaseHandler) user(ctx *router.Context) *models.User {
return auth.User(ctx.Request).(*models.User)
}Resource Handlers
Velocity supports RESTful resource handlers:
// internal/handlers/api/user_handler.go
package api
import (
"strconv"
"myapp/internal/models"
"github.com/velocitykode/velocity/pkg/router"
"github.com/velocitykode/velocity/pkg/auth"
"github.com/velocitykode/velocity/pkg/validation"
)
type UserHandler struct {
BaseHandler
}
// GET /api/users
func (c *UserHandler) Index(ctx *router.Context) error {
page := ctx.Query("page")
if page == "" {
page = "1"
}
users, err := models.User{}.
With("Profile").
Paginate(page, 15)
if err != nil {
return c.Error(ctx, "Failed to load users")
}
return c.JSON(ctx, users)
}
// GET /api/users/{id}
func (c *UserHandler) Show(ctx *router.Context) error {
id := ctx.Param("id")
user, err := models.User{}.With("Profile", "Posts").Find(id)
if err != nil {
return c.NotFound(ctx)
}
return c.JSON(ctx, user)
}
// POST /api/users
func (c *UserHandler) Store(ctx *router.Context) error {
data, err := validation.Validate(ctx.Request, validation.Rules{
"name": "required|string|max:255",
"email": "required|email|unique:users,email",
"password": "required|string|min:8",
})
if err != nil {
return ctx.JSON(422, map[string]interface{}{
"errors": err.Errors(),
})
}
hashedPassword, _ := auth.Hash(data["password"].(string))
user := models.User{
Name: data["name"].(string),
Email: data["email"].(string),
Password: hashedPassword,
}
if err := user.Save(); err != nil {
return c.Error(ctx, "Failed to create user")
}
return ctx.JSON(201, user)
}
// PUT /api/users/{id}
func (c *UserHandler) Update(ctx *router.Context) error {
id := ctx.Param("id")
user, err := models.User{}.Find(id)
if err != nil {
return c.NotFound(ctx)
}
data, err := validation.Validate(ctx.Request, validation.Rules{
"name": "sometimes|string|max:255",
"email": "sometimes|email|unique:users,email," + strconv.Itoa(int(user.ID)),
})
if err != nil {
return ctx.JSON(422, map[string]interface{}{
"errors": err.Errors(),
})
}
if err := user.Update(data); err != nil {
return c.Error(ctx, "Failed to update user")
}
return c.JSON(ctx, user)
}
// DELETE /api/users/{id}
func (c *UserHandler) Destroy(ctx *router.Context) error {
id := ctx.Param("id")
user, err := models.User{}.Find(id)
if err != nil {
return c.NotFound(ctx)
}
if err := user.Delete(); err != nil {
return c.Error(ctx, "Failed to delete user")
}
ctx.Response.WriteHeader(204)
return nil
}Handler Middleware
Apply middleware to handlers:
// routes/web.go
package routes
import (
"myapp/internal/handlers"
"myapp/internal/middleware"
"github.com/velocitykode/velocity/pkg/router"
)
func WebRoutes(r *router.Router) {
// Public routes
homeHandler := &handlers.HomeHandler{}
postHandler := &handlers.PostHandler{}
r.Get("/", homeHandler.Index)
r.Get("/posts", postHandler.Index)
r.Get("/posts/{id}", postHandler.Show)
// Protected routes
r.Group(func(r *router.Router) {
r.Use(middleware.Auth)
r.Get("/posts/create", postHandler.Create)
r.Post("/posts", postHandler.Store)
r.Get("/posts/{id}/edit", postHandler.Edit)
r.Put("/posts/{id}", postHandler.Update)
r.Delete("/posts/{id}", postHandler.Destroy)
})
}Form Handling
Handle form submissions:
func (c *PostHandler) Store(ctx *router.Context) error {
// Parse form data
if err := ctx.Request.ParseForm(); err != nil {
return c.Error(ctx, "Invalid form data")
}
// Manual validation
title := strings.TrimSpace(ctx.Request.FormValue("title"))
body := strings.TrimSpace(ctx.Request.FormValue("body"))
if title == "" {
c.WithErrors(ctx, map[string][]string{
"title": {"Title is required"},
})
return nil
}
if len(body) < 10 {
c.WithErrors(ctx, map[string][]string{
"body": {"Body must be at least 10 characters"},
})
return nil
}
// Create post
post := models.Post{
Title: title,
Body: body,
UserID: c.user(ctx).ID,
}
if err := post.Save(); err != nil {
return c.Error(ctx, "Failed to create post")
}
// Flash success message
view.Flash(ctx.Response, "success", "Post created successfully")
return c.Redirect(ctx, fmt.Sprintf("/posts/%d", post.ID))
}File Uploads
Handle file uploads in handlers:
func (c *UserHandler) UpdateAvatar(ctx *router.Context) error {
// Parse multipart form
if err := ctx.Request.ParseMultipartForm(10 << 20); err != nil { // 10MB max
return c.Error(ctx, "File too large")
}
file, header, err := ctx.Request.FormFile("avatar")
if err != nil {
return c.Error(ctx, "No file uploaded")
}
defer file.Close()
// Validate file type
if !strings.HasPrefix(header.Header.Get("Content-Type"), "image/") {
return c.Error(ctx, "File must be an image")
}
// Save file
fileName := fmt.Sprintf("avatars/%d_%s", c.user(ctx).ID, header.Filename)
filePath := filepath.Join("storage", fileName)
dst, err := os.Create(filePath)
if err != nil {
return c.Error(ctx, "Failed to save file")
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
return c.Error(ctx, "Failed to save file")
}
// Update user avatar
user := c.user(ctx)
user.Update(map[string]any{
"avatar": fileName,
})
return c.JSON(ctx, map[string]string{
"message": "Avatar updated successfully",
"avatar": fileName,
})
}Request/Response Helpers
Useful helpers for handlers:
// Get query parameters
func (c *BaseHandler) getQuery(ctx *router.Context, key string, defaultValue ...string) string {
value := ctx.Query(key)
if value == "" && len(defaultValue) > 0 {
return defaultValue[0]
}
return value
}
// Get form value
func (c *BaseHandler) getForm(ctx *router.Context, key string, defaultValue ...string) string {
value := ctx.Request.FormValue(key)
if value == "" && len(defaultValue) > 0 {
return defaultValue[0]
}
return value
}
// Parse JSON body
func (c *BaseHandler) parseJSON(ctx *router.Context, v interface{}) error {
return ctx.Bind(v)
}
// Get client IP
func (c *BaseHandler) getClientIP(ctx *router.Context) string {
forwarded := ctx.Request.Header.Get("X-Forwarded-For")
if forwarded != "" {
return strings.Split(forwarded, ",")[0]
}
return ctx.Request.RemoteAddr
}Error Handling
Centralized error handling:
// internal/handlers/error_handler.go
package handlers
import (
"github.com/velocitykode/velocity/pkg/router"
"github.com/velocitykode/velocity/pkg/view"
"github.com/velocitykode/velocity/pkg/log"
)
type ErrorHandler struct{}
func (c *ErrorHandler) NotFound(ctx *router.Context) error {
ctx.Response.WriteHeader(404)
view.Render(ctx.Response, ctx.Request, "errors/404", view.Props{
"url": ctx.Request.URL.Path,
})
return nil
}
func (c *ErrorHandler) InternalError(ctx *router.Context, err error) error {
log.Error("Internal server error", "error", err, "url", ctx.Request.URL.Path)
ctx.Response.WriteHeader(500)
view.Render(ctx.Response, ctx.Request, "errors/500", view.Props{
"error": err.Error(),
})
return nil
}
func (c *ErrorHandler) Forbidden(ctx *router.Context) error {
ctx.Response.WriteHeader(403)
view.Render(ctx.Response, ctx.Request, "errors/403", view.Props{})
return nil
}Testing Handlers
Test your handlers:
// internal/handlers/user_handler_test.go
package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"net/url"
"strings"
"github.com/stretchr/testify/assert"
"myapp/internal/models"
"github.com/velocitykode/velocity/pkg/router"
)
func TestUserHandler_Index(t *testing.T) {
// Setup test database
setupTestDB()
defer teardownTestDB()
// Create test user
user := models.User{
Name: "Test User",
Email: "test@example.com",
}
user.Save()
// Create request and response recorder
req := httptest.NewRequest("GET", "/users", nil)
w := httptest.NewRecorder()
// Create context
ctx := &router.Context{
Request: req,
Response: w,
}
// Call handler
handler := &UserHandler{}
err := handler.Index(ctx)
// Assert response
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "Test User")
}
func TestUserHandler_Store(t *testing.T) {
setupTestDB()
defer teardownTestDB()
// Create form data
form := url.Values{}
form.Add("name", "New User")
form.Add("email", "new@example.com")
req := httptest.NewRequest("POST", "/users", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
// Create context
ctx := &router.Context{
Request: req,
Response: w,
}
// Call handler
handler := &UserHandler{}
err := handler.Store(ctx)
// Assert response
assert.NoError(t, err)
assert.Equal(t, http.StatusFound, w.Code)
// Verify user was created
user, err := models.User{}.FindBy("email", "new@example.com")
assert.NoError(t, err)
assert.Equal(t, "New User", user.Name)
}Best Practices
- Keep handlers thin - Move business logic to models or services
- Use base handler - Share common functionality across handlers
- Validate input - Always validate user input before processing
- Handle errors gracefully - Provide meaningful error messages
- Use middleware - Apply cross-cutting concerns like authentication
- Return appropriate status codes - Use correct HTTP status codes
- Test handlers - Write unit tests for handler methods
- Separate concerns - Keep API and web handlers separate
