·5 min read

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.

traktapiexpressreacttmdb

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:

javascript
// 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:

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

typescript
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
MetricWithout CacheWith 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:

  1. Create a Trakt app at trakt.tv/oauth/applications — set the redirect URI to urn:ietf:wg:oauth:2.0:oob
  2. Get a TMDB API key at themoviedb.org/settings/api (free, optional but recommended for posters)
  3. Run the token scriptnode get-token.js walks you through a one-time OAuth flow
  4. 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:

FailureResponse
Trakt API downReturns { ok: false }, component shows "no recent activity"
TMDB unavailableWidget still works, just without the poster image
Token expiredAuto-refreshes, persists rotated token
Database offlineFalls back to environment variable
No watch historyFriendly empty state

Dependencies

The backend is intentionally minimal:

json
{
  "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.