Devlog Week 15: Frontend Final Wrap-Up (From Kickoff to Deployed App)#
When I wrote the first frontend entry (Week 9), the React app was basically a shell: Vite setup, a layout route, a drawer menu, and a couple of placeholder pages driven by mock data.
This is the “where we ended up” post.
The Maintenance Log frontend is now a mobile-first React application with:
real API integration against the Java/Javalin backend
nestable protected routes with role-based access
manager/admin create flows and admin actions
centralized error handling and a clean session-expiry UX
It’s deployed at https://mlf.heltsort.dk/ and talks to https://maintenancelog.heltsort.dk/.
What’s New Since the Early Frontend Posts#
Compared to Week 9/10 (kickoff + plumbing), the biggest change is that the app stopped being “a UI prototype” and became an actual client for my backend system.
1) Auth became a first-class feature#
In Week 9, login was basically a form that logged credentials. Now the frontend has a proper auth layer:
AuthContextowns user state (authUser) and readiness (authReady)- an initial auth check calls
getMe()on load when a token exists - a
hasRole(requiredRole)helper enforces the same role hierarchy as the backend
This alone is what made the rest of the frontend possible to build in a sane way.
2) Routing evolved from “nested layout” into “nested permissions”#
The early setup already had nested layout routes and <Outlet />. The change since then is that the same nesting idea is now used for access control.
ProtectedRoute is reusable and nestable:
- no role requirement → must be authenticated
- with role requirement → must be authenticated and satisfy role hierarchy
- waits for
authReady(prevents redirects during the initial auth check)
That ended up being a clean way to express rules like “everyone can view assets, but only TECHNICIAN+ can create logs, and only MANAGER+ can create employees/assets”.
3) The API layer went from “fetch sprinkled everywhere” to a consistent client#
The frontend now has a small API client stack that I’m honestly pretty happy with overall:
apiClient.jshandles auth headers, token reads/writes, and shared request behavior- domain modules (
assetApi.js,employeeApi.js,logApi.js) stay thin and predictable - client-side argument assertions (
assertPositiveIntegerId,assertRequiredParam) catch bad call sites early
This made the page components simpler: they call small functions like getAssets(active) or createLog(assetId, payload) instead of duplicating fetch boilerplate.
An unexpected benefit was that it pushed me toward clearer contracts between the frontend and backend. Pages generally don’t care how a request is made or how a token is attached; they care about receiving data or handling a failure. Having a consistent API layer made it much easier to centralize error handling later in the project.
4) Placeholder pages became real workflows#
The biggest “feature” from early posts is simply that most of the app is now implemented.
Styling also matured from global CSS + a basic token setup into a more component-scoped approach using CSS Modules for forms/buttons. It does however still suffer from alot of hardcoded variables, but i intentionally didnt use alot of time on design, because i wanted to focus on the React part of the app.
The application was built mobile-first from the beginning. The visual design is fairly simple, but working within smaller screen constraints early helped keep navigation and workflows straightforward throughout development.
Assets
- list + detail
- active/inactive filtering
- log list + filtering (status/taskType)
- create log (TECHNICIAN+)
- admin actions on assets (ADMIN)
Employees
- list with filtering (active server-side, role client-side)
- profiles with edit flows
- view performed logs (MANAGER+)
- create employee (MANAGER+)
- password change (for the logged-in user)
The pattern that repeats across these pages is consistent on purpose: load state + error state, predictable fetch lifecycle, and UI that reflects permissions.
The Finished Route Map#
The final routing structure looks roughly like this:
/login(public)/(protected)- layout:
Header+<Outlet /> /assets/assets/:id/logs/employees/employees/:id/employees/me- TECHNICIAN+ (nested protected subset)
/assets/:id/createlog
- MANAGER+ (nested protected subset)
/assets/create/employees/create/employees/:id/logs
- layout:
Centralized Error Handling + Session Expiry (The Biggest UX Win)#
This was added last, but it ended up being the thing that made the frontend feel “done”.
In early iterations, errors were handled wherever they happened. That works until you start dealing with:
- token expiry
- inconsistent backend error payloads
- network failures
- role errors that should be phrased consistently
The final setup:
apiRequest()maps status codes to user-facing messages (403/404/409/400/503/500)- token expiry clears auth state and forces a full reload to
/login sessionStorageis used as a tiny message for the “session expired” flash message- the message key + default copy are kept in a small
sessionMessages.jsmodule (so it isn’t duplicated across pages) - every thrown error gets an
error.causeobject with structured debug info (useful without leaking raw backend exceptions into the UI)
The “full reload” choice (window.location.assign) was intentional: it clears React state, avoids edge cases where you half-redirect with stale context, and works even when the redirect happens outside component scope.
A Few Architectural Choices I’m Happy With#
A lot of this project was me trying to resist the urge to over-abstract.
Intentional duplication by domain
AdminActionsandAssetAdminActionslook similar, but they’re separated because they’re different domains that can diverge
State placement discipline
- auth state lives in context
- drawer state lives in the header/layout
- form state lives in form components (or uses
FormDatawhen that’s enough)
Promise.allfor independent data loads in employee + performed logsSeparating asset and log fetch lifecycles
- an earlier version refetched both asset details and logs whenever a log filter changed
- technically it worked, but it also caused the page to briefly fall back to a loading state because asset data was being reloaded unnecessarily
- splitting the effects so asset data depends only on the route id, while logs depend on the active filters, reduced API calls and made filtering feel much smoother
Client-side vs server-side filtering
- server-side where the backend supports it (assets active filter, log status/taskType)
- client-side where it doesn’t (employee role filter, asset-name filter in employee logs)
None of these are revolutionary, but together they made the codebase stay understandable as it grew.
Some Things I’d Probably Change#
If I started over, I’d likely separate data-fetching concerns earlier.
A few pages initially fetched multiple independent resources inside the same effect because it was quick and easy to reason about at the time. As more filtering and interactions were added, those dependencies became less obvious and occasionally resulted in unnecessary requests or loading states.
None of these issues were difficult to fix, but they reinforced a lesson I ran into several times during the semester: data that changes for different reasons should usually be fetched independently.
Another smaller design decision I would probably revisit is how reference data is shared between the backend and frontend.
While roles, task types, and statuses are currently represented as shared constants in the frontend, they ultimately originate from backend enums. For a project of this size that tradeoff was perfectly reasonable, but it highlighted an important lesson: API design is not only about exposing business data, but also about exposing the metadata the frontend needs to build its UI.
If the project were extended further, I would likely expose those values through a small metadata endpoint so the frontend could derive its options directly from the backend rather than maintaining separate definitions.
Deployment Notes (Frontend + Backend Integration)#
The frontend is static and the backend is an authenticated API, which means the real headaches are always “integration edges”:
- environment detection (localhost vs deployed)
- consistent error handling across environments
- making sure session expiry doesn’t produce confusing half-broken screens
Final Thoughts#
This frontend ended up being a pretty good “capstone” for the semester because it forced me to connect a lot of concepts that are easy to learn in isolation:
- routing and layouts
- auth + role-based authorization
- API design assumptions and error contracts
- state placement, reuse, and avoiding accidental coupling
It’s not a huge application, but it feels complete as an MVP.
What I’m happiest about is that the frontend and backend reached a point where they form a complete vertical slice. Assets, employees, logs, authentication, authorization, administration workflows, and deployment all exist end-to-end rather than as isolated demonstrations.
There are plenty of things I would improve with more time, but the system solves the problem it set out to solve, and that was ultimately the goal of the project.