Back to the home page

A sample formative review

Formative review of a bootcamp final project — translated from Spanish.

This is the kind of long-form review I send to learners. The original went to a 4Geeks student in Spanish about their capstone (a Flask + React project called ShadowMap); below is an English translation, kept as close as possible to the original tone. Shared with the student's consent. The intent is not to grade — it's to give the learner a prioritised path back to good standing.

How this was written

I'm being explicit because the question matters: Claude read the student's repository and produced a first-draft review. I then went through it line-by-line — correcting misreads of the code, sharpening the framing, removing anything that wasn't pedagogically useful, and personalising the message — before the student saw it. The English version below is also an AI-assisted translation that I reviewed.

I think this is a fair example of what the Agentic AI Coding track should teach: where AI saves real hours, where the human still has to do the work, and how to be honest about the split.

Original (Spanish): github.com/enaguero/shadowmap-review

Formative Review — ShadowMap

Hi. This document is a review of your project designed so you learn, not so you get discouraged. There are things that are really well done, things that are one step away from being great, and some that are worth reviewing before showing the project to an employer.

How to read it:

  1. Start with what's working well — because you've earned it.
  2. Continue with what has the most impact if you fix it — the highest effort-to-reward points.
  3. Then the review by dimensions with examples from your own code.
  4. At the end you have a prioritised action plan and a reference grade.

If something isn't clear or you disagree, perfect — that means it's worth discussing in class.

1. What's working well

Before any suggestions, there are decisions you made correctly and deserve recognition:

When you finish reviewing the improvements, come back to this section. It will remind you where you're starting from.

2. What has the most impact if you fix it

These observations have the best effort-to-benefit ratio. Most can be resolved in a few hours and significantly raise the perceived quality of your project.

2.1 Two Flask applications are configured but only one is used

In src/api/__init__.py you define a create_app():

# src/api/__init__.py:18
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///shadowmap.db"
...
from src.api.routes import api
app.register_blueprint(api, url_prefix="/api")

And in src/api/app.py you define a different create_app():

# src/api/app.py:26
DB_PATH = os.path.join(BASE_DIR, "instance", "database.db")
...
app.register_blueprint(auth_api, url_prefix="/api")
app.register_blueprint(routes_api, url_prefix="/api")
# ...8 more blueprints

They point to different databases and register different blueprints. Probably one was your first attempt and the other your final version. I suggest keeping app.py (the more complete one) and removing __init__.py as a factory, leaving only what's needed for src/api/ to remain a package.

2.2 routes.py defines login endpoints with a test token

# src/api/routes.py:58
return jsonify({
    "token": "fake-token",
    "user": user.serialize()
}), 200

This file is not registered in app.py, so it never executes. But the problem is: if someone (you, in a week) registers it accidentally, real authentication gets lost. You already have the proper implementation in routes_auth.py, so you can safely delete this one.

2.3 Premium activation fails at runtime

# src/api/routes_premium.py:21
user.is_premium = True

The is_premium field is not defined in your User model (models.py:11-33). When a user clicks "Activate Premium", the backend will throw AttributeError. The good news is the fix is straightforward:

  1. Add the field to the model: is_premium = db.Column(db.Boolean, default=False, nullable=False)
  2. Create the migration with Flask-Migrate.
  3. Delete src/api/premium.py (it's a duplicate with a broken import: uses from api.models instead of from src.api.models).

2.4 Some actions the frontend calls don't exist yet

In these views, actions are invoked that aren't defined:

When they execute, they fail silently because actions.createPlace is undefined. You have two valid paths:

Either path is perfectly legitimate. What I don't recommend is leaving it as-is.

2.5 Two authentication systems coexist

Both define token, user, login, logout… but the project only uses the second one. The first is orphaned. When a teammate (or you in six months) tries to understand the authority, it's confusing. My recommendation: pick one and delete the other. The Flux one is already integrated throughout the app, so probably quickest to remove AuthContext and authService.

Note: in the future, on professional projects, the Context + hooks pattern (which you had in AuthContext.jsx) tends to be more modern and clean than Flux. Your instinct was on the right track.

2.6 JWT_SECRET_KEY is hardcoded

# src/api/app.py:30
app.config["JWT_SECRET_KEY"] = "super-secret-key"

Anyone with repo access can forge valid tokens for any user. Change this first:

app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY")

And add the variable to .env.example. A 30-second change that moves the security dimension from level 1 to level 2.

2.7 CORS configuration isn't quite correct

# src/api/app.py:35
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True, ...)

The CORS spec forbids the combination origins="*" + supports_credentials=True. Today it "works" because your frontend doesn't send cookies, but the day you add something with credentials it will break mysteriously. Better be explicit:

CORS(
    app,
    resources={r"/*": {"origins": [
        "http://localhost:3000",
        os.getenv("FRONTEND_URL"),
    ]}},
    supports_credentials=True,
)

2.8 Missing ownership verification in sensitive operations

Example in POIs:

# src/api/routes_pois.py:65
@pois_api.route("/pois/<int:poi_id>", methods=["PUT"])
@jwt_required()
def update_poi(poi_id):
    poi = POI.query.get(poi_id)
    if not poi:
        return jsonify({"message": "POI not found"}), 404
    # ...

The @jwt_required() decorator ensures the user is authenticated, but not that they own the resource. Right now any logged-in user can modify or delete any POI in the system. Same issue in routes_premium.py (UPDATE/DELETE of others' premium routes) and routes_routes.py (sharing others' routes).

To fix it you need:

  1. Add user_id to the model (POI, Place, etc.) if there will be owners.
  2. In each PUT/DELETE/SHARE, verify ownership and return 403 otherwise.

This is one of the most typical security critiques in junior projects — fixing it in this project will stick with you forever.

2.9 The password recovery token doesn't expire

# src/api/routes_recover.py:20
token = secrets.token_urlsafe(32)
user.recovery_token = token

You generated a cryptographically secure token, which is perfect. But it lacks an expiration date. If someone intercepts that email once, they can use the token weeks later. I suggest adding user.recovery_token_expires_at = datetime.utcnow() + timedelta(minutes=15) and validating in reset_password that the date hasn't passed.

2.10 The email link doesn't match the frontend route

# src/api/routes_recover.py:24
reset_link = f"{os.getenv('FRONTEND_URL')}/reset-password/{token}"

But in layout.jsx:58 the router only knows /reset-password. The email takes the user to /reset-password/abc123 and they land on "Not found".

3. Review by dimensions

Dimension 1 — Structure · Level 2

What's working: clear separation between src/api and src/front. Folders views/, component/, store/, styles/.

Small improvements:

To reach level 3: single source per responsibility and zero orphaned files.

Dimension 2 — Frontend · Level 2

What's working: correct use of useState/useEffect/useContext. Separated views. Reasonably small components (with one exception).

Areas to polish:

  1. map.jsx has gotten large (405 lines). Mixes mission logic, route creation, two modals, and saved routes list. Extracting MissionPanel, RouteCreatorPanel, and SavedRoutesPanel will make the parent component much more readable.
  2. MapView.jsx:18 creates the icon on every render. The icon doesn't depend on props or state, so extract it outside the component or wrap it in useMemo. A detail, but reflects attention to immutability.
  3. Inline styles and CSS classes coexist in the same file. Either option is valid; what matters is consistency. For reusable pieces, CSS classes scale better.
  4. Events on <span> without accessibility — not keyboard-navigable and lacks a semantic role. Use <Link> from React Router or a <button>.
  5. alert() and confirm() for feedback — they block the thread and break the cinematic style you cared for elsewhere. A toast or custom modal fits the ShadowMap aesthetic much better.
  6. useEffect with // eslint-disable-next-line react-hooks/exhaustive-deps: silencing the rule is sometimes necessary, but add a comment explaining why. Otherwise, in six months even you won't know if it was intentional.

Dimension 3 — Backend · Level 2

What's working: separated Blueprints, models with consistent serialize(), @jwt_required() decorators where they belong.

Areas to polish:

  1. extensions.py and utils.py implement the same thing twice. Keep one (the Mailer class integrates better with Flask) and delete the other.
  2. print() for logging — convenient in development, lost or messy in production. The logging module with levels is the standard.
  3. HTML embedded in Python code — if you ever want to redesign the email, you edit Python to touch styles. Better move to a template file (flask.render_template).
  4. Very basic input validation — checks presence but not email format, password minimum length, or shortname rules. Marshmallow or Pydantic solve this with minimal code and raise perceived quality significantly.

Dimension 4 — State and communication · Level 2

Already covered in 2.5 (two auth systems). Additional details:

Dimension 5 — Security · Level 1

See critiques 2.6 (JWT secret), 2.7 (CORS), 2.8 (ownership), and 2.9 (token expiration). Actionable list:

On JWT in localStorage: this is what 4Geeks teaches and is acceptable for bootcamp. To explore further, HttpOnly cookies are the professional way, but outside this project's scope.

Dimension 6 — Functionality end-to-end · Level 2

Honest summary of what works and what doesn't:

The good: most are small, localised fixes. You're not far from having a fullstack app that demos well in an interview.

Dimension 7 — Git, docs · Level 2

Dimension 8 — UX, accessibility · Level 2

Your palette, typography, and animations are very good. It's a shame user feedback uses standard browser components.

Dimension 9 — Tests · Level 1

No tests yet. ESLint is configured, which is a good start.

What would earn you major points with minimal effort:

Dimension 10 — DevOps · Level 2

Dimension 11 — Originality and design · Level 3

Here is where you shine most, so I save it for last. The concept "ShadowMap = paranormal registry that watches you" has its own identity. The README copy, avatars with lore, names ("Identity Re-Stabilization", "Dark Route"), the final warning in the README… everything is cared for.

This is what a recruiter remembers. When they receive 30 projects called "TaskApp" or "WeatherForecast", yours is the haunted app. Cherish it and don't underestimate it. If the technical side reaches the design's level, this project is genuinely memorable.

4. Prioritised action plan

🔴 Urgent (without these, parts of the project don't work)

  1. Add is_premium to User model + migration (see 2.3).
  2. Delete src/api/routes.py (the one with "fake-token") and src/api/premium.py (duplicate).
  3. Decide on a single create_app() and remove the other (see 2.1).
  4. Implement missing actions in flux.js or remove views that call them (see 2.4).
  5. Move JWT_SECRET_KEY to .env (see 2.6).
  6. Fix email link mismatch vs /reset-password route (see 2.10).
  7. Pick one auth system and delete the other (see 2.5).

🟠 Important (moves you from "passing" to "good")

  1. Add user_id to POI/Place and verify ownership in PUT/DELETE.
  2. Centralise API_URL.
  3. Unify localStorage key for routes.
  4. Replace alert()/confirm() with custom toasts or modals.
  5. Restrict origins in CORS.
  6. Add expiration to recovery_token.
  7. Complete .env.example with all variables your code uses.
  8. Update README with environment variables section, migrations, troubleshooting.

🟢 Excellence (takes you to honours)

  1. Split map.jsx into smaller components.
  2. Move email HTML to a template.
  3. Add tests with pytest (minimum 5–10 on critical flows).
  4. Configure GitHub Actions with pytest + eslint.
  5. Input validation with Marshmallow or Pydantic.
  6. Replace print with logging.
  7. Consider React Query / SWR for fetches.

Practical recommendation: open a branch per block (fix/critical-issues, feat/ownership-and-security, etc.). You'll end up with a Git history that's great to show in interviews.

5. Reference grade

This grade is a proposal based on the current state of the code. If you self-evaluate and arrive at a similar score, that's an excellent sign: your technical judgement is already calibrated.

#DimensionWeightLevelPoints
1Structure8%24.00
2Frontend15%27.50
3Backend15%27.50
4State / API10%25.00
5Security15%13.75
6Functionality10%25.00
7Git / Docs5%22.50
8UX / Accessibility8%24.00
9Tests5%11.25
10DevOps5%22.50
11Originality4%33.00
Total100%≈ 46 / 100

Honest reading: you're not yet at the passing threshold, but the gap is very small. With the 🔴 block (~1 day of focused work) you probably jump to 55–60. With the 🟠 block (3–4 days) to 70–80. The 🟢 block takes you to honours.

The important thing is you don't need to rewrite the project. You need to finish it.

6. Final message

Your project has something many bootcamp projects don't have: soul. The narrative, the details, the theming, the copy… that matters a lot and isn't taught. When a recruiter opens your repo, they'll remember yours.

What this review is asking isn't "do it all over." It's asking you to finish what you started. 80% of real junior dev work is exactly this: finding the missing is_premium fields, the unimplemented createPlace, the hardcoded JWT_SECRET_KEY, and solving them before a user or teammate finds them.

If you focus on the 🔴 block for a day and then review this document again, you'll see progress very clearly. And when you reach the 🟢 level, your portfolio will have a project worth talking about in interviews.

Trust what you already did well. Finish it. You're closer than you think.


← Back to the home page