1 de septiembre del 2024

Paginación en clean architecture

En este artículo veremos como implementar un sistema de paginación sin romper las reglas fundamentales de la Clean Architecture sobre la dirección de las dependencias.

image

En la Clean Architecture se establecen distintas capas donde la dirección de dependencia es desde las capas externas hacia las capas internas. Las capas externas contienen detalles de implementación como conexiones a bases de datos, apis, frameworks y cualquier concepto que sea técnico; En la capa más interna tenemos tanto reglas de negocio de la aplicación como entidades de dominio muy abstractas, estas capas internas no tienen dependencias con librerías externas.

image

Definiremos el concepto de paginación como un detalle de implementación técnico, debido a esto, todo lo referente a paginación debe pertencer a la capa “Infraestructure”. Además la paginación contempla dos aspectos, debemos por un lado realizar nuestra obtención de datos de forma paginada y tambien debemos mostrar los datos con una estructura determinada que depende de la paginación.

Puedes revisar el código completo en Github.

A modo de ejemplo he desarrollado una pequeña aplicación estilo TODO, donde tenemos como entidad principal el concepto de taréa (“Task”):

// src/domain/entities/task.ts

export interface Task {
    id: string
    title: string
    description: string
    done: boolean
    createdAt: Date
    updatedAt: Date
}

Trabajaremos solo con esta entidad. Luego en la capa de aplicación (“application”) definimos un caso de uso para obtener un listado de tareas:

// src/application/usecases/getTaskListUseCase.ts

export class GetTaskListUseCase {
    constructor(private taskRepository: TaskRepository) {}

    async execute(): Promise<Task[]> {
        return await this.taskRepository.getList()
    }
}

Este caso de uso recibe por inyección de dependencia un repositorio con un método getList. Como se puede ver en el retorno del método execute en este caso solo se devuelve una promesa con una lista de entidades Task, no tenemos nada referente a la paginación.

La capa que nos interesa ahora es la de infraestructura, en esta capa tenemos la implementación del repositorio TaskRepository, en este caso solo se utiliza una lista en memoria con datos ficticios, pero en un caso real aquí tendriamos nuestra integración a una base de datos real. Tambien en esta capa podemos encontrar nuestra API rest usando express.

En primer lugar crearemos una clase de paginación, la cual tendrá los parámetros específicos de la paginación que estamos implementando. Para efectos de este ejemplo se ha implementado la paginación por offset que recibe un parámetro page y size.

// src/infraestructure/pagination/offsetPagination.ts

export interface OffsetPaginationResponse<T> {
    page: number,
    size: number,
    total?: number,
    items: T[],
}

export class OffsetPagination implements Pagination {
    public total?: number
    
    constructor(
        public page: number = 1,
        public size: number = 10,
    ) {}

    paginate<Response>(items: Response[]): OffsetPaginationResponse<Response> {
        return {
            page: this.page,
            size: this.size,
            ...(this.total ? { total: this.total } : {}),
            items
        }
    }
}

Los parámetros de esta clase son:

Tambien podemos ver el método paginate implementado desde la interfaz Pagination. Este método transforma la respuesta y la “encapsula” en nuestro objeto de paginación declarado en la interfaz OffsetPaginationResponse.

Ahora veremos la implementación del repositorio TaskRepository.

// src/infraestructure/database/fakerTaskRepository.ts

const database: Task[] = new Array(93).fill(0).map((_, i) => {
    return {
        id: i.toString(),
        title: faker.lorem.lines(1),
        description: faker.lorem.paragraph(),
        done: faker.datatype.boolean(),
        createdAt: faker.date.anytime(),
        updatedAt: faker.date.anytime(),
    }
})

export class FakerTaskRepository implements TaskRepository {
    constructor(
        private pagination?: OffsetPagination
    ){}

    async getList(): Promise<Task[]> {
        if(!this.pagination) {
            return database
        }

        const startIndex = (this.pagination.page - 1) * this.pagination.size
        const endIndex = startIndex + this.pagination.size
        this.pagination.total = database.length

        return database
            .slice(startIndex, endIndex)
    }
}

Como se muestra en el ejemplo nuestra clase FakerTaskRepository recibe por dependencia en el constructor un objeto OffsetPagination. En este caso nuestro repositorio tendrá una única forma de paginar la cual es una dependencia directa. Luego en el método getList vemos como en caso de existir el método de paginación realizaremos esta acción retornando solo los datos requeridos para esta página específica. La propiedad “total” es asignada en este punto, ya que solo se conoce la cantidad total de elementos de la base de datos en el repositorio.

Otro detalle interesante en el repositorio es que solo devolvemos la lista de entidades como un arreglo. Esta lista la recibe la capa de aplicación en el caso de uso, pero solo conoce el subconjunto de datos que ha resuelto nuesto repositorio pero no los detalles de la paginación.

Por último veremos nuestro endpoint a nivel de API.

// src/infraestructure/api/server.ts

const app = express()
const port = 3000

app.get('/tasks', async (request, response) => {
    const page = parseInt(request.query.page as string) || 1
    const size = parseInt(request.query.size as string) || 10

    const pagination = new OffsetPagination(page, size)
    const taskRepository = new FakerTaskRepository(pagination)
    const getListUseCase = new GetTaskListUseCase(taskRepository)

    const taskList = await getListUseCase.execute()

    const paginatedReponse = pagination.paginate(taskList)

    response.json(paginatedReponse)
})

app.listen(port, () => {
    console.log(`Listening in port: ${port}`)
})

La implementación del servidor de express es trivial en este caso, solo nos fijaremos en la obtención de los parámetros page y size y como estos son pasados en el constructor de nuestro objeto OffsetPagination. La instancia de nuestro objeto de paginación es pasada en el constructor de nuesto repositorio. La respuesta de la ejecución del caso de uso solo obtiene la lista ya paginada.

Luego de la ejecución del caso de uso, podemos observar que llamamos al método paginate de nuesto objeto de paginación, al que le pasamos como parámetro las entidades que queremos retornar en nuestra API. Esta será la respuesta paginada que devolveremos con respuesta en nuestro endpoint. Podemos ver un ejemplo de la respuesta a continuación:

{
    "page": 1,
    "size": 3,
    "total": 93,
    "items": [
        {
            "id": "0",
            "title": "Hic alveus statim itaque.",
            "description": "Ocer aperte catena adsuesco enim curiositas ab. Talis tersus saepe volubilis calco. Absens cimentarius aliquam adfectus quam suspendo crebro terebro.",
            "done": false,
            "createdAt": "2024-07-03T02:58:24.610Z",
            "updatedAt": "2023-09-23T06:25:15.727Z"
        },
        {
            "id": "1",
            "title": "Complectus titulus cattus umerus deduco amet coniecto accommodo.",
            "description": "Tabesco aveho audentia vindico conitor vitae pauper vorago. Quidem abduco non aufero contra. Sol amo adiuvo desidero.",
            "done": false,
            "createdAt": "2025-05-18T15:31:24.989Z",
            "updatedAt": "2025-08-02T00:55:09.442Z"
        },
        {
            "id": "2",
            "title": "Utique decor suscipit tabgo absens.",
            "description": "Quia creta volaticus vobis. Peior adicio harum sursum sulum clam damnatio adeo temperantia. Amaritudo umquam ars argumentum denuo virtus laborum vorax.",
            "done": true,
            "createdAt": "2025-08-10T15:05:11.910Z",
            "updatedAt": "2025-01-08T14:59:45.337Z"
        }
    ]
}

Consideraciones

Pueden existir casos donde nuestro caso particular nos obliga a considerar la paginación como “Lógica de negocio”, en estos casos estamos obligados a pasar parámetros referentes a la paginación a nuestro caso de uso. Se recomienda hacerlo lo mas abstracto posible pasar solo los datos necesarios para cumplir con nuestro caso especifico, aún así conviene mantener la mayor parte de nuestra paginación en la capa mas externa de la Clean Architecture.

Conclusión

En este artículo pudimos analizar una manera de implementar Clean Architecture en la que tratamos a la paginación como un detalle de implementación más, que podemos dejar en la capa más externa de nuestra arquitectura. Al utilizar este método las capas de aplicación y dominion no conocen en absoluto ningún concepto sobre paginación.