Published on
9 min read

Building social integration widgets with Next.js App Router

Authors
  • avatar
    Name
    Liliana Summers
    Twitter

Remember the glory days of MySpace? When your biggest technical challenge was making sure your auto-playing music didn't clash with your sparkly cursor trail? Those early web widgets – from hit counters to chat boxes – weren't just features; they were personalities encoded in pixels. Each one proudly proclaimed "this is who I am" to the digital world, even if who you were was someone who loved little digital pets and "Under Construction" GIFs.

As a developer who spends my days architecting enterprise solutions (and my nights alternating between Spotify-powered coding sessions and gaming marathons), I found myself missing that era of personal expression. But instead of settling for nostalgia, I decided to bring that concept into 2024 with the technical sophistication it deserves.

The result? A trinity of social widgets that reflect my digital life: my current gaming status across Steam and PlayStation Network, and whatever Spotify track is currently fueling my coding sessions. Built with Next.js 14's App Router and TypeScript, these widgets are more than just status displays – they're a testament to modern web architecture meeting personal expression.

Steam Widget
PlayStation Network Widget
Spotify Widget

But this isn't just another "look what I built" story. This is a deep dive into real-world API integration challenges, token management complexity, and the kind of architectural decisions that separate a full stack developer from someone who just knows how to fetch data. We'll explore how I balanced the technical ambition of real-time updates with the practical realities of edge function costs, and how I turned three very different APIs into a cohesive, maintainable system.

Whether you're a developer looking to level up your API integration game, a tech lead evaluating Next.js 14 features, or just someone who appreciates the artistry of well-structured code, grab your favorite debugging beverage and let's dive in. Just promise me you won't judge my Spotify history too harshly – sometimes you need a Limp Bizkit's "Break Stuff" to debug those particularly nasty production issues.

Let's explore how I turned my gaming and music habits into a showcase of modern web development practices (with a little dash of nostalgia), and maybe learn a few things about API optimization along the way. After all, what better way to flex your developer skills than by combining technical excellence with things you're passionate about?

The Evolution of the Project

What started as real-time widgets using Server-Sent Events (SSE) evolved into a more performant and cost-effective solution. While real-time updates were interesting technically, I realized that:

  1. The data rarely changed frequently enough to justify constant connections
  2. Vercel's edge function usage was unnecessarily high for the actual user benefit
  3. A simple page refresh provides nearly the same user experience for this use case

The Technical Stack

  • Next.js 14 with App Router: For efficient server components and API routes
  • TypeScript: For type safety and better developer experience
  • Tailwind CSS: For responsive and maintainable styling
  • Vercel: For deployment and edge functions

Widget Implementation

Each widget follows a similar pattern but handles its unique API authentication and data structure requirements:

interface WidgetData {
  // Common fields across widgets
  status: string
  lastUpdated: string
  // ... widget-specific fields
}

const SocialWidget: React.FC = () => {
  const [data, setData] = useState<WidgetData | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('/api/service-status')
      const data = await response.json()
      setData(data)
      setLoading(false)
    }

    fetchData()
  }, []) // Fetch once on component mount

  if (loading) return <LoadingState />
  if (!data) return <ErrorState />

  return <WidgetContent data={data} />
}

API Integration Challenges

Each service presented unique authentication and data handling challenges:

  1. Steam: Working with Steam's Web API required handling their unique authentication flow and mapping complex game data structures.
  2. PlayStation Network: PSN's API required careful token management and refresh logic to maintain stable access.
  3. Spotify: Managing OAuth refresh tokens and handling both "currently playing" and "recently played" states.

Performance Optimization

A key learning from this project was about balancing real-time updates with resource usage. Initial implementation used Server-Sent Events for live updates, but I optimized this to a simpler load-time fetch after analyzing:

  • Actual frequency of status changes
  • Edge function usage on Vercel
  • User experience requirements

This optimization reduced server costs while maintaining essentially the same user experience.

API Implementation Details

Steam Integration

Steam's API implementation was straightforward but required careful handling of their data structure. The API provides rich information about game status, including current game, recently played games, and online status.

Steam Widget
interface GameInfo {
  name: string
  appid: string | number
  header_image?: string
  steam_store_link?: string
  last_play_time?: number
}

interface SteamData {
  personaname: string
  avatarfull: string
  personastate: number // 0: Offline, 1: Online, 2: Busy, 3: Away
  lastlogoff: number
  currentGame: GameInfo | null
  recentGame: GameInfo | null
}

The implementation includes status mapping for different online states and automatic game image resolution enhancement:

const personaStates = [
  'Offline',
  'Online',
  'Busy',
  'Away',
  'Snooze',
  'Looking to trade',
  'Looking to play',
]

function getLargerImageUrl(url: string): string {
  return url.replace('/small/', '/large/').replace('_64.', '_512.')
}

PlayStation Network Integration

PSN's API required the most complex authentication handling. We need to manage NPSSO tokens and implement automatic refresh token handling to maintain stable access

PlayStation Network Widget
async function getAuthTokens() {
  if (!cachedAuthTokens || Date.now() - lastTokenRefresh > TOKEN_REFRESH_INTERVAL) {
    return refreshAccessToken()
  }

  // Check if token is about to expire
  const tokenExpiryDate = new Date(lastTokenRefresh + cachedAuthTokens.expiresIn * 1000)
  const now = new Date()
  if (tokenExpiryDate.getTime() - now.getTime() < TOKEN_EXPIRY_ALERT_THRESHOLD) {
    await sendAlertEmail(
      'PSN NPSSO Token Expiring Soon',
      `Token expires on ${tokenExpiryDate.toLocaleString()}`
    )
  }

  return cachedAuthTokens
}

I also implemented an email alert system using Postmark to notify me when tokens need refreshing, ensuring the widget never goes offline due to authentication issues.

Spotify Integration

Spotify's implementation showcases handling both "currently playing" and "recently played" states, with graceful fallbacks

Spotify Widget
async function fetchSpotifyData(accessToken: string): Promise<CachedSpotifyData> {
  const currentTrackResponse = await fetch(
    'https://api.spotify.com/v1/me/player/currently-playing',
    {
      headers: { Authorization: `Bearer ${accessToken}` },
    }
  )

  if (currentTrackResponse.status === 200) {
    // Handle currently playing track
    const currentTrack: CurrentlyPlayingResponse = await currentTrackResponse.json()
    return {
      isPlaying: currentTrack.is_playing,
      name: currentTrack.item.name,
      artist: currentTrack.item.artists.map((artist) => artist.name).join(', '),
      // ... other track details
    }
  } else {
    // Fallback to recently played
    const recentTracksResponse = await fetch(
      'https://api.spotify.com/v1/me/player/recently-played?limit=1'
    )
    // ... handle recently played logic
  }
}

Development Experience

To aid in development and debugging, I implemented a logging system that can be enabled in any environment through environment variables:

const ENABLE_LOGS = process.env.ENABLE_LOGS === 'true'

if (ENABLE_LOGS) {
  console.log('Debug data:', data)
}

This proved invaluable when debugging API responses and ensuring proper data transformation across all three services.

Environment Variables and Secrets Management

Managing multiple API tokens and secrets required careful consideration. The project uses several sensitive credentials:

# API Tokens
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_REFRESH_TOKEN=

STEAM_API_KEY=
STEAM_ID=

PSN_NPSSO=
PSN_USERNAME=

# Notification Services
POSTMARK_API_TOKEN=
ALERT_EMAIL_FROM=
NEXT_PUBLIC_ADMIN_EMAIL=

# Debug Settings
ENABLE_LOGS=

To manage these securely:

  1. Local development uses .env.local
  2. Production values are stored in Vercel's encrypted environment variables
  3. Different values for development/production environments where needed

Token Refresh and Maintenance

A critical aspect was implementing a robust token refresh system, particularly for PSN and Spotify. For PSN, I added email notifications using Postmark when the NPSSO token is near expiration:

const TOKEN*EXPIRY_ALERT_THRESHOLD = 7 * 24 _ 60 _ 60 _ 1000 // 7 days

async function checkTokenExpiry(tokenExpiryDate: Date) {
  const now = new Date()
  if (tokenExpiryDate.getTime() - now.getTime() < TOKEN_EXPIRY_ALERT_THRESHOLD) {
    await sendAlertEmail(
      'PSN NPSSO Token Expiring Soon',
      `Your PSN NPSSO token will expire on ${tokenExpiryDate.toLocaleString()}`
    )
  }
}

Accessibility and Performance

The deployment included considerations for:

  1. Proper image optimization using Next.js Image component
  2. Semantic HTML for accessibility
  3. Responsive design using Tailwind CSS
  4. Dark/Light/System mode mode support
<Image
  src={profileImage}
  alt={`${username}'s profile`}
  width={64}
  height={64}
  className="rounded-full"
/>

Future Considerations

  1. Implementing error boundaries for better error isolation
  2. Adding monitoring and analytics for widget usage
  3. Considering serverless caching solutions if needed
  4. Exploring static generation for parts of the widgets

TLDR

As both a developer and a dedicated gamer who can't code without my Spotify playlists, this project was a perfect blend of professional growth and personal passion. What started as "wouldn't it be cool if..." turned into a comprehensive exercise in modern web architecture and API integration. The technical implementation showcases several engineering considerations:

  • Sophisticated API integration with three complex platforms
  • Robust error handling and graceful degradation
  • Token lifecycle management and security best practices
  • Performance optimization and cost analysis
  • Type-safe development with TypeScript
  • Modern React patterns and Next.js 14 features

But beyond the code, this project demonstrates crucial developer skills:

  • Architectural decision-making (like pivoting from real-time updates when the cost-benefit analysis didn't add up)
  • System design with maintainability in mind
  • Resource optimization and cloud cost management
  • Documentation and code organization
  • Security considerations for API keys and tokens

The journey from "let's show off my PlayStation trophies" to a production-ready feature taught me more than I expected. It's one thing to fetch data from an API - it's another to handle token refreshes while making sure your Spotify widget doesn't expose your guilty pleasure playlist during code review (looking at you, "90s dance hits").

This project embodies what I love about web development: taking complex systems, making them talk to each other, and wrapping it all in a clean, maintainable package. Whether it's optimizing edge functions or making sure my Steam friends can see when I'm absolutely never touching grass because I'm in too deep in Baldur's Gate 3, every challenge was an opportunity to apply senior-level thinking to real-world problems.

Plus, I now have empirical data to prove that my best code is written while listening to synthwave. The Spotify widget doesn't lie and you can see these widgets in action in the "About me" page.

Comments

You must be signed in to comment or reply.

It's very quiet here 👻

Join the Discussion

To contribute to the discussion, we need to verify your email. We'll send you a one-time password to confirm your address. Once verified, you'll be able to post comments.

Only your name will be displayed with your comments and can be updated each time you sign in.