> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pocketbase/pocketbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom routes

> Learn how to add custom API endpoints to your PocketBase application

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:

```go theme={null}
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:

```javascript pb_hooks/routes.pb.js theme={null}
routerAdd("GET", "/api/hello", (e) => {
    return e.json(200, {message: "Hello, World!"})
})
```

## HTTP methods

PocketBase supports all standard HTTP methods:

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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()
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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)
    ```
  </Tab>
</Tabs>

## Path parameters

Capture dynamic segments from the URL path:

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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,
        })
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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})
    })
    ```
  </Tab>
</Tabs>

## Query parameters

Access URL query string parameters:

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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,
        })
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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})
    })
    ```
  </Tab>
</Tabs>

## Request body

Parse JSON request bodies:

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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)
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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)
    })
    ```
  </Tab>
</Tabs>

## Response types

### JSON responses

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    e.Router.GET("/api/data", func(re *core.RequestEvent) error {
        return re.JSON(http.StatusOK, map[string]any{
            "message": "Success",
            "data": []string{"item1", "item2"},
        })
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    routerAdd("GET", "/api/data", (e) => {
        return e.json(200, {
            message: "Success",
            data: ["item1", "item2"]
        })
    })
    ```
  </Tab>
</Tabs>

### String/HTML responses

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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)
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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)
    })
    ```
  </Tab>
</Tabs>

### File responses

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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)
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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)
    })
    ```
  </Tab>
</Tabs>

### Redirects

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    e.Router.GET("/old-path", func(re *core.RequestEvent) error {
        return re.Redirect(http.StatusMovedPermanently, "/new-path")
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    routerAdd("GET", "/old-path", (e) => {
        return e.redirect(301, "/new-path")
    })
    ```
  </Tab>
</Tabs>

## Middlewares

Middlewares allow you to process requests before they reach your handler:

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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(),
    )
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    // 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()
    )
    ```
  </Tab>
</Tabs>

## 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

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    // 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)
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    // 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)
    ```
  </Tab>
</Tabs>

## Error handling

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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)
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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)
    })
    ```
  </Tab>
</Tabs>

## Complete example

Here's a complete CRUD API example:

<Tabs>
  <Tab title="Go">
    ```go theme={null}
    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()
    })
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript pb_hooks/items-api.pb.js theme={null}
    // 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())
    ```
  </Tab>
</Tabs>

## Next steps

<CardGroup cols={2}>
  <Card title="Event hooks" icon="webhook" href="/extending/event-hooks">
    Learn about event hooks for more control
  </Card>

  <Card title="JavaScript hooks" icon="js" href="/extending/javascript-hooks">
    Explore JavaScript hook examples
  </Card>
</CardGroup>
