Basic routing
In Go
Add custom routes in theOnServe 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 therouterAdd() 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:- Go
- JavaScript
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()
})
routerAdd("GET", "/api/items", handleList)
routerAdd("POST", "/api/items", handleCreate)
routerAdd("PUT", "/api/items/:id", handleUpdate)
routerAdd("PATCH", "/api/items/:id", handlePartialUpdate)
routerAdd("DELETE", "/api/items/:id", handleDelete)
routerAdd("HEAD", "/api/items", handleHead)
Path parameters
Capture dynamic segments from the URL path:- Go
- JavaScript
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,
})
})
routerAdd("GET", "/api/users/:id", (e) => {
const id = e.request.pathValue("id")
const record = $app.findRecordById("users", id)
return e.json(200, record)
})
// Multiple parameters
routerAdd("GET", "/api/users/:userId/posts/:postId", (e) => {
const userId = e.request.pathValue("userId")
const postId = e.request.pathValue("postId")
return e.json(200, {userId, postId})
})
Query parameters
Access URL query string parameters:- Go
- JavaScript
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,
})
})
routerAdd("GET", "/api/search", (e) => {
const query = e.request.url.searchParams.get("q")
const page = e.request.url.searchParams.get("page")
const tags = e.request.url.searchParams.getAll("tags")
return e.json(200, {query, page, tags})
})
Request body
Parse JSON request bodies:- Go
- JavaScript
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)
})
routerAdd("POST", "/api/items", (e) => {
const data = $apis.requestInfo(e).body
// Validate
if (!data.name) {
throw new BadRequestError("Name is required")
}
// Process data...
return e.json(201, data)
})
Response types
JSON responses
- Go
- JavaScript
e.Router.GET("/api/data", func(re *core.RequestEvent) error {
return re.JSON(http.StatusOK, map[string]any{
"message": "Success",
"data": []string{"item1", "item2"},
})
})
routerAdd("GET", "/api/data", (e) => {
return e.json(200, {
message: "Success",
data: ["item1", "item2"]
})
})
String/HTML responses
- Go
- JavaScript
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)
})
routerAdd("GET", "/api/hello", (e) => {
return e.string(200, "Hello, World!")
})
routerAdd("GET", "/page", (e) => {
const html = "<html><body><h1>Hello</h1></body></html>"
return e.html(200, html)
})
File responses
- Go
- JavaScript
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)
})
routerAdd("GET", "/api/download", (e) => {
const file = $os.readFile("./path/to/file.pdf")
e.response.header().set("Content-Disposition", "attachment; filename=file.pdf")
e.response.header().set("Content-Type", "application/pdf")
return e.blob(200, "application/pdf", file)
})
Redirects
- Go
- JavaScript
e.Router.GET("/old-path", func(re *core.RequestEvent) error {
return re.Redirect(http.StatusMovedPermanently, "/new-path")
})
routerAdd("GET", "/old-path", (e) => {
return e.redirect(301, "/new-path")
})
Middlewares
Middlewares allow you to process requests before they reach your handler:- Go
- JavaScript
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(),
)
// Require authentication
routerAdd(
"GET",
"/api/protected",
(e) => {
return e.json(200, {
message: "You are authenticated!"
})
},
$apis.requireAuth()
)
// Require superuser
routerAdd(
"DELETE",
"/api/admin/cleanup",
handleCleanup,
$apis.requireSuperuserAuth()
)
// Multiple middlewares
routerAdd(
"POST",
"/api/data",
handleData,
$apis.requireAuth(),
$apis.bodyLimit(10 * 1024 * 1024), // 10MB
$apis.gzip()
)
Built-in middlewares
PocketBase provides several useful middlewares:| Middleware | Description |
|---|---|
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
- Go
- JavaScript
// 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)
// Custom rate limiting middleware
const rateLimitMiddleware = new Middleware((e) => {
const clientId = e.request.header.get("X-Client-Id")
// Check rate limit
if (isRateLimited(clientId)) {
throw new TooManyRequestsError("Rate limit exceeded")
}
return e.next()
}, 100, "rate-limit") // priority, id
// Use the middleware
routerAdd("GET", "/api/data", handler, rateLimitMiddleware)
Error handling
- Go
- JavaScript
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)
})
routerAdd("GET", "/api/data", (e) => {
// Return different error types
// 400 Bad Request
if (invalidInput) {
throw new BadRequestError("Invalid input")
}
// 401 Unauthorized
if (!authenticated) {
throw new UnauthorizedError("Authentication required")
}
// 403 Forbidden
if (!authorized) {
throw new ForbiddenError("Access denied")
}
// 404 Not Found
if (!found) {
throw new NotFoundError("Resource not found")
}
// 500 Internal Server Error
if (internalError) {
throw new InternalServerError("Something went wrong")
}
return e.json(200, data)
})
Complete example
Here’s a complete CRUD API example:- Go
- JavaScript
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()
})
pb_hooks/items-api.pb.js
// List items
routerAdd("GET", "/api/items", (e) => {
const records = $app.findRecordsByFilter(
"items",
"",
"-created",
100,
0
)
return e.json(200, records)
}, $apis.requireAuth())
// Get single item
routerAdd("GET", "/api/items/:id", (e) => {
const id = e.request.pathValue("id")
const record = $app.findRecordById("items", id)
return e.json(200, record)
}, $apis.requireAuth())
// Create item
routerAdd("POST", "/api/items", (e) => {
const collection = $app.findCollectionByNameOrId("items")
const record = new Record(collection, e.request.body)
$app.save(record)
return e.json(201, record)
}, $apis.requireAuth())
// Update item
routerAdd("PATCH", "/api/items/:id", (e) => {
const id = e.request.pathValue("id")
const record = $app.findRecordById("items", id)
record.load(e.request.body)
$app.save(record)
return e.json(200, record)
}, $apis.requireAuth())
// Delete item
routerAdd("DELETE", "/api/items/:id", (e) => {
const id = e.request.pathValue("id")
const record = $app.findRecordById("items", id)
$app.delete(record)
return e.noContent(204)
}, $apis.requireAuth())
Next steps
Event hooks
Learn about event hooks for more control
JavaScript hooks
Explore JavaScript hook examples