JavaScript routes and console commands
Add authenticated HTTP routes, middleware, error responses, and executable JavaScript console commands.
Use custom JavaScript routes for application-specific HTTP operations and console commands for maintenance or automation tasks. This guide adds an authenticated route, validates request data, returns safe errors, and registers a command that runs independently from the main server process.
Before you begin
Create a pb_hooks/*.pb.js file and run PocketBase locally with a test account. Choose a unique /api/<app-name>/... route prefix to avoid collisions with system routes. Decide whether the route is public, authenticated, or restricted to superusers before registering it.
Add an authenticated route
Use routerAdd() with an HTTP method, a route pattern, a handler, and $apis.requireAuth().
routerAdd("GET", "/api/myapp/greeting/{name}", (e) => {
const name = e.request.pathValue("name")
return e.json(200, { message: `Hello ${name}` })
}, $apis.requireAuth())Read path values with pathValue(), query values with e.request.url.query().get("name"), headers with e.request.header.get("Some-Header"), and parsed body data with e.requestInfo().body or e.bindBody().
Use e.json(), e.string(), e.noContent(), e.redirect(), e.fileFS(), e.stream(), or e.blob() according to the response type. Set response headers with e.response.header().set().
Throw BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, TooManyrequestsError, or InternalServerError when appropriate. PocketBase converts thrown or returned errors to generic API errors by default; the original error is visible in Dashboard logs or development mode.
Call the route without an authorization token and confirm it is rejected. Call it with a test authenticated session and confirm the JSON response contains the greeting and no private implementation details.
Add middleware
Use routerUse() for global middleware or pass middleware after a route handler for route-specific behavior. Middleware can inspect, reject, or enrich a request, then call e.next().
routerUse((e) => {
if (e.request.header.get("X-Request-Id") == "") {
throw new BadRequestError("X-Request-Id is required")
}
return e.next()
})Built-in helpers include $apis.requireGuestOnly(), $apis.requireAuth(), $apis.requireSuperuserAuth(), $apis.requireSuperuserOrOwnerAuth(), $apis.bodyLimit(), $apis.gzip(), and $apis.skipSuccessActivityLog(). Custom routes have a default request body limit of approximately 32 MB; use a narrower limit when the route expects small input.
Register a console command
Add a Command to $app.rootCmd and run it using the PocketBase executable.
$app.rootCmd.addCommand(new Command({
use: "hello",
run: (cmd, args) => {
console.log("Hello from PocketBase")
},
}))Run the command with:
./pocketbase helloConsole commands execute in their own application process. They run independently from the main serve process, so hook and realtime events between the two processes are not shared. Pass explicit inputs and make command output and failure behavior clear for automation.
Verify and troubleshoot
The route returns unauthorized
Confirm the request includes a valid authorization state and that the route has the intended $apis.requireAuth() or superuser middleware. Test public routes without accidentally adding an auth middleware.
A route pattern does not match
Check the method and pattern. Parameters use {paramName}; {paramName...} captures multiple path segments. A trailing slash is a wildcard unless the pattern ends in {$}.
The request body is rejected
Check the body size and content type. Apply $apis.bodyLimit(limitBytes) at the route or group level when the supported payload size differs from the default.
Clients receive a generic error
This is the default protection against leaking implementation details. Inspect Dashboard logs or development-mode output for the original error, then return an appropriate typed API error to the client.
The command cannot see server hook state
Commands run in a separate process from serve. Persist required state through supported application data or pass it as command input instead of relying on in-memory hook variables.
Next steps
Use JavaScript event hooks for lifecycle-driven behavior and JavaScript database and filesystem for records, transactions, and uploads.