Next.js Lesson 31 – Mini Project - Dashboard | Dataplexa
PROJECT

Mini Project – Dashboard

Build a complete admin dashboard with real-time data, charts, and interactive components using Next.js App Router.

Dashboards are everywhere. GitHub shows repository stats. Vercel displays deployment metrics. Your bank shows account summaries. A dashboard takes raw data and makes it visual, actionable, and easy to understand. The NewsWave team wants an admin dashboard to monitor their content. They need to see article performance, user engagement, and publishing metrics. This means combining server-side data fetching with client-side interactivity. Unlike a simple blog or static site, dashboards require dynamic data loading, state management, and real-time updates. You'll build components that fetch data on the server, then add client-side features like filtering and sorting.

Dashboard Architecture

Think of a dashboard as a mission control center. Each panel shows different information, but they all work together. Some data updates frequently (like page views), while other data stays stable (like total articles published). Your dashboard needs multiple data sources. Article statistics come from one API endpoint. User metrics come from another. Traffic data might come from a third source. Each piece loads independently so users see information as it becomes available. The architecture follows a hub-and-spoke pattern. The main dashboard page acts as the hub, coordinating data from multiple spokes (individual API routes). Each spoke handles one specific type of data.

Server Components

Initial data loading, SEO optimization, fast first paint

Client Components

Interactive filtering, real-time updates, user actions

API Routes

Data fetching, business logic, database queries

Data Sources

Mock APIs, external services, cached responses

Project Structure Setup

Dashboards need organized file structures. You'll separate data logic from presentation logic. Each dashboard section gets its own component file, and each data type gets its own API route. Start by creating the folder structure. The dashboard folder will contain all admin-related pages. The components folder gets dashboard-specific components.
# Create the dashboard structure
mkdir -p app/dashboard
mkdir -p components/dashboard
mkdir -p app/api/dashboard
Terminal
$ mkdir -p app/dashboard components/dashboard app/api/dashboard
Created dashboard directories
✓ Folder structure ready

What just happened?

You created three main folders: app/dashboard for pages, components/dashboard for UI components, and app/api/dashboard for data endpoints. This separation keeps related code together while maintaining clear boundaries.

Now create the file structure. Each file will handle one specific responsibility. This makes the codebase easier to understand and maintain as the dashboard grows.
NewsWave Dashboard Structure
📁 app
  📁 dashboard
    📄 page.js # Main dashboard page
    📄 layout.js # Dashboard layout
  📁 api
    📁 dashboard
      📄 stats/route.js # Article statistics
      📄 users/route.js # User metrics
📁 components
  📁 dashboard
    📄 StatsCard.js # Metric display cards
    📄 ChartView.js # Data visualization
    📄 DataTable.js # Article listing table

Dashboard API Routes

API routes provide the data your dashboard displays. Unlike static data, dashboard information changes frequently. Articles get published, users read content, and metrics update throughout the day. Each API route returns JSON data that components can consume. The route handles data processing, filtering, and formatting. Components just display what the API provides. Create the stats API route first. This endpoint will return article performance data like view counts, published articles, and engagement metrics.
// app/api/dashboard/stats/route.js
// Returns article statistics for NewsWave dashboard

export async function GET() {
  // Mock data - in real app, query your database
  const stats = {
    totalArticles: 1247,      // Total published articles
    totalViews: 89543,        // Total page views this month
    activeUsers: 2834,        // Users who read articles this week
    avgReadTime: 4.2          // Average minutes per article
  };

  return Response.json(stats); // Send stats as JSON response
}
Terminal
$ curl http://localhost:3000/api/dashboard/stats
{"totalArticles":1247,"totalViews":89543,"activeUsers":2834,"avgReadTime":4.2}
✓ Stats API endpoint working

What just happened?

You created an API route that returns dashboard statistics. The GET function responds to HTTP GET requests with JSON data. Try this: Visit the API endpoint directly in your browser to see the raw JSON response.

Now create a more complex API route for recent articles. This endpoint returns a list of articles with metadata like publication date, view counts, and author information.
// app/api/dashboard/articles/route.js
// Returns recent articles with performance metrics

export async function GET() {
  // Mock recent articles data
  const articles = [
    {
      id: 1,
      title: "AI Breakthrough in Language Models",
      author: "Sarah Chen",
      publishedAt: "2024-01-15T09:30:00Z",    // ISO date string
      views: 2847,                            // Individual article views
      category: "Tech",                       // Article category
      status: "published"                     // Publishing status
    },
    {
      id: 2,
      title: "Global Climate Summit Results",
      author: "Mike Rodriguez", 
      publishedAt: "2024-01-14T14:20:00Z",
      views: 1923,
      category: "World",
      status: "published"
    }
  ];

  return Response.json({ articles, total: articles.length });
}
Terminal
$ curl http://localhost:3000/api/dashboard/articles
{"articles":[{"id":1,"title":"AI Breakthrough..."}],"total":2}
✓ Articles API endpoint working

What just happened?

You created an articles API that returns structured data with metadata. Each article includes performance metrics and publication info. The response wraps articles in an object with a total count for pagination. Try this: Add more mock articles and see how the total count updates automatically.

Dashboard Components

Dashboard components display data in digestible formats. Raw numbers like "89543 views" mean little without context. A good component shows the number, adds visual emphasis, and provides comparison context. Stats cards are the building blocks of dashboards. Each card displays one key metric with formatting that makes the number easy to understand. Think GitHub's repository stats or Vercel's deployment metrics. Create a reusable stats card component. This component will display metrics like total articles, page views, and user engagement with proper formatting and visual hierarchy.
// components/dashboard/StatsCard.js
// Displays a single metric with formatting and context

export default function StatsCard({ title, value, change, icon }) {
  // Format large numbers with commas (1234 becomes 1,234)
  const formatNumber = (num) => {
    return new Intl.NumberFormat().format(num);
  };

  return (
    <div className="stats-card">
      <div className="stat-header">
        <span className="stat-icon">{icon}</span>    {/* Visual icon */}
        <h3 className="stat-title">{title}</h3>       {/* Metric name */}
      </div>
      <div className="stat-value">{formatNumber(value)}</div>  {/* Main number */}
      {change && (
        <div className="stat-change">
          {change > 0 ? '↗' : '↘'} {Math.abs(change)}%  {/* Trend indicator */}
        </div>
      )}
    </div>
  );
}
localhost:3000 — NewsWave Dashboard

What just happened?

You created a reusable stats card that formats numbers with commas and shows trend indicators. The component accepts props for title, value, change percentage, and icon. Each card displays its data in a clean, scannable format. Try this: Pass different values to see how number formatting and trend arrows change automatically.

Now create a data table component for displaying article lists. Tables work well for detailed information where users need to compare multiple attributes across different items.
// components/dashboard/DataTable.js
// Table component for displaying article data with sorting

'use client';    // This component needs client-side interactivity
import { useState } from 'react';

export default function DataTable({ articles }) {
  const [sortField, setSortField] = useState('views');  // Current sort column
  const [sortOrder, setSortOrder] = useState('desc');   // Sort direction

  // Sort articles based on current field and order
  const sortedArticles = [...articles].sort((a, b) => {
    if (sortOrder === 'asc') {
      return a[sortField] > b[sortField] ? 1 : -1;
    }
    return a[sortField] < b[sortField] ? 1 : -1;
  });

  return (
    <table className="data-table">
      <thead>
        <tr>
          <th onClick={() => setSortField('title')}>Title</th>
          <th onClick={() => setSortField('author')}>Author</th>
          <th onClick={() => setSortField('views')}>Views</th>
        </tr>
      </thead>
    </table>
  );
}
localhost:3000 — NewsWave Dashboard

What just happened?

You created an interactive table with client-side sorting. The 'use client' directive tells Next.js this component needs browser JavaScript. Clicking column headers sorts the data without server requests. Try this: Click different column headers to see the sorting change instantly.

Main Dashboard Page

The main dashboard page brings everything together. It fetches data from your API routes, then passes that data to your components. The page combines server-side data loading with client-side interactivity. Dashboard pages typically use a grid layout. Key metrics appear at the top in stats cards. Detailed information appears below in tables and charts. This creates a visual hierarchy that guides user attention. Create the main dashboard page. This page will fetch stats and articles data on the server, then render your components with that data.
// app/dashboard/page.js
// Main dashboard page that combines stats and article data

import StatsCard from '@/components/dashboard/StatsCard';
import DataTable from '@/components/dashboard/DataTable';

// Fetch dashboard data from internal API routes
async function getDashboardData() {
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
  
  // Fetch stats and articles in parallel for better performance
  const [statsRes, articlesRes] = await Promise.all([
    fetch(`${baseUrl}/api/dashboard/stats`),     // Get overall statistics
    fetch(`${baseUrl}/api/dashboard/articles`)   // Get recent articles
  ]);

  const stats = await statsRes.json();        // Parse stats JSON
  const articles = await articlesRes.json();  // Parse articles JSON
  
  return { stats, articles: articles.articles };
}
Terminal
$ curl http://localhost:3000/dashboard
Loading dashboard data...
✓ Stats fetched from /api/dashboard/stats
✓ Articles fetched from /api/dashboard/articles
Dashboard rendered with fresh data

What just happened?

You created a data fetching function that calls your API routes in parallel. Promise.all runs both requests simultaneously instead of waiting for each one. This cuts loading time in half. Try this: Add console logs to see the order of data loading.

Now complete the dashboard page component. This component will use the data fetching function and render your stats cards and data table with the retrieved data.
// app/dashboard/page.js (continued)
// Dashboard page component that renders the complete interface

export default async function DashboardPage() {
  const { stats, articles } = await getDashboardData();  // Load data on server

  return (
    <div className="dashboard-container">
      <header className="dashboard-header">
        <h1>NewsWave Dashboard</h1>                        {/* Page title */}
        <p>Monitor your content performance and user engagement</p>
      </header>

      <section className="stats-grid">
        <StatsCard 
          title="Total Articles" 
          value={stats.totalArticles} 
          change={12} 
          icon="📰"
        />
        <StatsCard 
          title="Page Views" 
          value={stats.totalViews} 
          change={-3} 
          icon="👁️"
        />
        <StatsCard 
          title="Active Users" 
          value={stats.activeUsers} 
          change={8} 
          icon="👥"
        />
      </section>

      <section className="articles-section">
        <h2>Recent Articles</h2>                          {/* Table section title */}
        <DataTable articles={articles} />               {/* Interactive table */}
      </section>
    </div>
  );
}
localhost:3000/dashboard — NewsWave Dashboard

What just happened?

You created a complete dashboard page that combines server-side data fetching with client-side interactivity. The page loads data once on the server, then passes it to components for rendering. Stats cards show key metrics while the data table provides detailed article information with sorting. Try this: Refresh the page to see how server-side rendering provides instant content.

Real-time Updates

Static dashboards become stale quickly. Real applications need fresh data without full page reloads. You can add real-time updates using client-side data fetching and periodic refreshes. The pattern involves moving from server-side data loading to client-side data loading for components that need frequent updates. Server components handle initial data loading for SEO and performance. Client components handle data refreshing for interactivity. Create a real-time stats component that updates every 30 seconds. This component will fetch fresh data from your API and update the display without disrupting user interaction.
// components/dashboard/LiveStats.js
// Stats component that updates automatically every 30 seconds

'use client';
import { useState, useEffect } from 'react';
import StatsCard from './StatsCard';

export default function LiveStats({ initialStats }) {
  const [stats, setStats] = useState(initialStats);    // Current stats data
  const [lastUpdated, setLastUpdated] = useState(new Date()); // Last refresh time

  useEffect(() => {
    // Function to fetch fresh stats from API
    const updateStats = async () => {
      try {
        const response = await fetch('/api/dashboard/stats');
        const newStats = await response.json();
        setStats(newStats);                             // Update state with fresh data
        setLastUpdated(new Date());                     // Record update time
      } catch (error) {
        console.error('Failed to update stats:', error);
      }
    };

    // Set up interval to update every 30 seconds
    const interval = setInterval(updateStats, 30000);  // 30000ms = 30 seconds

    return () => clearInterval(interval);              // Cleanup on unmount
  }, []);

  return (
    <div>
      <div style={{ marginBottom: '16px', fontSize: '14px', color: '#6b7280' }}>
        Last updated: {lastUpdated.toLocaleTimeString()}  {/* Show update time */}
      </div>
      <div className="stats-grid">
        <StatsCard title="Total Articles" value={stats.totalArticles} icon="📰" />
        <StatsCard title="Page Views" value={stats.totalViews} icon="👁️" />
        <StatsCard title="Active Users" value={stats.activeUsers} icon="👥" />
      </div>
    </div>
  );
}
localhost:3000/dashboard — Live Updates

What just happened?

You created a self-updating component using setInterval and useEffect. The component fetches fresh data every 30 seconds and updates the display automatically. The cleanup function prevents memory leaks by clearing the interval when the component unmounts. Try this: Watch the timestamp update to see the real-time refresh in action.

Project Summary

You've built a complete admin dashboard with multiple moving parts working together. The dashboard demonstrates key Next.js patterns: server-side data fetching, client-side interactivity, API routes, and component composition. Your dashboard includes server components for initial data loading, client components for user interaction, API routes for data provisioning, and real-time updates for fresh information. This architecture scales well as you add more features and data sources. The project shows how Next.js handles both static and dynamic content in the same application. Static elements render on the server for performance. Dynamic elements hydrate on the client for interactivity. Both approaches work together seamlessly.

Deployment Considerations

Real dashboards need environment variables for API endpoints, database connections for persistent data, authentication for user access, and caching strategies for performance. Consider using React Query or SWR for better data fetching patterns in production applications.

Quiz

1. What is the recommended architecture pattern for Next.js dashboards?


2. How should you fetch data from multiple API endpoints in parallel for a dashboard?


3. What's required to implement real-time updates in a Next.js dashboard component?


Up Next: Mini Project – Ecommerce

Build a complete online store with product catalogs, shopping carts, and checkout flows using Next.js App Router.