Skip to main content

Two Registration Approaches

pb-ext provides two ways to register API routes:
  1. Manual Registration — explicit control over each route
  2. CRUD Helper — convention-based registration for resource routes
Both approaches automatically register routes to:
  • PocketBase’s router (for runtime handling)
  • The documentation registry (for OpenAPI spec generation)
You can mix both approaches in the same application. Use manual registration for custom endpoints and CRUD helpers for standard resource operations.

Manual Route Registration

Basic Usage

func registerV1Routes(router *api.VersionedAPIRouter) {
    prefix := "/api/v1"
    
    router.GET(prefix+"/todos", getTodosHandler)
    router.POST(prefix+"/todos", createTodoHandler)
    router.GET(prefix+"/todos/{id}", getTodoHandler)
    router.PATCH(prefix+"/todos/{id}", updateTodoHandler)
    router.DELETE(prefix+"/todos/{id}", deleteTodoHandler)
}

HTTP Methods

MethodFunctionTypical Use
router.GET(path, handler)Read operationsList resources, get by ID
router.POST(path, handler)Create operationsNew resource creation
router.PUT(path, handler)Full replacementReplace entire resource
router.PATCH(path, handler)Partial updateUpdate specific fields
router.DELETE(path, handler)Delete operationsRemove resources
All methods return a *VersionedRouteChain that supports middleware binding.

Middleware Binding

.Bind() — Hook Handlers

Bind PocketBase hook handlers or plain functions:
router.POST("/api/v1/todos", createTodoHandler).Bind(apis.RequireAuth())
.Bind() accepts both *hook.Handler[*core.RequestEvent] and func(*core.RequestEvent) error. Plain functions are automatically wrapped in a hook handler.

.BindFunc() — Plain Functions Only

Ergonomic alternative to .Bind() when you only have plain functions:
func requestLoggerMW(e *core.RequestEvent) error {
    start := time.Now()
    err := e.Next()
    log.Printf("[%s] %s%s", e.Request.Method, e.Request.URL.Path, time.Since(start))
    return err
}

func registerV2Routes(router *api.VersionedAPIRouter) {
    router.GET("/api/v2/time", timeHandler).BindFunc(requestLoggerMW)
}
When to use .BindFunc():
  • You have simple middleware functions (not hook handlers)
  • You want cleaner syntax without manual wrapping
  • Middleware doesn’t need hook priority or ID
.BindFunc() is only for func(*core.RequestEvent) error. Hook handlers must use .Bind().

SetPrefix Usage

Reduce repetition by setting a path prefix:
func registerV1Routes(router *api.VersionedAPIRouter) {
    router.GET("/api/v1/todos", getTodosHandler)
    router.POST("/api/v1/todos", createTodoHandler)
    router.GET("/api/v1/todos/{id}", getTodoHandler)
    router.PATCH("/api/v1/todos/{id}", updateTodoHandler)
    router.DELETE("/api/v1/todos/{id}", deleteTodoHandler)
}
SetPrefix() returns a *PrefixedRouter that automatically prepends the prefix to all paths.

CRUD Convenience Method

Basic Usage

Register all standard resource routes in one call:
func registerV1Routes(router *api.VersionedAPIRouter) {
    v1 := router.SetPrefix("/api/v1")
    
    v1.CRUD("todos", api.CRUDHandlers{
        List:   getTodosHandler,
        Create: createTodoHandler,
        Get:    getTodoHandler,
        Update: updateTodoHandler,
        Patch:  updateTodoHandler,
        Delete: deleteTodoHandler,
    }, apis.RequireAuth())
}
This registers:
MethodPathHandlerMiddleware
GET/todosListNone
POST/todosCreateRequireAuth()
GET/todos/{id}GetNone
PUT/todos/{id}UpdateRequireAuth()
PATCH/todos/{id}PatchRequireAuth()
DELETE/todos/{id}DeleteRequireAuth()
Middleware (third argument) is applied only to mutating operations: Create, Update, Patch, and Delete. Read operations (List, Get) remain public.

CRUDHandlers Structure

type CRUDHandlers struct {
    List   func(*core.RequestEvent) error // GET /resource
    Create func(*core.RequestEvent) error // POST /resource
    Get    func(*core.RequestEvent) error // GET /resource/{id}
    Update func(*core.RequestEvent) error // PUT /resource/{id}
    Patch  func(*core.RequestEvent) error // PATCH /resource/{id}
    Delete func(*core.RequestEvent) error // DELETE /resource/{id}
}
All fields are optional. Omit handlers you don’t need:
v1.CRUD("todos", api.CRUDHandlers{
    List:   getTodosHandler,
    Get:    getTodoHandler,
    // Create, Update, Patch, Delete omitted — routes not registered
})

Multiple Auth Middlewares

v1.CRUD("orders", api.CRUDHandlers{
    List:   listOrdersHandler,
    Create: createOrderHandler,
    Get:    getOrderHandler,
    Delete: deleteOrderHandler,
}, apis.RequireAuth(), rateLimitMiddleware)
Variadic arguments allow any number of middlewares.

Path Parameters with Syntax

Both manual and CRUD registration support path parameters:
// Manual
router.GET("/api/v1/users/{userID}/posts/{postID}", getPostHandler)

// CRUD (automatically uses {id})
v1.CRUD("posts", api.CRUDHandlers{
    Get: getPostHandler, // Registered as GET /posts/{id}
})
Path parameter detection:
  • AST parser extracts parameters from c.Request.PathValue("id") calls
  • Parameters are automatically marked as required: true in OpenAPI spec
  • Multiple path params are supported (e.g., /users/{userID}/posts/{postID})
// API_DESC Get a specific todo by ID
// API_TAGS Todos
func getTodoHandler(c *core.RequestEvent) error {
    todoID := c.Request.PathValue("id")
    
    collection, err := c.App.FindCollectionByNameOrId("todos")
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]any{"error": "Collection not found"})
    }
    
    record, err := c.App.FindRecordById(collection, todoID)
    if err != nil {
        return c.JSON(http.StatusNotFound, map[string]any{"error": "Todo not found"})
    }
    
    return c.JSON(http.StatusOK, map[string]any{"todo": record})
}
OpenAPI spec will include:
{
  "parameters": [
    {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {"type": "string"}
    }
  ]
}

Full Example from routes.go

Here’s the complete registration code from the demo app:
func registerV1Routes(router *api.VersionedAPIRouter) {
    // Option 1: Manual route registration (explicit control)
    prefix := "/api/v1"
    router.GET(prefix+"/todos", getTodosHandler)
    router.POST(prefix+"/todos", createTodoHandler).Bind(apis.RequireAuth())
    router.GET(prefix+"/todos/{id}", getTodoHandler)
    router.PATCH(prefix+"/todos/{id}", updateTodoHandler).Bind(apis.RequireAuth())
    router.DELETE(prefix+"/todos/{id}", deleteTodoHandler).Bind(apis.RequireAuth())
}

Comparison: Manual vs CRUD

AspectManual RegistrationCRUD Helper
VerbosityMore code, explicit pathsLess code, convention-based
FlexibilityFull control over middleware per routeMiddleware applies to all mutating ops
Best forCustom endpoints, non-standard pathsStandard resource CRUD operations
Path paramsManual {id} in path stringAutomatic {id} for Get/Update/Patch/Delete
MiddlewarePer-route via .Bind()Shared across Create/Update/Patch/Delete
Most applications use both:
  • CRUD helper for standard resources (/todos, /users, /posts)
  • Manual registration for custom endpoints (/auth/login, /stats/summary, /webhooks/stripe)

Advanced Patterns

Nested Resources

v1 := router.SetPrefix("/api/v1")

// Parent resource
v1.CRUD("users", api.CRUDHandlers{
    List: listUsersHandler,
    Get:  getUserHandler,
})

// Nested resource (manual registration required)
v1.GET("/users/{userID}/posts", listUserPostsHandler)
v1.POST("/users/{userID}/posts", createUserPostHandler).Bind(apis.RequireAuth())

Conditional Middleware

func registerRoutes(router *api.VersionedAPIRouter) {
    v1 := router.SetPrefix("/api/v1")
    
    // Public list, authenticated creation
    v1.GET("/posts", listPostsHandler)
    v1.POST("/posts", createPostHandler).Bind(apis.RequireAuth())
    
    // Admin-only routes
    v1.DELETE("/posts/{id}", deletePostHandler).Bind(
        apis.RequireAuth(),
        requireAdminMiddleware,
    )
}

Custom Resource Names

// CRUD uses the resource name as the path segment
v1.CRUD("blog-posts", api.CRUDHandlers{
    List: listPostsHandler, // GET /blog-posts
    Get:  getPostHandler,   // GET /blog-posts/{id}
})

Best Practices

1

Use SetPrefix for version isolation

Always prefix routes with /api/v1, /api/v2, etc.
2

Choose the right pattern

CRUD for standard resources, manual for custom logic.
3

Apply auth to mutating operations

Protect POST, PUT, PATCH, DELETE with middleware.
4

Extract common middleware

Define reusable middleware functions for auth, logging, rate limiting.
5

Document handlers with directives

Add // API_DESC and // API_TAGS for better OpenAPI docs.

Next Steps