API de Pexels
Receta
Usa la API de Pexels para obtener fotos y videos de alta calidad y uso libre. Pexels utiliza un encabezado Authorization para la autenticación y proporciona colecciones curadas junto con búsqueda.
Paso 1: Obtén una clave API
- Crea una cuenta en pexels.com.
- Visita pexels.com/api y solicita una clave API.
- Almacena la clave en
.env.local:
PEXELS_API_KEY=your_api_key_herePaso 2: Crea un cliente API tipado
// lib/pexels.ts
interface PexelsPhotoSrc {
original: string;
large2x: string;
large: string;
medium: string;
small: string;
portrait: string;
landscape: string;
tiny: string;
}
interface PexelsPhoto {
id: number;
width: number;
height: number;
url: string;
photographer: string;
photographer_url: string;
photographer_id: number;
avg_color: string;
src: PexelsPhotoSrc;
alt: string;
}
interface PexelsSearchResponse {
total_results: number;
page: number;
per_page: number;
photos: PexelsPhoto[];
next_page?: string;
prev_page?: string;
}
const PEXELS_BASE = "https://api.pexels.com/v1";
async function pexelsFetch<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = new URL(`${PEXELS_BASE}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
}
const res = await fetch(url.toString(), {
headers: {
Authorization: process.env.PEXELS_API_KEY!,
},
next: { revalidate: 3600 },
});
if (!res.ok) {
throw new Error(`Pexels API error: ${res.status} ${res.statusText}`);
}
return res.json();
}
export async function searchPhotos(query: string, page = 1, perPage = 15) {
return pexelsFetch<PexelsSearchResponse>("/search", {
query,
page: String(page),
per_page: String(perPage),
});
}
export async function getCuratedPhotos(page = 1, perPage = 15) {
return pexelsFetch<PexelsSearchResponse>("/curated", {
page: String(page),
per_page: String(perPage),
});
}
export async function getPhoto(id: number) {
return pexelsFetch<PexelsPhoto>(`/photos/${id}`);
}
export type { PexelsPhoto, PexelsPhotoSrc, PexelsSearchResponse };Ejemplo trabajado
Un componente selector de imagen hero que permite a los editores buscar y seleccionar una imagen hero:
// app/hero-picker/page.tsx
import { getCuratedPhotos, searchPhotos } from "@/lib/pexels";
import { HeroImagePicker } from "./hero-image-picker";
interface PageProps {
searchParams: Promise<{ q?: string }>;
}
export default async function HeroPickerPage({ searchParams }: PageProps) {
const params = await searchParams;
const query = params.q;
const results = query
? await searchPhotos(query, 1, 12)
: await getCuratedPhotos(1, 12);
return (
<main className="mx-auto max-w-5xl px-4 py-8">
<h1 className="mb-2 text-3xl font-bold">Selector de imagen Hero</h1>
<p className="mb-6 text-gray-500">
{query ? `Resultados para "${query}"` : "Fotos curadas"}
</p>
<HeroImagePicker photos={results.photos} initialQuery={query || ""} />
</main>
);
}// app/hero-picker/hero-image-picker.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import type { PexelsPhoto } from "@/lib/pexels";
interface HeroImagePickerProps {
photos: PexelsPhoto[];
initialQuery: string;
}
export function HeroImagePicker({ photos, initialQuery }: HeroImagePickerProps) {
const [query, setQuery] = useState(initialQuery);
const [selected, setSelected] = useState<PexelsPhoto | null>(null);
const router = useRouter();
function handleSearch(e: React.FormEvent) {
e.preventDefault();
if (query.trim()) {
router.push(`/hero-picker?q=${encodeURIComponent(query.trim())}`);
}
}
return (
<div>
<form onSubmit={handleSearch} className="mb-6 flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Busca imágenes hero..."
className="flex-1 rounded-lg border border-gray-300 px-4 py-2"
/>
<button
type="submit"
className="rounded-lg bg-emerald-600 px-6 py-2 text-white hover:bg-emerald-700"
>
Buscar
</button>
</form>
{selected && (
<div className="mb-8 overflow-hidden rounded-2xl border-2 border-emerald-500">
<p className="bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-700">
Imagen Hero seleccionada
</p>
<div className="relative aspect-[21/9]">
<Image
src={selected.src.large2x}
alt={selected.alt}
fill
sizes="100vw"
priority
className="object-cover"
/>
</div>
<div className="flex items-center justify-between bg-gray-50 px-4 py-3 text-sm">
<span>
Foto de{" "}
<a
href={selected.photographer_url}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-emerald-600 hover:underline"
>
{selected.photographer}
</a>
{" en "}
<a
href="https://www.pexels.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium text-emerald-600 hover:underline"
>
Pexels
</a>
</span>
<span className="text-gray-500">
{selected.width} x {selected.height}
</span>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
{photos.map((photo) => (
<button
key={photo.id}
onClick={() => setSelected(photo)}
className={`group relative overflow-hidden rounded-lg transition-all ${
selected?.id === photo.id
? "ring-3 ring-emerald-500 ring-offset-2"
: "hover:ring-2 hover:ring-gray-300"
}`}
>
<div className="relative aspect-[3/2]" style={{ backgroundColor: photo.avg_color }}>
<Image
src={photo.src.medium}
alt={photo.alt}
fill
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-cover"
/>
</div>
<div className="absolute bottom-0 left-0 right-0 bg-black/50 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100">
{photo.photographer}
</div>
</button>
))}
</div>
</div>
);
}Análisis profundo
Cómo funciona
- La API de Pexels se autentica a través de un encabezado
Authorizationque contiene solo la clave API (sin prefijo "Bearer"). - El endpoint
/v1/searchaceptaquery,orientation(landscape, portrait, square),size(large, medium, small),color,locale,pageyper_page(máximo 80). - El endpoint
/v1/curateddevuelve fotos curadas editorialmente, útil para contenido predeterminado o destacado. - Cada objeto de foto incluye un objeto
srccon tamaños pregenerados:original,large2x(1880px),large(940px),medium(350px),small(130px),portrait(800x1200),landscape(1200x627) ytiny(280x200). - El campo
avg_colorproporciona el color dominante como una cadena hexadecimal, útil como fondo placeholder mientras se carga la imagen. - La paginación usa URLs
next_pageyprev_pageen el cuerpo de la respuesta en lugar de conteos totales de páginas.
Variaciones
Usar avg_color como un placeholder de carga:
<div
className="relative aspect-[3/2]"
style={{ backgroundColor: photo.avg_color }}
>
<Image src={photo.src.medium} alt={photo.alt} fill className="object-cover" />
</div>Obtener fotos por orientación:
const landscapePhotos = await pexelsFetch<PexelsSearchResponse>("/search", {
query: "mountains",
orientation: "landscape",
per_page: "10",
});Notas de TypeScript
- La interfaz
PexelsPhotoSrcse asigna directamente al objetosrcde la API. Cada clave de tamaño siempre está presente en la respuesta. - Exporta tipos desde tu módulo de cliente API para que los componentes puedan hacer referencia a ellos sin redeclararlos.
- Los campos
next_pageyprev_pageson cadenas opcionales (URLs) que pueden estar ausentes en la primera o última página.
Problemas conocidos
- El valor del encabezado
Authorizationes la clave API sin formato, noBearer <key>. Usar el prefijo Bearer devuelve un 401. - Pexels requiere atribución: acredita al fotógrafo y vincula a Pexels. Aunque no es legalmente obligatorio para todos los casos de uso, se recomienda encarecidamente y es parte de los términos de la API.
- El límite de velocidad es 200 solicitudes por hora y 20,000 por mes. Exceder el límite devuelve un estado 429.
- El máximo
per_pagees 80. Los valores por encima de 80 se establecen silenciosamente por defecto a 80. - Las URL de las fotos de Pexels usan
images.pexels.comcomo nombre de host. Agrega esto aremotePatternsennext.config.ts. - El campo
altde la API puede estar vacío o ser genérico. Considera agregar tu propio texto alternativo para la accesibilidad.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| API de Pexels | Buena calidad, endpoint curado, campo avg_color | Biblioteca más pequeña que Unsplash |
| API de Unsplash | Biblioteca de fotos libre más grande | Atribución estrictamente requerida |
| API de Pixabay | No se requiere atribución, incluye videos | Calidad promedio más baja |
| API de Shutterstock | Biblioteca masiva, contenido editorial | Pagado, licencias complejas |
Preguntas frecuentes
¿Cómo funciona la autenticación de la API de Pexels?
- Usa un encabezado
Authorizationcon la clave API sin formato como valor. - No uses un prefijo
Bearer; hacerlo devuelve un error 401. - Almacena la clave en
.env.localcomoPEXELS_API_KEY.
¿Qué es el endpoint /v1/curated y cuándo deberías usarlo?
- Devuelve fotos curadas editorialmente seleccionadas por el personal de Pexels.
- Úsalo para contenido predeterminado, secciones destacadas o cuando no se proporcione una consulta de búsqueda.
- Acepta parámetros
pageyper_page.
¿Qué tamaños de fotos incluye el objeto src de Pexels?
original,large2x(1880px),large(940px),medium(350px),small(130px).portrait(800x1200),landscape(1200x627),tiny(280x200).- Cada clave de tamaño siempre está presente en la respuesta.
¿Qué es el campo avg_color y cómo puedes usarlo?
<div style={{ backgroundColor: photo.avg_color }}>
<Image src={photo.src.medium} alt={photo.alt} fill />
</div>Proporciona el color dominante como una cadena hexadecimal, útil como fondo placeholder mientras se carga la imagen.
Problema conocido: ¿Cuál es el formato correcto para el encabezado Authorization de Pexels?
- El valor es la cadena de clave API sin formato, por ejemplo,
Authorization: abc123xyz. - Usar
Bearer abc123xyzdevuelve un error 401. - Esto difiere de la mayoría de las API que usan el prefijo Bearer.
¿Cuáles son los límites de velocidad de la API de Pexels?
- 200 solicitudes por hora.
- 20,000 solicitudes por mes.
- Exceder cualquiera de los límites devuelve un estado 429.
¿Se requiere atribución para las fotos de Pexels?
- No es legalmente obligatorio para todos los casos de uso.
- Sin embargo, acreditar al fotógrafo y vincular a Pexels se recomienda encarecidamente y es parte de los términos de la API.
¿Cómo funciona la paginación en la respuesta de la API de Pexels?
- La respuesta incluye
next_pageyprev_pagecomo cadenas de URL opcionales. - Estos pueden estar ausentes en la primera página (sin
prev_page) o última página (sinnext_page). - Esto difiere de las API que devuelven conteos totales de páginas.
¿Qué tipos de TypeScript deberías exportar desde el cliente API de Pexels?
export type { PexelsPhoto, PexelsPhotoSrc, PexelsSearchResponse };Exporta tipos junto con funciones de API para que los componentes consumidores puedan hacer referencia a ellos sin redeclararlos.
Problema conocido: ¿Puede el campo alt de la API de Pexels estar vacío?
- Sí, el campo
altpuede estar vacío o ser genérico. - Siempre considera agregar tu propio texto alternativo para una mejor accesibilidad.
¿Cuál es el máximo per_page que permite Pexels y qué sucede si lo superas?
- El máximo es 80.
- Los valores por encima de 80 se establecen silenciosamente por defecto a 80 sin un error.
¿Qué nombre de host debes agregar a remotePatterns para imágenes de Pexels?
- Agrega
images.pexels.comaremotePatternsennext.config.ts. - Sin esto,
next/imageno cargará fotos de Pexels.