☕ Java Web Development Complete Setup Guide

Frontend, Backend & Database Integration
Published: January 31, 2025 | Reading Time: 45 minutes | ★★★★★ Comprehensive Guide

1. Overview & Architecture

🎯 What You'll Build: A complete full-stack Java web application with modern architecture, including RESTful APIs, database integration, and responsive frontend.

Modern Java Web Development Stack

🔧 Backend

Spring Boot 3.x
Spring Security
Spring Data JPA
Maven/Gradle

🎨 Frontend

React 18 / Angular 17
TypeScript
Bootstrap / Material-UI
Axios / HTTP Client

🗄️ Database

PostgreSQL / MySQL
H2 (Development)
Redis (Caching)
JPA/Hibernate

🚀 DevOps

Docker
Docker Compose
GitHub Actions
AWS/Azure

Application Architecture

🏗️ Three-Tier Architecture

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Frontend │ │ Backend │ │ Database │ │ │ │ │ │ │ │ React/Angular │◄──►│ Spring Boot │◄──►│ PostgreSQL/ │ │ TypeScript │ │ REST APIs │ │ MySQL │ │ Bootstrap │ │ Spring Security│ │ Redis Cache │ │ │ │ JPA/Hibernate │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ Port 3000 Port 8080 Port 5432

Key Features We'll Implement

📋 Application Features:
  • ✅ User Authentication & Authorization
  • ✅ RESTful API with CRUD Operations
  • ✅ Database Integration with JPA
  • ✅ Responsive Frontend Interface
  • ✅ Real-time Data Updates
  • ✅ File Upload & Management
  • ✅ Email Notifications
  • ✅ API Documentation with Swagger
  • ✅ Unit & Integration Testing
  • ✅ Docker Containerization

2. Prerequisites & Environment Setup

Required Software Installation

1Java Development Kit (JDK)

# Download and install JDK 17 or later # Windows (using Chocolatey) choco install openjdk17 # macOS (using Homebrew) brew install openjdk@17 # Ubuntu/Debian sudo apt update sudo apt install openjdk-17-jdk # Verify installation java -version javac -version

2Build Tools (Maven & Gradle)

# Install Maven # Windows choco install maven # macOS brew install maven # Ubuntu/Debian sudo apt install maven # Install Gradle # Windows choco install gradle # macOS brew install gradle # Ubuntu/Debian sudo apt install gradle # Verify installations mvn -version gradle -version

3Node.js & npm (for Frontend)

# Install Node.js (LTS version) # Windows choco install nodejs # macOS brew install node # Ubuntu/Debian curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs # Verify installation node -version npm -version # Install Yarn (optional but recommended) npm install -g yarn

4Database Setup

# PostgreSQL Installation # Windows choco install postgresql # macOS brew install postgresql brew services start postgresql # Ubuntu/Debian sudo apt update sudo apt install postgresql postgresql-contrib # Start PostgreSQL service sudo systemctl start postgresql sudo systemctl enable postgresql # Create database and user sudo -u postgres psql CREATE DATABASE webappdb; CREATE USER webappuser WITH ENCRYPTED PASSWORD 'password123'; GRANT ALL PRIVILEGES ON DATABASE webappdb TO webappuser; \q

5Development Tools

  • IDE: IntelliJ IDEA / Eclipse / VS Code
  • API Testing: Postman / Insomnia
  • Database Client: DBeaver / pgAdmin
  • Version Control: Git
  • Container Platform: Docker Desktop

Environment Configuration

# Set JAVA_HOME environment variable # Windows (PowerShell) $env:JAVA_HOME = "C:\Program Files\OpenJDK\jdk-17" $env:PATH += ";$env:JAVA_HOME\bin" # macOS/Linux (add to ~/.bashrc or ~/.zshrc) export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH # Verify environment echo $JAVA_HOME which java
✅ Environment Verification Checklist:
  • Java 17+ installed and configured
  • Maven/Gradle working correctly
  • Node.js and npm available
  • PostgreSQL running and accessible
  • IDE configured with Java support
  • Docker Desktop installed (for deployment)

3. Backend Setup with Spring Boot

Project Initialization

1Create Spring Boot Project

🌐 Using Spring Initializr: Visit start.spring.io or use your IDE's Spring Boot project wizard.
# Using Spring Boot CLI (alternative method) spring init --dependencies=web,data-jpa,security,validation,actuator \ --java-version=17 \ --type=maven-project \ --group-id=com.example \ --artifact-id=webapp-backend \ --name=WebAppBackend \ --description="Java Web Application Backend" \ --package-name=com.example.webapp \ webapp-backend cd webapp-backend

2Maven Configuration (pom.xml)

4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.1 com.example webapp-backend 1.0.0 WebAppBackend Java Web Application Backend 17 2023.0.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-mail org.postgresql postgresql runtime com.h2database h2 runtime org.springframework.boot spring-boot-starter-data-redis io.jsonwebtoken jjwt-api 0.12.3 io.jsonwebtoken jjwt-impl 0.12.3 runtime io.jsonwebtoken jjwt-jackson 0.12.3 runtime org.springdoc springdoc-openapi-starter-webmvc-ui 2.3.0 org.projectlombok lombok true org.mapstruct mapstruct 1.5.5.Final org.springframework.boot spring-boot-starter-test test org.springframework.security spring-security-test test org.testcontainers junit-jupiter test org.testcontainers postgresql test org.testcontainers testcontainers-bom 1.19.3 pom import org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok org.apache.maven.plugins maven-compiler-plugin 3.11.0 17 17 org.projectlombok lombok ${lombok.version} org.mapstruct mapstruct-processor 1.5.5.Final

3Application Configuration

# src/main/resources/application.yml spring: application: name: webapp-backend profiles: active: dev datasource: url: jdbc:postgresql://localhost:5432/webappdb username: webappuser password: password123 driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true redis: host: localhost port: 6379 timeout: 2000ms mail: host: smtp.gmail.com port: 587 username: ${MAIL_USERNAME:your-email@gmail.com} password: ${MAIL_PASSWORD:your-app-password} properties: mail: smtp: auth: true starttls: enable: true servlet: multipart: max-file-size: 10MB max-request-size: 10MB server: port: 8080 servlet: context-path: /api management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when-authorized logging: level: com.example.webapp: DEBUG org.springframework.security: DEBUG org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE app: jwt: secret: ${JWT_SECRET:mySecretKey} expiration: 86400000 # 24 hours cors: allowed-origins: http://localhost:3000,http://localhost:4200 allowed-methods: GET,POST,PUT,DELETE,OPTIONS allowed-headers: "*" allow-credentials: true --- # Development Profile spring: config: activate: on-profile: dev datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console jpa: hibernate: ddl-auto: create-drop --- # Production Profile spring: config: activate: on-profile: prod datasource: url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/webappdb} username: ${DATABASE_USERNAME:webappuser} password: ${DATABASE_PASSWORD:password123} jpa: hibernate: ddl-auto: validate show-sql: false logging: level: com.example.webapp: INFO org.springframework.security: WARN org.hibernate.SQL: WARN

Project Structure Setup

src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── webapp/ │ │ ├── WebAppBackendApplication.java │ │ ├── config/ │ │ │ ├── SecurityConfig.java │ │ │ ├── CorsConfig.java │ │ │ ├── RedisConfig.java │ │ │ └── SwaggerConfig.java │ │ ├── controller/ │ │ │ ├── AuthController.java │ │ │ ├── UserController.java │ │ │ └── ProductController.java │ │ ├── service/ │ │ │ ├── AuthService.java │ │ │ ├── UserService.java │ │ │ └── ProductService.java │ │ ├── repository/ │ │ │ ├── UserRepository.java │ │ │ └── ProductRepository.java │ │ ├── model/ │ │ │ ├── entity/ │ │ │ │ ├── User.java │ │ │ │ ├── Product.java │ │ │ │ └── BaseEntity.java │ │ │ └── dto/ │ │ │ ├── UserDto.java │ │ │ ├── ProductDto.java │ │ │ └── AuthDto.java │ │ ├── security/ │ │ │ ├── JwtAuthenticationFilter.java │ │ │ ├── JwtTokenProvider.java │ │ │ └── UserPrincipal.java │ │ ├── exception/ │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── ResourceNotFoundException.java │ │ │ └── BadRequestException.java │ │ └── util/ │ │ ├── DateUtil.java │ │ └── ValidationUtil.java │ └── resources/ │ ├── application.yml │ ├── application-dev.yml │ ├── application-prod.yml │ ├── db/ │ │ └── migration/ │ │ └── V1__Initial_schema.sql │ └── static/ └── test/ └── java/ └── com/ └── example/ └── webapp/ ├── controller/ ├── service/ ├── repository/ └── integration/

4Main Application Class

package com.example.webapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableJpaAuditing @EnableCaching @EnableAsync @EnableTransactionManagement public class WebAppBackendApplication { public static void main(String[] args) { SpringApplication.run(WebAppBackendApplication.class, args); } }
✅ Backend Setup Completed:
  • Spring Boot project initialized with all dependencies
  • Multi-profile configuration (dev, prod)
  • Project structure organized by layers
  • Database and caching configured
  • Security and JWT setup prepared

4. Database Integration

Entity Design & JPA Configuration

1Base Entity Class

package com.example.webapp.model.entity; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Getter @Setter public abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; @Version private Long version; @Column(name = "is_active") private Boolean isActive = true; }

2User Entity

package com.example.webapp.model.entity; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.*; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import java.util.Set; @Entity @Table(name = "users", uniqueConstraints = { @UniqueConstraint(columnNames = "username"), @UniqueConstraint(columnNames = "email") }) @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class User extends BaseEntity implements UserDetails { @NotBlank @Size(max = 50) @Column(name = "username", nullable = false, unique = true) private String username; @NotBlank @Size(max = 100) @Email @Column(name = "email", nullable = false, unique = true) private String email; @NotBlank @Size(max = 100) @Column(name = "password", nullable = false) private String password; @Size(max = 50) @Column(name = "first_name") private String firstName; @Size(max = 50) @Column(name = "last_name") private String lastName; @Column(name = "phone_number") private String phoneNumber; @Column(name = "profile_image_url") private String profileImageUrl; @Column(name = "email_verified") private Boolean emailVerified = false; @Column(name = "account_non_expired") private Boolean accountNonExpired = true; @Column(name = "account_non_locked") private Boolean accountNonLocked = true; @Column(name = "credentials_non_expired") private Boolean credentialsNonExpired = true; @Column(name = "enabled") private Boolean enabled = true; @Column(name = "last_login") private LocalDateTime lastLogin; @Enumerated(EnumType.STRING) @Column(name = "role") private Role role = Role.USER; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private Set products; // UserDetails implementation @Override public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } @Override public boolean isAccountNonExpired() { return accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } public enum Role { USER, ADMIN, MODERATOR } }

3Product Entity

package com.example.webapp.model.entity; import jakarta.persistence.*; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.*; import java.math.BigDecimal; import java.util.List; @Entity @Table(name = "products") @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class Product extends BaseEntity { @NotBlank @Size(max = 100) @Column(name = "name", nullable = false) private String name; @Size(max = 500) @Column(name = "description") private String description; @NotNull @DecimalMin(value = "0.0", inclusive = false) @Column(name = "price", nullable = false, precision = 10, scale = 2) private BigDecimal price; @Column(name = "quantity_in_stock") private Integer quantityInStock = 0; @Size(max = 50) @Column(name = "category") private String category; @Size(max = 50) @Column(name = "brand") private String brand; @Column(name = "weight") private Double weight; @ElementCollection @CollectionTable(name = "product_images", joinColumns = @JoinColumn(name = "product_id")) @Column(name = "image_url") private List imageUrls; @ElementCollection @CollectionTable(name = "product_tags", joinColumns = @JoinColumn(name = "product_id")) @Column(name = "tag") private List tags; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; @Column(name = "featured") private Boolean featured = false; @Column(name = "average_rating") private Double averageRating = 0.0; @Column(name = "review_count") private Integer reviewCount = 0; }

Repository Layer

4User Repository

package com.example.webapp.repository; import com.example.webapp.model.entity.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { Optional findByUsername(String username); Optional findByEmail(String email); Optional findByUsernameOrEmail(String username, String email); Boolean existsByUsername(String username); Boolean existsByEmail(String email); List findByRole(User.Role role); Page findByIsActiveTrue(Pageable pageable); @Query("SELECT u FROM User u WHERE u.emailVerified = false AND u.createdAt < :cutoffDate") List findUnverifiedUsersOlderThan(@Param("cutoffDate") LocalDateTime cutoffDate); @Query("SELECT u FROM User u WHERE u.firstName LIKE %:name% OR u.lastName LIKE %:name%") Page findByNameContaining(@Param("name") String name, Pageable pageable); @Modifying @Query("UPDATE User u SET u.lastLogin = :loginTime WHERE u.id = :userId") void updateLastLogin(@Param("userId") Long userId, @Param("loginTime") LocalDateTime loginTime); @Modifying @Query("UPDATE User u SET u.emailVerified = true WHERE u.email = :email") void verifyEmail(@Param("email") String email); @Query("SELECT COUNT(u) FROM User u WHERE u.createdAt >= :startDate") Long countUsersCreatedAfter(@Param("startDate") LocalDateTime startDate); }

5Product Repository

package com.example.webapp.repository; import com.example.webapp.model.entity.Product; import com.example.webapp.model.entity.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.math.BigDecimal; import java.util.List; @Repository public interface ProductRepository extends JpaRepository { Page findByIsActiveTrue(Pageable pageable); Page findByUser(User user, Pageable pageable); Page findByCategory(String category, Pageable pageable); Page findByBrand(String brand, Pageable pageable); List findByFeaturedTrue(); @Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword% OR p.description LIKE %:keyword%") Page findByKeyword(@Param("keyword") String keyword, Pageable pageable); @Query("SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice") Page findByPriceRange(@Param("minPrice") BigDecimal minPrice, @Param("maxPrice") BigDecimal maxPrice, Pageable pageable); @Query("SELECT p FROM Product p WHERE p.quantityInStock > 0") Page findInStock(Pageable pageable); @Query("SELECT p FROM Product p WHERE p.averageRating >= :minRating") Page findByMinimumRating(@Param("minRating") Double minRating, Pageable pageable); @Query("SELECT DISTINCT p.category FROM Product p WHERE p.isActive = true ORDER BY p.category") List findAllCategories(); @Query("SELECT DISTINCT p.brand FROM Product p WHERE p.isActive = true ORDER BY p.brand") List findAllBrands(); @Query("SELECT COUNT(p) FROM Product p WHERE p.user = :user AND p.isActive = true") Long countActiveProductsByUser(@Param("user") User user); }

Database Migration

6Flyway Migration Script

-- src/main/resources/db/migration/V1__Initial_schema.sql -- Users table CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, first_name VARCHAR(50), last_name VARCHAR(50), phone_number VARCHAR(20), profile_image_url VARCHAR(255), email_verified BOOLEAN DEFAULT FALSE, account_non_expired BOOLEAN DEFAULT TRUE, account_non_locked BOOLEAN DEFAULT TRUE, credentials_non_expired BOOLEAN DEFAULT TRUE, enabled BOOLEAN DEFAULT TRUE, last_login TIMESTAMP, role VARCHAR(20) DEFAULT 'USER', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, version BIGINT DEFAULT 0, is_active BOOLEAN DEFAULT TRUE ); -- Products table CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, description VARCHAR(500), price DECIMAL(10,2) NOT NULL CHECK (price > 0), quantity_in_stock INTEGER DEFAULT 0, category VARCHAR(50), brand VARCHAR(50), weight DOUBLE PRECISION, user_id BIGINT NOT NULL, featured BOOLEAN DEFAULT FALSE, average_rating DOUBLE PRECISION DEFAULT 0.0, review_count INTEGER DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, version BIGINT DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Product images table CREATE TABLE product_images ( product_id BIGINT NOT NULL, image_url VARCHAR(255) NOT NULL, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE ); -- Product tags table CREATE TABLE product_tags ( product_id BIGINT NOT NULL, tag VARCHAR(50) NOT NULL, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE ); -- Indexes for better performance CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_role ON users(role); CREATE INDEX idx_users_created_at ON users(created_at); CREATE INDEX idx_products_name ON products(name); CREATE INDEX idx_products_category ON products(category); CREATE INDEX idx_products_brand ON products(brand); CREATE INDEX idx_products_price ON products(price); CREATE INDEX idx_products_user_id ON products(user_id); CREATE INDEX idx_products_featured ON products(featured); CREATE INDEX idx_products_created_at ON products(created_at); -- Insert sample data INSERT INTO users (username, email, password, first_name, last_name, role) VALUES ('admin', 'admin@example.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'Admin', 'User', 'ADMIN'), ('john_doe', 'john@example.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'John', 'Doe', 'USER'), ('jane_smith', 'jane@example.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'Jane', 'Smith', 'USER'); INSERT INTO products (name, description, price, quantity_in_stock, category, brand, user_id) VALUES ('Laptop Pro', 'High-performance laptop for professionals', 1299.99, 10, 'Electronics', 'TechBrand', 2), ('Wireless Headphones', 'Premium noise-cancelling headphones', 299.99, 25, 'Electronics', 'AudioTech', 2), ('Coffee Maker', 'Automatic drip coffee maker', 89.99, 15, 'Appliances', 'BrewMaster', 3);
⚠️ Database Security Note: The sample passwords above are hashed using BCrypt. In production, ensure all passwords are properly hashed and never store plain text passwords.

Database Configuration

7JPA Configuration Class

package com.example.webapp.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import java.util.Optional; @Configuration @EnableJpaAuditing(auditorAwareRef = "auditorProvider") public class JpaConfig { @Bean public AuditorAware auditorProvider() { return new AuditorAwareImpl(); } public static class AuditorAwareImpl implements AuditorAware { @Override public Optional getCurrentAuditor() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getPrincipal())) { return Optional.of("system"); } return Optional.of(authentication.getName()); } } }
✅ Database Integration Completed:
  • JPA entities with proper relationships
  • Repository layer with custom queries
  • Database migration scripts
  • Auditing and versioning configured
  • Performance indexes created
  • Sample data for testing

5. Frontend Development

🎯 Frontend Options: We'll cover both React and Angular setups. Choose the framework that best fits your team's expertise and project requirements.

React Frontend Setup

1Create React Application

# Create React app with TypeScript npx create-react-app webapp-frontend --template typescript cd webapp-frontend # Install additional dependencies npm install axios react-router-dom @types/react-router-dom npm install @mui/material @emotion/react @emotion/styled npm install @mui/icons-material @mui/lab npm install react-hook-form @hookform/resolvers yup npm install react-query @tanstack/react-query npm install react-toastify npm install jwt-decode npm install @types/jwt-decode # Development dependencies npm install --save-dev @types/node @types/react @types/react-dom npm install --save-dev prettier eslint-config-prettier

2Project Structure

src/ ├── components/ │ ├── common/ │ │ ├── Header.tsx │ │ ├── Footer.tsx │ │ ├── Loading.tsx │ │ └── ErrorBoundary.tsx │ ├── auth/ │ │ ├── LoginForm.tsx │ │ ├── RegisterForm.tsx │ │ └── ProtectedRoute.tsx │ ├── product/ │ │ ├── ProductList.tsx │ │ ├── ProductCard.tsx │ │ ├── ProductForm.tsx │ │ └── ProductDetail.tsx │ └── user/ │ ├── UserProfile.tsx │ └── UserList.tsx ├── pages/ │ ├── HomePage.tsx │ ├── LoginPage.tsx │ ├── RegisterPage.tsx │ ├── ProductsPage.tsx │ ├── ProfilePage.tsx │ └── DashboardPage.tsx ├── services/ │ ├── api.ts │ ├── authService.ts │ ├── productService.ts │ └── userService.ts ├── hooks/ │ ├── useAuth.ts │ ├── useApi.ts │ └── useLocalStorage.ts ├── context/ │ ├── AuthContext.tsx │ └── ThemeContext.tsx ├── types/ │ ├── auth.ts │ ├── product.ts │ └── user.ts ├── utils/ │ ├── constants.ts │ ├── helpers.ts │ └── validation.ts ├── styles/ │ ├── globals.css │ └── components.css └── App.tsx

3API Service Configuration

// src/services/api.ts import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import { toast } from 'react-toastify'; const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080/api'; class ApiService { private api: AxiosInstance; constructor() { this.api = axios.create({ baseURL: API_BASE_URL, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); this.setupInterceptors(); } private setupInterceptors(): void { // Request interceptor this.api.interceptors.request.use( (config) => { const token = localStorage.getItem('accessToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor this.api.interceptors.response.use( (response: AxiosResponse) => { return response; }, (error) => { if (error.response?.status === 401) { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; } else if (error.response?.status >= 500) { toast.error('Server error. Please try again later.'); } else if (error.response?.data?.message) { toast.error(error.response.data.message); } return Promise.reject(error); } ); } public async get(url: string, config?: AxiosRequestConfig): Promise { const response = await this.api.get(url, config); return response.data; } public async post(url: string, data?: any, config?: AxiosRequestConfig): Promise { const response = await this.api.post(url, data, config); return response.data; } public async put(url: string, data?: any, config?: AxiosRequestConfig): Promise { const response = await this.api.put(url, data, config); return response.data; } public async delete(url: string, config?: AxiosRequestConfig): Promise { const response = await this.api.delete(url, config); return response.data; } public async uploadFile(url: string, file: File, onProgress?: (progress: number) => void): Promise { const formData = new FormData(); formData.append('file', file); const config: AxiosRequestConfig = { headers: { 'Content-Type': 'multipart/form-data', }, onUploadProgress: (progressEvent) => { if (onProgress && progressEvent.total) { const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); onProgress(progress); } }, }; const response = await this.api.post(url, formData, config); return response.data; } } export const apiService = new ApiService();

4Authentication Service

// src/services/authService.ts import { apiService } from './api'; import { LoginRequest, RegisterRequest, AuthResponse, User } from '../types/auth'; import jwtDecode from 'jwt-decode'; interface JwtPayload { sub: string; exp: number; iat: number; authorities: string[]; } class AuthService { private readonly TOKEN_KEY = 'accessToken'; private readonly REFRESH_TOKEN_KEY = 'refreshToken'; private readonly USER_KEY = 'user'; async login(credentials: LoginRequest): Promise { const response = await apiService.post('/auth/login', credentials); this.setTokens(response.accessToken, response.refreshToken); this.setUser(response.user); return response; } async register(userData: RegisterRequest): Promise { const response = await apiService.post('/auth/register', userData); this.setTokens(response.accessToken, response.refreshToken); this.setUser(response.user); return response; } async logout(): Promise { try { await apiService.post('/auth/logout'); } catch (error) { console.error('Logout error:', error); } finally { this.clearTokens(); this.clearUser(); } } async refreshToken(): Promise { const refreshToken = this.getRefreshToken(); if (!refreshToken) { return null; } try { const response = await apiService.post<{ accessToken: string }>('/auth/refresh', { refreshToken, }); this.setToken(response.accessToken); return response.accessToken; } catch (error) { this.clearTokens(); return null; } } async getCurrentUser(): Promise { try { const user = await apiService.get('/auth/me'); this.setUser(user); return user; } catch (error) { return null; } } async forgotPassword(email: string): Promise { await apiService.post('/auth/forgot-password', { email }); } async resetPassword(token: string, newPassword: string): Promise { await apiService.post('/auth/reset-password', { token, newPassword }); } async verifyEmail(token: string): Promise { await apiService.post('/auth/verify-email', { token }); } // Token management private setTokens(accessToken: string, refreshToken: string): void { localStorage.setItem(this.TOKEN_KEY, accessToken); localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken); } private setToken(accessToken: string): void { localStorage.setItem(this.TOKEN_KEY, accessToken); } private clearTokens(): void { localStorage.removeItem(this.TOKEN_KEY); localStorage.removeItem(this.REFRESH_TOKEN_KEY); } getToken(): string | null { return localStorage.getItem(this.TOKEN_KEY); } private getRefreshToken(): string | null { return localStorage.getItem(this.REFRESH_TOKEN_KEY); } // User management private setUser(user: User): void { localStorage.setItem(this.USER_KEY, JSON.stringify(user)); } private clearUser(): void { localStorage.removeItem(this.USER_KEY); } getUser(): User | null { const userStr = localStorage.getItem(this.USER_KEY); return userStr ? JSON.parse(userStr) : null; } // Token validation isTokenValid(): boolean { const token = this.getToken(); if (!token) return false; try { const decoded: JwtPayload = jwtDecode(token); const currentTime = Date.now() / 1000; return decoded.exp > currentTime; } catch (error) { return false; } } isAuthenticated(): boolean { return this.isTokenValid(); } hasRole(role: string): boolean { const token = this.getToken(); if (!token) return false; try { const decoded: JwtPayload = jwtDecode(token); return decoded.authorities.includes(`ROLE_${role}`); } catch (error) { return false; } } } export const authService = new AuthService();

Angular Frontend Setup (Alternative)

5Create Angular Application

# Install Angular CLI npm install -g @angular/cli # Create Angular app ng new webapp-frontend --routing --style=scss --strict cd webapp-frontend # Install dependencies ng add @angular/material npm install @angular/flex-layout npm install rxjs npm install jwt-decode npm install @types/jwt-decode # Generate modules and components ng generate module core ng generate module shared ng generate module features/auth ng generate module features/products ng generate module features/users # Generate services ng generate service core/services/api ng generate service core/services/auth ng generate service features/products/services/product ng generate service features/users/services/user # Generate components ng generate component shared/components/header ng generate component shared/components/footer ng generate component features/auth/components/login ng generate component features/auth/components/register ng generate component features/products/components/product-list ng generate component features/products/components/product-detail

6Angular API Service

// src/app/core/services/api.service.ts import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; import { environment } from '../../../environments/environment'; export interface ApiResponse { data: T; message?: string; success: boolean; } @Injectable({ providedIn: 'root' }) export class ApiService { private readonly baseUrl = environment.apiUrl; constructor(private http: HttpClient) {} get(endpoint: string, params?: HttpParams): Observable { return this.http.get(`${this.baseUrl}${endpoint}`, { params }) .pipe( retry(1), catchError(this.handleError) ); } post(endpoint: string, data: any): Observable { return this.http.post(`${this.baseUrl}${endpoint}`, data) .pipe( catchError(this.handleError) ); } put(endpoint: string, data: any): Observable { return this.http.put(`${this.baseUrl}${endpoint}`, data) .pipe( catchError(this.handleError) ); } delete(endpoint: string): Observable { return this.http.delete(`${this.baseUrl}${endpoint}`) .pipe( catchError(this.handleError) ); } uploadFile(endpoint: string, file: File): Observable { const formData = new FormData(); formData.append('file', file); return this.http.post(`${this.baseUrl}${endpoint}`, formData) .pipe( catchError(this.handleError) ); } private handleError(error: HttpErrorResponse): Observable { let errorMessage = 'An unknown error occurred'; if (error.error instanceof ErrorEvent) { // Client-side error errorMessage = error.error.message; } else { // Server-side error errorMessage = error.error?.message || `Error Code: ${error.status}`; } console.error('API Error:', errorMessage); return throwError(() => new Error(errorMessage)); } }

Common Frontend Components

7Authentication Context (React)

// src/context/AuthContext.tsx import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { authService } from '../services/authService'; import { User } from '../types/auth'; interface AuthContextType { user: User | null; isAuthenticated: boolean; isLoading: boolean; login: (email: string, password: string) => Promise; register: (userData: any) => Promise; logout: () => Promise; refreshUser: () => Promise; } const AuthContext = createContext(undefined); interface AuthProviderProps { children: ReactNode; } export const AuthProvider: React.FC = ({ children }) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { initializeAuth(); }, []); const initializeAuth = async () => { try { if (authService.isAuthenticated()) { const currentUser = await authService.getCurrentUser(); setUser(currentUser); } } catch (error) { console.error('Auth initialization error:', error); authService.logout(); } finally { setIsLoading(false); } }; const login = async (email: string, password: string) => { setIsLoading(true); try { const response = await authService.login({ email, password }); setUser(response.user); } finally { setIsLoading(false); } }; const register = async (userData: any) => { setIsLoading(true); try { const response = await authService.register(userData); setUser(response.user); } finally { setIsLoading(false); } }; const logout = async () => { setIsLoading(true); try { await authService.logout(); setUser(null); } finally { setIsLoading(false); } }; const refreshUser = async () => { try { const currentUser = await authService.getCurrentUser(); setUser(currentUser); } catch (error) { console.error('Failed to refresh user:', error); } }; const value: AuthContextType = { user, isAuthenticated: !!user && authService.isAuthenticated(), isLoading, login, register, logout, refreshUser, }; return {children}; }; export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; };
✅ Frontend Setup Completed:
  • React/Angular project initialized with TypeScript
  • API service with interceptors and error handling
  • Authentication service with JWT management
  • Project structure organized by features
  • Context/Service providers for state management
  • Material-UI/Angular Material for UI components

6. API Integration & Communication

REST Controller Implementation

1Authentication Controller

package com.example.webapp.controller; import com.example.webapp.model.dto.AuthDto; import com.example.webapp.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/auth") @RequiredArgsConstructor @Tag(name = "Authentication", description = "Authentication management APIs") public class AuthController { private final AuthService authService; @PostMapping("/register") @Operation(summary = "Register a new user") public ResponseEntity register(@Valid @RequestBody AuthDto.RegisterRequest request) { AuthDto.Response response = authService.register(request); return ResponseEntity.ok(response); } @PostMapping("/login") @Operation(summary = "Authenticate user") public ResponseEntity login(@Valid @RequestBody AuthDto.LoginRequest request) { AuthDto.Response response = authService.login(request); return ResponseEntity.ok(response); } @PostMapping("/refresh") @Operation(summary = "Refresh access token") public ResponseEntity refresh(@Valid @RequestBody AuthDto.RefreshTokenRequest request) { AuthDto.TokenResponse response = authService.refreshToken(request.getRefreshToken()); return ResponseEntity.ok(response); } @PostMapping("/logout") @Operation(summary = "Logout user") public ResponseEntity logout(@RequestHeader("Authorization") String token) { authService.logout(token.substring(7)); // Remove "Bearer " prefix return ResponseEntity.ok().build(); } @PostMapping("/forgot-password") @Operation(summary = "Request password reset") public ResponseEntity forgotPassword(@Valid @RequestBody AuthDto.ForgotPasswordRequest request) { authService.forgotPassword(request.getEmail()); return ResponseEntity.ok().build(); } @PostMapping("/reset-password") @Operation(summary = "Reset password") public ResponseEntity resetPassword(@Valid @RequestBody AuthDto.ResetPasswordRequest request) { authService.resetPassword(request.getToken(), request.getNewPassword()); return ResponseEntity.ok().build(); } @PostMapping("/verify-email") @Operation(summary = "Verify email address") public ResponseEntity verifyEmail(@Valid @RequestBody AuthDto.VerifyEmailRequest request) { authService.verifyEmail(request.getToken()); return ResponseEntity.ok().build(); } @GetMapping("/me") @Operation(summary = "Get current user profile") public ResponseEntity getCurrentUser() { AuthDto.UserResponse response = authService.getCurrentUser(); return ResponseEntity.ok(response); } }

2Product Controller

package com.example.webapp.controller; import com.example.webapp.model.dto.ProductDto; import com.example.webapp.service.ProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.math.BigDecimal; import java.util.List; @RestController @RequestMapping("/products") @RequiredArgsConstructor @Tag(name = "Products", description = "Product management APIs") public class ProductController { private final ProductService productService; @GetMapping @Operation(summary = "Get all products with pagination and filtering") public ResponseEntity> getAllProducts( @Parameter(description = "Pagination information") Pageable pageable, @RequestParam(required = false) String keyword, @RequestParam(required = false) String category, @RequestParam(required = false) String brand, @RequestParam(required = false) BigDecimal minPrice, @RequestParam(required = false) BigDecimal maxPrice, @RequestParam(required = false, defaultValue = "false") Boolean inStockOnly) { Page products = productService.getAllProducts( pageable, keyword, category, brand, minPrice, maxPrice, inStockOnly); return ResponseEntity.ok(products); } @GetMapping("/{id}") @Operation(summary = "Get product by ID") public ResponseEntity getProductById(@PathVariable Long id) { ProductDto.DetailResponse product = productService.getProductById(id); return ResponseEntity.ok(product); } @PostMapping @PreAuthorize("hasRole('USER')") @Operation(summary = "Create a new product") public ResponseEntity createProduct(@Valid @RequestBody ProductDto.CreateRequest request) { ProductDto.Response product = productService.createProduct(request); return ResponseEntity.status(HttpStatus.CREATED).body(product); } @PutMapping("/{id}") @PreAuthorize("hasRole('USER') and @productService.isOwner(#id, authentication.name)") @Operation(summary = "Update product") public ResponseEntity updateProduct( @PathVariable Long id, @Valid @RequestBody ProductDto.UpdateRequest request) { ProductDto.Response product = productService.updateProduct(id, request); return ResponseEntity.ok(product); } @DeleteMapping("/{id}") @PreAuthorize("hasRole('USER') and @productService.isOwner(#id, authentication.name) or hasRole('ADMIN')") @Operation(summary = "Delete product") public ResponseEntity deleteProduct(@PathVariable Long id) { productService.deleteProduct(id); return ResponseEntity.noContent().build(); } @PostMapping("/{id}/images") @PreAuthorize("hasRole('USER') and @productService.isOwner(#id, authentication.name)") @Operation(summary = "Upload product images") public ResponseEntity> uploadProductImages( @PathVariable Long id, @RequestParam("files") List files) { List imageUrls = productService.uploadProductImages(id, files); return ResponseEntity.ok(imageUrls); } @GetMapping("/categories") @Operation(summary = "Get all product categories") public ResponseEntity> getAllCategories() { List categories = productService.getAllCategories(); return ResponseEntity.ok(categories); } @GetMapping("/brands") @Operation(summary = "Get all product brands") public ResponseEntity> getAllBrands() { List brands = productService.getAllBrands(); return ResponseEntity.ok(brands); } @GetMapping("/featured") @Operation(summary = "Get featured products") public ResponseEntity> getFeaturedProducts() { List products = productService.getFeaturedProducts(); return ResponseEntity.ok(products); } @GetMapping("/user/{userId}") @Operation(summary = "Get products by user") public ResponseEntity> getProductsByUser( @PathVariable Long userId, Pageable pageable) { Page products = productService.getProductsByUser(userId, pageable); return ResponseEntity.ok(products); } }

Service Layer Implementation

3Authentication Service

package com.example.webapp.service; import com.example.webapp.exception.BadRequestException; import com.example.webapp.exception.ResourceNotFoundException; import com.example.webapp.model.dto.AuthDto; import com.example.webapp.model.entity.User; import com.example.webapp.repository.UserRepository; import com.example.webapp.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; @Service @RequiredArgsConstructor @Slf4j @Transactional public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final AuthenticationManager authenticationManager; private final JwtTokenProvider tokenProvider; private final EmailService emailService; public AuthDto.Response register(AuthDto.RegisterRequest request) { // Check if user already exists if (userRepository.existsByUsername(request.getUsername())) { throw new BadRequestException("Username is already taken!"); } if (userRepository.existsByEmail(request.getEmail())) { throw new BadRequestException("Email is already in use!"); } // Create new user User user = User.builder() .username(request.getUsername()) .email(request.getEmail()) .password(passwordEncoder.encode(request.getPassword())) .firstName(request.getFirstName()) .lastName(request.getLastName()) .role(User.Role.USER) .emailVerified(false) .enabled(true) .accountNonExpired(true) .accountNonLocked(true) .credentialsNonExpired(true) .build(); User savedUser = userRepository.save(user); // Send verification email emailService.sendVerificationEmail(savedUser); // Generate tokens String accessToken = tokenProvider.generateAccessToken(savedUser); String refreshToken = tokenProvider.generateRefreshToken(savedUser); log.info("User registered successfully: {}", savedUser.getUsername()); return AuthDto.Response.builder() .accessToken(accessToken) .refreshToken(refreshToken) .tokenType("Bearer") .expiresIn(tokenProvider.getAccessTokenExpiration()) .user(mapToUserResponse(savedUser)) .build(); } public AuthDto.Response login(AuthDto.LoginRequest request) { // Authenticate user Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.getEmail(), request.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new ResourceNotFoundException("User not found")); // Update last login user.setLastLogin(LocalDateTime.now()); userRepository.save(user); // Generate tokens String accessToken = tokenProvider.generateAccessToken(user); String refreshToken = tokenProvider.generateRefreshToken(user); log.info("User logged in successfully: {}", user.getUsername()); return AuthDto.Response.builder() .accessToken(accessToken) .refreshToken(refreshToken) .tokenType("Bearer") .expiresIn(tokenProvider.getAccessTokenExpiration()) .user(mapToUserResponse(user)) .build(); } public AuthDto.TokenResponse refreshToken(String refreshToken) { if (!tokenProvider.validateToken(refreshToken)) { throw new BadRequestException("Invalid refresh token"); } String username = tokenProvider.getUsernameFromToken(refreshToken); User user = userRepository.findByUsername(username) .orElseThrow(() -> new ResourceNotFoundException("User not found")); String newAccessToken = tokenProvider.generateAccessToken(user); return AuthDto.TokenResponse.builder() .accessToken(newAccessToken) .tokenType("Bearer") .expiresIn(tokenProvider.getAccessTokenExpiration()) .build(); } public void logout(String token) { // Add token to blacklist (implement token blacklisting if needed) tokenProvider.invalidateToken(token); SecurityContextHolder.clearContext(); log.info("User logged out successfully"); } public void forgotPassword(String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new ResourceNotFoundException("User not found with email: " + email)); String resetToken = tokenProvider.generatePasswordResetToken(user); emailService.sendPasswordResetEmail(user, resetToken); log.info("Password reset email sent to: {}", email); } public void resetPassword(String token, String newPassword) { if (!tokenProvider.validatePasswordResetToken(token)) { throw new BadRequestException("Invalid or expired reset token"); } String username = tokenProvider.getUsernameFromToken(token); User user = userRepository.findByUsername(username) .orElseThrow(() -> new ResourceNotFoundException("User not found")); user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); log.info("Password reset successfully for user: {}", username); } public void verifyEmail(String token) { if (!tokenProvider.validateEmailVerificationToken(token)) { throw new BadRequestException("Invalid or expired verification token"); } String email = tokenProvider.getEmailFromToken(token); userRepository.verifyEmail(email); log.info("Email verified successfully: {}", email); } public AuthDto.UserResponse getCurrentUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = authentication.getName(); User user = userRepository.findByUsername(username) .orElseThrow(() -> new ResourceNotFoundException("User not found")); return mapToUserResponse(user); } private AuthDto.UserResponse mapToUserResponse(User user) { return AuthDto.UserResponse.builder() .id(user.getId()) .username(user.getUsername()) .email(user.getEmail()) .firstName(user.getFirstName()) .lastName(user.getLastName()) .phoneNumber(user.getPhoneNumber()) .profileImageUrl(user.getProfileImageUrl()) .role(user.getRole().name()) .emailVerified(user.getEmailVerified()) .createdAt(user.getCreatedAt()) .lastLogin(user.getLastLogin()) .build(); } }

Error Handling & Validation

4Global Exception Handler

package com.example.webapp.exception; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.multipart.MaxUploadSizeExceededException; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity handleResourceNotFoundException( ResourceNotFoundException ex, WebRequest request) { ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.NOT_FOUND.value()) .error("Not Found") .message(ex.getMessage()) .path(request.getDescription(false).replace("uri=", "")) .build(); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException( BadRequestException ex, WebRequest request) { ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.BAD_REQUEST.value()) .error("Bad Request") .message(ex.getMessage()) .path(request.getDescription(false).replace("uri=", "")) .build(); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationExceptions( MethodArgumentNotValidException ex, WebRequest request) { Map errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach((error) -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); ValidationErrorResponse errorResponse = ValidationErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.BAD_REQUEST.value()) .error("Validation Failed") .message("Invalid input parameters") .path(request.getDescription(false).replace("uri=", "")) .validationErrors(errors) .build(); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); } @ExceptionHandler(BadCredentialsException.class) public ResponseEntity handleBadCredentialsException( BadCredentialsException ex, WebRequest request) { ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.UNAUTHORIZED.value()) .error("Unauthorized") .message("Invalid username or password") .path(request.getDescription(false).replace("uri=", "")) .build(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity handleAccessDeniedException( AccessDeniedException ex, WebRequest request) { ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.FORBIDDEN.value()) .error("Forbidden") .message("Access denied") .path(request.getDescription(false).replace("uri=", "")) .build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse); } @ExceptionHandler(MaxUploadSizeExceededException.class) public ResponseEntity handleMaxUploadSizeExceededException( MaxUploadSizeExceededException ex, WebRequest request) { ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.PAYLOAD_TOO_LARGE.value()) .error("File Too Large") .message("File size exceeds maximum allowed limit") .path(request.getDescription(false).replace("uri=", "")) .build(); return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(errorResponse); } @ExceptionHandler(Exception.class) public ResponseEntity handleGlobalException( Exception ex, WebRequest request) { log.error("Unexpected error occurred", ex); ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) .error("Internal Server Error") .message("An unexpected error occurred") .path(request.getDescription(false).replace("uri=", "")) .build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } }
✅ API Integration Completed:
  • RESTful controllers with proper HTTP methods
  • Service layer with business logic
  • Global exception handling
  • Input validation and error responses
  • Security annotations and access control
  • API documentation with Swagger

7. Security Implementation

Spring Security Configuration

1Security Configuration

package com.example.webapp.config; import com.example.webapp.security.JwtAuthenticationEntryPoint; import com.example.webapp.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor public class SecurityConfig { private final UserDetailsService userDetailsService; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .exceptionHandling(exception -> exception.authenticationEntryPoint(jwtAuthenticationEntryPoint)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/auth/**").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers("/actuator/health").permitAll() .requestMatchers("/h2-console/**").permitAll() .requestMatchers("/products/featured", "/products/categories", "/products/brands").permitAll() .requestMatchers("/products/{id}").permitAll() .requestMatchers("/products").permitAll() .anyRequest().authenticated() ); http.authenticationProvider(authenticationProvider()); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // For H2 Console (development only) http.headers(headers -> headers.frameOptions().disable()); return http.build(); } }

2JWT Token Provider

package com.example.webapp.security; import com.example.webapp.model.entity.User; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component @Slf4j public class JwtTokenProvider { @Value("${app.jwt.secret}") private String jwtSecret; @Value("${app.jwt.expiration}") private long jwtExpiration; private static final long REFRESH_TOKEN_EXPIRATION = 604800000L; // 7 days private static final long PASSWORD_RESET_EXPIRATION = 3600000L; // 1 hour private static final long EMAIL_VERIFICATION_EXPIRATION = 86400000L; // 24 hours private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(jwtSecret.getBytes()); } public String generateAccessToken(User user) { Date expiryDate = new Date(System.currentTimeMillis() + jwtExpiration); Map claims = new HashMap<>(); claims.put("userId", user.getId()); claims.put("email", user.getEmail()); claims.put("role", user.getRole().name()); claims.put("emailVerified", user.getEmailVerified()); return Jwts.builder() .setClaims(claims) .setSubject(user.getUsername()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } public String generateRefreshToken(User user) { Date expiryDate = new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION); return Jwts.builder() .setSubject(user.getUsername()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } public String generatePasswordResetToken(User user) { Date expiryDate = new Date(System.currentTimeMillis() + PASSWORD_RESET_EXPIRATION); Map claims = new HashMap<>(); claims.put("type", "password_reset"); claims.put("userId", user.getId()); return Jwts.builder() .setClaims(claims) .setSubject(user.getUsername()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } public String generateEmailVerificationToken(User user) { Date expiryDate = new Date(System.currentTimeMillis() + EMAIL_VERIFICATION_EXPIRATION); Map claims = new HashMap<>(); claims.put("type", "email_verification"); claims.put("email", user.getEmail()); return Jwts.builder() .setClaims(claims) .setSubject(user.getUsername()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public String getEmailFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.get("email", String.class); } public Long getUserIdFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.get("userId", Long.class); } public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token); return true; } catch (SecurityException ex) { log.error("Invalid JWT signature"); } catch (MalformedJwtException ex) { log.error("Invalid JWT token"); } catch (ExpiredJwtException ex) { log.error("Expired JWT token"); } catch (UnsupportedJwtException ex) { log.error("Unsupported JWT token"); } catch (IllegalArgumentException ex) { log.error("JWT claims string is empty"); } return false; } public boolean validatePasswordResetToken(String token) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); String type = claims.get("type", String.class); return "password_reset".equals(type); } catch (Exception ex) { log.error("Invalid password reset token", ex); return false; } } public boolean validateEmailVerificationToken(String token) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); String type = claims.get("type", String.class); return "email_verification".equals(type); } catch (Exception ex) { log.error("Invalid email verification token", ex); return false; } } public void invalidateToken(String token) { // Implement token blacklisting logic here // This could involve storing invalidated tokens in Redis // or maintaining a blacklist in the database log.info("Token invalidated: {}", token.substring(0, 20) + "..."); } public long getAccessTokenExpiration() { return jwtExpiration; } }
⚠️ Security Best Practices:
  • Use strong, randomly generated JWT secrets in production
  • Implement token blacklisting for logout functionality
  • Use HTTPS in production environments
  • Implement rate limiting for authentication endpoints
  • Regular security audits and dependency updates

8. Testing Strategies

Backend Testing

1Unit Testing with JUnit 5

package com.example.webapp.service; import com.example.webapp.exception.ResourceNotFoundException; import com.example.webapp.model.entity.User; import com.example.webapp.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private UserService userService; private User testUser; @BeforeEach void setUp() { testUser = User.builder() .id(1L) .username("testuser") .email("test@example.com") .password("encodedPassword") .firstName("Test") .lastName("User") .role(User.Role.USER) .emailVerified(true) .enabled(true) .build(); } @Test void getUserById_WhenUserExists_ShouldReturnUser() { // Given when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); // When User result = userService.getUserById(1L); // Then assertThat(result).isNotNull(); assertThat(result.getId()).isEqualTo(1L); assertThat(result.getUsername()).isEqualTo("testuser"); verify(userRepository).findById(1L); } @Test void getUserById_WhenUserNotExists_ShouldThrowException() { // Given when(userRepository.findById(1L)).thenReturn(Optional.empty()); // When & Then assertThatThrownBy(() -> userService.getUserById(1L)) .isInstanceOf(ResourceNotFoundException.class) .hasMessage("User not found with id: 1"); verify(userRepository).findById(1L); } @Test void createUser_WhenValidData_ShouldCreateUser() { // Given when(userRepository.existsByUsername("newuser")).thenReturn(false); when(userRepository.existsByEmail("new@example.com")).thenReturn(false); when(passwordEncoder.encode("password")).thenReturn("encodedPassword"); when(userRepository.save(any(User.class))).thenReturn(testUser); // When User result = userService.createUser("newuser", "new@example.com", "password"); // Then assertThat(result).isNotNull(); verify(userRepository).existsByUsername("newuser"); verify(userRepository).existsByEmail("new@example.com"); verify(passwordEncoder).encode("password"); verify(userRepository).save(any(User.class)); } }

2Integration Testing with TestContainers

package com.example.webapp.integration; import com.example.webapp.model.entity.User; import com.example.webapp.repository.UserRepository; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWebMvc @Testcontainers @Transactional class AuthControllerIntegrationTest { @Container static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Autowired private UserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder; @BeforeEach void setUp() { userRepository.deleteAll(); } @Test void register_WhenValidData_ShouldCreateUser() throws Exception { // Given var registerRequest = """ { "username": "testuser", "email": "test@example.com", "password": "password123", "firstName": "Test", "lastName": "User" } """; // When & Then mockMvc.perform(post("/auth/register") .contentType(MediaType.APPLICATION_JSON) .content(registerRequest)) .andExpect(status().isOk()) .andExpect(jsonPath("$.accessToken").exists()) .andExpect(jsonPath("$.user.username").value("testuser")) .andExpect(jsonPath("$.user.email").value("test@example.com")); } @Test void login_WhenValidCredentials_ShouldReturnToken() throws Exception { // Given User user = User.builder() .username("testuser") .email("test@example.com") .password(passwordEncoder.encode("password123")) .firstName("Test") .lastName("User") .role(User.Role.USER) .emailVerified(true) .enabled(true) .build(); userRepository.save(user); var loginRequest = """ { "email": "test@example.com", "password": "password123" } """; // When & Then mockMvc.perform(post("/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(loginRequest)) .andExpect(status().isOk()) .andExpect(jsonPath("$.accessToken").exists()) .andExpect(jsonPath("$.user.email").value("test@example.com")); } }

Frontend Testing

3React Component Testing

// src/components/auth/__tests__/LoginForm.test.tsx import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthProvider } from '../../../context/AuthContext'; import LoginForm from '../LoginForm'; import { authService } from '../../../services/authService'; // Mock the auth service jest.mock('../../../services/authService'); const mockAuthService = authService as jest.Mocked; const renderWithProviders = (component: React.ReactElement) => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return render( {component} ); }; describe('LoginForm', () => { beforeEach(() => { jest.clearAllMocks(); }); test('renders login form with email and password fields', () => { renderWithProviders(); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument(); }); test('shows validation errors for empty fields', async () => { const user = userEvent.setup(); renderWithProviders(); const submitButton = screen.getByRole('button', { name: /sign in/i }); await user.click(submitButton); await waitFor(() => { expect(screen.getByText(/email is required/i)).toBeInTheDocument(); expect(screen.getByText(/password is required/i)).toBeInTheDocument(); }); }); test('submits form with valid credentials', async () => { const user = userEvent.setup(); const mockResponse = { accessToken: 'mock-token', user: { id: 1, email: 'test@example.com', username: 'testuser' }, }; mockAuthService.login.mockResolvedValueOnce(mockResponse); renderWithProviders(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); await user.type(emailInput, 'test@example.com'); await user.type(passwordInput, 'password123'); await user.click(submitButton); await waitFor(() => { expect(mockAuthService.login).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123', }); }); }); test('displays error message on login failure', async () => { const user = userEvent.setup(); const errorMessage = 'Invalid credentials'; mockAuthService.login.mockRejectedValueOnce(new Error(errorMessage)); renderWithProviders(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); await user.type(emailInput, 'test@example.com'); await user.type(passwordInput, 'wrongpassword'); await user.click(submitButton); await waitFor(() => { expect(screen.getByText(errorMessage)).toBeInTheDocument(); }); }); });

4E2E Testing with Cypress

// cypress/e2e/auth.cy.ts describe('Authentication Flow', () => { beforeEach(() => { cy.visit('/'); }); it('should allow user to register and login', () => { // Navigate to register page cy.get('[data-testid="register-link"]').click(); cy.url().should('include', '/register'); // Fill registration form cy.get('[data-testid="username-input"]').type('testuser'); cy.get('[data-testid="email-input"]').type('test@example.com'); cy.get('[data-testid="password-input"]').type('password123'); cy.get('[data-testid="confirm-password-input"]').type('password123'); cy.get('[data-testid="first-name-input"]').type('Test'); cy.get('[data-testid="last-name-input"]').type('User'); // Submit registration cy.get('[data-testid="register-button"]').click(); // Should redirect to dashboard after successful registration cy.url().should('include', '/dashboard'); cy.get('[data-testid="user-menu"]').should('contain', 'Test User'); // Logout cy.get('[data-testid="user-menu"]').click(); cy.get('[data-testid="logout-button"]').click(); // Should redirect to home page cy.url().should('eq', Cypress.config().baseUrl + '/'); // Login with the same credentials cy.get('[data-testid="login-link"]').click(); cy.get('[data-testid="email-input"]').type('test@example.com'); cy.get('[data-testid="password-input"]').type('password123'); cy.get('[data-testid="login-button"]').click(); // Should redirect to dashboard cy.url().should('include', '/dashboard'); cy.get('[data-testid="user-menu"]').should('contain', 'Test User'); }); it('should show error for invalid login credentials', () => { cy.get('[data-testid="login-link"]').click(); cy.get('[data-testid="email-input"]').type('invalid@example.com'); cy.get('[data-testid="password-input"]').type('wrongpassword'); cy.get('[data-testid="login-button"]').click(); cy.get('[data-testid="error-message"]') .should('be.visible') .and('contain', 'Invalid credentials'); }); });

9. Deployment & DevOps

Docker Configuration

1Backend Dockerfile

# Multi-stage build for Spring Boot application FROM eclipse-temurin:17-jdk-alpine AS builder WORKDIR /app COPY pom.xml . COPY src ./src # Download dependencies RUN ./mvnw dependency:go-offline -B # Build application RUN ./mvnw clean package -DskipTests FROM eclipse-temurin:17-jre-alpine AS runtime # Create non-root user RUN addgroup -g 1001 -S appgroup && \ adduser -u 1001 -S appuser -G appgroup WORKDIR /app # Copy built JAR COPY --from=builder /app/target/*.jar app.jar # Change ownership RUN chown -R appuser:appgroup /app # Switch to non-root user USER appuser # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/api/actuator/health || exit 1 EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

2Frontend Dockerfile

# Multi-stage build for React application FROM node:18-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ RUN npm ci --only=production # Copy source code COPY . . # Build application RUN npm run build FROM nginx:alpine AS runtime # Copy custom nginx config COPY nginx.conf /etc/nginx/nginx.conf # Copy built application COPY --from=builder /app/build /usr/share/nginx/html # Add non-root user RUN addgroup -g 1001 -S nginx && \ adduser -u 1001 -S nginx -G nginx # Change ownership RUN chown -R nginx:nginx /usr/share/nginx/html && \ chown -R nginx:nginx /var/cache/nginx && \ chown -R nginx:nginx /var/log/nginx && \ chown -R nginx:nginx /etc/nginx/conf.d # Switch to non-root user USER nginx EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

3Docker Compose

# docker-compose.yml version: '3.8' services: postgres: image: postgres:15-alpine container_name: webapp-postgres environment: POSTGRES_DB: webappdb POSTGRES_USER: webappuser POSTGRES_PASSWORD: password123 ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql networks: - webapp-network healthcheck: test: ["CMD-SHELL", "pg_isready -U webappuser -d webappdb"] interval: 30s timeout: 10s retries: 3 redis: image: redis:7-alpine container_name: webapp-redis ports: - "6379:6379" volumes: - redis_data:/data networks: - webapp-network healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 30s timeout: 10s retries: 3 backend: build: context: ./webapp-backend dockerfile: Dockerfile container_name: webapp-backend environment: SPRING_PROFILES_ACTIVE: prod DATABASE_URL: jdbc:postgresql://postgres:5432/webappdb DATABASE_USERNAME: webappuser DATABASE_PASSWORD: password123 REDIS_HOST: redis REDIS_PORT: 6379 JWT_SECRET: ${JWT_SECRET:-mySecretKey} MAIL_USERNAME: ${MAIL_USERNAME} MAIL_PASSWORD: ${MAIL_PASSWORD} ports: - "8080:8080" depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - webapp-network healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/actuator/health"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped frontend: build: context: ./webapp-frontend dockerfile: Dockerfile container_name: webapp-frontend environment: REACT_APP_API_URL: http://localhost:8080/api ports: - "3000:80" depends_on: backend: condition: service_healthy networks: - webapp-network restart: unless-stopped volumes: postgres_data: redis_data: networks: webapp-network: driver: bridge

CI/CD Pipeline

4GitHub Actions Workflow

# .github/workflows/ci-cd.yml name: CI/CD Pipeline on: push: branches: [ main, develop ] pull_request: branches: [ main ] env: REGISTRY: ghcr.io IMAGE_NAME: @{{ '${{ github.repository }}' }} jobs: test-backend: runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: testdb options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Cache Maven dependencies uses: actions/cache@v3 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-m2- - name: Run backend tests working-directory: ./webapp-backend run: ./mvnw clean test - name: Generate test report uses: dorny/test-reporter@v1 if: success() || failure() with: name: Backend Tests path: webapp-backend/target/surefire-reports/*.xml reporter: java-junit test-frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' cache-dependency-path: webapp-frontend/package-lock.json - name: Install dependencies working-directory: ./webapp-frontend run: npm ci - name: Run frontend tests working-directory: ./webapp-frontend run: npm test -- --coverage --watchAll=false - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./webapp-frontend/coverage/lcov.info build-and-push: needs: [test-backend, test-frontend] runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push backend image uses: docker/build-push-action@v5 with: context: ./webapp-backend push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-backend:latest labels: ${{ steps.meta.outputs.labels }} - name: Build and push frontend image uses: docker/build-push-action@v5 with: context: ./webapp-frontend push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:latest labels: ${{ steps.meta.outputs.labels }} deploy: needs: build-and-push runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - name: Deploy to production uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.SSH_KEY }} script: | cd /opt/webapp docker-compose pull docker-compose up -d docker system prune -f

10. Best Practices & Optimization

Performance Optimization

🚀 Backend Performance Tips:
  • Use database connection pooling (HikariCP)
  • Implement caching with Redis for frequently accessed data
  • Use pagination for large datasets
  • Optimize database queries with proper indexing
  • Implement lazy loading for JPA relationships
  • Use async processing for heavy operations
  • Enable GZIP compression
  • Monitor application metrics with Micrometer
⚡ Frontend Performance Tips:
  • Implement code splitting and lazy loading
  • Use React.memo() and useMemo() for expensive computations
  • Optimize bundle size with tree shaking
  • Implement virtual scrolling for large lists
  • Use service workers for caching
  • Optimize images with WebP format
  • Implement proper error boundaries
  • Use React Query for efficient data fetching

Security Best Practices

🔒 Security Checklist:
  • ✅ Use HTTPS in production
  • ✅ Implement proper CORS configuration
  • ✅ Validate all input data
  • ✅ Use parameterized queries to prevent SQL injection
  • ✅ Implement rate limiting
  • ✅ Use secure headers (HSTS, CSP, X-Frame-Options)
  • ✅ Regular dependency updates
  • ✅ Implement proper logging and monitoring
  • ✅ Use environment variables for sensitive data
  • ✅ Implement proper session management

Monitoring & Observability

1Application Metrics

@verbatim # application.yml - Monitoring configuration management: endpoints: web: exposure: include: health,info,metrics,prometheus,loggers endpoint: health: show-details: when-authorized show-components: always metrics: enabled: true metrics: export: prometheus: enabled: true distribution: percentiles-histogram: http.server.requests: true percentiles: http.server.requests: 0.5, 0.95, 0.99 sla: http.server.requests: 100ms, 500ms, 1s logging: level: com.example.webapp: INFO pattern: console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" file: name: logs/webapp.log max-size: 10MB max-history: 30

Production Deployment Checklist

✅ Pre-Production Checklist:
  • Environment variables configured
  • Database migrations tested
  • SSL certificates installed
  • Backup strategy implemented
  • Monitoring and alerting configured
  • Load testing completed
  • Security scan performed
  • Documentation updated
  • Rollback plan prepared
  • Team trained on deployment process

Conclusion

🎉 Congratulations! You've successfully set up a complete Java web development environment with:
  • ✅ Modern Spring Boot backend with security
  • ✅ React/Angular frontend with TypeScript
  • ✅ PostgreSQL database with JPA
  • ✅ Comprehensive testing strategy
  • ✅ Docker containerization
  • ✅ CI/CD pipeline with GitHub Actions
  • ✅ Production-ready configuration
📚 Next Steps:
  • Explore microservices architecture with Spring Cloud
  • Implement advanced caching strategies
  • Add real-time features with WebSockets
  • Integrate with cloud services (AWS, Azure, GCP)
  • Implement advanced monitoring with ELK stack
  • Add API versioning and documentation
  • Explore reactive programming with Spring WebFlux
Wesley Classen
Wesley's AI Assistant Usually replies in ~15 min Ask me anything — if I can't answer, Wesley gets pinged and replies personally.
Hello! I'm Wesley's AI assistant. How can I help you today?