API de Unsplash
Receta
Utiliza la API de Unsplash para buscar y mostrar fotos de alta calidad y libres de derechos en tu aplicación Next.js. Obtén datos en Server Components para mantener tu clave API segura y evitar limitaciones de velocidad en el lado del cliente.
Paso 1: Obtén una clave API
- Crea una cuenta de desarrollador en unsplash.com/developers.
- Crea una nueva aplicación para recibir una Access Key.
- Almacena la clave en
.env.local:
UNSPLASH_ACCESS_KEY=your_access_key_herePaso 2: Crea un cliente API tipado
// lib/unsplash.ts
interface UnsplashPhoto {
id: string;
alt_description: string | null;
urls: {
raw: string;
full: string;
regular: string;
small: string;
thumb: string;
};
user: {
name: string;
links: {
html: string;
};
};
links: {
download_location: string;
};
width: number;
height: number;
}
interface UnsplashSearchResponse {
total: number;
total_pages: number;
results: UnsplashPhoto[];
}
const UNSPLASH_BASE = "https://api.unsplash.com";
async function unsplashFetch<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
const url = new URL(`${UNSPLASH_BASE}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
}
const res = await fetch(url.toString(), {
headers: {
Authorization: `Client-ID ${process.env.UNSPLASH_ACCESS_KEY}`,
},
next: { revalidate: 3600 },
});
if (!res.ok) {
throw new Error(`Unsplash API error: ${res.status} ${res.statusText}`);
}
return res.json();
}
export async function searchPhotos(query: string, page = 1, perPage = 12) {
return unsplashFetch<UnsplashSearchResponse>("/search/photos", {
query,
page: String(page),
per_page: String(perPage),
orientation: "landscape",
});
}
export async function getRandomPhotos(count = 6, query?: string) {
const params: Record<string, string> = { count: String(count) };
if (query) params.query = query;
return unsplashFetch<UnsplashPhoto[]>("/photos/random", params);
}
export async function trackDownload(downloadLocation: string) {
await unsplashFetch<void>(downloadLocation.replace(UNSPLASH_BASE, ""));
}Ejemplo Funcional
Un componente de búsqueda de imágenes que obtiene datos de Unsplash con la atribución adecuada:
// app/photos/page.tsx
import { searchPhotos } from "@/lib/unsplash";
import { PhotoGrid } from "./photo-grid";
import { SearchForm } from "./search-form";
interface PageProps {
searchParams: Promise<{ q?: string; page?: string }>;
}
export default async function PhotosPage({ searchParams }: PageProps) {
const params = await searchParams;
const query = params.q || "nature";
const page = Number(params.page) || 1;
const results = await searchPhotos(query, page);
return (
<main className="mx-auto max-w-6xl px-4 py-8">
<h1 className="mb-6 text-3xl font-bold">Búsqueda de Fotos</h1>
<SearchForm initialQuery={query} />
<p className="mb-4 text-sm text-gray-500">
{results.total.toLocaleString()} resultados para "{query}"
</p>
<PhotoGrid photos={results.results} />
<div className="mt-6 flex justify-center gap-4">
{page > 1 && (
<a href={`/photos?q=${query}&page=${page - 1}`} className="rounded bg-gray-200 px-4 py-2">
Anterior
</a>
)}
{page < results.total_pages && (
<a href={`/photos?q=${query}&page=${page + 1}`} className="rounded bg-gray-200 px-4 py-2">
Siguiente
</a>
)}
</div>
</main>
);
}// app/photos/search-form.tsx
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export function SearchForm({ initialQuery }: { initialQuery: string }) {
const [query, setQuery] = useState(initialQuery);
const router = useRouter();
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (query.trim()) {
router.push(`/photos?q=${encodeURIComponent(query.trim())}`);
}
}
return (
<form onSubmit={handleSubmit} className="mb-6 flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar fotos..."
className="flex-1 rounded-lg border border-gray-300 px-4 py-2"
/>
<button type="submit" className="rounded-lg bg-black px-6 py-2 text-white hover:bg-gray-800">
Buscar
</button>
</form>
);
}// app/photos/photo-grid.tsx
import Image from "next/image";
interface Photo {
id: string;
alt_description: string | null;
urls: { regular: string; small: string };
user: { name: string; links: { html: string } };
width: number;
height: number;
}
export function PhotoGrid({ photos }: { photos: Photo[] }) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{photos.map((photo) => (
<figure key={photo.id} className="group relative overflow-hidden rounded-xl">
<div className="relative aspect-[3/2]">
<Image
src={photo.urls.regular}
alt={photo.alt_description || "Foto de Unsplash"}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
<figcaption className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-4 text-white opacity-0 transition-opacity group-hover:opacity-100">
Foto de{" "}
<a
href={`${photo.user.links.html}?utm_source=your_app&utm_medium=referral`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{photo.user.name}
</a>
{" en "}
<a
href="https://unsplash.com/?utm_source=your_app&utm_medium=referral"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Unsplash
</a>
</figcaption>
</figure>
))}
</div>
);
}Profundización
Cómo Funciona
- La API de Unsplash proporciona puntos de conexión RESTful para buscar, listar y recuperar fotos. La autenticación se realiza mediante un encabezado
Authorization: Client-ID <access_key>. - El punto de conexión
/search/photosacepta parámetrosquery,page,per_page,orientation,coloryorder_by. - El punto de conexión
/photos/randomdevuelve fotos aleatorias, opcionalmente filtradas porquery,collectionsotopics. - Las fotos vienen con múltiples tamaños de URL:
raw(original),full(JPEG de alta resolución),regular(1080px de ancho),small(400px de ancho) ythumb(200px de ancho). - Los términos de la API de Unsplash requieren que desencadenes un evento de descarga a través de la URL
download_locationcuando un usuario descargue o utilice una foto. - Los límites de velocidad son 50 solicitudes por hora para aplicaciones de demostración y 5,000 solicitudes por hora para aplicaciones de producción (aprobadas).
Variaciones
Componente de foto aleatoria para secciones heroicas:
// app/components/random-hero.tsx
import Image from "next/image";
import { getRandomPhotos } from "@/lib/unsplash";
export async function RandomHero() {
const [photo] = await getRandomPhotos(1, "landscape");
return (
<div className="relative h-[60vh] w-full overflow-hidden">
<Image
src={photo.urls.full}
alt={photo.alt_description || "Imagen heroica"}
fill
priority
sizes="100vw"
className="object-cover"
/>
<div className="absolute bottom-4 right-4 text-sm text-white/80">
Foto de{" "}
<a href={photo.user.links.html} className="underline">
{photo.user.name}
</a>
</div>
</div>
);
}Notas de TypeScript
- Define tipos de respuesta que coincidan con la estructura JSON de la API de Unsplash. El SDK oficial
unsplash-jsproporciona tipos, pero un fetch tipado ligero suele ser suficiente. - Utiliza
next: { revalidate }en las opciones de fetch para controlar el almacenamiento en caché del lado del servidor en Next.js.
// Opcional: utiliza el SDK oficial
// npm install unsplash-js
import { createApi } from "unsplash-js";
const unsplash = createApi({
accessKey: process.env.UNSPLASH_ACCESS_KEY!,
});Problemas Comunes
- La API de Unsplash requiere atribución: debes acreditar al fotógrafo con un enlace a su perfil de Unsplash y enlazarlos a Unsplash. Incluye
utm_source=your_app&utm_medium=referralen los enlaces de atribución. - Debes llamar al punto de conexión
download_locationcuando un usuario selecciona o descarga activamente una foto. Este es un requisito de los términos de la API, no opcional. - Las aplicaciones de demostración se limitan a 50 solicitudes por hora. Solicita el estado de producción para obtener 5,000 solicitudes por hora.
- Nunca expongas tu Access Key en código del lado del cliente. Siempre obtén datos desde Server Components o rutas de API.
- La URL
rawdevuelve una imagen sin procesar que puede ser muy grande (10MB+). Utilizaregularosmallpara mostrar yfullpara descargas. next/imagerequiereimages.remotePatternsparaimages.unsplash.comennext.config.ts. Sin esto, las imágenes no se cargarán.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| API de Unsplash | Fotos de la más alta calidad, biblioteca grande | Atribución requerida, límites de velocidad |
| API de Pexels | No se requiere atribución (pero se aprecia), límites generosos | Biblioteca más pequeña que Unsplash |
| API de Pixabay | Verdaderamente gratis, sin atribución necesaria | Calidad promedio más baja, anuncios en resultados |
| Lorem Picsum | Marcadores de posición simples basados en URL, sin clave de API | Solo fotos aleatorias, sin búsqueda |
Preguntas Frecuentes
¿Cómo te autenticas con la API de Unsplash?
- Utiliza un encabezado
Authorization: Client-ID <access_key>en cada solicitud. - Almacena la clave de acceso en
.env.localcomoUNSPLASH_ACCESS_KEY. - Nunca expongas la clave en código del lado del cliente.
¿Cuáles son los límites de velocidad de la API de Unsplash para aplicaciones de demostración frente a producción?
- Aplicaciones de demostración: 50 solicitudes por hora.
- Aplicaciones de producción (aprobadas): 5,000 solicitudes por hora.
- Solicita el estado de producción a través del panel de control del desarrollador de Unsplash.
¿Qué tamaños de URL de foto devuelve la API de Unsplash?
raw-- original, sin procesar (puede ser 10MB+).full-- JPEG de alta resolución.regular-- 1080px de ancho.small-- 400px de ancho.thumb-- 200px de ancho.
Problema común: ¿Qué atribución se requiere al usar fotos de Unsplash?
- Acredita al fotógrafo con un enlace a su perfil de Unsplash.
- Enlaza a Unsplash.
- Incluye
utm_source=your_app&utm_medium=referralen los enlaces de atribución. - También debes llamar al punto de conexión
download_locationcuando un usuario descarga o utiliza una foto.
¿Por qué debes obtener datos de Unsplash en Server Components en lugar de en el lado del cliente?
- Mantiene tu clave API segura (nunca se expone en el navegador).
- Evita problemas de limitación de velocidad del lado del cliente.
- Habilita
next: { revalidate }para el almacenamiento en caché del lado del servidor.
¿Cómo añades imágenes de Unsplash a los patrones remotos de next/image?
// next.config.ts
images: {
remotePatterns: [
{ protocol: "https", hostname: "images.unsplash.com" },
],
}Sin esto, next/image no podrá cargar fotos de Unsplash.
¿Qué enfoque de TypeScript se utiliza para el cliente de la API de Unsplash en esta receta?
- Define interfaces que coincidan con la estructura JSON de la API (
UnsplashPhoto,UnsplashSearchResponse). - Utiliza una función genérica
unsplashFetch<T>para solicitudes seguras de tipo. - El SDK oficial
unsplash-jstambién proporciona tipos, pero un fetch tipado ligero suele ser suficiente.
¿Cómo funciona la prop searchParams en el ejemplo de página de búsqueda de fotos?
interface PageProps {
searchParams: Promise<{ q?: string; page?: string }>;
}
const params = await searchParams;
const query = params.q || "nature";En el App Router de Next.js, searchParams es una Promise que debe ser esperada.
Problema común: ¿Qué sucede si utilizas la URL raw para propósitos de visualización?
- La URL
rawdevuelve una imagen sin procesar que puede ser muy grande (10MB+). - Utiliza
regularosmallpara mostrar. - Reserva
fullsolo para descargas.
¿Cómo funciona el almacenamiento en caché de revalidación en la función de búsqueda de Unsplash?
next: { revalidate: 3600 }almacena en caché la respuesta durante 1 hora en el servidor.- Después de 3600 segundos, la siguiente solicitud desencadena una revalidación en segundo plano.
- Esto reduce las llamadas a la API y se mantiene dentro de los límites de velocidad.
¿Qué es el punto de conexión download_location y cuándo debes llamarlo?
- Es una URL devuelta en el campo
links.download_locationde cada foto. - Debes llamarlo cuando un usuario selecciona o descarga activamente una foto.
- Este es un requisito de los términos de la API de Unsplash, no opcional.