trakt widget.
A real-time widget that displays the last movie or TV show I watched on Trakt.tv — powered by Express, TMDB posters, and PostgreSQL token persistence.
Trakt Widget
I watch a lot of movies and TV shows — probably too many, honestly. So instead of just saying "I like films" on my portfolio, I figured: why not show exactly what I watched last night?
This widget pulls my latest watch from Trakt.tv, grabs a poster from TMDB, and displays it right on the homepage. It's one of those small personal details that makes a portfolio feel like it belongs to an actual human.
Live Demo
Here's the actual widget running live — pulling my latest watch from Trakt right now, not a screenshot.
Why Build This?
There's something fun about showing visitors exactly what you binged last weekend. Maybe it's a Criterion Collection classic, maybe it's trashy TV — either way, it's authentic, and it's sparked some great conversations.
On the technical side, the challenge was compelling too:
- Stay authenticated forever — Trakt's OAuth token rotation needs to be handled transparently
- Enrich with poster art — TMDB provides the beautiful cover images
- Persist tokens safely — PostgreSQL stores refresh tokens so they survive server restarts
- Be efficient — smart caching to stay well within API rate limits
Architecture
The system has four moving parts that all work together seamlessly.
1. Express API Server
A lightweight Express.js server that exposes a single endpoint: /api/trakt/last. It handles CORS, initializes the database on startup, and self-pings every 10 minutes on Render's free tier to prevent spin-down.
2. Trakt Integration Layer
The brain of the operation — this module handles:
// Parallel calls for latest movie + episode
const [moviesResp, episodesResp] = await Promise.all([
fetch(`${TRAKT_API_BASE}/sync/history/movies?limit=1`, { headers }),
fetch(`${TRAKT_API_BASE}/sync/history/episodes?limit=1`, { headers }),
]);It fetches both my latest movie and latest episode in parallel, picks whichever was watched more recently, then enriches it with a TMDB poster. This parallel approach cuts response time roughly in half.
3. Token Management
This was the trickiest part to get right. Trakt can rotate your refresh token at any time, so the system needs to detect that and save the new one:
async function refreshAccessToken() {
const refreshToken = await getToken(); // from DB or env
const resp = await fetch(`${TRAKT_API_BASE}/oauth/token`, {
method: "POST",
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const data = await resp.json();
// If Trakt rotated the token, persist the new one
if (data.refresh_token && data.refresh_token !== refreshToken) {
await saveToken(data.refresh_token);
}
return data.access_token;
}The fallback chain is simple: Database → Environment Variable → Error. In development, you can skip the database entirely and just use your .env file.
4. Frontend Component
A React client component on the Next.js side that fetches from the Express API and renders the poster, title, and link:
const { data: response } = useSWR<TraktResponse>(
`${TRAKT_API_URL}/api/trakt/last`,
fetcher,
{
refreshInterval: 300000, // refresh every 5 minutes
refreshWhenHidden: false,
}
);The component gracefully handles movies vs. episodes (formatting "S2 E7" for TV), missing posters, and empty states.
Caching Strategy
This is what keeps the system efficient and respectful of API limits:
- 5-minute TTL server-side cache on the Express server
- Even with 100 visitors hitting the portfolio, it's still just ~12 API calls per hour
- That's an 84% reduction compared to no caching
| Metric | Without Cache | With Cache |
|---|---|---|
| Trakt API calls/day | ~300 | ~48 |
| TMDB API calls/day | ~300 | ~48 |
| Response time | ~800ms | ~5ms (cached) |
Both Trakt and TMDB allow ~1,000 requests/day, so we're well within limits.
Getting Started
If you want to build something similar, the setup is straightforward:
- Create a Trakt app at trakt.tv/oauth/applications — set the redirect URI to
urn:ietf:wg:oauth:2.0:oob - Get a TMDB API key at themoviedb.org/settings/api (free, optional but recommended for posters)
- Run the token script —
node get-token.jswalks you through a one-time OAuth flow - Deploy — Render, Railway, or Fly.io all work great for the Express backend
The refresh token works forever unless you revoke app access. One-time setup, zero maintenance.
Error Handling
Like the Spotify widget, the philosophy is never show something broken:
| Failure | Response |
|---|---|
| Trakt API down | Returns { ok: false }, component shows "no recent activity" |
| TMDB unavailable | Widget still works, just without the poster image |
| Token expired | Auto-refreshes, persists rotated token |
| Database offline | Falls back to environment variable |
| No watch history | Friendly empty state |
Dependencies
The backend is intentionally minimal:
{
"express": "^4.21.2",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"node-fetch": "^3.3.2",
"pg": "^8.13.1"
}What I Learned
Building this taught me that the most impactful portfolio features aren't always the most technically impressive — they're the ones that show who you are. The OAuth flow and caching system are solid engineering, sure, but what makes this special is that it's real.
When someone visits and sees I just watched the same show they're hooked on, we're not just developer and visitor anymore — we're two people who might geek out about the same stuff. That's the whole point.