Lee las estrellas de un repo de GitHub sin agotar el límite
Respuesta rápida
El número de estrellas de cualquier repositorio público es un campo de un endpoint, y no necesitas token para leerlo:
stargazers_count es el número. El endpoint devuelve el objeto completo del repositorio, así que jq está solo para sacar un campo de ahí.
Esa es toda la respuesta si estás haciendo un script. El resto del post es la parte que muerde cuando lo pones en una web: 60 peticiones por hora sin autenticar, 5.000 con token, y un contador que llame a la API en cada visita se come cualquiera de las dos.
El campo, y los que se confunden con él
El objeto del repositorio trae varios contadores que parecen intercambiables y no lo son:
| Campo | Qué cuenta |
|---|---|
stargazers_count | Estrellas. Es el número que se ve en la página del repo |
watchers_count | Históricamente un duplicado del contador de estrellas en este endpoint |
subscribers_count | Gente que de verdad está vigilando el repo para recibir notificaciones |
forks_count | Forks |
Si alguna vez te has preguntado por qué watchers_count coincide exactamente con tus estrellas, es por eso: el nombre del campo sobrevivió al cambio de significado de "starring". Usa stargazers_count y sé explícito.
Los límites, que son lo que de verdad decide tu diseño
La API REST de GitHub permite 60 peticiones por hora sin autenticar y 5.000 por hora con un token de acceso personal. Dos detalles importan más que los números:
El límite sin autenticar es por dirección IP. En un portátil, esa es tu cuota. En un host de cloud, un runner de CI o cualquier cosa detrás de un NAT compartido, estás repartiendo esas 60 peticiones por hora con el resto de inquilinos de la dirección — que es por lo que el mismo script que iba bien en local empieza a devolver 403 en cuanto corre en CI.
Leer un repo público también cuenta. No hay tarifa gratuita para "solo un número". Cada llamada a /repos/{owner}/{repo} gasta una petición leas un campo o todos.
Puedes comprobar cómo vas sin gastar cuota:
Juntando las dos cosas, el diseño se cae solo: una web pública no puede llamar a la API de GitHub por visitante. Necesita su propio endpoint, un token y una caché.
1 — Tu propio endpoint, con el token en el servidor
El token va en una variable de entorno leída en servidor. Un token enviado al navegador es un token publicado:
Dos cosas de ahí son correcciones a lo que este post publicaba antes, y merecen decirse claras.
catch (err: unknown), no catch (err: Error). La versión anterior de este post anotaba la variable del catch como Error, que no es TypeScript legal — el compilador lo rechaza con el TS1196, "Catch clause variable type annotation must be any or unknown if specified". Es un error duro, no una opción de estrictez: se puede lanzar cualquier cosa, así que el compilador no te deja afirmar lo contrario. Estrecha el tipo dentro del bloque si necesitas el mensaje.
No reenvíes al cliente el mensaje de error de arriba. La versión vieja devolvía message: err.message tal cual desde Octokit. Eso filtra: bajo límite de peticiones GitHub responde API rate limit exceeded for user ID 12345, que revela el id de la cuenta detrás del token, y con un token revocado responde Bad credentials, lo que convierte tu endpoint público en un oráculo en vivo de si el PAT sigue funcionando. Registra el detalle en servidor y devuelve un mensaje plano.
2 — La caché es todo el asunto
La cabecera Cache-Control de arriba es lo que hace gratis el contador:
s-maxage=3600 le dice al CDN que guarde la respuesta una hora, así que mil visitantes en esa hora producen una llamada a GitHub — 1 de tus 5.000, no 1.000. stale-while-revalidate=86400 es la parte que la gente se salta: durante un día después de caducar, el edge sigue sirviendo el último número conocido mientras refresca por detrás. Si GitHub está caído o te han limitado, el widget enseña un número algo viejo en vez de un error.
El trato sale barato porque un contador de estrellas no es urgente. Nadie nota un número de hace cincuenta minutos; todo el mundo nota un widget roto.
3 — El componente
Hacer el fetch en un useEffect y codificar los estados como números mágicos es lo que hacía la versión original de este post, y ha envejecido mal. 0 significaba cargando, -1 error, y había una rama para -2 que nadie asignaba nunca — código muerto renderizando un estado imposible.
Una librería de fetching te da los tres estados como estados de verdad:
dedupingInterval importa más de lo que parece: sin él, varios componentes pidiendo la misma clave montan y cada uno lanza su petición. Con él, comparten una.
El componente ya renderiza carga, error y éxito sin inventarse valores centinela:
El aria-label no es decoración. Sin él, todo el contenido de este enlace es un icono de estrella y un número pelado — y mientras carga, un spinner y nada más. Una comprobación de accesibilidad en esta web lo marcó como enlace sin nombre accesible, anunciado a un lector de pantalla como "enlace" y sin destino.
Si solo necesitas el número una vez
Todo lo anterior es para un contador en vivo. Si el número solo tiene que ser correcto en tiempo de build, sáltate el endpoint y léelo en getStaticProps: una petición por build, sin cuota en runtime y sin caché que razonar. Esta web no lo hace porque el valor tiene que actualizarse entre despliegues y el blog es estático — el endpoint existe para darle a una página estática un número vivo.
Los errores que conviene recordar
catch (err: Error)no compila. Usaunknown.- Nunca devuelvas el mensaje de error de arriba desde un endpoint público.
- Las 60 peticiones por hora son por IP, y las IPs compartidas la comparten.
- Cachea en el edge, no en una variable de módulo — las funciones serverless no la conservan.
stale-while-revalidatees lo que convierte una caída en un número algo viejo.
Ahora mismo solo tienes que mirar arriba para ver el resultado, el contador está resaltado con una estrella.
Por favor, si crees que este contenido te ha ayudado o te gusta, dame tu estrella. 🤩
