Paginación Web: Frontend y Backend Completo con JavaScript, Express, FastAPI y Spring Boot

Domina la paginación desde ambos enfoques: Frontend con JavaScript vanilla y Backend con las 3 tecnologías más populares del mercado

¿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:

15s Tiempo de carga promedio sin paginación
2s Con paginación optimizada

💡 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

<1K Elementos → Frontend
>1K Elementos → Backend
UX Prioridad → Frontend
SEO Prioridad → Backend

🟨 Cuándo usar Frontend (JavaScript)

🟢 Cuándo usar Backend

❌ Error común:

Usar Frontend para 10,000+ elementos causará crashes del navegador y experiencia terrible.

💡 Consejos profesionales

⚡ Performance

🔍 SEO

📱 UX

🎯 ¿Listo para implementar paginación profesional?

Domina el desarrollo web completo con nuestros tutorials paso a paso

👉 JavaScript createElement: Elementos DOM Dinámicos

Aprende 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.