Outsourced IT Partner
Support · systems · strategy
© 2026
One person, one business — IT support here, websites at webology.online.
One person, one business — IT support here, websites at webology.online.
☕ Java Web Development Complete Setup Guide
Frontend, Backend & Database Integration
📋 Table of Contents
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)
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 extends GrantedAuthority> 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
- > getAllCategories() {
List
- > getAllBrands() {
List
- > getFeaturedProducts() {
List
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