SkillGraph is a graph-powered frontend career exploration application I built with Next.js, CognoDB, and the official Neo4j JavaScript driver.
The idea is simple: a developer selects the frontend skills they already know and chooses a target role. SkillGraph then calculates a weighted role-match percentage, identifies matched and missing requirements, finds prerequisite-based learning paths, and visualizes those paths as an interactive graph.
- Live application:
https://sm-skillgraph.vercel.app - Repository:
https://github.com/smishra11/skillgraph
Frontend engineers often know a number of technologies but do not always have a clear view of how those skills map to the requirements of a role they want to move into.
I built SkillGraph around that problem.
Instead of presenting a flat checklist, the application models frontend skills, career roles, prerequisites, and related technologies as connected graph data. This lets the application answer questions such as:
- How well do my current skills match a Senior Frontend Engineer role?
- Which required skills am I missing?
- Which missing skills are most important for that role?
- Is there a prerequisite path from something I already know to something I need to learn?
- Which technologies are connected through multiple prerequisite steps?
The goal is to make the output useful for career planning rather than simply showing a list of technologies.
The most important part of this use case is not the individual skills or roles. It is the relationships between them.
A relational database could store the same information using tables and joins, but the learning-path part of the application becomes less natural when the number of prerequisite levels is not fixed.
For example, one of the questions SkillGraph needs to answer is:
Starting from skills the developer already knows, find prerequisite paths of up to four hops that lead to skills required by the selected role.
With the graph model, that relationship can be expressed directly in Cypher:
MATCH path =
(knownSkill:Skill)-[:PREREQUISITE_OF*1..4]->(missingSkill:Skill)This is one of the main reasons I chose a graph database for this project. The application works with:
- role-to-skill requirements,
- skill-to-skill prerequisites,
- related technologies,
- variable-length learning paths,
- connected knowledge rather than isolated records.
The graph is therefore part of the application logic, not just a different way of storing the same data.
- Next.js — App Router, React UI, and API routes
- React
- TypeScript
- Tailwind CSS
- shadcn/ui with Base UI / Nova
- CognoDB Cloud — managed graph database
- Neo4j JavaScript Driver — Bolt/openCypher connection to CognoDB
- React Flow (
@xyflow/react) — interactive learning-path visualization - tsx — TypeScript seed and verification scripts
I kept the graph model intentionally small so the relationships remain easy to understand.
SkillGraph has two node labels and three relationship types.
Skill
├── id
├── name
├── slug
├── category
├── level
└── description
Role
├── id
├── name
├── slug
├── level
└── description
(Role)-[:REQUIRES { importance: 1..5 }]->(Skill)
A REQUIRES relationship represents a skill needed for a role. The importance property is used in the weighted match calculation, so core requirements contribute more than lower-priority requirements.
(Skill)-[:PREREQUISITE_OF]->(Skill)
This represents a directed learning dependency.
For example:
JavaScript → React → React Testing Library → Playwright
(Skill)-[:RELATED_TO]->(Skill)
This represents technologies that are closely related but do not necessarily have a strict prerequisite relationship.
flowchart LR
Role[Role]
SkillA[Skill]
SkillB[Skill]
SkillC[Skill]
Role -->|REQUIRES\nimportance: 1..5| SkillA
SkillA -->|PREREQUISITE_OF| SkillB
SkillB -->|PREREQUISITE_OF| SkillC
SkillA -.->|RELATED_TO| SkillC
A concrete example from the domain:
flowchart LR
Senior[Senior Frontend Engineer]
JavaScript[JavaScript]
React[React]
RTL[React Testing Library]
Playwright[Playwright]
Senior -->|REQUIRES| React
Senior -->|REQUIRES| Playwright
JavaScript -->|PREREQUISITE_OF| React
React -->|PREREQUISITE_OF| RTL
RTL -->|PREREQUISITE_OF| Playwright
I created a seed dataset large enough to demonstrate realistic role matching and multi-hop traversal without making the project unnecessarily large.
Current dataset:
| Type | Count |
|---|---|
| Skill nodes | 42 |
| Role nodes | 10 |
REQUIRES relationships |
102 |
PREREQUISITE_OF relationships |
27 |
RELATED_TO relationships |
20 |
The roles included in the dataset are:
- Junior Frontend Developer
- Frontend Developer
- React Developer
- Next.js Developer
- UI Engineer
- Senior Frontend Engineer
- Frontend Platform Engineer
- Full Stack Developer
- Frontend Tech Lead
- Frontend Architect
The seed script uses MERGE, so it can be run again without intentionally creating duplicate graph entities.
Each REQUIRES relationship has an importance value between 1 and 5.
Rather than treating every requirement equally, SkillGraph calculates the role match using the weight of the requirements the user already covers:
matched requirement weight
-------------------------- × 100
total requirement weight
For example:
Matched weight = 14
Total weight = 53
Match = 14 / 53 × 100 ≈ 26%
This means matching a core requirement has a larger effect on the final score than matching a lower-priority requirement.
All application queries are parameterized through the official Neo4j driver. I do not concatenate user input into Cypher strings.
MATCH (role:Role)
RETURN
role.id AS id,
role.name AS name,
role.slug AS slug,
role.level AS level,
role.description AS description
ORDER BY role.nameThis query populates the target-role selector.
MATCH (skill:Skill)
RETURN
skill.id AS id,
skill.name AS name,
skill.slug AS slug,
skill.category AS category,
skill.level AS level,
skill.description AS description
ORDER BY skill.category, skill.nameThis query populates the searchable current-skills selector.
MATCH (role:Role {slug: $roleSlug})
-[requirement:REQUIRES]->
(skill:Skill)
RETURN
skill,
requirement.importance AS importance
ORDER BY requirement.importance DESC, skill.nameThis returns the skills required for the selected role together with the importance of each requirement.
MATCH (role:Role {slug: $roleSlug})
MATCH (role)-[requirement:REQUIRES]->(skill:Skill)
WITH
role,
skill,
requirement,
skill.slug IN $selectedSkillSlugs AS isMatched
RETURN
role,
skill,
requirement.importance AS importance,
isMatched
ORDER BY requirement.importance DESC, skill.nameThe application separates the returned requirements into matched and missing skills, sums their weights, and calculates the match percentage.
MATCH (role:Role {slug: $roleSlug})
-[:REQUIRES]->
(missingSkill:Skill)
WHERE NOT missingSkill.slug IN $selectedSkillSlugs
MATCH path =
(knownSkill:Skill)
-[:PREREQUISITE_OF*1..4]->
(missingSkill)
WHERE knownSkill.slug IN $selectedSkillSlugs
RETURN
knownSkill,
missingSkill,
length(path) AS hops,
nodes(path) AS path
ORDER BY hops ASCThis is the main graph traversal in SkillGraph.
It starts from skills the user already knows and searches up to four PREREQUISITE_OF relationships for missing skills required by the selected role. After receiving the paths, the application keeps the shortest useful path for each missing target before sending the result to the visualization.
In a relational schema, skill prerequisites would normally be stored in a self-referencing table. Following an unknown number of prerequisite levels would typically require recursive SQL/CTEs, followed by additional joins to reconnect those results to the selected role.
Cypher expresses the traversal directly:
[:PREREQUISITE_OF*1..4]This is the clearest place in the project where the graph model adds practical value.
Select current skills
+
Select target role
↓
Analyze career path
↓
POST /api/analyze
+
POST /api/learning-paths
↓
Weighted match percentage
Matched skills
Missing skills
Learning paths
↓
Interactive graph visualization
The learning graph supports:
- dragging nodes,
- zooming,
- panning,
- fit-to-view controls,
- selecting a skill to inspect it,
- highlighting directly connected nodes and edges.
| Route | Method | Purpose |
|---|---|---|
/api/health |
GET | Verify CognoDB connectivity |
/api/roles |
GET | Load career roles |
/api/skills |
GET | Load available skills |
/api/roles/[slug]/requirements |
GET | Load requirements for one role |
/api/analyze |
POST | Calculate weighted skill-gap analysis |
/api/learning-paths |
POST | Find graph-derived prerequisite paths |
I handle database failures at the API boundary. The underlying error is logged on the server, while the browser receives a safe 503 Service Unavailable response instead of raw Neo4j/CognoDB details.
app/
├── api/
│ ├── analyze/route.ts
│ ├── health/route.ts
│ ├── learning-paths/route.ts
│ ├── roles/route.ts
│ ├── roles/[slug]/requirements/route.ts
│ └── skills/route.ts
├── globals.css
├── layout.tsx
└── page.tsx
components/
├── analysis/
│ ├── analysis-results.tsx
│ ├── role-match-summary.tsx
│ └── skill-coverage.tsx
├── analyzer/
│ ├── analysis-workspace.tsx
│ ├── analyzer-hero.tsx
│ ├── profile-panel.tsx
│ └── workspace-empty-state.tsx
├── graph/
│ ├── build-skill-graph.ts
│ ├── graph-empty-state.tsx
│ ├── selected-node-details.tsx
│ ├── skill-graph.tsx
│ └── skill-node.tsx
├── role-selector.tsx
├── skill-selector.tsx
├── skillgraph-analyzer.tsx
└── ui/
├── badge.tsx
├── button.tsx
├── card.tsx
├── command.tsx
├── dialog.tsx
├── input-group.tsx
├── input.tsx
├── popover.tsx
├── select.tsx
└── textarea.tsx
lib/
├── db/
│ ├── driver.ts
│ ├── queries.ts
│ └── seed-data.ts
└── utils.ts
scripts/
├── seed.ts
├── verify-seed.ts
└── verify## Project Structure
```text
app/
├── api/
│ ├── analyze/route.ts
│ ├── health/route.ts
│ ├── learning-paths/route.ts
│ ├── roles/route.ts
│ ├── roles/[slug]/requirements/route.ts
│ └── skills/route.ts
├── globals.css
├── layout.tsx
└── page.tsx
components/
├── analysis/
│ ├── analysis-results.tsx
│ ├── role-match-summary.tsx
│ └── skill-coverage.tsx
├── analyzer/
│ ├── analysis-workspace.tsx
│ ├── analyzer-hero.tsx
│ ├── profile-panel.tsx
│ └── workspace-empty-state.tsx
├── graph/
│ ├── build-skill-graph.ts
│ ├── graph-empty-state.tsx
│ ├── selected-node-details.tsx
│ ├── skill-graph.tsx
│ └── skill-node.tsx
├── ui/
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── input-group.tsx
│ ├── input.tsx
│ ├── popover.tsx
│ ├── select.tsx
│ └── textarea.tsx
├── analysis-form.tsx
├── role-selector.tsx
├── skill-selector.tsx
└── skillgraph-analyzer.tsx
lib/
├── db/
│ ├── driver.ts
│ ├── queries.ts
│ └── seed-data.ts
└── utils.ts
scripts/
├── seed.ts
├── verify-seed.ts
└── verify-traversal.ts
docs/
public/
I deliberately kept the architecture small. Next.js API routes provide the server boundary, lib/db contains the CognoDB connection, queries, and seed data, and the components folder contains the application UI and graph visualization.
This project uses a CognoDB Cloud instance. I used a free c0 instance with the default cognodb username.
A CognoDB Bolt URI follows this format:
bolt+s://<instance-id>.databases.cognodb.cloud
Create .env.local from .env.example and provide the instance credentials:
COGNODB_URI=bolt+s://<instance-id>.databases.cognodb.cloud
COGNODB_USERNAME=cognodb
COGNODB_PASSWORD=<your-password>The real .env.local file is intentionally excluded from source control.
- Node.js
- npm
- a running CognoDB Cloud instance
git clone https://github.com/smishra11/skillgraph.git
cd skillgraph
npm installCreate .env.local using the environment variables shown above.
npm run seedThe seed script creates the uniqueness constraints, loads the Skill and Role nodes, and creates the REQUIRES, PREREQUISITE_OF, and RELATED_TO relationships.
npm run verify-seedExpected counts:
skills: 42
roles: 10
requires: 102
prerequisites: 27
related: 20
The traversal can also be checked with:
npm run verify-traversalnpm run devThe application is then available at:
http://localhost:3000
Database connectivity can be checked at:
http://localhost:3000/api/health
Before deploying the current version, I ran:
npm run lint
npm run build
npm run verify-seedI also tested the main flow end to end with different combinations of skills and roles.
The checks included:
- roles and skills loading correctly,
- analysis completing successfully,
- weighted match percentage rendering correctly,
- matched and missing requirements rendering correctly,
- all-covered and no-match empty states,
- learning-path graph rendering,
- graph pan, zoom, drag, and node selection,
- stale results being cleared when the form changes,
- loading and error states,
- responsive behavior across mobile, tablet, and desktop widths.
I wanted database failures to fail clearly without exposing implementation details.
When CognoDB is unavailable:
- the server logs the underlying database error,
- raw Neo4j/CognoDB errors are not returned to the browser,
- database-backed endpoints return a safe
503response, - the UI shows a non-technical error state,
- the initial-data state provides a retry action,
- a learning-graph failure does not discard an otherwise successful skill-gap analysis.
The project keeps database access on the server side.
- CognoDB URI, username, and password come from environment variables.
- Credentials are not hard-coded in application source files.
.env.localis excluded from source control.- Cypher queries use parameters rather than string concatenation.
- Database errors are logged server-side while safe messages are returned to the client.
A simple example of the parameterized query style used in the project:
await driver.executeQuery(`MATCH (role:Role {slug: $roleSlug}) RETURN role`, {
roleSlug,
});Relevent screenshots
I deployed the application to Vercel and configured the CognoDB credentials through Vercel environment variables:
COGNODB_URI
COGNODB_USERNAME
COGNODB_PASSWORD
The production application uses the same CognoDB instance as the local version. I also verified /api/health and ran a complete skill-gap analysis against the deployed application.
The CognoDB instance will remain available while the submission is being reviewed.
For the recorded demo, I use a simple end-to-end flow that shows both the role-matching and graph-traversal parts of the project:
- Open SkillGraph.
- Select a few known skills such as
JavaScript,React, andGit. - Select
Senior Frontend Engineeras the target role. - Click Analyze career path.
- Show the weighted match percentage and requirement coverage.
- Show the matched and missing skills.
- Open the interactive learning graph.
- Explain that the graph is built from prerequisite paths returned by a multi-hop CognoDB traversal.
- Drag or zoom the graph and select a node to show its connected skills.
- Briefly show the graph schema and parameterized Cypher in the repository.
The selected skills are request-time input rather than persisted account data. I intentionally kept them transient because authentication and user-profile persistence are not needed for the core use case.
The application is small enough that Next.js API routes provide a clear server boundary without introducing a second server application or separate deployment.
The browser never connects directly to CognoDB. Database access stays inside server-side code.
CognoDB is responsible for finding the graph paths. React Flow is only the visualization layer.
It provides the interactions I needed—dragging, pan/zoom, fit-view controls, and custom nodes—without requiring me to build those behaviors from scratch.
Cypher is responsible for finding valid prerequisite paths.
There can be multiple valid paths to the same missing skill, so after the query returns I keep the shortest useful path for each target. I chose to do that small cleanup in the application layer to keep the Cypher traversal easy to read and to avoid an unnecessarily noisy graph.
Skill selection represents the current analysis request. Since there is no account or saved-profile feature, persisting those selections would add state that the current product does not need.
I intentionally kept the assignment focused on one complete, explainable workflow rather than adding unrelated product features.
Some directions I would consider if the application were expanded further are:
- persisted user profiles,
- customizable role requirements,
- richer role recommendations,
- a larger skills and roles dataset,
- more graph relationship types,
- automatic graph layout for significantly larger result sets,
- historical progress tracking.
For the current version, I kept these outside the scope so the graph model, queries, application flow, and UI remain straightforward to understand.
Subhasish Mishra




