Skip to main content
Published / updated

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

MethodKeys
Full DevToolsF12
Straight to ElementsCtrl+Shift+C
Straight to ConsoleCtrl+Shift+J
Command menuCtrl+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.

PanelAnswers
ElementsWhat is on the page now, and why does it look like that
ConsoleWhat did JavaScript say
NetworkWhat was requested, and what came back
ApplicationWhat is stored — tokens, cookies, cached files
SourcesThe loaded files, with breakpoints
LighthouseAn 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 seeMeaning
Struck-through propertyOverridden by a higher-priority rule
element.styleAn inline style, usually set by JavaScript
Greyed ruleDoes not apply to this element
Warning triangleInvalid property or value

Three causes cover nearly every case:

  1. A more specific rule wins — your rule is listed, struck through.
  2. The selector does not match — your rule is not listed at all.
  3. 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 nullgetElementById 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.

ControlWhy
Fetch/XHR filterHides images and CSS; almost always what you want
Preserve logKeeps requests across navigation and redirects
Disable cacheForces fresh files while DevTools is open
ThrottlingSimulate 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

RangeWhose problem
2xxNobody
4xxThe request was wrong — usually your code
5xxThe server failed
CodeCause
400Validation failed — read Preview for the field errors
401Not authenticated — no token, or expired
403Authenticated, not permitted — wrong role
404Wrong URL, or the record does not exist
415Missing Content-Type: application/json
500Unhandled 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

StoreLifetimeNotes
localStorageUntil clearedReadable by any script on the page
sessionStorageUntil the tab closesSame, narrower
CookiesBy expirySent 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.

KeyDoes
F8Resume
F10Step over
F11Step into
Shift+F11Step out
Ctrl+POpen a file by name

Click a line number to set a breakpoint.

Right-click a breakpoint for a conditional breakpointstudent.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

FileContains
package.jsonDeclared dependencies, with version ranges
package-lock.jsonThe exact resolved version of every package
{
"dependencies": {
"react": "^18.3.1",
"axios": "~1.7.2"
}
}
PrefixMeansExample allows
^Minor and patch updates^18.3.1 → any 18.x.x
~Patch updates only~1.7.2 → any 1.7.x
noneThat exact version1.7.2 only

Do not add comments to explain this in the file itselfpackage.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 exposedVITE_ 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

SymptomCause
Cannot find module 'x'Not installed, or node_modules stale
Works locally, fails in CILock file not committed, or install used instead of ci
EACCES on installGlobal install without permission — use a version manager
Different behaviour between machinesNode version mismatch
Build passes, dev server failsDifferent config paths for build and serve
Deep link 404s after deployNo SPA fallback on the server
Old code after deployindex.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 seeCauseWhere to look
Button does nothingNo request was sentNetwork — is anything there?
Unexpected token <Response was HTML, not JSONNetwork → status code
CSS ignoredOverridden, not matching, or not loadedElements → Styles
401 on everythingToken missing or expiredApplication → Storage
Change not appearingStale cache or service workerApplication → Service Workers
npm ci failsLock file out of step with package.jsonRegenerate 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 !important before 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.log instead of logpoints and conditional breakpoints
  • Trusting Lighthouse as a complete accessibility check
  • Not committing package-lock.json
  • npm install instead of npm ci in CI
  • Committing node_modules/
  • npm audit fix --force without reading it
  • A secret in an exposed environment variable
  • Not running preview before deploying

Practice

The course exercises are inspect browser errors and use Network and Console together.

  1. Open a page with DevTools and visit every panel once.
  2. Compare Ctrl+U with Elements on a React or Angular page.
  3. Find a struck-through CSS property and work out which rule beat it.
  4. Use Computed to trace a value back to its rule.
  5. Force :hover on a button and inspect its style.
  6. Select an element in Elements and use $0 in Console.
  7. Clear the console, reproduce an error, and read the top stack frame.
  8. Trigger Unexpected token < in JSON. Find the real cause in Network.
  9. Open Network after clicking, and confirm nothing was recorded.
  10. Filter to Fetch/XHR and read all five tabs of one request.
  11. POST without Content-Type: application/json. Confirm the 415.
  12. Trigger a 400 and read the errors object in Preview.
  13. Trigger a 401 and a 403 and describe the difference.
  14. Call an API from a different port to trigger CORS. Find the OPTIONS request.
  15. Copy a failing request as cURL and replay it in Postman.
  16. Read the Timing tab and decide whether time went to the server or the payload.
  17. Find a token in Application, decode it at jwt.io, and read exp.
  18. Set a conditional breakpoint for one record in a loop, then a logpoint.
  19. Set a DOM breakpoint on attribute modification and find what changes an element.
  20. Run Lighthouse, fix every finding, then do the keyboard test and record what it missed.
  21. Delete package-lock.json, run npm install on two machines, and compare the resolved versions.
  22. Run npm ci with a mismatched lock file and read the failure.
  23. Run npm run build and search the output for your VITE_ variable.
  24. Run npm run preview and 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 ci and say why it beats npm install

Review questions

  1. What is the difference between View Source and the Elements panel?
  2. What does a 4xx tell you about whose problem it is?
  3. Why does the same request work in Postman but fail in the browser?
  4. Why must package-lock.json be committed, and why use npm ci in CI?

Next: Workstation setup lab