--- Nx Monorepo Series 2: Full-Stack Development with Spring Boot & React
Home โ†’ Blog โ†’ Nx Monorepo Series 2: Full-Stack Development with Spring Boot & React

Nx Monorepo Series 2: Full-Stack Development with Spring Boot & React

Build a complete full-stack application with Spring Boot microservices, shared libraries, and React frontend in an Nx monorepo - learn the architecture used by modern tech companies.

Nx Monorepo Series 2: Full-Stack Development with Spring Boot & React

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-a depends on utility-library
  • service-b depends on utility-library
  • frontend (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:

  1. Ensure CORS configuration exists in both services
  2. Restart backend services after adding CORS config
  3. 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.