Skip to main content
Custom routes allow you to extend PocketBase’s REST API with your own endpoints. You can use them to implement custom business logic, integrate with external services, or create specialized APIs.

Basic routing

In Go

Add custom routes in the OnServe hook:
package main

import (
    "net/http"
    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    app := pocketbase.New()

    app.OnServe().BindFunc(func(e *core.ServeEvent) error {
        // Add a simple GET route
        e.Router.GET("/api/hello", func(re *core.RequestEvent) error {
            return re.JSON(http.StatusOK, map[string]string{
                "message": "Hello, World!",
            })
        })
        
        return e.Next()
    })

    app.Start()
}

In JavaScript

Use the routerAdd() function:
pb_hooks/routes.pb.js
routerAdd("GET", "/api/hello", (e) => {
    return e.json(200, {message: "Hello, World!"})
})

HTTP methods

PocketBase supports all standard HTTP methods:
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
    // GET request
    e.Router.GET("/api/items", handleList)
    
    // POST request
    e.Router.POST("/api/items", handleCreate)
    
    // PUT request
    e.Router.PUT("/api/items/:id", handleUpdate)
    
    // PATCH request
    e.Router.PATCH("/api/items/:id", handlePartialUpdate)
    
    // DELETE request
    e.Router.DELETE("/api/items/:id", handleDelete)
    
    // HEAD request
    e.Router.HEAD("/api/items", handleHead)
    
    return e.Next()
})

Path parameters

Capture dynamic segments from the URL path:
e.Router.GET("/api/users/:id", func(re *core.RequestEvent) error {
    id := re.Request.PathValue("id")
    
    record, err := app.FindRecordById("users", id)
    if err != nil {
        return re.NotFoundError("", err)
    }
    
    return re.JSON(http.StatusOK, record)
})

// Multiple parameters
e.Router.GET("/api/users/:userId/posts/:postId", func(re *core.RequestEvent) error {
    userId := re.Request.PathValue("userId")
    postId := re.Request.PathValue("postId")
    
    // Your logic here
    return re.JSON(http.StatusOK, map[string]string{
        "userId": userId,
        "postId": postId,
    })
})

Query parameters

Access URL query string parameters:
e.Router.GET("/api/search", func(re *core.RequestEvent) error {
    // Get single query parameter
    query := re.Request.URL.Query().Get("q")
    page := re.Request.URL.Query().Get("page")
    
    // Get all values for a parameter
    tags := re.Request.URL.Query()["tags"]
    
    return re.JSON(http.StatusOK, map[string]any{
        "query": query,
        "page":  page,
        "tags":  tags,
    })
})

Request body

Parse JSON request bodies:
e.Router.POST("/api/items", func(re *core.RequestEvent) error {
    type RequestData struct {
        Name        string `json:"name"`
        Description string `json:"description"`
        Price       float64 `json:"price"`
    }
    
    data := new(RequestData)
    if err := re.BindBody(data); err != nil {
        return re.BadRequestError("Invalid JSON", err)
    }
    
    // Validate
    if data.Name == "" {
        return re.BadRequestError("Name is required", nil)
    }
    
    // Process data...
    return re.JSON(http.StatusCreated, data)
})

Response types

JSON responses

e.Router.GET("/api/data", func(re *core.RequestEvent) error {
    return re.JSON(http.StatusOK, map[string]any{
        "message": "Success",
        "data": []string{"item1", "item2"},
    })
})

String/HTML responses

e.Router.GET("/api/hello", func(re *core.RequestEvent) error {
    return re.String(http.StatusOK, "Hello, World!")
})

e.Router.GET("/page", func(re *core.RequestEvent) error {
    html := "<html><body><h1>Hello</h1></body></html>"
    return re.HTML(http.StatusOK, html)
})

File responses

import "os"

e.Router.GET("/api/download", func(re *core.RequestEvent) error {
    filePath := "./path/to/file.pdf"
    
    file, err := os.Open(filePath)
    if err != nil {
        return re.NotFoundError("File not found", err)
    }
    defer file.Close()
    
    re.Response.Header().Set("Content-Disposition", "attachment; filename=file.pdf")
    return re.Stream(http.StatusOK, "application/pdf", file)
})

Redirects

e.Router.GET("/old-path", func(re *core.RequestEvent) error {
    return re.Redirect(http.StatusMovedPermanently, "/new-path")
})

Middlewares

Middlewares allow you to process requests before they reach your handler:
import "github.com/pocketbase/pocketbase/apis"

// Require authentication
e.Router.GET(
    "/api/protected",
    func(re *core.RequestEvent) error {
        return re.JSON(http.StatusOK, map[string]string{
            "message": "You are authenticated!",
        })
    },
).Bind(apis.RequireAuth())

// Require superuser
e.Router.DELETE(
    "/api/admin/cleanup",
    handleCleanup,
).Bind(apis.RequireSuperuserAuth())

// Multiple middlewares
e.Router.POST(
    "/api/data",
    handleData,
).Bind(
    apis.RequireAuth(),
    apis.BodyLimit(10 << 20), // 10MB
    apis.Gzip(),
)

Built-in middlewares

PocketBase provides several useful middlewares:
MiddlewareDescription
RequireAuth()Require authenticated user
RequireSuperuserAuth()Require superuser authentication
RequireSuperuserOrOwnerAuth("ownerIdParam")Require superuser or resource owner
RequireGuestOnly()Require unauthenticated request
BodyLimit(bytes)Limit request body size
Gzip()Enable gzip compression
SkipSuccessActivityLog()Skip logging successful requests

Custom middlewares

// Simple logging middleware
loggingMiddleware := func(re *core.RequestEvent) error {
    start := time.Now()
    
    // Call next handler
    err := re.Next()
    
    duration := time.Since(start)
    log.Printf("%s %s - %v\n", 
        re.Request.Method,
        re.Request.URL.Path,
        duration,
    )
    
    return err
}

// Use the middleware
e.Router.GET("/api/data", handler).BindFunc(loggingMiddleware)

Error handling

import "github.com/pocketbase/pocketbase/tools/router"

e.Router.GET("/api/data", func(re *core.RequestEvent) error {
    // Return different error types
    
    // 400 Bad Request
    if invalidInput {
        return router.NewBadRequestError("Invalid input", nil)
    }
    
    // 401 Unauthorized
    if !authenticated {
        return router.NewUnauthorizedError("Authentication required", nil)
    }
    
    // 403 Forbidden
    if !authorized {
        return router.NewForbiddenError("Access denied", nil)
    }
    
    // 404 Not Found
    if !found {
        return router.NewNotFoundError("Resource not found", nil)
    }
    
    // 500 Internal Server Error
    if internalError != nil {
        return router.NewInternalServerError("Something went wrong", internalError)
    }
    
    return re.JSON(http.StatusOK, data)
})

Complete example

Here’s a complete CRUD API example:
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
    // List items
    e.Router.GET("/api/items", func(re *core.RequestEvent) error {
        records, err := app.FindRecordsByFilter(
            "items",
            "",
            "-created",
            100,
            0,
        )
        if err != nil {
            return err
        }
        
        return re.JSON(http.StatusOK, records)
    }).Bind(apis.RequireAuth())
    
    // Get single item
    e.Router.GET("/api/items/:id", func(re *core.RequestEvent) error {
        id := re.Request.PathValue("id")
        
        record, err := app.FindRecordById("items", id)
        if err != nil {
            return re.NotFoundError("Item not found", err)
        }
        
        return re.JSON(http.StatusOK, record)
    }).Bind(apis.RequireAuth())
    
    // Create item
    e.Router.POST("/api/items", func(re *core.RequestEvent) error {
        collection, _ := app.FindCollectionByNameOrId("items")
        record := core.NewRecord(collection)
        
        if err := re.BindBody(record); err != nil {
            return re.BadRequestError("Invalid data", err)
        }
        
        if err := app.Save(record); err != nil {
            return err
        }
        
        return re.JSON(http.StatusCreated, record)
    }).Bind(apis.RequireAuth())
    
    // Update item
    e.Router.PATCH("/api/items/:id", func(re *core.RequestEvent) error {
        id := re.Request.PathValue("id")
        
        record, err := app.FindRecordById("items", id)
        if err != nil {
            return re.NotFoundError("Item not found", err)
        }
        
        if err := re.BindBody(record); err != nil {
            return re.BadRequestError("Invalid data", err)
        }
        
        if err := app.Save(record); err != nil {
            return err
        }
        
        return re.JSON(http.StatusOK, record)
    }).Bind(apis.RequireAuth())
    
    // Delete item
    e.Router.DELETE("/api/items/:id", func(re *core.RequestEvent) error {
        id := re.Request.PathValue("id")
        
        record, err := app.FindRecordById("items", id)
        if err != nil {
            return re.NotFoundError("Item not found", err)
        }
        
        if err := app.Delete(record); err != nil {
            return err
        }
        
        return re.NoContent(http.StatusNoContent)
    }).Bind(apis.RequireAuth())
    
    return e.Next()
})

Next steps

Event hooks

Learn about event hooks for more control

JavaScript hooks

Explore JavaScript hook examples