Go routes, middleware, and templates
Build custom Go routes, middleware, static assets, and rendered templates in PocketBase.
Use app.OnServe() to add custom Go endpoints and middleware to PocketBase. This guide shows a public route, an authenticated route group, static-file handling, and HTML rendered with PocketBase’s template registry.
Before you begin
You need an embedded Go application that creates app := pocketbase.New() and calls app.Start(). Create a views directory for templates and use an application-specific /api/... prefix to reduce collisions with system routes.
Keep the application source and the public and views directories available to the process. Decide which endpoints are public before registering them; authentication middleware is bound explicitly to a group.
Add routes and middleware
Bind to OnServe and register a route:
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.GET("/hello/{name}", func(e *core.RequestEvent) error {
return e.String(http.StatusOK, "Hello "+e.Request.PathValue("name"))
})
return se.Next()
})A request to /hello/ada returns Hello ada.
Bind authentication once for a group:
group := se.Router.Group("/api/myapp")
group.Bind(apis.RequireAuth())
group.GET("/settings", func(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, map[string]bool{"success": true})
})The JSON response is returned only after authentication permits the request.
Use BindFunc and call e.Next() when the request may continue:
group.BindFunc(func(e *core.RequestEvent) error {
if e.Request.Header.Get("X-Example-Request") == "" { return e.BadRequestError("X-Example-Request is required", nil) }
return e.Next()
})Middleware can be global, group-scoped, or route-scoped. A named hook.Handler can later be removed with Unbind.
Use FileFS from a route handler:
se.Router.GET("/assets/{path...}", func(e *core.RequestEvent) error {
return e.FileFS(os.DirFS("public"), e.Request.PathValue("path"))
})The handler serves the requested path from public. Do not expose credentials or private data through that directory.
Load composed templates through a registry:
registry := template.NewRegistry()
se.Router.GET("/page/{name}", func(e *core.RequestEvent) error {
html, err := registry.LoadFiles("views/layout.html", "views/hello.html").Render(map[string]any{"name": e.Request.PathValue("name")})
if err != nil { return e.NotFoundError("Template not found", err) }
return e.HTML(http.StatusOK, html)
})The utility applies contextual escaping by default; reserve raw rendering for trusted content.
Verify the routes
Start the application and request /hello/ada, the authenticated API route with a valid client, the static path, and /page/ada. Confirm each response separately. If a template cannot be loaded, the example returns a not-found error instead of a partial page.
apis.RequireAuth(), apis.RequireSuperuserAuth(), or another appropriate middleware when a route handles private data.Troubleshoot route behavior
If a route is not available, confirm that the OnServe binding is registered before Start and that the request path matches the registered pattern. If the protected endpoint returns an authorization failure, check that the client supplied valid authentication and that the group has apis.RequireAuth() bound. If a static asset or template is missing, check the relative public or views path used by the handler; the template example returns a not-found error when loading fails.
Next step
After the routes return the expected status and body, continue with Go records and database operations or Go hooks and event lifecycle.
Continue with Go records and database operations or Go hooks and event lifecycle.