This document explains how DevPath is structured, how data flows through the system, and why each module exists.
Browser
|
| HTTP request (GET / or POST /api/recommend)
v
Flask Application (app.py)
|
|-- Blueprint registration
v
routes/main_routes.py <-- receives requests, validates, delegates
|
|-- calls utils/recommender.py (scoring + filtering logic)
|-- calls utils/data_loader.py (reads projects.json)
|-- calls utils/file_server.py (resolves + serves starter code)
|
v
templates/ + static/ <-- rendered HTML + JS + CSS served to browser
The entry point. Its only jobs are:
- Create the Flask app instance
- Register the
mainBlueprint fromroutes/ - Register the 404 and 500 error handlers
- Start the dev server when run directly
No business logic, no data access, no file handling.
Contains all URL route handlers, registered as a Flask Blueprint named main.
Each route handler follows the same pattern:
- Read and strip input from the request
- Call one or more utility functions
- Return a rendered template or a JSON response
Routes defined:
| Method | Path | Description |
|---|---|---|
| GET | / |
Render homepage |
| POST | /api/recommend |
Return matching project JSON |
| GET | /project/<id> |
Render project detail page |
| GET | /project/<id>/code |
Return starter code content JSON |
| GET | /project/<id>/download |
Serve starter code as download |
Handles all reading and lookup of project data.
Functions:
load_all_projects()— reads and returns the full JSON arrayfind_project_by_id(project_id)— returns a single project dict or None
The data file path is resolved relative to the module file itself, so the app works correctly regardless of the working directory it is started from.
Contains all recommendation logic. Nothing in this module knows about HTTP, Flask, or file paths.
Functions:
parse_skills(skills_string)— converts"Python, HTML"to["python", "html"]score_single_project(project, user_skills, level, interest, time)— returns an integer scoreget_recommendations(skills, level, interest, time)— returns the top N projectsvalidate_recommendation_inputs(...)— returns a list of error strings
Scoring weights are named module-level constants:
WEIGHT_SKILL = 3 # Points per matching skill
WEIGHT_LEVEL = 2 # Points for matching experience level
WEIGHT_INTEREST = 2 # Points for matching interest area
WEIGHT_TIME = 1 # Points for matching time availabilityChanging a weight number changes the relative influence of each criterion across all recommendations.
Handles safe resolution and serving of starter code files.
Functions:
resolve_starter_file(project)— returns the absolute path to the file, or Noneread_starter_code(project)— returns{"filename": ..., "code": ...}or Noneget_starter_code_dir()— returns the directory path forsend_from_directory
The os.path.basename() call in resolve_starter_file ensures that a
malicious starter_code value in the JSON (such as ../../etc/passwd) cannot
cause a path traversal vulnerability.
1. User submits form
|
2. script.js sends POST /api/recommend
{skills: "Python", level: "Beginner", interest: "Data", time: "Low"}
|
3. main_routes.recommend() reads and strips each field
|
4. validate_recommendation_inputs() checks for empty fields
- Returns 400 JSON error if any field missing
|
5. get_recommendations() is called
|
5a. parse_skills("Python") -> ["python"]
|
5b. load_all_projects() reads data/projects.json (7 projects)
|
5c. For each project, score_single_project() computes:
- Skill matches x3 points each
- Level match +2 points
- Interest match +2 points
- Time match +1 point
|
5d. Projects with score > 0 are collected
|
5e. Sorted descending by score
|
5f. Top 3 returned
|
6. main_routes.recommend() returns 200 JSON {projects: [...]}
|
7. script.js receives response and calls renderResults()
|
8. buildProjectCard() creates DOM elements for each project
|
9. Cards inserted into #results-grid and section scrolled into view
1. User clicks "View Full Project" link -> GET /project/1
|
2. main_routes.project_detail(1) calls find_project_by_id(1)
|
3. project dict passed to render_template("project.html", project=project)
|
4. Jinja2 renders the template, iterating roadmap steps with loop.index
|
5. Browser receives complete HTML
|
6. User clicks "View Code" button
|
7. script.js calls GET /project/1/code
|
8. main_routes.view_code(1):
- find_project_by_id(1) -> project dict
- read_starter_code(project) -> {filename, code}
- Returns 200 JSON
|
9. script.js injects filename + code into the slide-up code panel
DevPath uses Flask's built-in Jinja2 templating. Templates receive data through
render_template() keyword arguments. The project detail template uses
{{ project.title }}, {% for step in project.roadmap %}, and similar
expressions to render dynamic content server-side.
No client-side template engine is used. The frontend JavaScript only handles interactivity (form submission, chip management, code panel) and rendering of the recommendation cards (which come back from the API as JSON).
CSS and JavaScript are served from the static/ directory by Flask's built-in
static file handler. In production, these would ideally be served directly by
a web server like Nginx rather than through Flask.
Tests live in tests/test_basic.py and are grouped into four categories:
- Data loader tests — verify the JSON file loads and has correct structure
- Recommender unit tests — test scoring, parsing, and validation in isolation
- HTTP route tests — use Flask's test client to verify status codes and response shapes
- Edge case tests — empty inputs, missing IDs, no-match scenarios
Run with: python tests/test_basic.py or pytest tests/
The most common extension points are:
| What to change | Where to change it |
|---|---|
| Add a new project | data/projects.json |
| Change scoring weights | utils/recommender.py (top constants) |
| Add a new route | routes/main_routes.py |
| Change recommendation count | utils/recommender.py (MAX_RESULTS) |
| Add a new interest area | data/projects.json + templates/index.html dropdown |
| Change UI styling | static/style.css CSS variables block |