¿Tu web tiene 10, 100 o 1000 elementos y no sabes cómo manejar la navegación? La paginación es fundamental en cualquier aplicación web moderna.
En este tutorial aprenderás paginación desde ambos enfoques: Frontend para UX instantánea y Backend para manejo masivo de datos. Con código real y proyectos funcionales.
🤔 ¿Qué vamos a aprender?
- Frontend: Paginación con JavaScript vanilla (UX instantánea)
- Backend: Express/Node.js, FastAPI/Python, Spring Boot/Java
- Cuándo usar cada método según tu proyecto
- Proyectos reales: Blog, catálogo, sistema de usuarios
📦 ¿Qué es la paginación y por qué la necesitas?
La paginación divide grandes conjuntos de datos en páginas manejables. Sin ella, imagina un blog con 1000 artículos cargando todos a la vez:
💡 Tipos de paginación
Frontend: Cargas todos los datos una vez, navegas instantáneamente
Backend: El servidor envía solo los datos de la página actual
🟨 Paginación Frontend con JavaScript
Perfecta para datasets pequeños (hasta 1000 elementos). La UX es instantánea porque todos los datos están en memoria.
📝 Ejemplo: Blog con paginación
// Configuración de paginación
const ARTICLES_PER_PAGE = 12;
let currentPage = 1;
let filteredArticles = [];
// Obtener todos los artículos
const articleCards = document.querySelectorAll('.article-card');
// ✅ FUNCIÓN PRINCIPAL: Mostrar página específica
function displayPage(page) {
currentPage = page;
// Ocultar todos los artículos
articleCards.forEach(card => {
card.style.display = 'none';
});
// Calcular qué artículos mostrar
const startIndex = (page - 1) * ARTICLES_PER_PAGE;
const endIndex = startIndex + ARTICLES_PER_PAGE;
const articlesToShow = filteredArticles.slice(startIndex, endIndex);
// Mostrar artículos con animación
articlesToShow.forEach((card, index) => {
card.style.display = 'block';
setTimeout(() => {
card.style.opacity = '1';
}, index * 50);
});
}
📝 Crear controles de navegación
function createPaginationControls() {
const totalPages = Math.ceil(filteredArticles.length / ARTICLES_PER_PAGE);
let paginationHTML = '<div class="pagination">';
// Botón Anterior
paginationHTML += `
<button class="pagination-btn prev" ${currentPage === 1 ? 'disabled' : ''}>
← Anterior
</button>
`;
// Números de página
for (let i = 1; i <= totalPages; i++) {
paginationHTML += `
<button class="pagination-btn page-num ${i === currentPage ? 'active' : ''}"
data-page="${i}">
${i}
</button>
`;
}
// Botón Siguiente
paginationHTML += `
<button class="pagination-btn next" ${currentPage === totalPages ? 'disabled' : ''}>
Siguiente →
</button>
</div>`;
document.querySelector('.pagination-container').innerHTML = paginationHTML;
}
💪 Ventajas Frontend:
- UX instantánea: Sin esperas al cambiar página
- Búsqueda rápida: Filtros sin consultas al servidor
- Offline: Funciona sin conexión
❌ Limitaciones Frontend:
Solo para datasets pequeños. Con 10,000 productos tu navegador colapsaría.
🟢 Paginación Backend con Express/Node.js
Para datasets grandes. El servidor envía solo los datos necesarios, manteniendo la performance óptima.
📝 Configuración inicial
// package.json dependencies
{
"express": "^5.1.0",
"mongoose": "^8.18.0"
}
// Instalación
// npm install express@5.1.0 mongoose@8.18.0
📝 API con paginación
const express = require('express');
const mongoose = require('mongoose');
const app = express();
// Modelo de artículo
const ArticleSchema = new mongoose.Schema({
title: String,
content: String,
category: String,
createdAt: { type: Date, default: Date.now }
});
const Article = mongoose.model('Article', ArticleSchema);
// ✅ RUTA PRINCIPAL: API de artículos con paginación
app.get('/api/articles', async (req, res) => {
try {
// Parámetros de query (ej: ?page=2&limit=12&category=tutorial)
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 12;
const category = req.query.category;
// Filtros opcionales
let filter = {};
if (category) {
filter.category = category;
}
// Calcular skip (saltar registros)
const skip = (page - 1) * limit;
// Consultas en paralelo para optimizar
const [articles, totalCount] = await Promise.all([
Article.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit),
Article.countDocuments(filter)
]);
// Metadatos de paginación
const totalPages = Math.ceil(totalCount / limit);
res.json({
success: true,
data: articles,
pagination: {
currentPage: page,
totalPages: totalPages,
totalCount: totalCount,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
}
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
📝 Frontend para consumir la API
class BlogPagination {
constructor() {
this.currentPage = 1;
this.limit = 12;
this.loadArticles();
}
async loadArticles(category = '') {
try {
const params = new URLSearchParams({
page: this.currentPage,
limit: this.limit
});
if (category) params.append('category', category);
const response = await fetch(`/api/articles?${params}`);
const result = await response.json();
if (result.success) {
this.displayArticles(result.data);
this.updatePagination(result.pagination);
}
} catch (error) {
console.error('Error loading articles:', error);
}
}
displayArticles(articles) {
const container = document.querySelector('.articles-grid');
// ✅ MEJOR: Limpiar contenedor con replaceChildren()
container.replaceChildren();
articles.forEach(article => {
// Crear elementos con createElement
const articleCard = document.createElement('div');
articleCard.className = 'article-card';
const title = document.createElement('h3');
title.textContent = article.title;
const excerpt = document.createElement('p');
excerpt.textContent = article.content.substring(0, 150) + '...';
const category = document.createElement('span');
category.className = 'category';
category.textContent = article.category;
// Ensamblar la tarjeta
articleCard.appendChild(title);
articleCard.appendChild(excerpt);
articleCard.appendChild(category);
// Agregar al contenedor
container.appendChild(articleCard);
});
}
}
🐍 Paginación Backend con FastAPI/Python
FastAPI ofrece paginación elegante con validación automática y documentación OpenAPI.
📝 Instalación y configuración
# requirements.txt
# fastapi==0.116.1
# uvicorn==0.24.0
# sqlalchemy==2.0.23
# psycopg2-binary==2.9.9
# Instalación
# pip install -r requirements.txt
📝 Modelos con SQLAlchemy
from sqlalchemy import Column, Integer, String, DateTime, Text
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
Base = declarative_base()
# Modelo de base de datos
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True)
name = Column(String(200), nullable=False)
description = Column(Text)
price = Column(Integer) # En centavos
category = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow)
# Schemas de Pydantic para validación
class ProductBase(BaseModel):
name: str
description: Optional[str] = None
price: int
category: Optional[str] = None
class ProductResponse(ProductBase):
id: int
created_at: datetime
class Config:
from_attributes = True
# ✅ Schema de paginación reutilizable
class PaginatedResponse(BaseModel):
items: List[ProductResponse]
total: int
page: int
size: int
pages: int
📝 API con FastAPI
from fastapi import FastAPI, Depends, Query
from sqlalchemy.orm import Session
import math
app = FastAPI(title="Catálogo API")
# ✅ ENDPOINT PRINCIPAL: Productos con paginación
@app.get("/products", response_model=PaginatedResponse)
async def get_products(
page: int = Query(1, ge=1, description="Número de página"),
size: int = Query(12, ge=1, le=100, description="Elementos por página"),
category: Optional[str] = Query(None, description="Filtrar por categoría"),
db: Session = Depends(get_db)
):
# Query base
query = db.query(Product)
# Filtro opcional por categoría
if category:
query = query.filter(Product.category == category)
# Contar total de elementos
total = query.count()
# Calcular offset
offset = (page - 1) * size
# Obtener productos paginados
products = query.order_by(Product.created_at.desc()).offset(offset).limit(size).all()
# Calcular total de páginas
total_pages = math.ceil(total / size)
return PaginatedResponse(
items=products,
total=total,
page=page,
size=size,
pages=total_pages
)
# Ejecutar servidor
# uvicorn main:app --reload
💪 Ventajas de FastAPI:
- Validación automática: Los parámetros se validan sin código extra
- Documentación OpenAPI: Swagger UI automático en /docs
- Type hints: Mejor IDE support y menos errores
☕ Paginación Backend con Spring Boot/Java
Spring Boot ofrece la paginación más robusta y empresarial con Spring Data JPA.
📝 Dependencias Maven
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
📝 Entidad JPA
// User.java
package com.studycodepro.demo.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false, unique = true)
private String email;
private String role;
private Boolean active = true;
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
// Constructors, getters y setters
public User() {}
public User(String username, String email, String role) {
this.username = username;
this.email = email;
this.role = role;
}
// Getters y setters...
}
📝 Repository con paginación
// UserRepository.java
package com.studycodepro.demo.repository;
import com.studycodepro.demo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ✅ MÉTODOS AUTOMÁTICOS: Spring genera el SQL
Page<User> findByRoleContainingIgnoreCase(String role, Pageable pageable);
Page<User> findByActiveTrue(Pageable pageable);
Page<User> findByUsernameContainingIgnoreCaseOrEmailContainingIgnoreCase(
String username,
String email,
Pageable pageable
);
}
📝 Controller con Pageable
// UserController.java
package com.studycodepro.demo.controller;
import com.studycodepro.demo.entity.User;
import com.studycodepro.demo.repository.UserRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ✅ ENDPOINT PRINCIPAL: Usuarios con paginación automática
@GetMapping
public Page<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "12") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "desc") String sortDir,
@RequestParam(required = false) String role,
@RequestParam(required = false) String search
) {
// Crear objeto de ordenamiento
Sort sort = sortDir.equalsIgnoreCase("desc") ?
Sort.by(sortBy).descending() :
Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
// Filtros opcionales
if (search != null && !search.trim().isEmpty()) {
return userRepository.findByUsernameContainingIgnoreCaseOrEmailContainingIgnoreCase(
search, search, pageable
);
}
if (role != null && !role.trim().isEmpty()) {
return userRepository.findByRoleContainingIgnoreCase(role, pageable);
}
// Todos los usuarios activos
return userRepository.findByActiveTrue(pageable);
}
}
💪 Ventajas de Spring Boot:
- Zero SQL: Los métodos de repository se generan automáticamente
- Pageable interface: Sorting y paginación integrados
- Empresarial: Robusto para aplicaciones grandes
🎯 ¿Cuál elegir? Decisiones estratégicas
🟨 Cuándo usar Frontend (JavaScript)
- Datasets pequeños: Menos de 1000 elementos
- UX prioritaria: Navegación instantánea sin cargas
- Búsqueda local: Filtros rápidos sin consultas
- Aplicaciones SPA: React, Vue, Angular
🟢 Cuándo usar Backend
- Datasets grandes: Miles o millones de registros
- Performance crítica: Memoria limitada del navegador
- SEO importante: URLs indexables (/page/2)
- Múltiples usuarios: Datos actualizados en tiempo real
❌ Error común:
Usar Frontend para 10,000+ elementos causará crashes del navegador y experiencia terrible.
💡 Consejos profesionales
⚡ Performance
- Frontend: Usa `virtual scrolling` para listas enormes
- Backend: Indexa las columnas de ordenamiento en BD
- Caché: Guarda las páginas frecuentes en localStorage/Redis
🔍 SEO
- URLs amigables: `/blog/page/2` mejor que `?page=2`
- Meta tags dinámicos: Cada página con title único
- Server-side rendering: Para contenido indexable
📱 UX
- Loading states: Spinners durante cargas
- Infinite scroll: Alternativa moderna a paginación
- Breadcrumbs: "Página 2 de 15" para orientación
🎯 ¿Listo para implementar paginación profesional?
Domina el desarrollo web completo con nuestros tutorials paso a paso
👉 JavaScript createElement: Elementos DOM DinámicosAprende a crear elementos HTML con JavaScript como en este tutorial
🚀 Conclusiones
La paginación es esencial en aplicaciones web modernas. Frontend para UX instantánea, Backend para escalabilidad masiva.
Tu siguiente paso: Implementa la paginación apropiada según tu dataset. ¿Menos de 1000 elementos? Frontend. ¿Más? Backend sin dudarlo.