How to Build a Custom Interactive Developer Roadmap with Roadmap.sh JSON Schema in Next.js (2025)

How to Build a Custom Interactive Developer Roadmap with Roadmap.sh JSON Schema in Next.js (2025)

Roadmap.sh has revolutionized how developers plan their learning paths with interactive, clickable roadmaps. But what if you need to create a custom roadmap for your team, bootcamp, or open-source project? This guide walks you through building your own interactive roadmap using the same JSON schema structure that powers roadmap.sh, deployed on Next.js.

Why Build a Custom Roadmap Instead of Using Roadmap.sh Directly?

While roadmap.sh offers comprehensive paths for Frontend, Backend, DevOps, and over 50 other specializations, there are scenarios where a custom implementation makes sense:

  • Company-specific tech stacks: Your organization uses a unique combination of tools
  • Proprietary frameworks: Internal libraries and frameworks need documentation
  • Niche specializations: Emerging fields like DevSecOps or MLOps require tailored paths
  • Educational institutions: Bootcamps and courses need aligned curriculum roadmaps
  • White-label solutions: Client-facing products that need branded roadmaps

Understanding the Roadmap.sh JSON Schema Structure

Roadmap.sh uses a node-based JSON structure where each topic is a clickable node. Here's the core schema:

{
  "nodes": [
    {
      "id": "javascript-basics",
      "title": "JavaScript Fundamentals",
      "description": "Variables, data types, operators",
      "links": [
        {
          "title": "MDN JavaScript Guide",
          "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide"
        }
      ],
      "position": { "x": 100, "y": 200 },
      "children": ["dom-manipulation", "async-javascript"]
    }
  ],
  "edges": [
    {
      "from": "javascript-basics",
      "to": "dom-manipulation"
    }
  ]
}

Each node contains:

  • id: Unique identifier
  • title: Display name
  • description: Short explanation
  • links: External resources
  • position: X/Y coordinates for rendering
  • children: Connected nodes

Step 1: Setting Up Your Next.js Project

First, create a new Next.js project with TypeScript support:

npx create-next-app@latest custom-roadmap --typescript --tailwind --app
cd custom-roadmap
npm install react-flow-renderer zustand

We're using:

  • react-flow-renderer: For interactive node rendering (same library family as roadmap.sh uses)
  • zustand: Lightweight state management
  • Tailwind CSS: Styling that matches roadmap.sh's aesthetic

Step 2: Creating the Roadmap Data Structure

Create lib/roadmap-schema.ts:

export interface RoadmapNode {
  id: string;
  title: string;
  description: string;
  links: Array<{
    title: string;
    url: string;
    type?: 'article' | 'video' | 'documentation';
  }>;
  position: { x: number; y: number };
  children: string[];
  completed?: boolean;
  priority?: 'required' | 'recommended' | 'optional';
}

export interface RoadmapEdge {
  from: string;
  to: string;
  label?: string;
}

export interface Roadmap {
  id: string;
  title: string;
  description: string;
  nodes: RoadmapNode[];
  edges: RoadmapEdge[];
  metadata: {
    version: string;
    lastUpdated: string;
    author: string;
  };
}

Step 3: Building the Interactive Roadmap Component

Create components/InteractiveRoadmap.tsx:

'use client';
import { useCallback, useState } from 'react';
import ReactFlow, {
  Node,
  Edge,
  Controls,
  Background,
  useNodesState,
  useEdgesState,
  addEdge,
  Connection,
} from 'react-flow-renderer';
import { Roadmap, RoadmapNode } from '@/lib/roadmap-schema';

interface InteractiveRoadmapProps {
  roadmap: Roadmap;
}

export default function InteractiveRoadmap({ roadmap }: InteractiveRoadmapProps) {
  const [selectedNode, setSelectedNode] = useState<RoadmapNode | null>(null);

  // Transform roadmap data to ReactFlow format
  const initialNodes: Node[] = roadmap.nodes.map((node) => ({
    id: node.id,
    type: 'default',
    data: { 
      label: node.title,
      description: node.description,
      priority: node.priority 
    },
    position: node.position,
    style: {
      background: node.priority === 'required' ? '#3b82f6' : '#6b7280',
      color: 'white',
      border: '1px solid #1f2937',
      borderRadius: '8px',
      padding: '10px',
    },
  }));

  const initialEdges: Edge[] = roadmap.edges.map((edge, idx) => ({
    id: `e${idx}`,
    source: edge.from,
    target: edge.to,
    label: edge.label,
    animated: true,
  }));

  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
    const roadmapNode = roadmap.nodes.find(n => n.id === node.id);
    setSelectedNode(roadmapNode || null);
  }, [roadmap.nodes]);

  return (
    <div className="h-screen w-full flex">
      <div className="flex-1">
        <ReactFlow
          nodes={nodes}
          edges={edges}
          onNodesChange={onNodesChange}
          onEdgesChange={onEdgesChange}
          onNodeClick={onNodeClick}
          fitView
        >
          <Controls />
          <Background color="#1f2937" gap={16} />
        </ReactFlow>
      </div>
      
      {selectedNode && (
        <div className="w-96 bg-gray-900 text-white p-6 overflow-y-auto">
          <h2 className="text-2xl font-bold mb-4">{selectedNode.title}</h2>
          <p className="text-gray-300 mb-6">{selectedNode.description}</p>
          
          <h3 className="text-lg font-semibold mb-3">Resources</h3>
          <ul className="space-y-2">
            {selectedNode.links.map((link, idx) => (
              <li key={idx}>
                <a 
                  href={link.url}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-blue-400 hover:underline"
                >
                  {link.title}
                </a>
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

Step 4: Creating Your First Custom Roadmap

Create data/nextjs-advanced-roadmap.json:

{
  "id": "nextjs-advanced-2025",
  "title": "Next.js Advanced Development Path",
  "description": "Master Next.js 14+ with App Router, Server Components, and modern patterns",
  "nodes": [
    {
      "id": "app-router",
      "title": "App Router Fundamentals",
      "description": "Understanding Next.js 14 App Router architecture",
      "links": [
        {
          "title": "Next.js App Router Docs",
          "url": "https://nextjs.org/docs/app"
        }
      ],
      "position": { "x": 250, "y": 100 },
      "children": ["server-components", "route-handlers"],
      "priority": "required"
    },
    {
      "id": "server-components",
      "title": "React Server Components",
      "description": "Leverage RSC for optimal performance",
      "links": [
        {
          "title": "RSC Deep Dive",
          "url": "https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components"
        }
      ],
      "position": { "x": 250, "y": 250 },
      "children": ["streaming-ssr"],
      "priority": "required"
    }
  ],
  "edges": [
    {
      "from": "app-router",
      "to": "server-components",
      "label": "Build on"
    }
  ],
  "metadata": {
    "version": "1.0.0",
    "lastUpdated": "2025-01-15",
    "author": "Your Team"
  }
}

Step 5: Implementing Progress Tracking

Add progress tracking with localStorage:

// hooks/useRoadmapProgress.ts
import { useState, useEffect } from 'react';

export function useRoadmapProgress(roadmapId: string) {
  const [completedNodes, setCompletedNodes] = useState<Set<string>>(new Set());

  useEffect(() => {
    const saved = localStorage.getItem(`roadmap-progress-${roadmapId}`);
    if (saved) {
      setCompletedNodes(new Set(JSON.parse(saved)));
    }
  }, [roadmapId]);

  const toggleNode = (nodeId: string) => {
    setCompletedNodes((prev) => {
      const next = new Set(prev);
      if (next.has(nodeId)) {
        next.delete(nodeId);
      } else {
        next.add(nodeId);
      }
      localStorage.setItem(`roadmap-progress-${roadmapId}`, JSON.stringify(Array.from(next)));
      return next;
    });
  };

  return { completedNodes, toggleNode };
}

Deployment Comparison: Vercel vs Render vs DigitalOcean

| Feature | Vercel | Render | DigitalOcean App Platform | |---------|--------|--------|---------------------------| | Next.js Support | Native | Good | Requires config | | Free Tier | 100GB bandwidth | 750 hours/month | $200 credit | | Build Time | < 1 min | 2-3 min | 2-4 min | | Edge Functions | Yes | No | Limited | | Best For | Next.js apps | Full-stack apps | Custom infrastructure | | Cost (Pro) | $20/month | $7/month | $5/month |

Common Pitfalls and Solutions

Pitfall 1: React Flow SSR Issues

Problem: React Flow doesn't render during SSR

Solution: Use dynamic imports with ssr: false:

import dynamic from 'next/dynamic';

const InteractiveRoadmap = dynamic(
  () => import('@/components/InteractiveRoadmap'),
  { ssr: false }
);

Pitfall 2: Large JSON Files Slow Initial Load

Problem: Roadmap JSON files exceed 100KB

Solution: Implement lazy loading:

// app/roadmap/[id]/page.tsx
export async function generateStaticParams() {
  return [
    { id: 'nextjs-advanced' },
    { id: 'devops-mastery' },
  ];
}

export default async function RoadmapPage({ params }: { params: { id: string } }) {
  const roadmap = await import(`@/data/${params.id}.json`);
  return <InteractiveRoadmap roadmap={roadmap} />;
}

Pitfall 3: Mobile Responsiveness

Problem: ReactFlow doesn't work well on mobile

Solution: Implement a list view for mobile:

const isMobile = useMediaQuery('(max-width: 768px)');

return isMobile ? <RoadmapListView /> : <InteractiveRoadmap />;

Advanced Features: AI-Powered Roadmap Generation

Integrate OpenAI to generate custom roadmaps:

// app/api/generate-roadmap/route.ts
import OpenAI from 'openai';

export async function POST(req: Request) {
  const { topic, experience } = await req.json();
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

  const completion = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      {
        role: 'system',
        content: 'Generate a learning roadmap in JSON format matching the roadmap.sh schema'
      },
      {
        role: 'user',
        content: `Create a ${experience} level roadmap for ${topic}`
      }
    ],
  });

  return Response.json(JSON.parse(completion.choices[0].message.content));
}

Conclusion

Building a custom interactive roadmap with roadmap.sh's JSON schema gives you full control over content, branding, and features. This approach works perfectly for:

  • Internal training platforms
  • Course curriculum visualization
  • Open-source project contribution guides
  • Team onboarding workflows

Deploy your roadmap on Vercel for the best Next.js experience, or use Render if you need full-stack capabilities with databases. For enterprise needs with custom infrastructure, DigitalOcean App Platform provides the flexibility you need.

The roadmap.sh project proves that interactive learning paths significantly improve knowledge retention. By implementing your own version, you can create targeted, specialized roadmaps that address your exact use case while maintaining the intuitive, clickable experience that makes roadmap.sh so effective.

Recommended Tools