Teaching briefing · MERN and Next people

Django is your Express server

Not Next. Not Mongo. In a MERN app you already split “React UI” and “Express API.” This take-home is that same split: Next.js is the frontend, Django is server.js. Django Ninja is app.post. hos.py is a plain helper you would have put in utils/planTrip.js.

The picture you already have

MERN was already two processes. Keep that.

Typical MERN: a React or Next UI on port 3000, an Express API on port 8000, Mongo somewhere else. The browser fetch("http://localhost:8000/api/...") or you proxy /api to Express. CORS, express.json(), routers, controllers, a utils folder for the real logic.

This assignment is that exact shape. Next.js is the “R”. Django is the “E” (and we drop the “M”). There is no second backend. There is no app/api/trips/plan/route.ts. That file in Next would be cheating the same way putting business logic in a Next Route Handler while you also run Express would be cheating.

MERN / Next thing Django name What it means
backend/server.js + app.listen(8000) manage.py runserver / gunicorn + config/wsgi.py A Node-like process that stays alive and handles HTTP. Not a Vercel serverless function.
backend/.env + some config in server.js config/settings.py CORS, allowed hosts, secret key, env vars. One big config file instead of scattered process.env.
app.use("/api", tripRoutes) config/urls.py The mount table. Django does not use file-based routes like app/api/....
routes/trips.js Ninja router in trips/api.py router.post("/trips/plan") ≈ @api.post("/trips/plan").
controllers/tripController.js The Python function under that decorator Read body, call helper, res.json. Return a dict; Ninja serializes it.
utils/planTrip.js trips/hos.py Pure logic. No req, no Express, no Django.
models/User.js Mongoose trips/models.py Do not write this. No Mongo, no Postgres, no users.
EJS / Handlebars / Next pages Django templates + admin Django can render HTML. We will not. Next draws every pixel.
package.json scripts manage.py npm run dev → python manage.py runserver.
package.json dependencies requirements.txt npm install → pip install -r requirements.txt.
node_modules/ .venv/ See venv section. This is the one Python footgun.
cors middleware django-cors-headers Same problem: Next on :3000, API on :8000.
express.json() Ninja parses JSON for you You type the request model; you get a Python object.
Jest / Vitest on the helper pytest on hos.py Test the clocks with no HTTP and no Mongo.
Render / Railway for Express Render + gunicorn Same host class. Vercel stays the Next app.

Folders

“Django app” does not mean Next app/

Two commands exist because Django splits the server from a feature. That is Express server.js versus routes/trips.js, not Next’s app/ directory.

startproject config writes the server: settings, root urls, WSGI (the http.createServer equivalent). startapp trips writes a feature folder. Until you add "trips" to INSTALLED_APPS and mount its urls, it is a file you wrote and never app.use’d. Dead code. Same as creating routes/trips.js and forgetting it in server.js.

MERN backend you know

Express

backend/
  server.js          # listen, cors, mount routes
  routes/trips.js    # POST /plan
  controllers/tripController.js
  utils/planTrip.js  # real logic
  models/            # mongoose — skip here
  .env
  package.json

This take-home

Django

backend/
  manage.py          # npm scripts
  requirements.txt   # package.json deps
  config/
    settings.py      # .env + cors + installed apps
    urls.py          # app.use("/api", ...)
    wsgi.py          # http.createServer
  trips/             # one Express router bundle
    api.py           # routes + controller
    routing.py       # fetch Nominatim/OSRM
    hos.py           # utils/planTrip.js
    models.py        # leave empty / unused

Line for line

Your Express API, rewritten in Django Ninja

Read this as one program. Left is what you would write today. Right is what we will write. Same three layers: mount, route, pure helper.

server.js

Mount + listen

import express from "express"
import cors from "cors"
import tripRoutes from "./routes/trips.js"

const app = express()
app.use(cors({ origin: "http://localhost:3000" }))
app.use(express.json())
app.use("/api", tripRoutes)
app.listen(8000)

settings.py + urls.py

Mount + listen

# settings.py
INSTALLED_APPS = [..., "corsheaders", "trips"]
MIDDLEWARE = [..., "corsheaders.middleware.CorsMiddleware"]
CORS_ALLOWED_ORIGINS = ["http://localhost:3000"]

# urls.py
from trips.api import api
urlpatterns = [path("api/", api.urls)]

# terminal, not a file
python manage.py runserver 8000

routes + controller

POST handler

// routes/trips.js
router.post("/trips/plan", planTrip)

// controllers/tripController.js
export async function planTrip(req, res) {
  const body = req.body
  const route = await getRoute(body)
  const plan = planHos(route, body.cycle_used_hours)
  res.json(plan)
}

trips/api.py

POST handler

api = NinjaAPI()

class PlanIn(Schema):
    current_location: str
    pickup_location: str
    dropoff_location: str
    cycle_used_hours: float

@api.post("/trips/plan")
def plan_trip(request, body: PlanIn):
    route = get_route(body)
    return plan_hos(route, body.cycle_used_hours)

Return value

No res.json

In Express you must call res.json or the request hangs. In Django Ninja you return a dict (or a Schema). The framework writes the JSON and the status code. request is Express’s req; we barely use it because the body is already parsed into PlanIn.

Django also has older “views” that look like def plan(request): return JsonResponse(...). That is Express-without-router, one function per file. We are using Ninja so it feels like router.post, not like 2014 Django tutorials that render HTML.

Frontend

Next talks to Django the same way it talks to Express

In MERN with a Next frontend you already chose one of two patterns: browser fetch to http://localhost:8000 with CORS, or a Next rewrite so the browser thinks /api is same-origin. We will do the rewrite in next.config.ts for local dev, and NEXT_PUBLIC_API_URL in production pointing at Render.

A client component is unchanged:

const res = await fetch(`${apiBase}/api/trips/plan`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    current_location,
    pickup_location,
    dropoff_location,
    cycle_used_hours,
  }),
})
const plan = await res.json()

What you must not do: create app/api/trips/plan/route.ts and put HOS math there. That is the Next Route Handler you use when you do not have Express. Here you have Django. The Route Handler would be a second Express. The assignment requires the first one.

Request path

Same middleware chain you already debug

Express: request → cors → json parser → /api router → controller → res.json.

Django: request → CORS middleware (from MIDDLEWARE) → urlpatterns match api/ → Ninja matches POST /trips/plan → your function → returned dict becomes JSON.

If you get 404, you forgot to mount urls (forgot app.use). If you get 403/CSRF, you are posting a form without a token; JSON APIs with Ninja + CORS should not need CSRF if we are not using cookies. If you get “No module named django”, the venv is not active (you ran Node commands without node_modules on PATH).

The actual new concept

A virtualenv is Node, not node_modules

npm drops packages into the repo. Python drops packages into the interpreter. System Python is like running node with no project and wondering why require("express") fails.

python3 -m venv .venv creates a private Node-shaped environment: its own python and pip. source .venv/bin/activate puts those binaries first on PATH. Your prompt usually grows (.venv). From that moment, pip install django is npm install express, and it only exists inside that folder.

Close the terminal, lose the activation. Open a new one, activate again. That is the whole ritual. .venv/ is gitignored, like node_modules/. requirements.txt is the lock-file-ish list you commit so the next machine can pip install -r requirements.txt.

You type in MERN You type here
npm install source .venv/bin/activate && pip install -r requirements.txt
npm install cors pip install django-cors-headers then freeze into requirements
npm run dev in backend/ python manage.py runserver
npx jest pytest
Forgot node_modules: “Cannot find module” Forgot venv: “No module named django”

Language

JavaScript you already write, with different punctuation

JavaScript Python
const x = 1 x = 1 (no const; convention: do not rebind)
{ a: 1, b: 2 } {"a": 1, "b": 2} or dict(a=1, b=2)
[1, 2] [1, 2]
null / undefined None
true / false True / False
=== == (Python has no ===; types do not coerce)
obj.foo and obj["foo"] object attr obj.foo; dict key obj["foo"]. Mixing these is the usual bug.
braces { } for blocks indent. The colon at the end of def / if starts a block.
`miles ${n}` f"miles {n}"
array.map(fn) [fn(x) for x in array]
await fetch we will use httpx (sync first is fine). No hidden event loop required.
export function plan() def plan(): in a module; from trips.hos import plan

Takeaway

Learn three files, ignore the rest

If you can write Express server.js, a router, and a helper, you can write this Django API. settings.py and urls.py are the server. trips/api.py is the route. trips/hos.py is the helper. Mongo, Mongoose, Django templates, Django admin, and Next Route Handlers stay on the bench.