Full-Stack Development: Bridging Frontend and Backend in a Real Project
Building MediaDroppy as a full-stack application highlighted the challenges of coordinating between a React frontend and multiple Spring Boot microservices. While the separation provided architectural benefits, it also introduced integration complexities that required careful planning and execution. This article explores the practical challenges of full-stack development in a distributed system.
The Authentication Dance
Implementing secure authentication across frontend and backend proved more complex than anticipated. The flow involves multiple steps: user login triggers OAuth2 authentication, the backend issues JWT tokens, the frontend stores them securely, and every API request includes the token for verification.
Challenge: Token management. Deciding where to store tokens (localStorage vs. memory vs. httpOnly cookies) involved security tradeoffs. localStorage is vulnerable to XSS attacks, memory-only storage loses tokens on page refresh, and httpOnly cookies complicate CORS.
We settled on a hybrid approach: short-lived access tokens in memory with refresh tokens in httpOnly cookies. This required implementing token refresh logic that gracefully handled expired tokens and retry failed requests.
Challenge: Authentication state synchronization. The React application needed to maintain authentication state across components. Initially using prop drilling became unmaintainable, leading to the implementation of an AuthProvider context. This centralized auth state management but required careful handling of state updates to prevent unnecessary re-renders.
File Uploads: Bridging Binary Data
Implementing file uploads exposed the complexity of handling binary data across the stack. The frontend needed to support drag-and-drop, show upload progress, handle large files efficiently, and provide clear error feedback.
Key implementation decisions:
- FormData API: Using FormData for multipart uploads simplified sending files from React to the Spring Boot backend. However, implementing upload progress required careful use of XMLHttpRequest or axios interceptors, as fetch() doesn't support progress events.
- Chunked uploads: For large files, implementing chunked uploads prevented timeout issues and allowed resumable uploads. This required coordination between frontend chunk logic and backend reassembly.
- Client-side validation: Validating file types and sizes on the frontend improved UX by providing immediate feedback, but backend validation was still necessary for security.
API Design and Versioning
As the application evolved, maintaining API stability while adding features became critical. Breaking changes in backend APIs would break the frontend, requiring coordinated releases.
Lessons learned:
- Version APIs from the start: Adding /v1/ to API paths early provides flexibility for future changes. MediaDroppy now supports versioned endpoints, making backward-compatible changes manageable.
- Contract-first development: Defining API contracts (request/response structures) before implementation helped coordinate frontend and backend work. OpenAPI/Swagger documentation proved invaluable for this.
- Graceful degradation: When possible, design the frontend to handle missing backend features gracefully rather than failing completely.
Error Handling Across Layers
Proper error handling in a full-stack application requires coordination across multiple layers. Backend exceptions must be translated to meaningful HTTP status codes, which the frontend then interprets and presents to users.
We implemented a consistent error response format:
{
"error": "RESOURCE_NOT_FOUND",
"message": "The requested file does not exist",
"timestamp": "2025-11-20T10:30:00Z"
}
The frontend error handling middleware intercepts failed requests and translates error codes to user-friendly messages. This centralized approach prevented duplicate error handling logic across components.
The challenge: Network errors, backend errors, and application errors all require different handling strategies. Distinguishing between these error types and responding appropriately took several iterations to get right.
State Management and Data Flow
Managing application state across a React frontend interfacing with multiple backend services revealed the importance of thoughtful data flow architecture.
Initial problems:
- Components fetching data independently led to redundant API calls
- Stale data appeared when multiple components displayed the same information
- Loading states weren't consistently handled across the application
Solutions implemented:
- Centralized API methods in a ServiceMethods module prevented duplicate fetch logic
- Implementing a basic caching layer reduced unnecessary API calls
- Standardizing loading states with reusable components improved UX consistency
Development Environment Coordination
Running the full stack locally for development required orchestrating multiple services. Initially, this meant starting six separate processes manually, which was error-prone and time-consuming.
We addressed this through:
- Docker Compose for local development: A docker-compose.yml file brings up all backend services with a single command, ensuring consistent local environments.
- Environment-specific configuration: Using environment variables allows the React app to point to either local backend services or production APIs.
- API mocking for frontend development: Creating mock API responses allows frontend development to proceed independently when backend services are unavailable or under development.
CORS: The Inevitable Headache
Cross-Origin Resource Sharing (CORS) caused more debugging hours than I care to admit. With the React app served from one domain and APIs from another, every request triggered preflight OPTIONS requests.
Key lessons:
- Configure CORS properly on the backend from the start, not as an afterthought
- Understand the difference between simple requests and preflighted requests
- Be aware that credentials (cookies, authorization headers) require explicit CORS configuration
- Development proxying (via Create React App's proxy feature) can simplify local development but masks CORS issues until production
Key Takeaways
- API contracts are critical: Define clear API contracts early and maintain documentation. This enables parallel development of frontend and backend.
- Security requires end-to-end thinking: Authentication and authorization must be carefully implemented across all layers, not just added to one side.
- Error handling deserves first-class treatment: Implement consistent error handling strategies that span the entire stack.
- Developer experience matters: Invest in tooling that makes running the full stack locally straightforward. Docker Compose, mock APIs, and proper environment configuration pay dividends.
- Performance requires collaboration: Optimizing full-stack applications requires understanding both frontend (bundle size, lazy loading) and backend (query optimization, caching) concerns.
- Separate but coordinated: While separating frontend and backend provides flexibility, they're not truly independent. Changes often ripple across both layers, requiring coordination.
Full-stack development in a microservices architecture amplifies both the benefits and challenges of separating concerns. The key to success lies in establishing clear contracts, implementing consistent patterns across layers, and investing in tooling that makes the development experience manageable. MediaDroppy taught me that being a full-stack developer means more than knowing both frontend and backend technologies—it means understanding how they integrate and interact as a cohesive system.