Browser DevTools and Frontend Tooling
Before you start
You need: a frontend calling an API — from Track 09, 11 or 12.
Time: about 55 minutes, at the keyboard.
Learning objective
Diagnose a frontend problem using the browser's own tools, and run the Node-based build tooling an Angular or React project needs.
Topics
- Opening DevTools
- Elements and CSS
- Console
- Network
- Application and storage
- Sources and breakpoints
- Lighthouse
- Node and npm as tooling
- Diagnosing a build failure
Opening DevTools
| Method | Keys |
|---|---|
| Full DevTools | F12 |
| Straight to Elements | Ctrl+Shift+C |
| Straight to Console | Ctrl+Shift+J |
| Command menu | Ctrl+Shift+P |
Chrome and Edge share the same DevTools, so any tutorial for one applies to the other.
Ctrl+Shift+P inside DevTools is the command menu — "Disable JavaScript", "Capture screenshot", "Show coverage" and every other panel by name.
| Panel | Answers |
|---|---|
| Elements | What is on the page now, and why does it look like that |
| Console | What did JavaScript say |
| Network | What was requested, and what came back |
| Application | What is stored — tokens, cookies, cached files |
| Sources | The loaded files, with breakpoints |
| Lighthouse | An automated audit |
For a .NET developer consuming an API, Network and Console are where most of the time goes.
Elements
Elements shows the live DOM, after JavaScript has run — not the HTML the server sent.
<!-- View Source: what the server sent -->
<tbody id="studentRows"></tbody>
<!-- Elements: after the fetch and render -->
<tbody id="studentRows">
<tr data-id="NCA-2024-0012"><td>Ravi Kumar</td><td>10th</td></tr>
</tbody>
Ctrl+U (View Source) and Elements are different things. On a React or Angular application, View Source is often an empty <div id="root"></div>. Searching it for an element JavaScript created wastes real time.
Ctrl+F inside Elements searches the live DOM and accepts CSS selectors — tr[data-id] finds every student row.
Why the CSS is not working
The Styles pane lists every rule affecting the selected element, most specific first.
| What you see | Meaning |
|---|---|
| Struck-through property | Overridden by a higher-priority rule |
element.style | An inline style, usually set by JavaScript |
| Greyed rule | Does not apply to this element |
| Warning triangle | Invalid property or value |
Three causes cover nearly every case:
- A more specific rule wins — your rule is listed, struck through.
- The selector does not match — your rule is not listed at all.
- The file did not load — nothing of yours is listed. Check Network for a 404.
Read the panel before adding !important. Two seconds there replaces an hour of escalation — and !important makes the next override worse.
Computed shows the single winning value per property, with an arrow to the rule that produced it. Start there when the cascade is deep.
Force state — right-click an element → Force state → :hover — is how you inspect a hover style. Hovering manually is impossible, because moving the mouse to DevTools ends the hover.
Everything in Elements is editable and nothing is saved. Test a fix, then write it in the stylesheet.
Console
$0 // the element selected in Elements
$$('tr[data-id]') // querySelectorAll
$('#searchBox') // querySelector
$_ // the last evaluated result
localStorage.getItem('authToken')
document.querySelectorAll('tr').length
await fetch('/api/students?schoolId=1').then(r => r.json())
$0 and $$() are the shortcuts worth memorising. Selecting an element in Elements and typing $0.classList in Console is faster than writing a selector.
Reading a stack trace:
Uncaught TypeError: Cannot read properties of null (reading 'value')
at getSearchTerm (studentList.js:42:31)
at handleSearch (studentList.js:18:22)
The top frame is where it broke. Frames below show how execution got there. Click any of them to open that line in Sources.
Three errors cover most frontend work:
Cannot read properties of null — getElementById returned null. A misspelled id, or the script ran before the element existed.
X is not a function — a typo, or a library that did not load.
Unexpected token < in JSON at position 0 — .json() was called on an HTML response. Check Network — it is almost always a 404 or a 500, not a JSON bug.
Clear the console (Ctrl+L), then reproduce. Everything on screen was then caused by the action you just took, rather than buried under four hundred lines of prior noise.
Never paste code into the Console because someone told you to. It runs with your full session — cookies, token, permissions. The "paste this to unlock a feature" scam exists precisely because it works.
Network
Open Network before reproducing — it only records while open.
| Control | Why |
|---|---|
| Fetch/XHR filter | Hides images and CSS; almost always what you want |
| Preserve log | Keeps requests across navigation and redirects |
| Disable cache | Forces fresh files while DevTools is open |
| Throttling | Simulate Slow 3G |
Click a request for five tabs: Headers, Payload, Preview, Response, Timing.
Preview parses JSON into a tree; Response is raw text. Use Response only when the body is not valid JSON — which is itself the diagnosis.
Status codes
| Range | Whose problem |
|---|---|
| 2xx | Nobody |
| 4xx | The request was wrong — usually your code |
| 5xx | The server failed |
| Code | Cause |
|---|---|
| 400 | Validation failed — read Preview for the field errors |
| 401 | Not authenticated — no token, or expired |
| 403 | Authenticated, not permitted — wrong role |
| 404 | Wrong URL, or the record does not exist |
| 415 | Missing Content-Type: application/json |
| 500 | Unhandled exception on the server |
Saying "the API is broken" on a 400 means the Network tab was not read. 4xx means the request was wrong.
401 versus 403 is the interview question and the practical one: 401 means authenticate, 403 means authenticated and refused — a new token changes nothing.
ASP.NET Core returns a field-level errors object on 400. Always open Preview on a 400 — it names exactly which field failed.
CORS
Access to fetch at 'https://api.nexcoding.in/api/students'
from origin 'https://portal.nexcoding.in' has been blocked by CORS policy.
The request usually reached the server and succeeded. The server returned 200; the browser then refused to hand the response to your JavaScript because Access-Control-Allow-Origin was missing.
That is why the same call works in Postman — Postman is not a browser and does not enforce CORS.
No frontend change fixes it. The permission comes from the server's response. An OPTIONS request returning 404 or 405 in Network means CORS middleware is not wired up at all; one returning 401 means it is registered after authentication.
Copy as cURL
Right-click a request → Copy → Copy as cURL, then import into Postman.
Works in Postman, fails in the browser → a frontend problem: CORS, a header, or the token. Fails in both → a backend problem.
Two minutes, and it ends most frontend-versus-backend arguments.
A copied cURL contains your live Authorization header. Redact it before pasting it anywhere.
Timing
The Timing tab splits a slow request:
- Time in Waiting (TTFB) is the server thinking
- Time in Content Download is payload size
That distinction decides where to look. "The API is slow" and "the response is 4 MB" need entirely different fixes.
Application
| Store | Lifetime | Notes |
|---|---|---|
localStorage | Until cleared | Readable by any script on the page |
sessionStorage | Until the tab closes | Same, narrower |
| Cookies | By expiry | Sent with every request |
localStorage.getItem('authToken')
The 401 nobody can explain: everything works, then after some hours every call returns 401. The token is still in storage — it expired. Paste it into jwt.io and read the exp claim.
The bug is not authentication. It is that the application never checks expiry, so a stale token is sent forever.
A token in localStorage is readable by any JavaScript on the page, including a compromised third-party library. An HttpOnly cookie cannot be read by JavaScript at all, and needs CSRF protection instead. Knowing that trade-off is the interview answer.
Application → Clear site data before concluding a deployment is broken. Cached JavaScript running against a new API produces symptoms that look like server bugs.
Sources
Sources is Visual Studio's debugger for JavaScript, with the same keys.
| Key | Does |
|---|---|
F8 | Resume |
F10 | Step over |
F11 | Step into |
Shift+F11 | Step out |
Ctrl+P | Open a file by name |
Click a line number to set a breakpoint.
Right-click a breakpoint for a conditional breakpoint — student.rollNumber === 'NCA-2024-0012' stops on one row out of four hundred.
A logpoint prints a message and continues: a console.log that is not in your source and cannot be committed by accident.
DOM breakpoints answer "what is changing this element?" — right-click an element in Elements → Break on → attribute modifications. That question is otherwise very hard.
If Sources shows minified code, source maps are missing. Angular and React dev builds enable them by default; a production build needs them uploaded to an error-tracking service rather than served publicly.
Lighthouse
DevTools → Lighthouse → run Performance, Accessibility, Best Practices and SEO.
It catches missing alt text, contrast failures, missing form labels, missing lang, duplicate ids and render-blocking resources.
Automated tools catch roughly a third of accessibility issues. The keyboard test — put the mouse away and tab through the page — catches most of the rest, and it takes two minutes.
Run Lighthouse in an incognito window; extensions distort the results.
Node and npm as tooling
Node here is build tooling, not a backend you are learning. It runs the TypeScript compiler, the dev server and the package manager. The backend in this curriculum is ASP.NET Core.
node --version # 20 or later
npm --version
npm install # install everything in package.json
npm install axios # add a dependency
npm install -D vitest # add a dev dependency
npm uninstall axios
npm outdated
npm audit
npm audit fix
npm run dev # dev server
npm run build # production build
npm run test
npm run lint
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint src"
}
}
npm run with no arguments lists every available script. That is the first thing to run in an unfamiliar frontend project.
package.json and the lock file
| File | Contains |
|---|---|
package.json | Declared dependencies, with version ranges |
package-lock.json | The exact resolved version of every package |
{
"dependencies": {
"react": "^18.3.1",
"axios": "~1.7.2"
}
}
| Prefix | Means | Example allows |
|---|---|---|
^ | Minor and patch updates | ^18.3.1 → any 18.x.x |
~ | Patch updates only | ~1.7.2 → any 1.7.x |
| none | That exact version | 1.7.2 only |
Do not add comments to explain this in the file itself — package.json is strict JSON, and a // line breaks every npm command with a parse error.
Commit package-lock.json. Without it, two developers running npm install from the same commit get different code — and so does the build server.
npm ci
Use npm ci in CI and Docker. It installs exactly what the lock file specifies, fails if the lock and package.json disagree, and is faster because it does no resolution.
Never commit node_modules/. It is hundreds of megabytes of platform-specific code, rebuilt from the lock file in seconds.
Security
npm audit
npm audit fix
npm audit fix --force # may introduce breaking changes
Read what --force proposes before running it. It upgrades across major versions to resolve an advisory, which can break the build.
Not every advisory matters. A vulnerability in a build-only dependency that never runs in the browser is a different risk from one in a shipped library. npm audit --production filters to what actually ships.
Check the transitive dependency count before adding a package. One convenience package can pull in twenty others, each now yours to keep patched.
Environment variables
# .env.development
VITE_API_URL=https://localhost:7099
const baseUrl = import.meta.env.VITE_API_URL;
Only prefixed variables are exposed — VITE_ for Vite, NG_APP_ or an environment file for Angular. That prefix is a deliberate safeguard.
Exposed means shipped to the browser. Run the build and search the output — the value is there in plain text. URLs and feature flags belong here; never a secret. There is no such thing as a client-side secret.
Diagnosing a build failure
| Symptom | Cause |
|---|---|
Cannot find module 'x' | Not installed, or node_modules stale |
| Works locally, fails in CI | Lock file not committed, or install used instead of ci |
EACCES on install | Global install without permission — use a version manager |
| Different behaviour between machines | Node version mismatch |
| Build passes, dev server fails | Different config paths for build and serve |
| Deep link 404s after deploy | No SPA fallback on the server |
| Old code after deploy | index.html cached |
rm -rf node_modules package-lock.json
npm install
A last resort, not a first step. It resolves a genuine class of corruption and it hides the cause. Read the error first.
Pin the Node version so everyone uses the same:
{
"engines": { "node": ">=20.0.0" }
}
# .nvmrc
20
Run npm run build && npm run preview before every deployment. It serves the built output rather than the dev server, and it is where the SPA-fallback and environment problems appear — the ones invisible in npm run dev.
Errors you will hit
| What you see | Cause | Where to look |
|---|---|---|
| Button does nothing | No request was sent | Network — is anything there? |
Unexpected token < | Response was HTML, not JSON | Network → status code |
| CSS ignored | Overridden, not matching, or not loaded | Elements → Styles |
| 401 on everything | Token missing or expired | Application → Storage |
| Change not appearing | Stale cache or service worker | Application → Service Workers |
npm ci fails | Lock file out of step with package.json | Regenerate the lock file |
Preserve log before reproducing, or a redirect wipes the evidence you needed.
Common mistakes
- Searching View Source for an element JavaScript created
- Adding
!importantbefore reading the cascade - Opening Network after reproducing
- Not filtering to Fetch/XHR
- Saying "the API is broken" on a 4xx
- Confusing 401 and 403
- Trying to fix CORS in the frontend
- Not checking token expiry in Application
- Pasting untrusted code into the Console
- Sharing a cURL containing a live token
console.loginstead of logpoints and conditional breakpoints- Trusting Lighthouse as a complete accessibility check
- Not committing
package-lock.json npm installinstead ofnpm ciin CI- Committing
node_modules/ npm audit fix --forcewithout reading it- A secret in an exposed environment variable
- Not running
previewbefore deploying
Practice
The course exercises are inspect browser errors and use Network and Console together.
- Open a page with DevTools and visit every panel once.
- Compare
Ctrl+Uwith Elements on a React or Angular page. - Find a struck-through CSS property and work out which rule beat it.
- Use Computed to trace a value back to its rule.
- Force
:hoveron a button and inspect its style. - Select an element in Elements and use
$0in Console. - Clear the console, reproduce an error, and read the top stack frame.
- Trigger
Unexpected token < in JSON. Find the real cause in Network. - Open Network after clicking, and confirm nothing was recorded.
- Filter to Fetch/XHR and read all five tabs of one request.
- POST without
Content-Type: application/json. Confirm the 415. - Trigger a 400 and read the
errorsobject in Preview. - Trigger a 401 and a 403 and describe the difference.
- Call an API from a different port to trigger CORS. Find the
OPTIONSrequest. - Copy a failing request as cURL and replay it in Postman.
- Read the Timing tab and decide whether time went to the server or the payload.
- Find a token in Application, decode it at jwt.io, and read
exp. - Set a conditional breakpoint for one record in a loop, then a logpoint.
- Set a DOM breakpoint on attribute modification and find what changes an element.
- Run Lighthouse, fix every finding, then do the keyboard test and record what it missed.
- Delete
package-lock.json, runnpm installon two machines, and compare the resolved versions. - Run
npm ciwith a mismatched lock file and read the failure. - Run
npm run buildand search the output for yourVITE_variable. - Run
npm run previewand refresh on a deep link.
Exercises 9, 15 and 21 are the three that save the most time.
You can now
- Diagnose a frontend problem from the browser's own tools
- Decide from the Network tab whether a failure is frontend or backend
- Read status codes and the CORS message
- Inspect storage and decode a token
- Use
npm ciand say why it beatsnpm install
Review questions
- What is the difference between View Source and the Elements panel?
- What does a 4xx tell you about whose problem it is?
- Why does the same request work in Postman but fail in the browser?
- Why must
package-lock.jsonbe committed, and why usenpm ciin CI?
Next: Workstation setup lab