Skip to content

My main role

Frontend Engineer

Building web interfaces that stay fast, accessible and maintainable as products grow

This map goes from web fundamentals (HTML, modern CSS, JavaScript, TypeScript, accessibility) to React 19, the Next.js App Router, testing, performance and frontend architecture at scale. It is for people who have built a few React pages and want to reach intermediate and then advanced level, understanding the why and not just the how.

Start at stage 01
stages
10stages
topics
51topics
weeks, part-time
~34weeks, part-time
  • Foundation
  • Core
  • Advanced
  • TypeScriptTypeScript
  • ReactReact
  • Next.jsNext.js
  • TanStackTanStack Query
  • ViteVite
  • VitestVitest
  • PlaywrightPlaywright
  • TurborepoTurborepo

My take

I am a frontend developer, and this is the role I have worked in every day since interning at AMIT Group in 2022 and now at Estuary Solutions. My work centers on React, TypeScript, Next.js, Zustand, TanStack Query and a Bun + Turborepo monorepo that runs this very blog. While mentoring two juniors, I noticed that many roadmaps only list technologies without saying in what order to learn them or why they matter. This map is the order I would follow if I started again today, updated to the state of the ecosystem in late 2026.

What I wish I had known earlier

  • When I started, I jumped into a framework too early, so many bugs I hunted in React were really CSS bugs, event loop bugs or HTML bugs.
  • Server state and client state are different things; separating them early shrinks the global store a lot and stale data bugs almost disappear.
  • Accessibility is not extra work at the end of a sprint: using the right element from the start is far cheaper than patching ARIA later.
  • What I wish I had known sooner is to measure before optimizing: one profiling session in DevTools is worth more than an afternoon of guessing which component is slow.
0 of 10 stages passed

Foundation01–03

  1. 01Stage 01, 3 weeksSemantic HTML and modern CSSBuild responsive, semantic layouts without a UI library and understand exactly what the cascade is doing.
    I used to think CSS was the easy part, until I hit stacked z-index issues and styles overridden for no obvious reason. Learning the cascade and layout properly once saved me many hours of debugging later.

    What to learn

    Semantic HTML and native forms
    Use landmarks (header, nav, main, aside), ordered headings, button for actions and a for navigation. Native forms with label, fieldset, input types and constraint validation (required, pattern, :user-invalid) give you a lot of behavior for free. The dialog element with showModal() and the popover attribute handle focus, the top layer and Esc without custom code.
    Layout: Flexbox, Grid, subgrid
    Flexbox for one-dimensional layout, Grid for two dimensions, and subgrid so child cards align to the parent grid. Learn min(), max(), clamp(), the dvh/svh units and logical properties (margin-inline, padding-block) so layouts also work in RTL languages.
    Container queries and :has()
    Container queries (@container, cqi units) let a component respond to the size of its container instead of the viewport, which fits design systems well. :has() is a parent selector: style a card when it contains an image, or a form when an input is invalid, removing many conditional classes set from JavaScript.
    Cascade layers, nesting and specificity
    @layer lets you declare priority order (reset, base, components, utilities) independently of specificity, so library CSS stops overriding yours in confusing ways. Combine it with native CSS nesting, :where() to keep specificity at zero, and custom properties as design tokens.
    View transitions and scroll-driven animation
    The View Transition API animates state or page changes with document.startViewTransition() and view-transition-name, and same-document transitions now work across the major browsers. Scroll-driven animations and anchor positioning replace many JavaScript libraries; always check Baseline status and respect prefers-reduced-motion.

    Do

    • Check the Baseline status of a CSS feature before using it in production.
    • Organize CSS with @layer from the start of a project instead of raising specificity to win.
    • Prefer native elements such as button, dialog and details before building them from divs.
    • Use custom properties for color, spacing and radius so a theme changes in one place.

    Watch out for

    • Using a div with onClick instead of a button, losing focus, Enter/Space handling and the role for screen readers.
    • Writing media queries for specific devices instead of letting layouts flex naturally with Grid and clamp().
    • Reaching for !important to patch conflicts instead of understanding the cascade.

    Checkpoint project: A product page with no JavaScript

    Build a product listing page with cards using container queries, filters using :has() and a detail view with the dialog element. It passes when the layout works from 320px to wide screens, has no horizontal scroll and is fully navigable by keyboard.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4StarterWhy should <div onClick={save}> be replaced with <button onClick={save}>?
    2. Question 2 of 4StarterGiven @layer reset, base, components, utilities;, the rule #app .card { padding: 2rem } is in the components layer and .p-4 { padding: 1rem } is in the utilities layer. What padding applies?
    3. Question 3 of 4StarterA card component is used in both a narrow sidebar and a wide main column, and should switch to a horizontal layout when it has room. What is the right approach?
    4. Question 4 of 4StarterYou need a confirmation modal with focus handling, Esc to close and rendering above everything else. Which native approach fits?
  2. 02Stage 02, 4 weeksModern JavaScript and TypeScriptRead and write correct asynchronous code and model data with safe types.
    When mentoring juniors, most bugs I saw were not in React but in stale closures, unawaited promises or any types spreading everywhere. Investing here pays off in every framework.

    What to learn

    Event loop, promises and async
    Tell apart macrotasks, microtasks and when the browser renders, so you understand why a heavy loop freezes the UI. Get fluent with async/await, Promise.all, Promise.allSettled, AbortController for cancelling requests and structuredClone for deep copies.
    Modules, closures and immutability
    ES modules (import/export, dynamic import()) underpin code splitting and tree shaking. Understanding closures and references is the key to avoiding stale state in React; non-mutating methods such as toSorted, toSpliced, with and Object.groupBy keep immutable code short.
    TypeScript for real applications
    Turn on strict, use discriminated unions to model states (idle, loading, success, error), satisfies to check without widening, and constrained generics for utilities. Learn utility types (Pick, Omit, Partial, ReturnType) and narrowing instead of casting with as.
    Validate at system boundaries
    Types only exist at compile time; data from APIs, the URL or localStorage needs runtime validation with a schema library such as Zod or Valibot. Infer types from the schema (z.infer) so there is one source of truth for both validation and types.
    Tooling: Vite, ESLint, tsc
    Vite is the default choice for SPAs and libraries; understand that the dev server serves native ESM while builds go through a bundler. Run typechecking (tsc --noEmit) separately from the build and use typescript-eslint with type-aware rules. TypeScript is being ported to native code (Go) for much faster typechecking, so keep an eye on that roadmap.

    Do

    • Enable strict and noUncheckedIndexedAccess in tsconfig for every new project.
    • Model state with discriminated unions instead of several loose booleans.
    • Validate every piece of external input with a schema before using it.
    • Always pass an AbortSignal to requests that may be cancelled when the user navigates away.

    Watch out for

    • Using any or as to silence errors instead of fixing types, letting bugs slip to runtime.
    • Forgetting to await or catch promises, leading to unhandled rejections that are hard to trace.
    • Trusting that API types are correct without validating the real data.

    Checkpoint project: A fully typed API client

    Write a module that fetches from a public API with a Zod schema, supports cancellation, bounded retries and returns a discriminated union. It passes when strict tsc reports no errors, there is no any, and a test covers data that does not match the schema.

    • JavaScriptJavaScript
    • TypeScriptTypeScript
    • ViteVite

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4StarterWhat does console.log('A'); setTimeout(() => console.log('B')); Promise.resolve().then(() => console.log('C')); console.log('D'); print?
    2. Question 2 of 4StarterYou already have a User type in TypeScript. Why parse the API response with a Zod schema anyway?
    3. Question 3 of 4StarterWhy model request state as { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: E } instead of isLoading and isError booleans plus data?
    4. Question 4 of 4StarterA search box fires fetch on every keystroke, and older responses sometimes arrive after newer ones and overwrite the results. What is the right fix?
  3. 03Stage 03, 2 weeksAccessibility in practiceBuild components that work with keyboard and screen readers and meet WCAG 2.2 level AA.
    I learned accessibility late and had to go back and fix many hand-rolled modals and dropdowns. Now I treat it as part of the definition of done, not a separate item.

    What to learn

    WCAG 2.2 and the POUR principles
    Know the common criteria: color contrast, visible focus that is not hidden (Focus Not Obscured), minimum target size (Target Size) and not depending on dragging. Many markets now require WCAG AA by law, for example the European Accessibility Act in force since mid 2025.
    Keyboard and focus management
    Tab order follows the DOM, trap focus inside modals, return focus to the trigger on close, and use :focus-visible for focus rings. The inert attribute disables the background while an overlay is open without hacks.
    ARIA done right
    The first rule of ARIA is not to use ARIA if a native element can do the job. When you build combobox, tabs or menu yourself, follow the patterns in the ARIA Authoring Practices Guide and use aria-live for dynamic messages such as toasts or search results.
    Testing accessibility
    Combine automated checks (axe-core, eslint-plugin-jsx-a11y, Lighthouse) with manual testing by keyboard and a screen reader such as NVDA or VoiceOver. Automated tools catch only part of the issues; the rest needs a real person trying it.

    Do

    • Give every interactive control a visible label or an accessible name.
    • Go through each new feature using only the keyboard before opening a PR.
    • Use headless libraries with built-in a11y such as Radix or React Aria for complex components.
    • Run axe in component tests to catch regressions early.

    Watch out for

    • Removing the focus outline for aesthetic reasons without replacing it with another focus style.
    • Sprinkling role and aria-* attributes carelessly, which makes screen readers announce wrong information, worse than none at all.

    Checkpoint project: An accessible component set

    Build an autocomplete combobox and a confirm modal following the ARIA APG. It passes when both are fully usable by keyboard, axe reports no violations, and they are announced correctly in at least one screen reader.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4StarterWhat does the first rule of ARIA recommend?
    2. Question 2 of 4StarterA page passes axe with zero issues. Why can you still not call it accessible?
    3. Question 3 of 4StarterA user opens a modal with the Edit button and presses Esc to close it. Where should focus go?
    4. Question 4 of 4StarterSearch results update as the user types, but screen reader users do not know the result count changed. What is the right fix?

Core04–07

  1. 04Stage 04, 4 weeksThe React 19 mental modelExplain when and why React renders, and use hooks, Actions, Suspense and the React Compiler correctly.
    I moved from React 18 to 19 at work, and the biggest change was not the new APIs but the way of thinking: fewer effects, less manual memoization, letting the framework handle async work. Understanding render as a pure function makes everything else easier.

    What to learn

    Render, commit and purity
    A component is a pure function from props and state to UI; render may run many times and only the commit touches the DOM. Understand keys, how state is tied to its position in the tree, and why StrictMode renders twice in development to expose misplaced side effects.
    Hooks, and when you might not need an Effect
    Most useEffect calls that sync derived state are unnecessary: compute it during render or handle it in the event handler. useEffect is for syncing with external systems; React 19.2 adds useEffectEvent to pull event-like logic out of an Effect and avoid needless reconnections.
    Actions and forms in React 19
    Actions are async functions tied to transitions: useActionState manages the result and pending state, useOptimistic handles optimistic updates, and useFormStatus reads form state from child components. Forms can take an action directly, and ref is now a regular prop with no forwardRef needed.
    Suspense, use() and concurrent rendering
    Suspense declares where to wait for data or code, and use() reads a promise or context during render. startTransition and useDeferredValue keep the UI responsive during heavy updates; the Activity component in React 19.2 keeps the state of hidden UI and defers its updates.
    React Compiler
    React Compiler 1.0 is stable and memoizes components and values automatically, so manual useMemo, useCallback and memo are mostly unnecessary. It is only safe when code follows the Rules of React, so enable the new rules in eslint-plugin-react-hooks first, even before turning the compiler on.

    Do

    • Compute derived state during render instead of syncing it with useEffect.
    • Enable eslint-plugin-react-hooks with the compiler rules and fix every warning.
    • Use useActionState and useOptimistic for mutations instead of hand-managed loading flags.
    • Place Suspense boundaries around meaningful UI regions, not around the whole page.

    Watch out for

    • Fetching data in useEffect without cleanup, causing race conditions when parameters change quickly.
    • Scattering useMemo and useCallback everywhere without measuring, making code harder to read but not faster.
    • Mutating objects in state directly and wondering why the UI does not update.

    Checkpoint project: A notes app built on Actions

    Build a notes app using a form action, useActionState for validation errors, useOptimistic for add and delete, and Suspense for loading the list. It passes when the React Compiler is enabled with no lint warnings and no useEffect is used to sync state.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateIn development with StrictMode, a component renders twice and its Effect runs mount, cleanup, mount. Why does React do this?
    2. Question 2 of 4IntermediateA component has const [fullName, setFullName] = useState('') and useEffect(() => setFullName(first + ' ' + last), [first, last]). What is better?
    3. Question 3 of 4Intermediateitems.push(newItem); setItems(items); does not update the UI. Why, and what is the fix?
    4. Question 4 of 4IntermediateA Like button should show the new count instantly and roll back if the server fails. Which React 19 tool fits?
  2. 05Stage 05, 3 weeksClient state, server state and URL statePick the right home for each kind of state and build data flows with caching, refetching and optimistic updates.
    In my first projects I put everything, including API data, into a global store and had to hand-write loading, caching and invalidation. Moving server data to TanStack Query and keeping only UI state in Zustand made the code noticeably smaller.

    What to learn

    Classifying state
    Split state into local UI (useState), shared client state (Zustand or context), server state (TanStack Query) and URL state (search params). Each has a different lifecycle and source of truth; mixing them up is the root of stale data and extra renders.
    TanStack Query v5
    Design structured query keys with a queryOptions factory, understand staleTime and gcTime, call invalidateQueries after mutations, and do optimistic updates via onMutate or the mutation variables. Use useSuspenseQuery to combine with Suspense, and prefetch on the server then hydrate when working with Next.js.
    Zustand for client state
    Keep small, feature-scoped stores, read with selectors so components only re-render when the slice they need changes, and use useShallow when a selector returns an object. Use the persist middleware for data stored on the device and devtools for debugging; do not turn Zustand into a copy of the server cache.
    The URL is state
    Filters, pagination and the active tab belong in the URL so sharing, back/forward and reload all work. Libraries such as nuqs give typed parsing and serialization of search params and work with the Next.js App Router and React Router.
    Realtime and sync
    With WebSocket or Server-Sent Events, push events into the TanStack Query cache (setQueryData or invalidate) instead of keeping a parallel store. Handle reconnects, event ordering and pausing when the tab is hidden.

    Do

    • Bundle query keys and query functions into shared queryOptions used by both prefetching and components.
    • Set a deliberate staleTime per kind of data instead of leaving the default of zero.
    • Read from Zustand through narrow selectors, never the whole store.
    • Put filters and pagination in the URL from day one.

    Watch out for

    • Copying data from useQuery into useState or a store, creating two sources of truth that drift apart.
    • Using ad hoc string query keys so invalidation misses or clears the wrong cache.

    Checkpoint project: An order management dashboard

    Build an order list with URL-based filters, pagination, order status updates with optimistic updates and realtime updates from a mocked WebSocket. It passes when reload keeps the filters, failed mutations roll back correctly and no server data lives in Zustand.

    • TanStackTanStack Query
    • Zustand
    • State

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateWhy is const { data } = useQuery(todosQuery); const [todos, setTodos] = useState(data); a design mistake?
    2. Question 2 of 4IntermediateWith the default staleTime of 0, what does TanStack Query do when a component using the same query mounts again while the data is still cached?
    3. Question 3 of 4IntermediateA product list has filters and pagination. Users complain that shared links lose the filters and Back does not return to the previous filter. What is the right fix?
    4. Question 4 of 4IntermediateA cart badge in the header uses const cart = useCartStore() and re-renders whenever any part of the store changes. What is the right fix?
  3. 06Stage 06, 4 weeksNext.js App Router, RSC and formsDesign a Next.js 16 app with clear server/client boundaries, deliberate caching and safe forms.
    Server Components forced me to rethink where data is fetched and where JavaScript really needs to run. At first I marked 'use client' far too high; pushing the boundary down to small leaves made bundles much lighter.

    What to learn

    Server and Client Components
    Server Components are the default: they fetch data directly and ship no JavaScript to the client. Put 'use client' only on leaf components that need state or events, pass Server Components into Client Components through children, and use server-only to block accidental imports of secret code.
    Routing and streaming
    Learn layouts, loading.tsx, error.tsx, route groups, and parallel and intercepting routes. Streaming with Suspense shows the static part immediately and fills in slow parts later, improving both time to content and perceived speed.
    Cache Components and 'use cache'
    Next.js 16 introduces Cache Components (enabled with cacheComponents): caching becomes explicit through the 'use cache' directive together with cacheLife and cacheTag, while dynamic parts stream inside Suspense. Understand when to invalidate by tag, when data must always be fresh, and that the default is now dynamic rather than implicitly cached.
    Server Functions and forms
    Server Functions ('use server') take form actions, pair with useActionState to return validation errors, and revalidate data after writes. Every Server Function is a public endpoint: always validate input with a schema and check authorization inside it, never relying on the UI hiding a button.
    Shared client and server validation
    One Zod schema serves both client validation (React Hook Form or Conform) and server validation, helped by the Standard Schema spec that many form libraries accept. Keep progressive enhancement: the form still submits before JavaScript has loaded.
    proxy.ts and the network boundary
    In Next.js 16, middleware.ts is renamed to proxy.ts to make clear it is a request interception layer at the edge of the app. Use it for redirects, rewrites and light session checks; real authorization still belongs in the data layer and Server Functions.

    Do

    • Push 'use client' down to the smallest possible leaf component.
    • Create a separate data access layer that checks auth before returning data to components.
    • Validate input and check permissions in every Server Function.
    • Tag cached data with cacheTag and invalidate by tag after writes.

    Watch out for

    • Passing whole database objects to Client Components, leaking sensitive fields into the HTML.
    • Checking permissions only in proxy or a layout and assuming every child route is safe.
    • Being slow to apply React and Next.js security patches; the remote code execution flaw in React Server Components in late 2025 is the clearest example.

    Checkpoint project: A blog with an admin area

    Build a Next.js 16 blog whose public pages are cached by tag and whose admin area creates posts through a Server Function validated with a shared schema. It passes when signed-out users cannot call the Server Function, editing a post updates the public page immediately, and the form works with JavaScript disabled.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateThe button that calls the deleteUser Server Function is only shown to admins. Why must the Server Function still check authorization?
    2. Question 2 of 4IntermediateWhy push 'use client' down to the smallest leaf instead of putting it on a layout?
    3. Question 3 of 4IntermediateA Server Component renders <ProfileCard user={await db.user.findUnique({ where: { id } })} />, where ProfileCard is a Client Component. What is the risk?
    4. Question 4 of 4IntermediateIn Next.js 16 with Cache Components, product data is cached with 'use cache' and cacheTag('products'). After an admin edits a product in a Server Function, what is the right way to show fresh data?
  4. 07Stage 07, 3 weeksA frontend testing strategyBuild a fast, trustworthy test suite that checks user behavior instead of implementation details.
    I am not a QA specialist, but I have seen tests written against implementation details break on every refactor. Tests that follow how users interact are what actually give me confidence when changing code.

    What to learn

    The testing trophy and choosing layers
    Favor integration tests at the component and feature level, unit tests for pure logic, a few end-to-end tests for the most critical flows, and static analysis (TypeScript, ESLint) as the base. The goal is confidence per minute of CI, not high coverage numbers.
    Vitest and Browser Mode
    Vitest is fast and shares configuration with Vite. Since Vitest 4, Browser Mode is stable, running component tests in a real browser through Playwright instead of jsdom, with visual regression via screenshots.
    Testing Library and MSW
    Query by role and accessible name (getByRole) so tests check behavior and indirectly check accessibility. Use user-event to simulate real interaction and MSW to mock APIs at the network level, sharing handlers across tests, Storybook and development.
    Playwright for end-to-end
    Role-based locators, auto-waiting and web-first assertions make tests far less flaky. Use fixtures, storageState to reuse logins, the trace viewer to debug CI failures, and sharding to run in parallel.
    Testing Server Components and Actions
    Async Server Components are still awkward to unit test directly, so extract data logic into pure functions and test those, and cover the full flow with end-to-end tests. Test Server Functions like an API: bad input, missing permission, success.

    Do

    • Query elements by role and label as users see them, avoiding test ids when a better option exists.
    • Mock at the network level with MSW instead of mocking the fetch module.
    • Enable trace on-first-retry for Playwright in CI.
    • Keep tests independent, never relying on run order or shared data.

    Watch out for

    • Snapshotting large component trees and updating snapshots whenever they fail without reading the diff.
    • Using fixed sleeps in end-to-end tests instead of waiting on conditions, causing flaky runs.

    Checkpoint project: A test suite for the order dashboard

    Write integration tests with Vitest and Testing Library for the dashboard from stage 05, mock the API with MSW, and add two Playwright end-to-end flows running in CI. It passes when everything runs in a few minutes and stays green across 10 consecutive runs.

    • VitestVitest
    • PlaywrightPlaywright
    • Testing

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateWhy prefer getByRole('button', { name: 'Save' }) over getByTestId('save-btn')?
    2. Question 2 of 4IntermediateWhy is mocking the API at the network level with MSW better than vi.mock('./api')?
    3. Question 3 of 4IntermediateThe Playwright test await page.getByRole('button', { name: 'Save' }).click(); await page.waitForTimeout(2000); expect(await page.locator('.toast').textContent()).toBe('Saved'); sometimes fails in CI. What is the right fix?
    4. Question 4 of 4IntermediateYou need to test the deletePost Server Function. Which approach makes the most sense?

Advanced08–10

  1. 08Stage 08, 3 weeksPerformance and Core Web VitalsMeasure, diagnose and improve LCP, INP and CLS based on real user data.
    I used to optimize by feel and spent effort where it did not matter. Learning to read the flame chart in the Performance panel was the turning point: I could see exactly which task was blocking the main thread.

    What to learn

    Core Web Vitals and field data
    LCP measures how fast the main content loads, INP measures responsiveness across the whole session, and CLS measures layout stability; the good thresholds are about 2.5s, 200ms and 0.1 at the 75th percentile. Tell lab data (Lighthouse) apart from field data (CrUX, RUM via the web-vitals library).
    Optimizing INP
    INP is made of input delay, processing time and presentation delay. Break up long tasks, yield to the main thread with scheduler.yield() (with a fallback), use startTransition for non-urgent updates, and use the Long Animation Frames API to find slow scripts.
    Optimizing LCP and CLS
    Prioritize the LCP resource with fetchpriority=high, avoid lazy-loading above-the-fold images, and serve AVIF or WebP with srcset. Reserve space for images and embeds with width/height or aspect-ratio, and self-host fonts with font-display and size-adjust to reduce layout shift.
    Bundles and code loading
    Analyze bundles, split code by route and by interaction with dynamic import, and drop heavy libraries or replace them with native APIs. Server Components keep render-only libraries out of the client bundle.
    Efficient rendering in React
    Use the React DevTools Profiler and the Performance Tracks in Chrome DevTools to see which components render and for how long. Virtualize long lists (TanStack Virtual) and use content-visibility: auto for off-screen regions.

    Do

    • Collect Core Web Vitals from real users and track them per route.
    • Set a bundle performance budget and enforce it in CI.
    • Profile before optimizing and measure again after the fix.
    • Test on mid-range devices and slow networks, not only on your dev machine.

    Watch out for

    • Chasing a Lighthouse score while field data stays poor.
    • Lazy-loading the LCP image as well, which makes the page render later.

    Checkpoint project: Improve a real page

    Pick a page from your own project, record a baseline with web-vitals and the Performance panel, then fix the three biggest problems. It passes when you have a before/after table for LCP, INP, CLS and bundle size, with an explanation of the cause behind each improvement.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4AdvancedLighthouse gives the page 100 but CrUX field data shows poor INP. What is the most likely reason?
    2. Question 2 of 4AdvancedWhy does putting loading="lazy" on the hero image (the LCP element) make LCP worse?
    3. Question 3 of 4AdvancedA click handler filters 20,000 items in one synchronous loop and INP is 600ms. What is the right fix?
    4. Question 4 of 4AdvancedAn ad banner loads late and pushes content down, with CLS at 0.25. What is the right fix?
  2. 09Stage 09, 4 weeksDesign systems and architecture at scaleBuild a design system with tokens and reusable components, and organize a monorepo with clear module boundaries.
    I used Ant Design and Material UI for years, but only understood the real work when running the Bun + Turborepo monorepo for this blog: the hard part is not writing components but keeping package boundaries intact as the code grows.

    What to learn

    Design tokens
    Organize tokens in tiers: primitive (the palette), semantic (color.text.muted) and component. The W3C Design Tokens Community Group format and tools such as Style Dictionary generate CSS variables, light and dark themes, and keep things in sync with Figma.
    Headless components and component APIs
    Use headless primitives (Radix, React Aria, Base UI) for behavior and accessibility and own the styling; the shadcn/ui model copies code into your project so you fully own it. Design APIs through composition (compound components, asChild) instead of piling up dozens of boolean props.
    Documenting and testing components
    Storybook serves as the workshop and living documentation, combined with interaction tests, accessibility checks and visual regression. Every component has stories for its states: loading, error, empty and long content.
    Monorepos with Turborepo
    Split apps and packages (ui, config, utils), use pnpm or Bun workspaces, cache tasks and only rerun what is affected. Share tsconfig and ESLint configuration through dedicated packages so every app stays consistent.
    Module boundaries
    Organize code by feature rather than by file type, with each feature exposing a public API through an entry point. Enforce boundaries with lint rules or dependency checking tools, and consider micro-frontends only when several teams truly need independent deploys.

    Do

    • Use semantic tokens in components, never raw color values.
    • Write stories for every component state before using it in an app.
    • Block deep imports into another package or feature's internals with a lint rule.
    • Version shared packages and keep changelogs with Changesets.

    Watch out for

    • Building a design system too early, abstracting before there are at least three real use cases.
    • Letting the ui package depend back on app code, creating a dependency cycle.

    Checkpoint project: A monorepo with a ui package

    Set up a Turborepo monorepo with two apps and a ui package containing tokens, light and dark themes, five headless-based components and Storybook. It passes when builds are cached, changing one token updates both apps, and lint blocks imports that cross boundaries.

    • Design System
    • Monorepo
    • Architecture

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4AdvancedWhy should components use the color.text.muted token instead of gray-500?
    2. Question 2 of 4AdvancedA Card component has showHeader, showFooter, compact, headerIcon, footerAlign... Why move to compound components like <Card><Card.Header /><Card.Body /></Card>?
    3. Question 3 of 4AdvancedIn a monorepo, packages/ui imports a function from apps/web/lib/format. What is the problem and the right fix?
    4. Question 4 of 4AdvancedA team has one product with exactly one data table and wants to build a shared DataTable package with 30 props. What is the most sensible advice?
  3. 10Stage 10, 4 weeksProduction: observability, security, AIShip a frontend to production with error monitoring, XSS defenses through CSP, and an effective AI workflow that keeps quality under control.
    Frontend is easy to dismiss as just display, but our code runs on user devices and handles login tokens. I use AI coding assistants every day, and the biggest lesson is that AI writes fast but I am still the one responsible for the review.

    What to learn

    Client-side observability
    Capture runtime errors with source maps (Sentry or similar), attach release and user context, and record Web Vitals per route. OpenTelemetry for the browser can link a trace from a user click through the API, though the browser side is still maturing, so evaluate it carefully first.
    XSS and how React protects you
    React escapes content by default, but dangerouslySetInnerHTML, javascript: URLs in href and data embedded in inline scripts are still holes. Sanitize HTML with DOMPurify and never trust data just because it comes from your own API.
    CSP and Trusted Types
    A strict Content Security Policy with nonces and strict-dynamic blocks most injected scripts; Next.js supports generating a nonce per request. Trusted Types lock down dangerous sinks such as innerHTML, so roll them out gradually in report-only mode first.
    Auth and the supply chain
    Keep sessions in HttpOnly, Secure, SameSite cookies rather than localStorage, and guard write requests against CSRF. Pin dependencies with a lockfile, turn on vulnerability alerts and apply framework patches quickly, because modern frontends run server code too.
    Working with AI coding assistants
    Use AI to scaffold, write tests, refactor and explain unfamiliar code, but review every diff as you would a teammate's. Provide context through project instruction files and MCP servers (docs, Figma, the browser), and let tests, typechecking and lint act as the safety net for generated code.
    Building UI for AI features
    Interfaces for LLMs need token streaming, mid-request cancellation, visible tool-call status, and handling of errors or wrong answers. The Vercel AI SDK and similar hooks help manage streams; markdown rendered from a model must be sanitized like any untrusted data.

    Do

    • Upload source maps to your error monitoring tool but do not serve them publicly.
    • Roll out CSP in report-only mode before enforcing it.
    • Review all AI-generated code and require tests to come with it.
    • Follow your framework's security advisories and apply patches within days.

    Watch out for

    • Storing access tokens in localStorage, where any XSS script can read them.
    • Merging AI-generated code you do not understand, especially around auth and data.
    • Watching only server logs without knowing what errors users hit in the browser.

    Checkpoint project: Ship the app to production with monitoring

    Deploy the stage 06 app with source-mapped error tracking, a nonce-based CSP, secure session cookies and a chat feature that streams from an LLM. It passes when errors arrive with original stack traces, the enforced CSP reports no violations, and model markdown output is sanitized.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4AdvancedWhy should access tokens not be stored in localStorage?
    2. Question 2 of 4AdvancedWhy roll out CSP in report-only mode before enforcing it?
    3. Question 3 of 4AdvancedA chat feature converts model markdown to HTML and renders it with dangerouslySetInnerHTML. What is the right approach?
    4. Question 4 of 4AdvancedYou want readable stack traces in Sentry without exposing your source code. What is the right approach?

What matters right now, and where things are heading in the next few years.

Now

  • Server-first React is the default

    With Next.js 16 and React 19, Server Components, Server Functions and streaming are the default way to build new apps. The key skill is placing the server/client boundary well and understanding explicit caching, not just writing hooks.

  • The compiler replaces manual memoization

    React Compiler 1.0 is stable and recommended for new apps, so render optimization is now mostly about writing code that follows the Rules of React. The new lint rules catch problems early, even before the compiler is enabled.

  • The web platform does more of the work

    Container queries, :has(), cascade layers, view transitions, anchor positioning, popover and dialog are now widely available enough to replace many JavaScript libraries. Fewer dependencies means lighter bundles and fewer vulnerabilities.

  • AI is part of the workflow

    AI coding assistants are part of daily work, from scaffolding to writing tests. The difference lies in who can review, set context and hold the quality bar with tests, types and lint.

Next

  • Agents working inside the codebase

    AI agents will take on larger tasks: running tests, opening a browser to check UI and fixing bugs across many files. Prepare with a well-tested codebase, clear scripts and short architecture docs so both agents and new hires can work effectively.

  • Native, faster tooling

    TypeScript is being ported to native code, and Rust-based bundlers and linters such as Turbopack, Rolldown and Oxc keep gaining ground. Master the concepts (module graph, typechecking, transforms) so you can switch tools easily instead of being tied to one tool's config.

  • Frontend takes on server responsibilities

    As Server Functions and edge runtimes spread, frontend engineers need to understand server security, authorization and end-to-end observability. Learning the basics of HTTP, CDN caching, databases and OpenTelemetry will be an advantage.

  • Interfaces for AI products

    More products ship streaming UI, generative UI and flows with agents running in the background. Designing waiting states, cancellation, action confirmation and showing how reliable a result is will become baseline skills.

By the end of this map you can

  • You can build responsive, accessible layouts with modern CSS without depending on a UI library.
  • You can explain how React renders and write components that follow the Rules of React, ready for the React Compiler.
  • You can design data flows that clearly separate server state, client state and URL state.
  • You can build a Next.js 16 app with Server Components, explicit caching and secure Server Functions.
  • You can write a layered test suite with Vitest, Testing Library and Playwright that runs reliably in CI.
  • You can measure and improve LCP, INP and CLS using real user data.
  • You can organize a monorepo with a design system, monitor production errors and apply a CSP against XSS.

Related articles

All articles

Accessible Custom Widgets in React: ARIA Patterns That Actually Work

When native elements run out, ARIA is a contract you have to keep. The accessibility tree and name computation, roving tabindex vs aria-activedescendant, a combobox and tabs built step by step, live regions that really announce, focus on route changes, and how to test it all.

Frontend & UI20 min

Frontend Engineer interview questions

See all 244

Other roles

Roadmap

Designs, builds and runs the APIs, data and services behind every product

10 stages, 52 topics, ~37 weeks

Build iOS and Android apps that feel smooth, work offline and ship safely

10 stages, 50 topics, ~34 weeks

Find risk before users do and turn quality into something the team can measure

10 stages, 50 topics, ~36 weeks

Builds infrastructure, pipelines and platforms so teams ship fast, safely and reliably

10 stages, 56 topics, ~42 weeks

Build products on top of LLMs: APIs, RAG, agents, evals and running it all in production

10 stages, 58 topics, ~31 weeks