feature: migrate Angular Conduit app to React + TypeScript + Vite#32
feature: migrate Angular Conduit app to React + TypeScript + Vite#32devin-ai-integration[bot] wants to merge 3 commits into
Conversation
Complete rewrite of the Angular RealWorld (Conduit) app to React 18 + Vite, preserving all routes, JWT auth, feature modules (articles, profile, settings, auth, feeds), and the framework-agnostic e2e contract. - Angular services/RxJS -> React Context + fetch/async-await (AbortController) - Angular guards/interceptors -> React Router guards + fetch api client - Angular pipes -> utility functions; lazy NgModules -> React.lazy + Suspense - Vitest + RTL unit tests; preserved Playwright e2e + CI workflows - Removed Angular source after React app verified building/running Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| if (!response.ok) { | ||
| let body: unknown = null; | ||
| try { | ||
| body = await response.json(); | ||
| } catch { | ||
| body = null; | ||
| } | ||
|
|
||
| const normalized = | ||
| body && typeof body === 'object' && 'errors' in body | ||
| ? (body as Errors) | ||
| : { errors: { network: ['Unable to connect. Please check your internet connection.'] } }; | ||
|
|
||
| throw { ...normalized, status: response.status } as ApiError; |
There was a problem hiding this comment.
🔴 Missing global 401 handling for non-/user endpoints causes broken auth state on token expiry
The Angular app's errorInterceptor (src/app/core/interceptors/error.interceptor.ts:40) called purgeAuth() on any 401 response from non-/user endpoints, handling the "token expired mid-session" scenario. The React api.ts has no equivalent — it simply throws the error to the caller. This means if a user's JWT expires during a session and they perform any action (favorite, comment, follow, etc.), they receive a 401 error but remain in the authenticated state with a stale token in localStorage. The UI continues showing them as logged in (header shows username/avatar, protected routes remain accessible) but every subsequent API call fails. Users must manually log out or refresh the page to recover.
Prompt for agents
The Angular error interceptor (src/app/core/interceptors/error.interceptor.ts) had global 401 handling that called purgeAuth() for all endpoints except GET /user. The React migration lost this behavior.
The challenge is that api.ts is a plain module with no access to the AuthContext. There are several approaches to restore this:
1. Add a callback/listener pattern: api.ts could accept a registered callback (e.g., onUnauthorized) that AuthContext sets up on mount. When api.ts sees a 401 on a non-/user endpoint, it calls this callback, which triggers purgeAuth().
2. Emit a custom event: api.ts could dispatch a custom DOM event like 'conduit:unauthorized'. AuthContext could listen for this event and call purgeAuth().
3. Use a shared module-level variable: Export a mutable reference from a shared module that AuthContext populates with its purgeAuth function.
The key constraint is that the /user endpoint must be excluded from this handling (it has its own 4XX vs 5XX logic in AuthContext.fetchCurrentUser). So the 401 handler in api.ts should skip paths ending in '/user'.
Relevant files: src/services/api.ts (add 401 detection), src/context/AuthContext.tsx (register the purgeAuth callback).
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Investigated this — it's a real behavioral difference from Angular, but restoring a global purgeAuth() on non-/user 401s breaks this repo's e2e contract, so I'm intentionally not applying it.
The repo's e2e/error-handling.spec.ts explicitly pins the intended mid-session 401 behavior to surface the error and keep the user on the page, not log out/redirect:
:99401 on settings submit → expects.error-messagesvisible and the form still usable (no redirect).:131401 on posting a comment → expects the article page (nav.navbar,.article-content) to stay rendered (no redirect).:63401 on article creation (the/editorroute is guarded) → expects.error-messagesvisible andinput[name="title"]still visible.
I verified empirically: I added the exact suggested callback (api.ts calls a registered setUnauthorizedHandler on any non-/user 401, AuthContext registers purgeAuth), then ran the suite. error-handling.spec.ts:63 fails because purgeAuth() flips the guard and <RequireAuth> redirects /editor → /login before the error can render:
1) e2e/error-handling.spec.ts:63 › should handle 400 on article creation
Error: expect(locator('.error-messages')).toBeVisible() failed
waiting for locator('.error-messages') — element(s) not found
1 failed, 30 passed
(Reverted that experiment; the branch is back to the green state.)
The "invalid token" case that genuinely warrants logout is preserved — it's just handled at the auth boundary rather than globally: AuthContext.fetchCurrentUser calls purgeAuth() on a GET /user 4XX (and enters unavailable + backoff retry on 5XX/network), which is what e2e/user-fetch-errors.spec.ts asserts. So an expired token is detected and cleared on the next app load / auth refresh; mid-session action 401s render an inline error instead of yanking the user to /login, matching the contract above.
| if (!response.ok) { | ||
| let body: unknown = null; | ||
| try { | ||
| body = await response.json(); | ||
| } catch { | ||
| body = null; | ||
| } | ||
|
|
||
| const normalized = | ||
| body && typeof body === 'object' && 'errors' in body | ||
| ? (body as Errors) | ||
| : { errors: { network: ['Unable to connect. Please check your internet connection.'] } }; | ||
|
|
||
| throw { ...normalized, status: response.status } as ApiError; |
There was a problem hiding this comment.
🔴 Missing global 401 handler causes broken state when token expires mid-session
The Angular errorInterceptor (src/app/core/interceptors/error.interceptor.ts:40) globally handled 401 responses on all endpoints except /user by calling purgeAuth() — this was the safety net for "token expired mid-session" scenarios. The new React api.ts request function throws the error but never triggers a logout on 401. When a user's JWT expires while they're browsing, API calls (favorite, follow, comment, delete, etc.) silently fail with 401, the user stays visually "authenticated" in the UI, and they're stuck in a broken state where all actions fail. They must manually log out or hard-refresh to recover.
Angular error interceptor that was removed
The original interceptor at src/app/core/interceptors/error.interceptor.ts:40:
if (err.status === 401 && !req.url.endsWith('/user')) {
userService.purgeAuth();
}
The new api.ts request() function at line 66-79 throws errors but has no equivalent 401→logout logic.
Prompt for agents
The api.ts request() function is missing the global 401 handler that the Angular errorInterceptor provided. In the Angular version, any 401 response on endpoints OTHER than /user would trigger purgeAuth() to log the user out (handling token-expired-mid-session scenarios).
The fix needs to replicate this in the React architecture. The challenge is that api.ts is a plain module without access to React context (AuthContext). Possible approaches:
1. Add a callback registration mechanism: api.ts exports a function like registerUnauthorizedHandler(callback). AuthContext calls this on mount with its purgeAuth function. The request() function calls this callback when it gets a 401 on a non-/user endpoint.
2. Use a module-level event emitter or pub/sub pattern: api.ts publishes a 401-unauthorized event. AuthContext subscribes and calls purgeAuth.
3. Import jwt.destroyToken directly in api.ts and call it on 401 (simpler but doesn't update React state).
Option 1 is closest to the original architecture. The key logic is: if response.status === 401 AND the path does not end with /user, call the registered unauthorized handler (which should be purgeAuth from AuthContext).
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Full rewrite of the Angular RealWorld (Conduit) app to React 18 + TypeScript + Vite, following the 4-phase migration playbook. All routes, JWT auth, feature modules (articles, profile, settings, auth, feeds), styling, and the framework-agnostic Playwright e2e contract are preserved. The Angular source was removed only after the React app built and every page was verified running.
The external API (
https://api.realworld.show/api) and all data contracts are unchanged. No new UI libraries were introduced (onlyreact,react-router-dom, plusmarked+dompurifyfor the markdown rendering the Angular app already did).Angular → React pattern mapping
HttpClient+ RxJS Observablesfetch+async/await,AbortControllerfor cancellationAuthContext) + hookscanActivate<RequireAuth>/<GuestRoute>wrappersapiClientinsrc/services/api.tssrc/utils/ngOnInit/ngOnDestroyuseEffect+ cleanupNgModulesReact.lazy+<Suspense>Notable correctness details (non-obvious from the diff)
src/services/api.ts: a non-/user401 is surfaced to the calling component (so the editor/comment forms can render the error) rather than force-redirecting. Invalid-token logout still happens at app init viaGET /user(4XX → purge, 5XX/network → "unavailable" + exponential-backoff retry), matching the AngularerrorInterceptor4XX-vs-5XX logic.src/services/articles.ts:create()POSTs to/articles/(trailing slash) to match the Angular reference and the e2e contract's request-abort expectation.src/pages/Article.tsx: the article fetch effect depends only onslug;canModifyis computed (currentUser?.username === article.author.username) instead of stored, avoiding an abort/refetch churn whenAuthContextrefreshes the user object.src/pages/Settings.tsx: password is only included in the update payload when non-empty (...(password ? { password } : {})) — empty-string passwords made the API return 422.window.__conduit_debug__is exposed fromAuthContextso the shared e2e helpers can read auth state regardless of framework.Migration phases
AuthContext, utils, and SCSS-derived styles./,/login,/register,/settings,/editor,/editor/:slug,/article/:slug,/profile/:username,/profile/:username/favorites).dist/), removedsrc/app/,angular.json, and Angular configs, and updated the README.Verification
bun run build(tsc -b && vite build) — passes, zero TypeScript errors.bun run format:check(Prettier) — passes.bun run test(Vitest + RTL) — 12/12 pass.bun run test:e2e(framework-agnostic Playwright suite, 122 tests) — passes against the shared live demo API; a few create/comment/session tests can be intermittently slow under rapid sequential load on the shared backend, which the suite'sretries: 2in CI absorbs.localhost:4200) and visually verified home feed, tags, navigation, login, and article/profile/settings pages render with live API data.Running app
Home (Global Feed + Popular Tags):
Sign in page:
Link to Devin session: https://app.devin.ai/sessions/b693a89126e544bba16931605bfb5414
Requested by: @lburgers
Devin Review
d09b48a