Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Travlr Getaways

MEAN stack project — Module Seven: authentication and security.

Module Four connected the site to MongoDB. Module Five pulled the database access out of the website and into a separate Express application, app_api, published at /api. Module Six added an Angular single-page application, app_admin, that lets staff add and edit trips, and extended the API with the write endpoints it needs.

This module secures it. Until now anyone who knew the admin URL could add, edit, or delete a trip. There is now a login form on the admin site, users are stored with a salted PBKDF2 hash, and the write endpoints reject any request that does not carry a valid JSON Web Token.

The rule is read is public, write is guarded. The GET endpoints stay open because the customer-facing website consumes them and its visitors are anonymous — locking the whole API down would have broken the public site.

The API now has three clients: the Express website, the Angular admin SPA, and any external caller such as Postman. All three read the same data; two of them can write it, and only with a token.


There are three pieces:

Piece Folder URL
Customer website (Express + Handlebars) app_server http://localhost:3000
REST API (Express + Mongoose) app_api http://localhost:3000/api
Admin SPA (Angular) app_admin http://localhost:4200

Requirements

  • Node.js 18 or newer (this project uses the built-in fetch, added in Node 18)
  • MongoDB running locally on the default port (27017)
  • An internet connection for the two npm install steps, and for the Bootstrap CDN the admin SPA loads in app_admin/src/index.html
  • Angular CLI, for the admin SPA: npm install -g @angular/cli. Optional — every ng command below also works as npx ng, which uses the copy of the CLI that npm install already put in app_admin/node_modules.

Setup

Install dependencies:

npm install

Create the environment file. .env is gitignored — a signing key in version control is a signing key everybody has — so a fresh clone from GitHub has to make its own:

copy .env.example .env

On macOS or Linux that is cp .env.example .env. If you are working from travlr.zip, skip this step — a working .env is already in the archive so the project runs as-is.

Seed the trips, then the mock admin account:

npm run seed
npm run seed:user

Start the Express site and API:

npm start

Then, in a second terminal, start the Angular admin SPA:

cd app_admin && npm install && npx ng serve

npx ng serve works whether or not the Angular CLI is installed globally. With a global CLI, plain ng serve is equivalent.

Both servers must be running for the admin console to work: the SPA at http://localhost:4200 makes its API calls to the Express server on port 3000.

Sign in at http://localhost:4200/login with the seeded account:

Field Value
Email admin@travlr.com
Password Password123

Mock credentials for a local database — nothing real is behind them.

To use a different database server, set MONGODB_URI or DB_HOST before starting.


Separation of concerns: what changed

app_server no longer requires mongoose anywhere — its only imports are express, its own controllers, and the static data file used by the home and news pages. Every Mongoose call in the project now lives under app_api/.

Files added

File Purpose
app_api/controllers/trips.js Mongoose data access for the trips resource.
app_api/routes/index.js Route table for the API; mounted at /api by app.js.

Files relocated

The models folder moved from app_server to app_api, taking the database connection, the schema, and the seed script with it:

From To
app_server/models/db.js app_api/models/db.js
app_server/models/travlr.js app_api/models/travlr.js
app_server/models/seed.js app_api/models/seed.js

data/trips.json stays in the project's upper-level /data folder where it was created. app_server/data/travlr.js (static copy for the home and news pages) also stays where it was.

Files modified

File Change
app.js Requires ./app_api/models/db, and wires apiRouter up to the /api path.
app_server/controllers/travel.js Rewritten as a presentation-only controller that fetches GET /api/trips.
app_server/views/travel.hbs Trip image and title HREFs retargeted at /api/trips/{{code}}.
app_server/routes/travel.js Removed GET /travel/json, now superseded by GET /api/trips.
package.json seed script points at the relocated app_api/models/seed.js.

Module Seven: what changed

Back end

File Change
.env / .env.example new — holds JWT_SECRET; .env is gitignored
app_api/models/user.js new — user schema, setPassword, validPassword, generateJwt
app_api/models/seed-user.js new — creates the mock admin account
app_api/config/passport.js new — Passport local strategy
app_api/controllers/authentication.js new — register and login
app_api/models/db.js Registers the user model alongside the trip model
app_api/controllers/trips.js getUser() helper wraps the three write handlers
app_api/routes/index.js /register and /login; auth middleware on POST, PUT, DELETE
app.js dotenv first, passport.initialize(), UnauthorizedError → 401 JSON
package.json + jsonwebtoken, passport, passport-local, express-jwt, dotenv; new seed:user script

Front end

File Change
models/user.ts, models/authresponse.ts new — types for the auth flow
services/storage.service.ts new — BROWSER_STORAGE injection token
services/authentication.service.ts new — login, register, token handling
guards/auth.guard.ts new — keeps anonymous users off the write screens
login/, register/ new — the HTML login form and account creation
services/trip-data.service.ts Attaches Authorization: Bearer to the three write calls
app.routes.ts /login, /register; canActivate on /add-trip and /edit-trip
app.component.* Navbar shows the signed-in user and a Log out button
trip-listing/, trip-card/ Add / Edit / Delete render only when signed in; Delete button added
add-trip/, edit-trip/ A 401 now reports an expired session rather than a generic failure
assets/css/styles.css Styles for the navbar auth controls and the logged-out prompts

API reference

Base URL: http://localhost:3000/api

Method Endpoint Description Auth Success
POST /api/register Create an account public 200 + { token }
POST /api/login Exchange credentials for a token public 200 + { token }
GET /api/trips All trips public 200 + JSON array
GET /api/trips/:tripCode One trip, located by its unique code public 200 + JSON array of one
POST /api/trips Add a trip token 201 + the stored trip
PUT /api/trips/:tripCode Update that trip token 201 + the updated trip
DELETE /api/trips/:tripCode Remove that trip token 200 + confirmation

The protected endpoints expect the token in an Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Bearer is the scheme name the JWT spec uses and the exact word express-jwt looks for — one space, correct capitalization, or the middleware will not find the token and will answer 401 as though none had been sent.

Naming conventions

Routes follow a single, predictable convention so a client can guess a URL correctly:

  • Resources are plural, lowercase nouns — /trips.
  • A single member is addressed by its business key as a named parameter — /trips/:tripCode, e.g. /api/trips/GALR210214.
  • The HTTP verb carries the action. No verbs appear in paths — there is no /getTrips or /tripFind.
  • Every route for a path is grouped with router.route(), so adding POST, PUT, or DELETE in a later module is a one-line change per verb.

:tripCode is matched against the code field rather than Mongo's _id because code is the identifier the business actually uses, it is human-readable, and the schema places a unique index on it — so the lookup is a single indexed query.

Status codes

Code When
200 OK The request succeeded; the body is the requested JSON. An empty collection returns 200 with [] — the resource exists, it just has no members yet.
201 Created A POST or PUT succeeded; the body is the saved trip.
400 Bad Request The submitted trip failed schema validation, or its code duplicates an existing trip. Also returned by /register and /login when a required field is missing.
401 Unauthorized No token, a malformed token, a bad signature, an expired token, or a token naming an account that no longer exists. Also returned by /login on bad credentials.
404 Not Found No trip carries the requested code.
500 Internal Server Error The query failed — most often because MongoDB is unreachable.

Error responses share one body shape, so a client only has to read one field:

{ "message": "Trip not found" }

Testing with Postman

Start MongoDB, run npm run seed and npm run seed:user, then npm start.

Public reads — no token needed

# Request URL Expected Verifies
1 GET http://localhost:3000/api/trips 200, JSON array of 6 trips Model.find({}) — FIND with an empty filter returns the whole collection
2 GET http://localhost:3000/api/trips/GALR210214 200, JSON array of 1 trip Model.find({ code }) — FIND with a filter returns a single trip
3 GET http://localhost:3000/api/trips/ZZZZ999999 404 + { "message": "Trip not found" } A code with no match is handled, not crashed

Writes with no token — the security check

Send these with no Authorization header. All three succeeded before Module Seven; all three are now rejected before the controller ever runs.

# Method and URL Expected
4 POST http://localhost:3000/api/trips 401 + { "message": "Unauthorized: a valid token is required for this request." }
5 PUT http://localhost:3000/api/trips/GALR210214 401, same body
6 DELETE http://localhost:3000/api/trips/GALR210214 401, same body

The user endpoints

Body tab → raw → JSON.

# Method and URL Body Expected
7 POST /api/login { "email": "admin@travlr.com", "password": "wrong" } 401 + { "message": "Incorrect email or password." }
8 POST /api/login { "email": "nobody@travlr.com", "password": "Password123" } 401, identical message
9 POST /api/login { "email": "admin@travlr.com" } 400 + both-fields-required message
10 POST /api/login { "email": "admin@travlr.com", "password": "Password123" } 200 + { "token": "eyJhbGci..." }
11 POST /api/register { "name": "Dup", "email": "admin@travlr.com", "password": "Password123" } 400 + already-exists message
12 POST /api/register { "name": "X", "email": "x@travlr.com" } 400 + all-fields-required message
13 POST /api/register a fresh name/email/password 200 + a usable token

Requests 7 and 8 returning the same message is deliberate. Saying which half of the credential pair was wrong would let a caller enumerate valid accounts.

Copy the token from request 10. Paste the payload segment (between the two dots) into any base64 decoder to see what a JWT actually carries:

{ "_id": "6a81eb8d...", "email": "admin@travlr.com",
  "name": "Travlr Admin", "exp": 1787504175, "iat": 1786899375 }

Readable, because a JWT is signed, not encrypted. Which is exactly why the payload holds nothing but identity claims.

Bad tokens

On the Authorization tab choose Bearer Token and paste a broken one.

# Token Expected Verifies
14 A real token with the last three characters changed 401 The signature check catches tampering even though the payload is still valid JSON naming a real admin
15 not-a-jwt 401 A malformed token is rejected, not crashed on

Authorized writes

Same requests as 4–6, but with the good token from request 10 on the Authorization tab. Body: code, name, length, start, resort, perPerson, image, description.

# Method and URL Expected Verifies
16 POST /api/trips 201 + the stored trip Create works when authorized
17 POST the same body again 400 + duplicate-code message The unique index on code is still enforced
18 PUT /api/trips/<code> 201 + the updated trip findOneAndUpdate saves the changes
19 PUT with a code that does not exist 404 A missing record is reported, not created
20 DELETE /api/trips/<code> 200 + confirmation The trip is removed
21 DELETE the same code again 404 Deleting something already gone is handled

17, 19, and 21 matter: the security layer did not swallow the existing error handling. A valid token gets a caller in; it does not excuse them from validation.

To see the 500 path, stop MongoDB and re-run request 1.

The GET endpoints can still be checked straight from the browser address bar. The write endpoints need Postman — a browser address bar can only issue GET, and it cannot attach an Authorization header.

Rubric checkpoints

  • Individual trip returns JSON — request 2 returns the trip whose code matches.
  • Collection returns JSON — request 1 returns all trips with all eight schema fields on every element.
  • All four verbs exercised — GET (1–3), POST (16–17), PUT (18–19), DELETE (20–21).
  • User-class endpoints verified — /login on requests 7–10, /register on 11–13.
  • Authentication tested with mock data — the admin@travlr.com account created by npm run seed:user, exercised on both the success path (10) and the failure paths (7–9).
  • Endpoints are actually secured — requests 4–6 prove the writes reject an anonymous caller; 14–15 prove they reject a forged one.
  • Errors return correct HTTP status codes — 400 on 9, 11, 12, 17; 401 on 4–8, 14, 15; 404 on 3, 19, 21; stopping MongoDB covers 500.

Verifying the front end is wired to the API

With the server running, open http://localhost:3000/travel. The page renders the same six trips as before — but the data now arrives over HTTP from /api/trips rather than from a database call inside the page controller.

To prove the website is genuinely reading from the API, watch the terminal while loading /travel. Morgan logs two requests: the page request and the internal API call it makes.

GET /api/trips 200 12.418 ms - 4181
GET /travel 200 31.006 ms - 8022

Clicking any trip image now navigates to /api/trips/<code> and displays the raw JSON for that single trip. This is not how a production site would link its images — it is a quick way to demonstrate that the filtered API call works end to end from the rendered page.

The controller also handles two failure conditions: a response that is not an array (the API returned an error body) shows "API lookup error", and an empty array shows "No trips exist in the database".


The Angular admin SPA

The admin console lives in app_admin and talks to the same /api endpoints the public site uses. It is a single-page application: the Angular router swaps components in and out of <router-outlet> without ever reloading the page.

Piece File Job
Trip listing app_admin/src/app/trip-listing/ Fetches all trips through the data service and lays out one card per trip
Trip card app_admin/src/app/trip-card/ Renders a single trip and its Edit / Delete buttons
Add trip app_admin/src/app/add-trip/ Reactive form that POSTs a new trip
Edit trip app_admin/src/app/edit-trip/ Loads one trip, pre-fills the form, and PUTs the changes
Login app_admin/src/app/login/ The HTML login form; POSTs to /api/login
Register app_admin/src/app/register/ Creates an account through /api/register
Trip data service app_admin/src/app/services/trip-data.service.ts Every HTTP call to the API, in one place
Auth service app_admin/src/app/services/authentication.service.ts Login, register, and everything token-related
Storage token app_admin/src/app/services/storage.service.ts Injects localStorage instead of reaching for the global
Auth guard app_admin/src/app/guards/auth.guard.ts Keeps anonymous users off the write screens
Trip model app_admin/src/app/models/trip.ts TypeScript interface matching the Mongoose schema
User models app_admin/src/app/models/user.ts, authresponse.ts Types for the auth flow

The card rendering started out inside the trip listing. Pulling it into its own trip-card component means the listing only decides which trips to show while the card decides how one is drawn, so adding a list view later means swapping a selector instead of rewriting the listing.

TripDataService is the same idea applied to data access. The components never call HttpClient themselves, so if the API moves or an endpoint changes, only that one file changes.

Routes

Path Component Guard
/ Trip listing —
/login Login form —
/register Register form —
/add-trip Add trip form authGuard
/edit-trip Edit trip form authGuard

The listing stays open because GET /api/trips is public. Hitting /add-trip while logged out redirects to /login?returnUrl=%2Fadd-trip.

The edit screen needs to know which trip to load. The card stashes the trip code in localStorage before routing, and the edit component reads it back in ngOnInit.

CORS

The SPA runs on port 4200 and the API on port 3000. Browsers treat those as different origins and block the calls unless the API opts in, so app.js sets Access-Control-Allow-Origin, -Headers, and -Methods on /api. The -Methods header matters: without it a browser only allows GET and POST, so PUT and DELETE would fail even though the routes exist.

A note on the date field

MongoDB stores start as a full ISO timestamp, but <input type="date"> only accepts yyyy-MM-dd. The edit component trims the timestamp before patching it into the form, so the date box populates correctly instead of rendering empty with a console warning.


Security

How a request gets authorized

login form  ──POST /api/login──▶  passport local strategy
                                        │  validPassword() re-hashes with the
                                        │  stored salt and compares
                                        ▼
                                  generateJwt()  ──▶  { token }
                                                        │
    localStorage ◀───── saveToken() ────────────────────┘
         │
         └─▶ TripDataService adds  Authorization: Bearer <token>
                        │
                        ▼
              express-jwt verifies the signature  ──▶  req.auth
                        │
                        ▼
              getUser() confirms the account still exists
                        │
                        ▼
                  the controller finally runs

Where each job lives

File Responsibility
app_api/models/user.js Schema + setPassword, validPassword, generateJwt
app_api/config/passport.js Local strategy — are these credentials good?
app_api/controllers/authentication.js /register and /login responses
app_api/routes/index.js Which endpoints require a token
app.js dotenv, passport.initialize(), the 401 error handler

Splitting it this way means an unguarded write route is visible at a glance in the route table — it is the line without auth on it. If the check lived inside each controller function, auditing the same thing would mean opening five files.

Passwords

The user schema has no password field. setPassword() generates a 16-byte random salt and runs the plaintext through 1000 rounds of PBKDF2-SHA512, storing only the salt and the hex hash. validPassword() re-hashes the submitted password with the same salt and compares; the stored hash is never reversed.

The per-user salt is what stops two admins who both chose Password123 from getting byte-identical hashes, which is what a rainbow table attacks.

Node's built-in crypto is used rather than bcrypt, so installing the project needs no native build step.

Tokens

generateJwt() signs { _id, email, name, exp } with JWT_SECRET. Tokens last seven days.

A JWT is signed, not encrypted. The payload is base64 and anyone holding the token can read it, so nothing secret goes in it — no salt, no hash, no password. What the signature guarantees is that the payload has not been altered, because only the server knows the secret.

express-jwt is pinned to algorithms: ['HS256']. That is not boilerplate: it blocks the alg: none attack, where a caller submits an unsigned token whose header claims no algorithm and asks the library to trust it.

Two checks, not one

express-jwt proves the signature is valid. The getUser() helper in app_api/controllers/trips.js proves the account named by the token still exists. Those are different facts — a token lives seven days, so an account deleted on day two would otherwise keep writing until day seven.

What the Angular guard is and is not

auth.guard.ts is a usability feature, not a security control. All it does is read localStorage, and the user owns their localStorage.

This was tested directly: a hand-built token with a real-looking payload and a garbage signature, pasted into localStorage, walked straight past the guard and made the navbar read "Signed in as Totally Legit" — and POST /api/trips with that same token came back 401. The guard's actual job is to stop an honest user from filling out a long form and only discovering it was pointless when Save returns an error. The server is what protects the data.

The .env file

JWT_SECRET lives in .env, which is gitignored — a signing key in version control is a signing key everybody has. .env.example is committed so a fresh clone can see which variables the app expects.

require('dotenv').config() is the first line of app.js. JWT_SECRET is read at require-time when the express-jwt middleware is constructed, so loading dotenv any later leaves the secret undefined and every token check fails.

Known trade-offs

  • The token is in localStorage, so a refresh does not log you out. The cost is that any JavaScript on the page can read it, so an XSS hole exposes the token. An HttpOnly cookie is safer against XSS but brings CSRF along and does not work cleanly across the two origins this project uses. Named here rather than hidden.
  • Logout is client-side only. The API keeps no session, so discarding the token is the whole operation. The token itself stays technically valid until exp passes — which is why the expiry is seven days and not seven months.
  • HTTP, not HTTPS. The password crosses the network in the request body. Fine on localhost; a real deployment has to be TLS.

User schema

Defined in app_api/models/user.js.

Field Type Validation
name String required, max 100 chars
email String required, unique, indexed, lowercased, must match an email pattern
salt String required — 16 random bytes, hex
hash String required — PBKDF2-SHA512, 1000 rounds, 64 bytes, hex

timestamps: true adds createdAt and updatedAt.

Trip schema

Defined in app_api/models/travlr.js.

Field Type Validation
code String required, unique, indexed, uppercase, max 20 chars
name String required, indexed, max 100 chars
length String required
start Date required
resort String required
perPerson String required, must match a currency format
image String required
description String required

timestamps: true adds createdAt and updatedAt so records can be audited.

Verifying the data

In MongoDB Compass, connect to mongodb://127.0.0.1:27017 and open the travlr database, trips collection. Confirm 6 documents with the fields above, and check the Indexes tab for the unique index on code.

Then open the users collection. The single document should have name, email, salt, hash, createdAt, and updatedAt — and no password field anywhere. That absence is the point of the whole hashing exercise.

From the command line:

mongosh
use travlr
db.trips.countDocuments()
db.trips.find().pretty()
db.users.find().pretty()

Troubleshooting

Cannot find module 'http-errors' — run npm install first.

Could not connect to MongoDB — start the MongoDB service, then re-run npm run seed.

API returns 200 with [] — the connection works but the collection is empty. Run npm run seed.

Travel page says "No trips exist in the database" — same cause: the API returned a valid but empty array. Run npm run seed.

Travel page says "API lookup error" — the API returned an error body instead of an array. Check GET http://localhost:3000/api/trips in Postman and look at the terminal for the underlying Mongoose error.

secretOrPrivateKey must have a value, or every write returns 401 — .env is missing or JWT_SECRET is empty. Run copy .env.example .env and restart the server.

Login returns 401 with correct credentials — the users collection is empty. Run npm run seed:user.

A write returns 401 from the SPA even though the navbar says you are signed in — the token expired. Log out and back in. The forms report this as "Your session has expired".

Everything worked, then every write started returning 401 after a restart — check that JWT_SECRET did not change. Tokens are signed with it, so a new secret invalidates every token already issued.

ng is not recognized — the Angular CLI is not installed globally. Use npx ng serve instead, or run npm install -g @angular/cli.

Unexpected "var" in JSON during ng build — esbuild walks up the directory tree looking for a package.json and found a malformed one in a parent folder. Move the project somewhere with a clean path (not inside a temp directory) and rebuild. This is an environment problem, not a project one.

The admin pages render unstyled — Bootstrap is loaded from a CDN in app_admin/src/index.html, so the SPA needs an internet connection the first time a browser loads it.

Running this on a different machine

The zip contains no node_modules, no build output, and no absolute paths, so it is portable. What the target machine has to supply:

Needs Why
Node.js 18+ bin/www and the seed scripts
MongoDB on 127.0.0.1:27017 Nothing is bundled; the database has to exist locally
Internet, once Two npm install runs and the Bootstrap CDN

Everything else travels with the archive, including .env, so no secret has to be regenerated. Both package-lock.json files are current, so the installs are reproducible.

Full sequence on a clean machine:

npm install
npm run seed && npm run seed:user
npm start
cd app_admin && npm install && npx ng serve

To point at a MongoDB somewhere other than localhost, set MONGODB_URI or DB_HOST before starting — no code change needed.

The one thing that is genuinely hard-coded is the API base URL, http://localhost:3000/api, in app_admin/src/app/services/trip-data.service.ts and authentication.service.ts. That is fine as long as both servers run on the same machine, which is how this project is meant to be graded. Serving the API from a different host would mean changing those two lines — properly, it belongs in an Angular environment file, which is a refactor for a later module.

Project structure

travlr/
├── .env                         JWT_SECRET (gitignored)
├── .env.example                 committed template
├── app.js                       dotenv, DB, passport, /api, 401 handler
├── bin/www
├── data/trips.json              seed data (6 sample trips)
├── app_api/                     REST API — owns all database access
│   ├── config/passport.js       local strategy: are these credentials good?
│   ├── controllers/
│   │   ├── trips.js             Mongoose logic + getUser() on the writes
│   │   └── authentication.js    /register and /login
│   ├── models/
│   │   ├── db.js                Mongoose connection; registers both models
│   │   ├── travlr.js            trip schema + model
│   │   ├── user.js              user schema, hashing, generateJwt
│   │   ├── seed.js              loads data/trips.json into MongoDB
│   │   └── seed-user.js         creates the mock admin account
│   └── routes/index.js          route table — where auth is applied
├── app_admin/                   Angular admin SPA (runs on port 4200)
│   └── src/app/
│       ├── trip-listing/        fetches and lays out the trip cards
│       ├── trip-card/           renders one trip + Edit / Delete
│       ├── add-trip/            reactive form, POSTs a new trip
│       ├── edit-trip/           loads a trip, PUTs the changes
│       ├── login/               the HTML login form
│       ├── register/            account creation
│       ├── guards/auth.guard.ts keeps anonymous users off the write screens
│       ├── services/            trip-data, authentication, storage token
│       ├── models/              trip.ts, user.ts, authresponse.ts
│       ├── app.routes.ts        '', login, register, add-trip, edit-trip
│       └── app.config.ts        provideRouter + provideHttpClient
├── app_server/                  website — no database access
│   ├── controllers/             travel.js fetches from the API
│   ├── data/travlr.js           static copy for home/news pages
│   ├── routes/
│   └── views/
└── public/

Module Seven Journal: Reflection

Architecture

I built the front end three ways here. The original /public folder was static HTML with all six trips typed into travel.html, so changing a price meant editing markup. Handlebars fixed that: /travel hits a route, the controller gets the data, and res.render('travel', { trips }) pours it into a template that works the same for six trips or six hundred. The page arrives finished, which is fast, but every click is a full reload.

The Angular admin works the opposite way. It loads once and the router swaps components in and out of <router-outlet> without reloading. Deleting a trip refreshes nothing: the card emits an event, the listing re-fetches, only the list redraws. It feels like a desktop app, but far more logic lives in the browser, and I had to track state in a way Handlebars never required.

MongoDB made sense once I built the API. A trip document already looks like what the API returns, so it goes into Mongo, back through Mongoose, out through res.json(), and into a Trip object in Angular with no conversion anywhere. The catch is that Mongo would happily store a trip with no price. That is what Mongoose is for: the schema in app_api/models/travlr.js holds the real rules, including a unique index on code.

Functionality

I used "JavaScript" and "JSON" interchangeably for too long. JavaScript is a language that runs. JSON borrowed its object-literal syntax and dropped everything else. It is data sitting still.

That matters because JSON is the only thing crossing boundaries here. Inside Node I have Mongoose documents with methods like generateJwt(). res.json() flattens that into text, it travels as a string, and Angular's HttpClient parses it back into an object. My Trip interface and my Mongoose schema describe the same JSON shape in two languages, which is why the field names match.

The biggest refactor was Module 5. My website controller queried MongoDB and rendered the page in the same function, so building the API meant writing the same Model.find({}) twice. I moved the Mongoose code into app_api/controllers/trips.js, rewrote the website controller to fetch /api/trips, and deleted the duplicate GET /travel/json route. The payoff came in Module 6, when I built the SPA and wrote zero new database queries.

I also pulled the trip card out of trip-listing.component.html into its own TripCardComponent. Adding Edit and Delete in Module 7 meant changing one file, and every card got them. Same with TripDataService: when the API started requiring a token I wrote authHeaders() once and touched three methods. No component knows a token exists.

Testing

A method is the verb, an endpoint is the noun. GET /api/trips and DELETE /api/trips/GALR210214 sit on related URLs but are different operations, which is why I have no /getTrips route.

I tested all seven endpoints in Postman before wiring up the SPA, so when something broke later I knew the API was fine. Security made that harder. Testing a PUT now meant calling /api/login first, copying the token, and pasting it into an Authorization: Bearer header with exactly one space. I collected a lot of 401s over formatting.

The real shift was testing failures on purpose. A passing request with a good token proves nothing by itself, so I ran every write endpoint three ways: no token, a garbage token, and a valid one. The first two have to return 401, and the GETs still have to return 200 without a token, since the customer site reads those anonymously. The check is layered too: express-jwt proves the signature is valid but not that the account still exists, so every write controller also runs getUser().

Reflection

Coming in, I could build a page. I could not build an application.

What I have now is the ability to follow the whole path: a click in an Angular component, through a service, over HTTP, into an Express route, past middleware, through a controller, into Mongoose, into MongoDB, and back. Those layers used to be black boxes. I can also explain why passwords are salted and hashed rather than encrypted, and what a JWT signature actually protects.

The less glamorous skill was handling the gap between a tutorial and reality. The course materials use an older Angular with NgModules; I built on Angular 19 with standalone components. express-jwt v8 puts the payload on req.auth, not req.payload like most examples online. Figuring out where the docs and the instructions disagreed felt more like real work than anything else I did. The bugs helped too: my home route was silently shadowed by public/index.html until I set index: false on express.static.

Next I want to learn Jasmine and Karma so I am not testing by hand, and I want to deploy something, because there is a real gap between localhost and a server. Mostly I am glad I have something I can show someone. It has an API, a validated database, two front ends, and a login that works.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages