Nx Monorepo Series 2: Full-Stack Development with Spring Boot & React
Part 2 of 5: Building Complete Full-Stack Applications in Nx Monorepo
Learn how to build a complete full-stack application with multiple Spring Boot microservices, shared utility libraries, and a React frontend - all managed in a single Nx monorepo.
What Weโll Build
In this tutorial, weโll create a complete full-stack application with:
Backend Architecture
- Service A: User Management Service (Spring Boot)
- Service B: Order Management Service (Spring Boot)
- Utility Library: Shared code between services (Java)
Frontend
- React Application: Dashboard consuming both backend services
- TypeScript: Type-safe API integration
- Component-based UI: UserList and OrderList components
Architecture Overview
demo-monorepository-2/
โโโ service-a/ # User Management API (Port 8081)
โ โโโ src/
โ โ โโโ main/java/com/kreasipositif/servicea/
โ โ โโโ controller/ # REST Controllers
โ โ โโโ model/ # Domain Models
โ โ โโโ service/ # Business Logic
โ โ โโโ config/ # CORS Configuration
โ โโโ pom.xml
โโโ service-b/ # Order Management API (Port 8082)
โ โโโ src/
โ โ โโโ main/java/com/kreasipositif/serviceb/
โ โ โโโ controller/ # REST Controllers
โ โ โโโ model/ # Domain Models
โ โ โโโ service/ # Business Logic
โ โ โโโ config/ # CORS Configuration
โ โโโ pom.xml
โโโ utility-library/ # Shared Java Utilities
โ โโโ src/
โ โ โโโ main/java/com/kreasipositif/utility/
โ โโโ pom.xml
โโโ frontend/ # React Dashboard (Port 3000)
โ โโโ src/
โ โ โโโ components/ # React Components
โ โ โโโ services/ # API Service Layer
โ โ โโโ App.tsx
โ โโโ package.json
โโโ nx.json # Nx Configuration
โโโ pom.xml # Root Maven Configuration
Prerequisites
Before starting, ensure you have:
# Node.js v18 or higher
node --version
# Java 17 or higher
java -version
# Maven 3.6+
mvn --version
# Git
git --version
Completed Series 1: You should have a basic Nx monorepo setup. If not, check out Series 1: Introduction & Spring Boot Integration.
Part 1: Setting Up Maven Multi-Module Structure
Step 1: Create Root pom.xml
Create the root Maven configuration that manages all Java projects:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kreasipositif</groupId>
<artifactId>demo-monorepository</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>Demo Monorepository</name>
<description>Nx Monorepo with Spring Boot and React</description>
<modules>
<module>utility-library</module>
<module>service-a</module>
<module>service-b</module>
</modules>
<properties>
<java.version>21</java.version>
<spring-boot.version>3.2.1</spring-boot.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
Key Features:
<packaging>pom</packaging>: Makes this a parent POM<modules>: Lists all Java subprojects<dependencyManagement>: Centralizes version management- Spring Boot 3.2.1 with Java 21
Part 2: Creating the Utility Library
Step 1: Create Utility Library Structure
mkdir -p utility-library/src/main/java/com/kreasipositif/utility
mkdir -p utility-library/src/test/java/com/kreasipositif/utility
Step 2: Create utility-library/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.kreasipositif</groupId>
<artifactId>demo-monorepository</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>utility-library</artifactId>
<packaging>jar</packaging>
<name>Utility Library</name>
<description>Shared utilities for all services</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Step 3: Create Shared Utility Classes
Create utility-library/src/main/java/com/kreasipositif/utility/StringUtil.java:
package com.kreasipositif.utility;
public class StringUtil {
public static boolean isNullOrEmpty(String str) {
return str == null || str.trim().isEmpty();
}
public static String capitalize(String str) {
if (isNullOrEmpty(str)) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}
public static String generateId(String prefix) {
return prefix + "-" + System.currentTimeMillis();
}
}
Create utility-library/src/main/java/com/kreasipositif/utility/DateUtil.java:
package com.kreasipositif.utility;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateUtil {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String getCurrentDateTime() {
return LocalDateTime.now().format(FORMATTER);
}
public static String formatDateTime(LocalDateTime dateTime) {
return dateTime.format(FORMATTER);
}
}
Step 4: Create Nx Project Configuration
Create utility-library/project.json:
{
"name": "utility-library",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "utility-library/src",
"targets": {
"build": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "mvn clean install -pl utility-library -am",
"cwd": "."
}
},
"test": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "mvn test -pl utility-library",
"cwd": "."
}
}
},
"tags": ["type:library", "platform:jvm"]
}
Step 5: Build the Utility Library
./nx build utility-library
Part 3: Creating Service A (User Management)
Step 1: Generate Spring Boot Project
mkdir -p service-a/src/main/java/com/kreasipositif/servicea
mkdir -p service-a/src/main/resources
mkdir -p service-a/src/test/java/com/kreasipositif/servicea
Step 2: Create service-a/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.kreasipositif</groupId>
<artifactId>demo-monorepository</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>service-a</artifactId>
<packaging>jar</packaging>
<name>Service A - User Management</name>
<description>User Management Service</description>
<dependencies>
<!-- Shared Utility Library -->
<dependency>
<groupId>com.kreasipositif</groupId>
<artifactId>utility-library</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 3: Create Application Configuration
Create service-a/src/main/resources/application.properties:
spring.application.name=service-a
server.port=8081
Step 4: Create Main Application Class
Create service-a/src/main/java/com/kreasipositif/servicea/ServiceAApplication.java:
package com.kreasipositif.servicea;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceAApplication.class, args);
}
}
Step 5: Create User Model
Create service-a/src/main/java/com/kreasipositif/servicea/model/User.java:
package com.kreasipositif.servicea.model;
public class User {
private String id;
private String name;
private String email;
private String phone;
private String createdAt;
public User() {}
public User(String id, String name, String email, String phone, String createdAt) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.createdAt = createdAt;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
}
Step 6: Create User Service
Create service-a/src/main/java/com/kreasipositif/servicea/service/UserService.java:
package com.kreasipositif.servicea.service;
import com.kreasipositif.servicea.dto.CreateUserRequest;
import com.kreasipositif.servicea.model.User;
import com.kreasipositif.utility.DateUtil;
import com.kreasipositif.utility.StringUtil;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
private final List<User> users = new ArrayList<>();
public List<User> getAllUsers() {
return new ArrayList<>(users);
}
public Optional<User> getUserById(String id) {
return users.stream()
.filter(user -> user.getId().equals(id))
.findFirst();
}
public User createUser(CreateUserRequest request) {
String id = StringUtil.generateId("USER");
String createdAt = DateUtil.getCurrentDateTime();
User user = new User(
id,
request.getName(),
request.getEmail(),
request.getPhone(),
createdAt
);
users.add(user);
return user;
}
}
Step 7: Create REST Controller
Create service-a/src/main/java/com/kreasipositif/servicea/controller/UserController.java:
package com.kreasipositif.servicea.controller;
import com.kreasipositif.servicea.dto.CreateUserRequest;
import com.kreasipositif.servicea.model.User;
import com.kreasipositif.servicea.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable String id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public User createUser(@RequestBody CreateUserRequest request) {
return userService.createUser(request);
}
}
Step 8: Create CORS Configuration
Create service-a/src/main/java/com/kreasipositif/servicea/config/CorsConfig.java:
package com.kreasipositif.servicea.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
Step 9: Create Nx Project Configuration
Create service-a/project.json:
{
"name": "service-a",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "service-a/src",
"targets": {
"build": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "mvn clean package -pl service-a -am",
"cwd": "."
}
},
"serve": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "mvn spring-boot:run -pl service-a",
"cwd": "."
}
},
"test": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "mvn test -pl service-a",
"cwd": "."
}
}
},
"tags": ["type:app", "platform:jvm", "api:user"]
}
Step 10: Test Service A
# Build the service
./nx build service-a
# Run the service
./nx serve service-a
# In another terminal, test the API
curl http://localhost:8081/api/users
# Output: []
Part 4: Creating Service B (Order Management)
Follow similar steps to create Service B with order management functionality.
Step 1: Create service-b/pom.xml
Similar to Service A, but with service-b as artifactId and port 8082.
Step 2: Create Order Model and Service
Create order management similar to user management in Service A.
Step 3: Create Nx Configuration
Create service-b/project.json with port 8082 configuration.
Part 5: Creating React Frontend
Step 1: Create React Application
# Create frontend directory
mkdir -p frontend
# Initialize React app with TypeScript
cd frontend
npx create-react-app . --template typescript
cd ..
Step 2: Create Environment Configuration
Create frontend/.env:
REACT_APP_SERVICE_A_URL=http://localhost:8081
REACT_APP_SERVICE_B_URL=http://localhost:8082
Step 3: Create API Service Layer
Create frontend/src/services/userService.ts:
// Service A API - User Management
const SERVICE_A_BASE_URL =
process.env.REACT_APP_SERVICE_A_URL || "http://localhost:8081";
export interface User {
id: string;
name: string;
email: string;
phone: string;
createdAt: string;
}
export interface CreateUserRequest {
name: string;
email: string;
phone: string;
}
export const userService = {
getAllUsers: async (): Promise<User[]> => {
const response = await fetch(`${SERVICE_A_BASE_URL}/api/users`);
if (!response.ok) {
throw new Error("Failed to fetch users");
}
return response.json();
},
getUserById: async (id: string): Promise<User> => {
const response = await fetch(`${SERVICE_A_BASE_URL}/api/users/${id}`);
if (!response.ok) {
throw new Error("Failed to fetch user");
}
return response.json();
},
createUser: async (userData: CreateUserRequest): Promise<User> => {
const response = await fetch(`${SERVICE_A_BASE_URL}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error("Failed to create user");
}
return response.json();
},
};
Create frontend/src/services/orderService.ts similarly for Service B.
Step 4: Create React Components
Create frontend/src/components/UserList.tsx:
import React, { useEffect, useState } from "react";
import { userService, User } from "../services/userService";
import "./UserList.css";
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
fetchUsers();
}, []);
useEffect(() => {
// Auto-retry if there's an error (backend still starting up)
if (error && retryCount < 10) {
const timer = setTimeout(() => {
setRetryCount(retryCount + 1);
fetchUsers();
}, 3000); // Retry every 3 seconds
return () => clearTimeout(timer);
}
}, [error, retryCount]);
const fetchUsers = async () => {
try {
setLoading(true);
const data = await userService.getAllUsers();
setUsers(data);
setError(null);
setRetryCount(0);
} catch (err) {
setError("Service A is starting up... Retrying automatically.");
} finally {
setLoading(false);
}
};
if (loading) return <div className="loading">Loading users...</div>;
return (
<div className="user-list">
<h2>๐ฅ User Management (Service A)</h2>
{error && <div className="error">{error}</div>}
<div className="list">
{users.length === 0 ? (
<p className="empty">No users found. Create your first user!</p>
) : (
users.map((user) => (
<div key={user.id} className="card">
<h3>{user.name}</h3>
<p>
<strong>Email:</strong> {user.email}
</p>
<p>
<strong>Phone:</strong> {user.phone}
</p>
<p className="date">
<strong>Created:</strong> {user.createdAt}
</p>
</div>
))
)}
</div>
</div>
);
};
export default UserList;
Create frontend/src/components/OrderList.tsx similarly for orders.
Step 5: Create Type Declaration for CSS
Create frontend/src/global.d.ts:
declare module "*.css";
Step 6: Create Main App Component
Update frontend/src/App.tsx:
import React from "react";
import UserList from "./components/UserList";
import OrderList from "./components/OrderList";
import "./App.css";
function App() {
return (
<div className="App">
<header className="App-header">
<h1>๐ Nx Monorepo Full-Stack Demo</h1>
<p>Spring Boot + React Integration</p>
</header>
<main className="App-main">
<UserList />
<OrderList />
</main>
</div>
);
}
export default App;
Step 7: Create Nx Project Configuration
Create frontend/project.json:
{
"name": "frontend",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "frontend/src",
"targets": {
"serve": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "npm start",
"cwd": "frontend"
}
},
"build": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "npm run build",
"cwd": "frontend"
}
},
"test": {
"executor": "@nx/workspace:run-commands",
"options": {
"command": "npm test",
"cwd": "frontend"
}
}
},
"tags": ["type:app", "platform:web"]
}
Part 6: Running the Full Stack
Option 1: Run All Services Together
./nx run-many -t serve -p service-a,service-b,frontend
Note: With the auto-retry logic in the React components, the frontend will automatically connect once the backend services are ready (typically 10-30 seconds).
Option 2: Sequential Startup Script
Create start-all.sh:
#!/bin/bash
echo "๐ Starting all services in order..."
# Start backend services first
echo "๐ฆ Starting Service A (port 8081)..."
./nx serve service-a &
SERVICE_A_PID=$!
echo "๐ฆ Starting Service B (port 8082)..."
./nx serve service-b &
SERVICE_B_PID=$!
# Wait for backends to be ready
echo "โณ Waiting 30 seconds for backend services to start..."
sleep 30
# Start frontend
echo "๐จ Starting Frontend (port 3000)..."
./nx serve frontend &
FRONTEND_PID=$!
echo ""
echo "โ
All services started!"
echo ""
echo "Service A PID: $SERVICE_A_PID (http://localhost:8081)"
echo "Service B PID: $SERVICE_B_PID (http://localhost:8082)"
echo "Frontend PID: $FRONTEND_PID (http://localhost:3000)"
echo ""
echo "Press Ctrl+C to stop all services"
# Wait for Ctrl+C
trap "echo ''; echo '๐ Stopping all services...'; kill $SERVICE_A_PID $SERVICE_B_PID $FRONTEND_PID 2>/dev/null; exit" INT
# Keep script running
wait
Make it executable:
chmod +x start-all.sh
./start-all.sh
Option 3: Restart Backend Only
Create restart-backends.sh:
#!/bin/bash
echo "๐ Restarting backend services..."
# Kill existing Java processes
lsof -ti:8081 | xargs kill -9 2>/dev/null
lsof -ti:8082 | xargs kill -9 2>/dev/null
sleep 2
# Start backend services
./nx serve service-a &
./nx serve service-b &
echo "โ
Backend services restarted!"
Part 7: Testing the Full Stack
Step 1: Access the Frontend
Open your browser and navigate to:
http://localhost:3000
You should see:
- User Management section showing Service A data
- Order Management section showing Service B data
Step 2: Test API Endpoints
# Test Service A (Users)
curl http://localhost:8081/api/users
# Test Service B (Orders)
curl http://localhost:8082/api/orders
# Create a new user
curl -X POST http://localhost:8081/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com","phone":"1234567890"}'
# Create a new order
curl -X POST http://localhost:8082/api/orders \
-H "Content-Type: application/json" \
-d '{"customerId":"USER-123","productName":"Widget","quantity":5,"unitPrice":19.99}'
Step 3: Verify Nx Dependency Graph
./nx graph
You should see all four projects and their dependencies:
service-adepends onutility-libraryservice-bdepends onutility-libraryfrontend(no direct dependency, but consumes both services via HTTP)
Part 8: Key Architecture Patterns
1. Code Sharing via Utility Library
Both Service A and Service B use shared utilities:
// In Service A or Service B
import com.kreasipositif.utility.StringUtil;
import com.kreasipositif.utility.DateUtil;
String id = StringUtil.generateId("USER");
String timestamp = DateUtil.getCurrentDateTime();
2. CORS Configuration
Both backend services allow requests from the React frontend:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
3. Type-Safe API Integration
React services with TypeScript interfaces:
export interface User {
id: string;
name: string;
email: string;
phone: string;
createdAt: string;
}
4. Auto-Retry Pattern
Frontend handles race conditions when services start:
useEffect(() => {
if (error && retryCount < 10) {
const timer = setTimeout(() => {
setRetryCount(retryCount + 1);
fetchUsers();
}, 3000);
return () => clearTimeout(timer);
}
}, [error, retryCount]);
Part 9: Nx Commands for Full Stack
Running Multiple Projects
# Run all services
./nx run-many -t serve -p service-a,service-b,frontend
# Build all projects
./nx run-many -t build -p utility-library,service-a,service-b,frontend
# Test all projects
./nx run-many -t test -p utility-library,service-a,service-b,frontend
Affected Commands
# See affected projects
./nx show projects --affected
# Build only affected
./nx affected -t build
# Test only affected
./nx affected -t test
Dependency Graph
# View full dependency graph
./nx graph
# View affected graph
./nx graph --affected
Part 10: Common Issues and Solutions
Issue 1: CORS Errors
Problem: Frontend canโt access backend APIs
Solution:
- Ensure CORS configuration exists in both services
- Restart backend services after adding CORS config
- Use
./restart-backends.sh
Issue 2: Port Already in Use
Problem: Service fails to start
Solution:
# Check what's running on port
lsof -i :8081
# Kill the process
kill -9 <PID>
Issue 3: Race Condition
Problem: Frontend shows โFailed to loadโ even though services are running
Solution: The auto-retry logic should handle this. If not, wait 30 seconds after starting backends before starting frontend.
Part 11: Best Practices
1. Project Organization
โ
Good:
- service-a/ (focused, single responsibility)
- service-b/ (focused, single responsibility)
- utility-library/ (shared code)
- frontend/ (presentation layer)
โ Avoid:
- everything-service/ (too broad)
- utils-1, utils-2/ (fragmented utilities)
2. Dependency Management
<!-- Root pom.xml: Centralize versions -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. API Design
// โ
RESTful endpoints
GET /api/users // List all
GET /api/users/{id} // Get one
POST /api/users // Create
PUT /api/users/{id} // Update
DELETE /api/users/{id} // Delete
// โ Avoid
GET /api/getAllUsers
POST /api/createNewUser
4. Error Handling
// โ
Graceful degradation with retry
const fetchUsers = async () => {
try {
const data = await userService.getAllUsers();
setUsers(data);
setError(null);
setRetryCount(0);
} catch (err) {
setError("Service starting... Retrying automatically.");
}
};
5. Environment Configuration
# โ
Use environment variables
REACT_APP_SERVICE_A_URL=http://localhost:8081
REACT_APP_SERVICE_B_URL=http://localhost:8082
# โ Avoid hardcoding
const API_URL = 'http://localhost:8081';
๐ Congratulations!
Youโve successfully built a complete full-stack application in an Nx monorepo!
What Youโve Accomplished
- โ Created Maven multi-module structure
- โ Built shared utility library
- โ Developed two Spring Boot microservices
- โ Created React frontend with TypeScript
- โ Implemented API integration with CORS
- โ Set up auto-retry pattern for resilience
- โ Managed all projects with Nx
- โ Built real-world full-stack architecture
Architecture Highlights
- Backend: 2 Spring Boot services + 1 shared library
- Frontend: React with TypeScript
- Communication: REST APIs with CORS
- Build System: Maven + npm managed by Nx
- Ports: 8081 (Service A), 8082 (Service B), 3000 (Frontend)
๐ Coming in Series 3: Advanced Code Sharing
In the next part, weโll explore:
Whatโs Next
- ๐ Advanced Shared Libraries
- ๐ Code Generation & Templates
- ๐งช Cross-Project Testing
- ๐ Monorepo Analytics
- ๐ง Custom Nx Executors
Stay tuned for Series 3! ๐โจ
Additional Resources
Documentation
Example Repository
Community
About This Series
This is Part 2 of the Nx Monorepo Series:
- โ Series 1: Introduction to Nx & Spring Boot Integration
- โ Series 2: Full-Stack Development with Spring Boot & React (You are here)
- ๐ Series 3: Advanced Code Sharing & Libraries
- ๐ Series 4: CI/CD & Deployment Strategies
- ๐ Series 5: Multi-Technology Monorepo at Scale
Repository: github.com/kreasipositif/demo-monorepository-2
Master modern full-stack development with industry-proven monorepo architecture. Join our comprehensive courses at Kreasi Positif Indonesia and learn from experienced software engineers.