# Implementing Pagination and Filtering for List Endpoints in Go: A Clean Architecture Guide

> Learn how to implement pagination and filtering for list endpoints in Go. Explore a clean architecture approach using DTOs, repository interfaces, and dynamic GORM queries for efficient data retrieval.

- Repository: [manato/golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture)
- Tags: how-to-guide
- Published: 2026-03-06

---

**To implement pagination and filtering in the `manakuro/golang-clean-architecture` project, you define a `UserListParams` DTO in the use-case layer, propagate it through repository interfaces, and build dynamic GORM queries with `Limit`, `Offset`, and `Where` clauses based on HTTP query parameters.**

This approach leverages **Clean Architecture** principles to keep your HTTP layer agnostic of database specifics while providing flexible list endpoints. The following implementation extends the existing `GET /users` endpoint to support optional pagination and field filtering without breaking existing contracts.

## Understanding the Clean Architecture Flow

In this repository, requests flow through distinct layers: the **controller** handles HTTP concerns, the **use case** orchestrates business logic, and the **repository** manages data persistence. When implementing pagination and filtering for list endpoints in Go, each layer receives a data transfer object (DTO) containing the query parameters, ensuring that changes remain isolated to their respective domains.

The key is introducing a request-level struct that travels from the controller down to the repository, allowing the database layer to construct the appropriate SQL without the HTTP layer knowing about GORM or SQL specifics.

## Step 1: Define the Pagination and Filtering DTO

Start by creating a struct that captures pagination fields (`Page`, `Size`) and filter criteria (`Name`, `Email`) in the use-case layer.

**File:** [`pkg/usecase/usecase/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/user.go)

```go
type UserListParams struct {
    Page   int    // 1‑based page number
    Size   int    // rows per page
    Name   string // optional name filter (partial match)
    Email  string // optional email filter (exact match)
}

```

Place this definition at the top of the file after the imports. This struct is pure data with no business logic, making it safe to pass across layer boundaries.

## Step 2: Update the Use Case Interface and Implementation

Modify the `User` interface to accept the new parameters struct, then update the concrete implementation to forward these values to the repository.

**File:** [`pkg/usecase/usecase/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/user.go)

Update the interface definition:

```go
type User interface {
    // List now receives pagination / filter params.
    List(p UserListParams) ([]*model.User, error)
    Create(u *model.User) (*model.User, error)
}

```

Update the concrete method:

```go
func (uu *userUsecase) List(p UserListParams) ([]*model.User, error) {
    // delegate to repository, passing the same DTO
    return uu.userRepository.FindAll(p)
}

```

This change preserves the **Dependency Rule** by ensuring the use case depends only on abstractions, not concrete implementations.

## Step 3: Adapt the Repository Interface

Extend the repository interface to accept the pagination and filtering DTO, allowing the use case to remain decoupled from database specifics.

**File:** [`pkg/usecase/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/repository/user.go)

```go
type UserRepository interface {
    // FindAll receives pagination / filter params.
    FindAll(p usecase.UserListParams) ([]*model.User, error)
    Create(u *model.User) (*model.User, error)
}

```

Note that you must import the `usecase` package to reference `UserListParams`. This interface definition ensures any repository implementation—whether GORM, SQLx, or mock—can satisfy the contract.

## Step 4: Implement GORM Pagination and Filtering Logic

In the concrete repository, build a dynamic query using GORM's fluent API to apply filters and calculate the correct offset and limit.

**File:** [`pkg/adapter/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user.go)

```go
func (ur *userRepository) FindAll(p usecase.UserListParams) ([]*model.User, error) {
    var users []*model.User
    query := ur.db.Model(&model.User{})

    // ---------- filtering ----------
    if p.Name != "" {
        // Partial match on name (ILIKE for case‑insensitive)
        query = query.Where("name ILIKE ?", "%"+p.Name+"%")
    }
    if p.Email != "" {
        query = query.Where("email = ?", p.Email)
    }

    // ---------- pagination ----------
    // Default size = 20, page = 1
    size := p.Size
    if size <= 0 {
        size = 20
    }
    page := p.Page
    if page <= 0 {
        page = 1
    }
    offset := (page - 1) * size
    query = query.Limit(size).Offset(offset)

    // Execute the query
    if err := query.Find(&users).Error; err != nil {
        return nil, err
    }
    return users, nil
}

```

This implementation handles **default values** for missing parameters and constructs the SQL query conditionally, ensuring you only filter when parameters are provided.

## Step 5: Parse Query Parameters in the Controller

Extract the pagination and filter values from the URL query string and map them into the `UserListParams` DTO before calling the use case.

**File:** [`pkg/adapter/controller/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/controller/user.go)

```go
func (uc *userController) GetUsers(ctx Context) error {
    // Extract pagination & filter values from the URL query string.
    // Echo’s Context implements QueryParam(name string) string.
    page, _ := strconv.Atoi(ctx.QueryParam("page"))
    size, _ := strconv.Atoi(ctx.QueryParam("size"))
    name := ctx.QueryParam("name")
    email := ctx.QueryParam("email")

    params := usecase.UserListParams{
        Page:  page,
        Size:  size,
        Name:  name,
        Email: email,
    }

    users, err := uc.userUsecase.List(params)
    if err != nil {
        return err
    }
    return ctx.JSON(http.StatusOK, users)
}

```

Add the `strconv` import to handle string-to-integer conversion for the numeric parameters. This layer is responsible only for **transport concerns**—parsing HTTP requests and formatting responses.

## Step 6: Verify the Router Configuration

No changes are required to the router because the existing `GET /users` endpoint already routes to the updated controller method.

**File:** [`pkg/infrastructure/router/router.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/router/router.go)

```go
e.GET("/users", func(context echo.Context) error { return c.User.GetUsers(context) })

```

The endpoint now implicitly supports query strings like `?page=2&size=10&name=alice` without requiring new route definitions.

## Usage Examples

**Request with pagination only:**

```http
GET /users?page=2&size=10 HTTP/1.1
Host: localhost:8080

```

Returns users 11–20 assuming a page size of 10.

**Request with filtering and pagination:**

```http
GET /users?name=alice&size=5 HTTP/1.1
Host: localhost:8080

```

Returns up to 5 users whose name contains "alice" (case-insensitive), starting from page 1.

**cURL example:**

```bash
curl "http://localhost:8080/users?page=1&size=15&email=jane@example.com"

```

## Summary

- **Define** a `UserListParams` struct in [`pkg/usecase/usecase/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/user.go) to transport pagination and filter data across layers.
- **Extend** the use-case interface and implementation to accept and forward this DTO to the repository.
- **Update** the repository interface in [`pkg/usecase/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/repository/user.go) to declare the new method signature.
- **Implement** dynamic SQL construction in [`pkg/adapter/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user.go) using GORM's `Where`, `Limit`, and `Offset` methods.
- **Parse** query parameters in [`pkg/adapter/controller/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/controller/user.go) and map them to the DTO before invoking the use case.

This pattern maintains strict separation of concerns while adding powerful querying capabilities to your list endpoints.

## Frequently Asked Questions

### How do I set default values for pagination parameters?

The repository implementation in [`pkg/adapter/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user.go) handles defaults by checking if `Size` or `Page` are less than or equal to zero, defaulting to 20 items per page and page 1 respectively. This ensures the database query always receives valid integers even when the HTTP client omits the parameters.

### Where should I validate pagination input parameters?

Validation should occur in the controller layer ([`pkg/adapter/controller/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/controller/user.go)) or a dedicated validator package before creating the `UserListParams` DTO. Check for reasonable limits—such as maximum page sizes—to prevent resource exhaustion, then return appropriate HTTP 400 responses for invalid input.

### Can I add sorting to this pagination implementation?

Yes. Add a `SortBy` and `SortOrder` field to the `UserListParams` struct, then append GORM's `Order` method to the query builder in [`pkg/adapter/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user.go). Validate the sort fields against a whitelist in the controller to prevent SQL injection through user-supplied sort parameters.

### How does this pattern preserve Clean Architecture principles?

Each layer depends only on the abstractions defined in inner layers. The controller knows nothing about GORM, the use case knows nothing about HTTP, and the repository implements interface contracts defined by the use case. The `UserListParams` DTO acts as a boundary object that safely crosses these layers without leaking implementation details.