What is Next.js?
If you're new to Next.js, let me explain it simply. Next.js is React with superpowers. Regular React runs entirely in the browser — the server sends an empty HTML page, then JavaScript builds the page in the user's browser. Next.js can render pages on the server before sending them, so users see content instantly.
Why does this matter for our fleet dashboard? Our operators open the dashboard first thing in the morning, often on slow office WiFi. With server-side rendering, they see the fleet map immediately instead of staring at a loading spinner.
SSR vs CSR vs ISR — When to Use What
| Strategy | How It Works | Best For | Our Usage |
|---|---|---|---|
| SSR (Server-Side Rendering) | Page rendered on server for each request | Dynamic data that changes per-request | Dashboard main page |
| CSR (Client-Side Rendering) | Page rendered in browser | Highly interactive UIs, after initial load | Map interactions, real-time updates |
| ISR (Incremental Static Regeneration) | Pre-built pages, regenerated periodically | Content that changes infrequently | Reports, driver profiles |
| SSG (Static Site Generation) | Pre-built at build time | Content that never changes | Help pages, docs |
Senior Decision: Our dashboard uses SSR for the initial page load (get fresh data from the API) and then switches to CSR with WebSocket for real-time updates. This gives us the best of both worlds — fast initial load + live updates.
Project Setup
Let's set up our Next.js frontend with TypeScript in strict mode:
npx create-next-app@latest fleet-web --typescript --eslint --src-dir
cd fleet-web
TypeScript Strict Mode — Why It Matters
Open tsconfig.json and ensure strict mode is enabled:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Why strict mode? Because undefined is not a function is the most common error in JavaScript. Strict TypeScript catches these bugs at compile time, not at 2 AM when your operator can't see any trucks on the map.
Common Mistake: Many developers enable strict mode but then scatter any types everywhere to shut up the compiler. Don't do this. If you need any, you're probably missing a type definition.
Component Architecture
Before we write any components, let's plan our architecture. In a large application, component organization can make or break your productivity.
The Component Classification System
I organize components into 4 categories:
src/
├── components/
│ ├── ui/ # Reusable, stateless UI primitives
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ ├── Badge.tsx
│ │ └── Modal.tsx
│ │
│ ├── features/ # Feature-specific components with business logic
│ │ ├── fleet/
│ │ │ ├── FleetMap.tsx
│ │ │ ├── VehicleCard.tsx
│ │ │ └── VehicleStatusBadge.tsx
│ │ ├── driver/
│ │ │ ├── DriverList.tsx
│ │ │ └── DriverProfile.tsx
│ │ └── alert/
│ │ ├── AlertPanel.tsx
│ │ └── AlertItem.tsx
│ │
│ ├── layouts/ # Page layout components
│ │ ├── DashboardLayout.tsx
│ │ ├── Sidebar.tsx
│ │ └── TopBar.tsx
│ │
│ └── providers/ # Context providers
│ ├── AuthProvider.tsx
│ └── WebSocketProvider.tsx
Why this structure? When a new developer joins the team and needs to fix a bug in the fleet map, they know to look in components/features/fleet/. When someone needs to change a button style, they go to components/ui/. Clear organization = faster onboarding.
Building the Dashboard Layout
Let's build our dashboard layout step by step:
// src/components/layouts/DashboardLayout.tsx
import { ReactNode, useState } from 'react';
import Sidebar from './Sidebar';
import TopBar from './TopBar';
interface DashboardLayoutProps {
children: ReactNode;
title: string;
}
export default function DashboardLayout({ children, title }: DashboardLayoutProps) {
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
return (
<div className="dashboard">
<Sidebar isOpen={isSidebarOpen} />
<div className={`main-content ${isSidebarOpen ? 'with-sidebar' : 'full-width'}`}>
<TopBar
title={title}
onToggleSidebar={() => setIsSidebarOpen(prev => !prev)}
/>
<main className="page-content">
{children}
</main>
</div>
</div>
);
}
The Vehicle Card Component
This is a great example of a well-structured feature component:
// src/components/features/fleet/VehicleCard.tsx
import { Badge } from '@/components/ui/Badge';
// Always define your types explicitly
interface Vehicle {
id: string;
plateNumber: string;
driverName: string;
status: 'active' | 'idle' | 'maintenance' | 'offline';
fuelLevel: number; // 0-100 percentage
speed: number; // km/h
lastUpdate: Date;
location: {
lat: number;
lng: number;
address: string;
};
}
interface VehicleCardProps {
vehicle: Vehicle;
isSelected: boolean;
onSelect: (vehicleId: string) => void;
}
const STATUS_CONFIG = {
active: { label: 'Active', color: 'green' },
idle: { label: 'Idle', color: 'yellow' },
maintenance: { label: 'Maintenance', color: 'orange' },
offline: { label: 'Offline', color: 'red' },
} as const;
export default function VehicleCard({ vehicle, isSelected, onSelect }: VehicleCardProps) {
const statusConfig = STATUS_CONFIG[vehicle.status];
const timeSinceUpdate = getTimeSinceUpdate(vehicle.lastUpdate);
return (
<div
className={`vehicle-card ${isSelected ? 'selected' : ''}`}
onClick={() => onSelect(vehicle.id)}
role="button"
tabIndex={0}
aria-label={`Vehicle ${vehicle.plateNumber}, status: ${statusConfig.label}`}
>
<div className="vehicle-card__header">
<h3 className="vehicle-card__plate">{vehicle.plateNumber}</h3>
<Badge color={statusConfig.color}>{statusConfig.label}</Badge>
</div>
<div className="vehicle-card__details">
<p className="vehicle-card__driver">🧑✈️ {vehicle.driverName}</p>
<p className="vehicle-card__location">📍 {vehicle.location.address}</p>
<p className="vehicle-card__speed">🚚 {vehicle.speed} km/h</p>
</div>
<div className="vehicle-card__footer">
<FuelGauge level={vehicle.fuelLevel} />
<span className="vehicle-card__updated">{timeSinceUpdate}</span>
</div>
</div>
);
}
function FuelGauge({ level }: { level: number }) {
const getColor = (level: number): string => {
if (level > 50) return 'green';
if (level > 25) return 'yellow';
return 'red';
};
return (
<div className="fuel-gauge" aria-label={`Fuel level: ${level}%`}>
<div className="fuel-gauge__track">
<div
className="fuel-gauge__fill"
style={{
width: `${level}%`,
backgroundColor: `var(--color-${getColor(level)})`,
}}
/>
</div>
<span className="fuel-gauge__label">⛽ {level}%</span>
</div>
);
}
function getTimeSinceUpdate(date: Date): string {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
return `${Math.floor(seconds / 3600)}h ago`;
}
Notice the senior patterns here:
- Explicit TypeScript types — no
any, no guessing - Constant configuration objects —
STATUS_CONFIGinstead of if/else chains - Accessibility —
role,tabIndex,aria-labelon interactive elements - Small helper functions —
FuelGaugeandgetTimeSinceUpdateare extracted
State Management: When to Use What
State management is where many developers overthink. Here's my simple rule:
| State Type | Solution | Example |
|---|---|---|
| UI state | useState | Sidebar open/closed, modal visible |
| Shared UI state | React Context | Current theme, authenticated user |
| Server state | React Query / SWR | Fleet data, driver list |
| Real-time state | WebSocket + useReducer | Live vehicle positions |
Common Mistake: Using Redux or Zustand for everything. If your state comes from an API, use a data-fetching library like React Query. It handles caching, re-fetching, and loading states — things you'd have to build yourself with Redux.
API Service Layer
Instead of calling fetch directly in components, create a service layer:
// src/services/fleet.service.ts
import type { Vehicle } from '@/types/vehicle';
const API_BASE = process.env.NEXT_PUBLIC_API_URL;
export const fleetService = {
async getVehicles(): Promise<Vehicle[]> {
const res = await fetch(`${API_BASE}/api/vehicles`, {
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) {
throw new Error(`Failed to fetch vehicles: ${res.status}`);
}
return res.json();
},
async getVehicleById(id: string): Promise<Vehicle> {
const res = await fetch(`${API_BASE}/api/vehicles/${id}`);
if (!res.ok) {
throw new Error(`Vehicle not found: ${id}`);
}
return res.json();
},
};
Why a service layer? When the API endpoint changes, you update one file — not 15 components. When you add authentication headers, you add it in one place.
Performance Optimization Tips
1. Code Splitting with Dynamic Imports
The map library is huge (200KB+). Don't load it on pages that don't need it:
import dynamic from 'next/dynamic';
// Only load the map component when needed
const FleetMap = dynamic(() => import('@/components/features/fleet/FleetMap'), {
loading: () => <div className="map-skeleton">Loading map...</div>,
ssr: false, // Maps don't work on the server
});
2. Memoize Expensive Computations
import { useMemo } from 'react';
function FleetDashboard({ vehicles }: { vehicles: Vehicle[] }) {
// Only recalculate when vehicles array changes
const stats = useMemo(() => ({
total: vehicles.length,
active: vehicles.filter(v => v.status === 'active').length,
idle: vehicles.filter(v => v.status === 'idle').length,
lowFuel: vehicles.filter(v => v.fuelLevel < 25).length,
}), [vehicles]);
return (
<div className="fleet-stats">
<StatCard label="Total Vehicles" value={stats.total} />
<StatCard label="Active" value={stats.active} color="green" />
<StatCard label="Idle" value={stats.idle} color="yellow" />
<StatCard label="Low Fuel" value={stats.lowFuel} color="red" />
</div>
);
}
3. Image Optimization
import Image from 'next/image';
// Next.js automatically optimizes images
<Image
src="/images/truck-marker.png"
alt="Truck marker"
width={32}
height={32}
priority // Load immediately for above-the-fold images
/>
What's Next
In Part 3, we'll build the NestJS backend API — the engine behind our fleet dashboard. We'll cover:
- NestJS project setup and module architecture
- Building RESTful endpoints for vehicle CRUD
- DTOs, validation, and error handling
- Dependency injection explained with real examples
- Clean architecture layers
This is Part 2 of the Fleet Management System series. Each article builds on the previous one, gradually increasing complexity from beginner-friendly concepts to senior-level patterns.
