0% found this document useful (0 votes)
3 views143 pages

Ultimate 30-Day Full-Stack Java Developer

The document outlines a 30-day learning roadmap for recent software graduates to become proficient full-stack Java developers, covering topics from Spring Security to production deployment. Each week focuses on different aspects, including backend practices, frontend integration, and DevOps, with practical exercises and key takeaways provided. The roadmap emphasizes hands-on practice, theory, and review to ensure a comprehensive learning experience.

Uploaded by

redasarhani02
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views143 pages

Ultimate 30-Day Full-Stack Java Developer

The document outlines a 30-day learning roadmap for recent software graduates to become proficient full-stack Java developers, covering topics from Spring Security to production deployment. Each week focuses on different aspects, including backend practices, frontend integration, and DevOps, with practical exercises and key takeaways provided. The roadmap emphasizes hands-on practice, theory, and review to ensure a comprehensive learning experience.

Uploaded by

redasarhani02
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

30-Day Full-Stack Java Developer

Complete Learning Roadmap - Optimized &


Completed
From Spring Security to Production Deployment A Comprehensive Guide for Recent Software
Graduates

TABLE OF CONTENTS
Week 1: Spring Security & Testing Fundamentals (Days 1-7) Week 2: Advanced Backend & API
Best Practices (Days 8-14) Week 3: Modern Frontend & Integration (Days 15-21) Week 4:
DevOps, Advanced Topics & Portfolio Project (Days 22-30)

INTRODUCTION
Welcome to your optimized 30-day journey to becoming a competent full-stack Java developer.
This roadmap is designed for recent graduates who have mastered the basics (CRUD APIs, Spring
Boot, Thymeleaf, Bootstrap) and are ready to level up to industry standards.

WHAT YOU'LL BUILD:

Secured REST APIs with Spring Security and JWT authentication


Comprehensive test suites with unit and integration tests
Modern React frontend applications
Dockerized applications with CI/CD pipelines
A complete e-commerce portfolio project

HOW TO USE THIS ROADMAP: Each day is structured into:

1.
1. Theory & Concepts (1-2 hours)
2. Hands-On Practice (2-3 hours)
3. Review & Documentation (1 hour)

Aim for 4-6 hours of focused work per day. Quality over speed.

LEARNING METHODS USED THROUGHOUT:

CODE EXAMPLES -- working, copy-paste-ready code


PRACTICAL EXERCISES -- hands-on tasks
KEY TAKEAWAYS -- concepts to remember
RESOURCES -- official docs and trusted tutorials
MENTAL MODELS -- analogies to build intuition
COMMON MISTAKES -- pitfalls to avoid
WEEKLY REVIEWS -- consolidation checkpoints

WEEK 1: SPRING SECURITY &


TESTING FUNDAMENTALS
This week focuses on securing your applications and writing professional-grade tests.

DAY 1: SPRING SECURITY BASICS

Topics To Learn

Authentication vs Authorization
Authentication answers "Who are you?" while Authorization answers "What are you allowed to
do?"

Authentication: Verifying user identity (login with username/password)


Authorization: Determining user permissions (can this user delete posts?)
These work together but serve different purposes
Spring Security Architecture
Spring Security uses a chain of filters to intercept HTTP requests:

SecurityFilterChain: The main filter that processes all requests


SecurityContext: Holds the authenticated user's information
Authentication object: Contains user credentials and authorities
UserDetails: Interface representing user information

MENTAL MODEL: Think of Spring Security like a nightclub. The bouncer (SecurityFilterChain)
checks your ID (Authentication). Your wristband color (Authorization) determines which rooms
you can access.

CODE EXAMPLE: Basic Security Configuration

// [Link]

package [Link];

import [Link];

import [Link];

import [Link];

import [Link];

import [Link];

import [Link];

import [Link];

import [Link];

import [Link];

import [Link];

@Configuration

public class SecurityConfig {

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http)

throws Exception {

http
.authorizeHttpRequests(auth -> auth

.requestMatchers("/public/**").permitAll()

.requestMatchers("/admin/**").hasRole("ADMIN")

.anyRequest().authenticated()

.formLogin(form -> form

.defaultSuccessUrl("/dashboard", true)

.logout(logout -> logout

.logoutSuccessUrl("/")

);

return [Link]();

@Bean

public UserDetailsService userDetailsService() {

UserDetails user = [Link]()

.username("user")

.password(passwordEncoder().encode("password123"))

.roles("USER")

.build();

UserDetails admin = [Link]()

.username("admin")

.password(passwordEncoder().encode("admin123"))

.roles("ADMIN")

.build();

return new InMemoryUserDetailsManager(user, admin);

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();


}

PRACTICAL EXERCISE

1. Add Spring Security to your existing CRUD application's [Link]


2. Create the SecurityConfig class above
3. Start your application and try to access any endpoint -- you should be redirected to /login
4. Login with user/password123 -- should access user pages
5. Login with admin/admin123 -- should access admin pages
6. Try accessing /admin/* with user credentials -- should be denied

RESOURCES

Spring Security Reference: [Link]


Baeldung Spring Security: [Link]

KEY TAKEAWAYS

Spring Security automatically secures all endpoints by default


You must explicitly permit public endpoints with permitAll()
Always use BCryptPasswordEncoder for password hashing
In-memory users are only for development/testing
Roles follow the convention ROLE_* (Spring adds the prefix automatically)

DAY 2: FORM-BASED LOGIN & LOGOUT

Topics To Learn

Custom Login Forms with Thymeleaf


While Spring Security provides a default login page, you'll want custom forms matching your
application's branding.
CSRF Protection
Cross-Site Request Forgery (CSRF) tricks users into performing unwanted actions. Spring Security
automatically includes CSRF protection.

CSRF tokens prevent unauthorized POST requests


Thymeleaf automatically includes CSRF tokens in forms
Always use th:action for form submissions
CSRF can be disabled for stateless REST APIs

CODE EXAMPLE: Custom Login Page

<!-- [Link] -->

<!DOCTYPE html>

<html xmlns:th="[Link]

<head>

<title>Login</title>

<link href="[Link]

rel="stylesheet">

</head>

<body>

<div class="container mt-5">

<div class="row justify-content-center">

<div class="col-md-6">

<div class="card">

<div class="card-header"><h3>Login</h3></div>

<div class="card-body">

<div th:if="${[Link]}" class="alert alert-danger">

Invalid username or password

</div>

<div th:if="${[Link]}" class="alert alert-success">

You have been logged out

</div>

<form th:action="@{/login}" method="post">

<div class="mb-3">
<label for="username" class="form-label">Username</label>

<input type="text" class="form-control"

id="username" name="username" required>

</div>

<div class="mb-3">

<label for="password" class="form-label">Password</label>

<input type="password" class="form-control"

id="password" name="password" required>

</div>

<div class="mb-3 form-check">

<input type="checkbox" class="form-check-input"

id="remember-me" name="remember-me">

<label class="form-check-label" for="remember-me">

Remember Me

</label>

</div>

<button type="submit" class="btn btn-primary w-100">

Login

</button>

</form>

</div>

</div>

</div>

</div>

</div>

</body>

</html>

Updated [Link]:
@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http)

throws Exception {

http

.authorizeHttpRequests(auth -> auth

.requestMatchers("/login", "/css/**", "/js/**").permitAll()

.requestMatchers("/admin/**").hasRole("ADMIN")

.anyRequest().authenticated()

.formLogin(form -> form

.loginPage("/login")

.defaultSuccessUrl("/dashboard", true)

.permitAll()

.logout(logout -> logout

.logoutUrl("/logout")

.logoutSuccessUrl("/login?logout")

.invalidateHttpSession(true)

.deleteCookies("JSESSIONID")

.permitAll()

.rememberMe(remember -> remember

.key("uniqueAndSecret")

.tokenValiditySeconds(86400)

);

return [Link]();

[Link]:
package [Link];

import [Link];

import [Link];

@Controller

public class LoginController {

@GetMapping("/login")

public String login() {

return "login";

@GetMapping("/dashboard")

public String dashboard() {

return "dashboard";

Navbar with username display:


<!-- [Link] (Thymeleaf fragment) -->

<nav class="navbar navbar-expand-lg navbar-dark bg-dark"

xmlns:th="[Link]

xmlns:sec="[Link]

<div class="container-fluid">

<a class="navbar-brand" href="/">MyApp</a>

<div class="navbar-nav ms-auto">

<span class="navbar-text text-white me-3"

sec:authorize="isAuthenticated()">

Welcome, <span sec:authentication="name"></span>!

</span>

<form th:action="@{/logout}" method="post" class="d-inline">

<button type="submit" class="btn btn-outline-light btn-sm">

Logout

</button>

</form>

</div>

</div>

</nav>

PRACTICAL EXERCISE

Create the custom login page


Update SecurityConfig to use the custom login page
Create a LoginController
Add a navbar displaying the logged-in username
Implement a logout button
Test the "Remember Me" functionality
Style the login page to match your theme

RESOURCES

Form Login Docs: [Link]


/passwords/[Link]
Thymeleaf Spring Security: [Link]

KEY TAKEAWAYS

Custom login pages must be explicitly permitted in security config


CSRF tokens are automatically included by Thymeleaf with th:action
Remember-me uses a cookie to maintain sessions across browser closes
Logout should invalidate the session and delete cookies

DAY 3: DATABASE AUTHENTICATION


(UserDetailsService)

Topics To Learn

UserDetailsService Interface
The bridge between Spring Security and your database. It has one method: loadUserByUsername
(String username) that returns a UserDetails object. Spring Security calls this during authentication.

Password Encoding with BCrypt


Never store passwords in plain text. BCrypt is a one-way hashing function:

One-way hash: Cannot be reversed


Built-in salt: Each password gets a unique random salt
Adaptive: Computationally expensive to prevent brute-force

CODE EXAMPLES
User Entity:

// [Link]

@Entity

@Table(name = "users")

public class User {


@Id

@GeneratedValue(strategy = [Link])

private Long id;

@Column(unique = true, nullable = false)

private String username;

@Column(nullable = false)

private String password;

@Column(nullable = false)

private String email;

private boolean enabled = true;

@ManyToMany(fetch = [Link])

@JoinTable(

name = "user_roles",

joinColumns = @JoinColumn(name = "user_id"),

inverseJoinColumns = @JoinColumn(name = "role_id")

private Set<Role> roles;

public User() {}

public User(String username, String password, String email) {

[Link] = username;

[Link] = password;

[Link] = email;

// Getters and Setters

Role Entity:
// [Link]

@Entity

@Table(name = "roles")

public class Role {

@Id

@GeneratedValue(strategy = [Link])

private Long id;

@Column(unique = true, nullable = false)

private String name; // ROLE_USER, ROLE_ADMIN

@ManyToMany(mappedBy = "roles")

private Set<User> users;

public Role() {}

public Role(String name) { [Link] = name; }

// Getters and Setters

UserRepository and RoleRepository:

// [Link]

public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByUsername(String username);

boolean existsByUsername(String username);

boolean existsByEmail(String email);

// [Link]

public interface RoleRepository extends JpaRepository<Role, Long> {

Optional<Role> findByName(String name);

}
CustomUserDetailsService:

// [Link]

@Service

public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

public CustomUserDetailsService(UserRepository userRepository) {

[Link] = userRepository;

@Override

public UserDetails loadUserByUsername(String username)

throws UsernameNotFoundException {

User user = [Link](username)

.orElseThrow(() -> new UsernameNotFoundException(

"User not found: " + username));

Set<GrantedAuthority> authorities = [Link]().stream()

.map(role -> new SimpleGrantedAuthority([Link]()))

.collect([Link]());

return new [Link](

[Link](),

[Link](),

[Link](),

true, true, true,

authorities

);

UserService for Registration:


// [Link]

@Service

public class UserService {

private final UserRepository userRepository;

private final RoleRepository roleRepository;

private final PasswordEncoder passwordEncoder;

public UserService(UserRepository userRepository,

RoleRepository roleRepository,

PasswordEncoder passwordEncoder) {

[Link] = userRepository;

[Link] = roleRepository;

[Link] = passwordEncoder;

public User registerUser(String username, String password, String email) {

if ([Link](username))

throw new RuntimeException("Username already exists");

if ([Link](email))

throw new RuntimeException("Email already exists");

User user = new User();

[Link](username);

[Link]([Link](password));

[Link](email);

Role userRole = [Link]("ROLE_USER")

.orElseThrow(() -> new RuntimeException("Role not found"));

[Link]([Link](userRole));

return [Link](user);

}
DataInitializer:

// [Link]

@Configuration

public class DataInitializer {

@Bean

CommandLineRunner initDatabase(RoleRepository roleRepository) {

return args -> {

if ([Link]("ROLE_USER").isEmpty())

[Link](new Role("ROLE_USER"));

if ([Link]("ROLE_ADMIN").isEmpty())

[Link](new Role("ROLE_ADMIN"));

};

PRACTICAL EXERCISE

Create User and Role entities


Create UserRepository and RoleRepository
Implement CustomUserDetailsService
Create UserService with registration
Update SecurityConfig to use CustomUserDetailsService
Create a registration page and controller
Seed the database with roles
Test registration and login with database users

KEY TAKEAWAYS

UserDetailsService is called during authentication to load user data


Always use EAGER fetching for roles to avoid LazyInitializationException
BCrypt automatically generates and includes salt in the hash
Role names follow ROLE_* convention
Never store passwords in plain text
DAY 4: METHOD-LEVEL SECURITY &
AUTHORIZATION

Topics To Learn

@PreAuthorize and @Secured Annotations

@PreAuthorize: SpEL expressions for complex authorization logic


@Secured: Simple role-based access
@PostAuthorize: Check permissions after method execution
@RolesAllowed: Java standard (JSR-250)

CODE EXAMPLES
Enable Method Security:

@Configuration

@EnableMethodSecurity

public class MethodSecurityConfig {

// Method security is now enabled

Service with Method-Level Security:


@Service

public class BlogPostService {

@PreAuthorize("hasRole('ADMIN')")

public void deletePost(Long id) {

[Link](id);

@PreAuthorize("hasRole('USER')")

public Post createPost(Post post) {

return [Link](post);

@PreAuthorize("#username == [Link]")

public void updateUserProfile(String username, Profile profile) {

[Link](username, profile);

@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")

public List<Post> getAllPosts() {

return [Link]();

@PostAuthorize("[Link] == [Link]")

public Post getPostById(Long id) {

return [Link](id).orElseThrow();

SpEL Expression Reference:

hasRole('ADMIN') -- user has ADMIN role


hasAnyRole('ADMIN', 'USER') -- user has any of these roles
#username == [Link] -- param matches logged-in user
isAuthenticated() -- user is logged in
hasAuthority('WRITE_PRIVILEGE') -- user has specific authority

PRACTICAL EXERCISE

Enable method security


Add @PreAuthorize to service methods
Implement role hierarchy (ADMIN > USER)
Create admin-only CRUD operations
Test method security with different user roles
Write tests for secured methods

KEY TAKEAWAYS

Method-level security provides fine-grained access control


@PreAuthorize is more flexible than @Secured
SpEL expressions allow complex authorization logic
Always test secured methods with different user roles
Method security works at the service layer, not just URLs

DAY 5: UNIT TESTING WITH JUNIT 5

Topics To Learn

JUnit 5 Fundamentals
Annotations:

@Test -- marks a method as a test


@BeforeEach -- runs before each test
@AfterEach -- runs after each test
@BeforeAll -- runs once before all tests (must be static)
@AfterAll -- runs once after all tests (must be static)
@DisplayName -- provides a custom test name
@Disabled -- skips a test
Assertions:

assertEquals(expected, actual)
assertTrue(condition) / assertFalse(condition)
assertNull(object) / assertNotNull(object)
assertThrows([Link], () -> {})
assertAll(() -> {}, () -> {}) -- group multiple assertions

MENTAL MODEL: Think of tests as a contract. Each test says "given these inputs, I guarantee this
output." The AAA pattern (Arrange-Act-Assert) is your template for every contract.

CODE EXAMPLES

// [Link]

import [Link].*;

import static [Link].*;

class CalculatorTest {

private Calculator calculator;

@BeforeEach

void setUp() {

calculator = new Calculator();

@Test

@DisplayName("Test addition of two positive numbers")

void testAddition() {

// Arrange

int a = 5, b = 3;

// Act

int result = [Link](a, b);

// Assert

assertEquals(8, result, "5 + 3 should equal 8");


}

@Test

void testDivisionByZero() {

assertThrows([Link], () ->

[Link](10, 0)

);

@Test

void testMultipleAssertions() {

assertAll("calculator operations",

() -> assertEquals(4, [Link](2, 2)),

() -> assertEquals(0, [Link](2, 2)),

() -> assertEquals(4, [Link](2, 2)),

() -> assertEquals(1, [Link](2, 2))

);

PRACTICAL EXERCISE

Write 10 unit tests for a utility class


Test both happy path and edge cases
Use @BeforeEach for test setup
Practice AAA pattern
Test exception handling with assertThrows
Use assertAll for multiple related assertions
Add @DisplayName for readable test names

RESOURCES

JUnit 5 User Guide: [Link]


Effective Unit Testing: [Link]
KEY TAKEAWAYS

Follow AAA pattern: Arrange, Act, Assert


One logical concept per test
Use descriptive test names
Test both happy path and edge cases
Keep tests fast and independent
Tests are documentation of how code should work

DAY 6: MOCKITO FUNDAMENTALS

Topics To Learn

Why Mocking?
Testing services often requires dependencies. Mocking allows you to:

Test classes in isolation


Create fast tests that don't hit the database
Control dependency behavior
Verify method calls and interactions

Mockito Basics:

@Mock -- creates a mock object


@InjectMocks -- creates object with mocked dependencies injected
when(...).thenReturn(...) -- stub method behavior
verify(...) -- verify method was called
any(), anyString(), anyLong() -- argument matchers

CODE EXAMPLES

// [Link]

@ExtendWith([Link])

class UserServiceTest {
@Mock private UserRepository userRepository;

@Mock private PasswordEncoder passwordEncoder;

@Mock private EmailService emailService;

@InjectMocks private UserService userService;

@Test

void testRegisterUser_Success() {

// Arrange

String username = "testuser", password = "password123",

email = "test@[Link]";

when([Link](username)).thenReturn(false);

when([Link](email)).thenReturn(false);

when([Link](password)).thenReturn("hashedPassword");

User savedUser = new User(username, "hashedPassword", email);

when([Link](any([Link]))).thenReturn(savedUser);

// Act

User result = [Link](username, password, email);

// Assert

assertNotNull(result);

assertEquals(username, [Link]());

verify(userRepository, times(1)).existsByUsername(username);

verify(passwordEncoder, times(1)).encode(password);

verify(userRepository, times(1)).save(any([Link]));

@Test

void testRegisterUser_UsernameExists() {

when([Link]("existing")).thenReturn(true);
assertThrows([Link], () ->

[Link]("existing", "pass", "email@[Link]")

);

verify(userRepository, never()).save(any([Link]));

@Test

void testDeleteUser() {

doNothing().when(userRepository).deleteById(1L);

[Link](1L);

verify(userRepository, times(1)).deleteById(1L);

PRACTICAL EXERCISE

Write unit tests for UserService using Mockito


Mock the UserRepository dependency
Test at least 5 different service methods
Use when().thenReturn() for stubbing
Verify method calls with verify()
Test exception scenarios
Practice using argument matchers

RESOURCES

Mockito Documentation: [Link]


/[Link]
Baeldung Mockito Tutorial: [Link]

KEY TAKEAWAYS

Mockito allows testing in isolation


@Mock creates mock objects, @InjectMocks injects them
when().thenReturn() stubs method behavior
verify() ensures methods were called as expected
Mock only external dependencies, not the class under test

DAY 7: INTEGRATION TESTING WITH


SPRING BOOT

Topics To Learn

Integration Testing vs Unit Testing:

Unit tests: Test individual classes in isolation


Integration tests: Test multiple components working together
Integration tests verify the full stack (controller -> service -> repository)

Spring Boot Testing Annotations:

@SpringBootTest -- loads full application context


@WebMvcTest -- only loads web layer (controllers)
@DataJpaTest -- only loads JPA components
@MockBean -- creates and injects mock beans

MockMvc:

Simulates HTTP requests without starting a server


Tests controllers and request/response handling
Verifies HTTP status codes, headers, and response bodies

CODE EXAMPLES

// [Link]

@SpringBootTest

@AutoConfigureMockMvc

@Transactional

class UserControllerIntegrationTest {
@Autowired private MockMvc mockMvc;

@Test

void testGetAllUsers() throws Exception {

[Link](get("/api/users"))

.andExpect(status().isOk())

.andExpect(content().contentType(MediaType.APPLICATION_JSON))

.andExpect(jsonPath("$", hasSize(greaterThan(0))));

@Test

void testCreateUser() throws Exception {

String userJson = """

"username": "newuser",

"password": "password123",

"email": "newuser@[Link]"

""";

[Link](post("/api/users")

.contentType(MediaType.APPLICATION_JSON)

.content(userJson))

.andExpect(status().isCreated())

.andExpect(jsonPath("$.username").value("newuser"));

@Test

void testGetUserById_NotFound() throws Exception {

[Link](get("/api/users/999"))

.andExpect(status().isNotFound());

@Test
void testValidationError() throws Exception {

String invalidUser = """

"username": "ab",

"password": "123",

"email": "invalid-email"

""";

[Link](post("/api/users")

.contentType(MediaType.APPLICATION_JSON)

.content(invalidUser))

.andExpect(status().isBadRequest())

.andExpect(jsonPath("$.errors").exists());

Repository Layer Testing:


@DataJpaTest

class UserRepositoryTest {

@Autowired private UserRepository userRepository;

@Autowired private TestEntityManager entityManager;

@Test

void testFindByUsername() {

User user = new User("testuser", "pass", "test@[Link]");

[Link](user);

[Link]();

Optional<User> found = [Link]("testuser");

assertTrue([Link]());

assertEquals("testuser", [Link]().getUsername());

PRACTICAL EXERCISE

Write integration tests for 3 REST endpoints


Test POST, GET, PUT, DELETE operations
Verify HTTP status codes and response bodies
Test validation errors (400)
Test not found scenarios (404)
Use @Transactional to rollback test data

WEEK 1 REVIEW CHECKPOINT


Before moving to Week 2, confirm you can:

[ ] Configure Spring Security with custom login


[ ] Authenticate users from a database with UserDetailsService
[ ] Use @PreAuthorize for method-level security
[ ] Write unit tests using JUnit 5 AAA pattern
[ ] Mock dependencies with Mockito
[ ] Write integration tests with MockMvc

KEY TAKEAWAYS

Integration tests verify the full application stack


@SpringBootTest loads the entire context
@WebMvcTest is lighter for controller testing
MockMvc simulates HTTP requests without starting a server
Use @Transactional to rollback database changes

WEEK 2: ADVANCED BACKEND &


API BEST PRACTICES

DAY 8: DTOs AND MAPSTRUCT

Topics To Learn

Why DTOs?

Separate API contracts from database entities


Hide internal implementation details
Control exactly what data is exposed
Prevent over-fetching and circular dependencies
Validate input data separately

MapStruct:

Compile-time object mapper


Type-safe and fast
Generates implementation code automatically
Cleaner than manual mapping

MENTAL MODEL: Think of your Entity as the internal blueprint of a building (with wiring,
pipes, load-bearing walls). The DTO is the floor plan you hand to clients -- showing only what's
relevant, hiding what's dangerous to expose.

CODE EXAMPLES
Entity vs DTO:
// Entity (internal)

@Entity

public class User {

@Id private Long id;

private String username;

private String password; // NEVER expose!

private String email;

@ManyToMany private Set<Role> roles;

private LocalDateTime createdAt;

// UserDTO (read -- external API)

public class UserDTO {

private Long id;

private String username;

private String email;

private List<String> roles;

// No password field!

// CreateUserDTO (write -- for registration)

public class CreateUserDTO {

@NotBlank

@Size(min = 3, max = 20)

private String username;

@NotBlank

@Size(min = 8)

private String password;

@Email

private String email;

}
MapStruct Mapper:

<!-- [Link] -->

<dependency>

<groupId>[Link]</groupId>

<artifactId>mapstruct</artifactId>

<version>[Link]</version>

</dependency>

<dependency>

<groupId>[Link]</groupId>

<artifactId>mapstruct-processor</artifactId>

<version>[Link]</version>

<scope>provided</scope>

</dependency>
@Mapper(componentModel = "spring")

public interface UserMapper {

@Mapping(target = "roles",

expression = "java(mapRoles([Link]()))")

UserDTO toDTO(User user);

List<UserDTO> toDTOList(List<User> users);

@Mapping(target = "id", ignore = true)

@Mapping(target = "createdAt", ignore = true)

@Mapping(target = "roles", ignore = true)

User toEntity(CreateUserDTO dto);

default List<String> mapRoles(Set<Role> roles) {

return [Link]()

.map(Role::getName)

.collect([Link]());

Controller using DTOs:


@RestController

@RequestMapping("/api/users")

public class UserController {

private final UserService userService;

private final UserMapper userMapper;

@GetMapping

public List<UserDTO> getAllUsers() {

return [Link]([Link]());

@GetMapping("/{id}")

public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {

return [Link](id)

.map(userMapper::toDTO)

.map(ResponseEntity::ok)

.orElse([Link]().build());

@PostMapping

public ResponseEntity<UserDTO> createUser(

@Valid @RequestBody CreateUserDTO dto) {

User user = [Link](dto);

User saved = [Link](user);

return [Link]([Link])

.body([Link](saved));

PRACTICAL EXERCISE

Create DTOs for all your entities


Set up MapStruct in your project
Create mapper interfaces for entities <-> DTOs
Refactor controllers to use DTOs
Add validation annotations to DTOs
Test the mapping with unit tests

RESOURCES

MapStruct Documentation: [Link]


DTO Pattern: [Link]

KEY TAKEAWAYS

Never expose entities directly in REST APIs


DTOs control exactly what data is shared
MapStruct generates type-safe mappers at compile time
Use separate DTOs for create/update operations
Validate DTOs, not entities

DAY 9: EXCEPTION HANDLING &


VALIDATION

Topics To Learn

Global Exception Handling:

@ControllerAdvice -- global exception handler class


@ExceptionHandler -- handles specific exception types
Return consistent error responses across all endpoints

Bean Validation:

@Valid -- triggers validation on request body


@NotNull, @NotBlank, @Size, @Email, @Min, @Max, @Pattern
Custom validators for complex rules
CODE EXAMPLES
Custom Exceptions:

public class ResourceNotFoundException extends RuntimeException {

public ResourceNotFoundException(String message) {

super(message);

public class DuplicateResourceException extends RuntimeException {

public DuplicateResourceException(String message) {

super(message);

Error Response DTO:

public class ErrorResponse {

private LocalDateTime timestamp;

private int status;

private String error;

private String message;

private String path;

private Map<String, String> validationErrors;

// Constructors, getters, setters

Global Exception Handler:

@RestControllerAdvice

public class GlobalExceptionHandler {

@ExceptionHandler([Link])
public ResponseEntity<ErrorResponse> handleNotFound(

ResourceNotFoundException ex, WebRequest request) {

ErrorResponse error = new ErrorResponse();

[Link]([Link]());

[Link](HttpStatus.NOT_FOUND.value());

[Link]("Not Found");

[Link]([Link]());

[Link]([Link](false));

return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);

@ExceptionHandler([Link])

public ResponseEntity<ErrorResponse> handleDuplicate(

DuplicateResourceException ex, WebRequest request) {

ErrorResponse error = new ErrorResponse();

[Link]([Link]());

[Link]([Link]());

[Link]("Conflict");

[Link]([Link]());

[Link]([Link](false));

return new ResponseEntity<>(error, [Link]);

@ExceptionHandler([Link])

public ResponseEntity<ErrorResponse> handleValidationErrors(

MethodArgumentNotValidException ex, WebRequest request) {

Map<String, String> errors = new HashMap<>();

[Link]().getFieldErrors().forEach(e ->

[Link]([Link](), [Link]())

);

ErrorResponse error = new ErrorResponse();

[Link]([Link]());

[Link](HttpStatus.BAD_REQUEST.value());

[Link]("Validation Failed");

[Link]("Invalid input data");


[Link]([Link](false));

[Link](errors);

return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);

@ExceptionHandler([Link])

public ResponseEntity<ErrorResponse> handleGlobal(

Exception ex, WebRequest request) {

ErrorResponse error = new ErrorResponse();

[Link]([Link]());

[Link](HttpStatus.INTERNAL_SERVER_ERROR.value());

[Link]("Internal Server Error");

[Link]("An unexpected error occurred");

[Link]([Link](false));

return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);

Validation in DTOs:
public class CreateUserDTO {

@NotBlank(message = "Username is required")

@Size(min = 3, max = 20, message = "Username must be 3-20 characters")

@Pattern(regexp = "^[a-zA-Z0-9_]+$",

message = "Username can only contain letters, numbers, underscores")

private String username;

@NotBlank(message = "Password is required")

@Size(min = 8, message = "Password must be at least 8 characters")

private String password;

@NotBlank(message = "Email is required")

@Email(message = "Email must be valid")

private String email;

@NotNull

@Min(value = 18, message = "Age must be at least 18")

@Max(value = 120, message = "Age must be realistic")

private Integer age;

Custom Validator:
// Annotation

@Target({[Link]})

@Retention([Link])

@Constraint(validatedBy = [Link])

public @interface UniqueUsername {

String message() default "Username already exists";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

// Validator

public class UniqueUsernameValidator

implements ConstraintValidator<UniqueUsername, String> {

@Autowired

private UserRepository userRepository;

@Override

public boolean isValid(String username,

ConstraintValidatorContext context) {

if (username == null) return true;

return ![Link](username);

PRACTICAL EXERCISE

Create custom exception classes


Implement @RestControllerAdvice for global exception handling
Create ErrorResponse DTO
Add validation annotations to all DTOs
Test validation errors (400)
Create a custom validator
Return proper HTTP status codes
KEY TAKEAWAYS

Always use @ControllerAdvice for global exception handling


Return consistent error responses
Use proper HTTP status codes (400, 404, 409, 500)
Validate all input data with Bean Validation
Create custom validators for complex logic

DAY 10: REST API BEST PRACTICES

Topics To Learn

HTTP Status Codes:

200 OK -- successful GET, PUT, PATCH


201 Created -- successful POST
204 No Content -- successful DELETE
400 Bad Request -- validation errors
401 Unauthorized -- not authenticated
403 Forbidden -- not authorized
404 Not Found -- resource doesn't exist
409 Conflict -- duplicate resource
500 Internal Server Error

Resource Naming:

Use nouns, not verbs: /users not /getUsers


Use plural nouns: /users not /user
Use kebab-case: /blog-posts not /blogPosts
Hierarchical: /users/1/posts

CODE EXAMPLES
Proper HTTP Status Codes in Controller:

@RestController
@RequestMapping("/api/users")

public class UserController {

@GetMapping

public ResponseEntity<List<UserDTO>> getAllUsers() {

return [Link]([Link]()); // 200

@GetMapping("/{id}")

public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {

return [Link](id)

.map(ResponseEntity::ok) // 200

.orElse([Link]().build()); // 404

@PostMapping

public ResponseEntity<UserDTO> createUser(

@Valid @RequestBody CreateUserDTO dto) {

UserDTO created = [Link](dto);

return ResponseEntity

.status([Link]) // 201

.header("Location", "/api/users/" + [Link]())

.body(created);

@PutMapping("/{id}")

public ResponseEntity<UserDTO> updateUser(

@PathVariable Long id,

@Valid @RequestBody UpdateUserDTO dto) {

return [Link]([Link](id, dto)); // 200

@DeleteMapping("/{id}")

public ResponseEntity<Void> deleteUser(@PathVariable Long id) {

[Link](id);
return [Link]().build(); // 204

Pagination:

@GetMapping

public ResponseEntity<PageResponse<UserDTO>> getAllUsers(

@RequestParam(defaultValue = "0") int page,

@RequestParam(defaultValue = "10") int size,

@RequestParam(defaultValue = "id") String sortBy,

@RequestParam(defaultValue = "ASC") String direction) {

[Link] sortDirection = [Link](direction);

Pageable pageable = [Link](page, size,

[Link](sortDirection, sortBy));

Page<UserDTO> users = [Link](pageable);

return [Link]([Link](users));

Custom PageResponse:
public class PageResponse<T> {

private List<T> content;

private int pageNumber;

private int pageSize;

private long totalElements;

private int totalPages;

private boolean isFirst;

private boolean isLast;

public static <T> PageResponse<T> of(Page<T> page) {

PageResponse<T> response = new PageResponse<>();

[Link]([Link]());

[Link]([Link]());

[Link]([Link]());

[Link]([Link]());

[Link]([Link]());

[Link]([Link]());

[Link]([Link]());

return response;

API Versioning:

@RestController

@RequestMapping("/api/v1/users")

public class UserControllerV1 { /* Version 1 */ }

@RestController

@RequestMapping("/api/v2/users")

public class UserControllerV2 { /* Version 2 */ }


PRACTICAL EXERCISE

Refactor all endpoints to use proper HTTP status codes


Implement pagination for list endpoints
Add sorting and filtering
Use proper resource naming conventions
Implement API versioning
Add Location header to POST responses

KEY TAKEAWAYS

Use nouns for resource names, not verbs


Return proper HTTP status codes
Always paginate list endpoints
Version your API for evolution
Return Location header for created resources

DAY 11: SWAGGER / OpenAPI


DOCUMENTATION

Topics To Learn

Why API Documentation?

Helps frontend developers understand your API


Provides interactive testing interface
Acts as living documentation
Auto-generates client code

SpringDoc OpenAPI:

Modern OpenAPI 3 for Spring Boot


Auto-generates from annotations
Provides Swagger UI for testing
CODE EXAMPLES
Setup:

<dependency>

<groupId>[Link]</groupId>

<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>

<version>2.2.0</version>

</dependency>

OpenAPI Configuration:

@Configuration

public class OpenAPIConfig {

@Bean

public OpenAPI customOpenAPI() {

return new OpenAPI()

.info(new Info()

.title("User Management API")

.version("1.0")

.description("API for managing users, posts, and comments")

.contact(new Contact()

.name("Your Name")

.email("you@[Link]"))

.license(new License().name("Apache 2.0")));

Documented Controller:

@RestController

@RequestMapping("/api/users")

@Tag(name = "User Management", description = "APIs for managing users")


public class UserController {

@Operation(summary = "Get all users",

description = "Retrieve a paginated list of all users")

@ApiResponses({

@ApiResponse(responseCode = "200",

description = "Successfully retrieved users"),

@ApiResponse(responseCode = "401",

description = "Not authenticated")

})

@GetMapping

public ResponseEntity<PageResponse<UserDTO>> getAllUsers(

@Parameter(description = "Page number (0-indexed)")

@RequestParam(defaultValue = "0") int page,

@Parameter(description = "Items per page")

@RequestParam(defaultValue = "10") int size) {

// ...

@Operation(summary = "Get user by ID")

@ApiResponses({

@ApiResponse(responseCode = "200", description = "User found"),

@ApiResponse(responseCode = "404", description = "User not found")

})

@GetMapping("/{id}")

public ResponseEntity<UserDTO> getUserById(

@Parameter(description = "User ID", required = true)

@PathVariable Long id) {

// ...

Documented DTO:
@Schema(description = "User data transfer object")

public class UserDTO {

@Schema(description = "User ID", example = "1",

accessMode = [Link].READ_ONLY)

private Long id;

@Schema(description = "Username", example = "johndoe")

private String username;

@Schema(description = "Email address", example = "john@[Link]")

private String email;

Permit Swagger UI in Security Config:

.requestMatchers(

"/v3/api-docs/**",

"/swagger-ui/**",

"/[Link]"

).permitAll()

Access at: [Link]

PRACTICAL EXERCISE

Add SpringDoc dependency


Configure OpenAPI with project info
Document all REST endpoints with @Operation
Add @ApiResponse for all response codes
Document DTOs with @Schema
Test API through Swagger UI
Add examples to all parameters
KEY TAKEAWAYS

API documentation is essential for team collaboration


SpringDoc auto-generates from code annotations
Document all response codes -- including errors
Swagger UI provides interactive testing
Keep documentation in sync with code

DAY 12: CACHING WITH SPRING CACHE

Topics To Learn

Why Caching?

Improve performance by avoiding repeated database calls


Reduce database load
Faster response times for frequently accessed data

Spring Cache Annotations:

@Cacheable -- cache method results


@CachePut -- update cache on writes
@CacheEvict -- remove from cache
@Caching -- combine multiple cache operations

CODE EXAMPLES
Setup:
<dependency>

<groupId>[Link]</groupId>

<artifactId>spring-boot-starter-cache</artifactId>

</dependency>

<dependency>

<groupId>[Link]</groupId>

<artifactId>caffeine</artifactId>

</dependency>

Enable Caching:

@SpringBootApplication

@EnableCaching

public class Application { ... }

Caffeine Cache Config:

@Configuration

public class CacheConfig {

@Bean

public CacheManager cacheManager() {

CaffeineCacheManager cacheManager =

new CaffeineCacheManager("users", "posts", "products");

[Link]([Link]()

.expireAfterWrite(10, [Link])

.maximumSize(1000)

.recordStats());

return cacheManager;

Service with Caching:


@Service

public class UserService {

@Cacheable(value = "users", key = "#id")

public User findById(Long id) {

return [Link](id)

.orElseThrow(() ->

new ResourceNotFoundException("User not found"));

@CachePut(value = "users", key = "#[Link]")

public User update(Long id, UpdateUserDTO dto) {

User user = findById(id);

[Link]([Link]());

return [Link](user);

@CacheEvict(value = "users", key = "#id")

public void delete(Long id) {

[Link](id);

@CacheEvict(value = "users", allEntries = true)

public User create(CreateUserDTO dto) {

User user = [Link](dto);

return [Link](user);

PRACTICAL EXERCISE

Enable caching in your application


Add @Cacheable to frequently accessed methods
Implement cache eviction on updates/deletes
Configure Caffeine with expiration
Test cache hit/miss with logging
Measure performance improvement

KEY TAKEAWAYS

Caching dramatically improves performance


Use @Cacheable for reads, @CacheEvict for deletes, @CachePut for updates
Configure expiration to prevent stale data
Monitor cache hit rates
Don't cache everything -- only frequently accessed, slow-changing data

DAY 13: LOGGING BEST PRACTICES

Topics To Learn

Log Levels:

TRACE -- very detailed, rarely used


DEBUG -- detailed debugging information
INFO -- general informational messages
WARN -- potential problems
ERROR -- error events

SLF4J and Logback:

SLF4J: Simple Logging Facade (interface)


Logback: Default implementation in Spring Boot

CODE EXAMPLES
Basic Logging:
@Slf4j // Lombok annotation -- no need to declare Logger manually

@Service

public class UserService {

public User create(CreateUserDTO dto) {

[Link]("Creating new user: {}", [Link]());

try {

User saved = [Link]([Link](dto));

[Link]("User created successfully with ID: {}", [Link]());

return saved;

} catch (Exception e) {

[Link]("Failed to create user: {}", [Link](), e);

throw e;

public void delete(Long id) {

[Link]("Deleting user with ID: {}", id);

[Link](id);

Request/Response Logging Filter:


@Component

public class LoggingFilter extends OncePerRequestFilter {

private static final Logger log =

[Link]([Link]);

@Override

protected void doFilterInternal(HttpServletRequest request,

HttpServletResponse response,

FilterChain filterChain)

throws ServletException, IOException {

long startTime = [Link]();

[Link]("Incoming: {} {}", [Link](),

[Link]());

[Link](request, response);

long duration = [Link]() - startTime;

[Link]("Completed: {} {} -- Status: {} -- {}ms",

[Link](), [Link](),

[Link](), duration);

Logback Configuration ([Link]):


<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<appender name="CONSOLE"

class="[Link]">

<encoder>

<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>

</encoder>

</appender>

<appender name="FILE"

class="[Link]">

<file>logs/[Link]</file>

<encoder>

<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>

</encoder>

<rollingPolicy

class="[Link]">

<fileNamePattern>logs/application-%d{yyyy-MM-dd}.log</fileNamePattern>

<maxHistory>30</maxHistory>

<totalSizeCap>3GB</totalSizeCap>

</rollingPolicy>

</appender>

<logger name="[Link]" level="DEBUG"/>

<logger name="[Link]" level="INFO"/>

<logger name="[Link]" level="DEBUG"/>

<root level="INFO">

<appender-ref ref="CONSOLE"/>

<appender-ref ref="FILE"/>

</root>

</configuration>
PRACTICAL EXERCISE

Replace all [Link] with proper logging


Configure Logback with console and file appenders
Set appropriate log levels per package
Add request/response logging filter
Use structured logging for important events
Test log rotation

KEY TAKEAWAYS

Never use [Link] in production


Use appropriate log levels (DEBUG, INFO, WARN, ERROR)
Include context in log messages (user ID, operation)
Configure log rotation to manage disk space
Never log sensitive data (passwords, tokens)

DAY 14: JWT AUTHENTICATION


[PREVIOUSLY MISSING -- NOW COMPLETE]

Topics To Learn

What is JWT?
JSON Web Token (JWT) is a compact, self-contained token for securely transmitting information
between parties. JWTs are used for stateless authentication -- the server doesn't need to store
session data.

A JWT has three parts:

Header: Algorithm and token type


Payload: Claims (user ID, roles, expiry)
Signature: Verifies the token hasn't been tampered with
MENTAL MODEL: Think of a JWT like a sealed, signed envelope from the government. Anyone
can read the address on the front (header/payload), but only the government can create a valid
stamp (signature). You trust the contents because the stamp is genuine.

JWT Flow:

1. User logs in with username/password


2. Server validates credentials, returns a signed JWT
3. Client stores JWT (localStorage or cookie)
4. Client sends JWT in "Authorization: Bearer <token>" header
5. Server validates the JWT signature on each request -- no DB lookup needed

CODE EXAMPLES
Add dependency:

<dependency>

<groupId>[Link]</groupId>

<artifactId>jjwt-api</artifactId>

<version>0.11.5</version>

</dependency>

<dependency>

<groupId>[Link]</groupId>

<artifactId>jjwt-impl</artifactId>

<version>0.11.5</version>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>[Link]</groupId>

<artifactId>jjwt-jackson</artifactId>

<version>0.11.5</version>

<scope>runtime</scope>

</dependency>

[Link]:
[Link]=your-very-long-secret-key-at-least-256-bits-long-please-change-this

[Link]=86400000

JwtService:

// [Link]

@Service

public class JwtService {

@Value("${[Link]}")

private String secretKey;

@Value("${[Link]}")

private long expirationMs;

private Key getSigningKey() {

byte[] keyBytes = [Link](secretKey);

return [Link](keyBytes);

public String generateToken(UserDetails userDetails) {

return [Link]()

.setSubject([Link]())

.setIssuedAt(new Date())

.setExpiration(

new Date([Link]() + expirationMs))

.signWith(getSigningKey(), SignatureAlgorithm.HS256)

.compact();

public String extractUsername(String token) {

return extractClaim(token, Claims::getSubject);

public boolean isTokenValid(String token, UserDetails userDetails) {


final String username = extractUsername(token);

return [Link]([Link]())

&& !isTokenExpired(token);

private boolean isTokenExpired(String token) {

return extractClaim(token, Claims::getExpiration).before(new Date());

private <T> T extractClaim(String token,

Function<Claims, T> claimsResolver) {

Claims claims = [Link]()

.setSigningKey(getSigningKey())

.build()

.parseClaimsJws(token)

.getBody();

return [Link](claims);

JWT Authentication Filter:

// [Link]

@Component

@RequiredArgsConstructor

public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtService jwtService;

private final UserDetailsService userDetailsService;

@Override

protected void doFilterInternal(HttpServletRequest request,

HttpServletResponse response,

FilterChain filterChain)

throws ServletException, IOException {


final String authHeader = [Link]("Authorization");

if (authHeader == null || ![Link]("Bearer ")) {

[Link](request, response);

return;

String jwt = [Link](7);

String username = [Link](jwt);

if (username != null &&

[Link]().getAuthentication() == null) {

UserDetails userDetails =

[Link](username);

if ([Link](jwt, userDetails)) {

UsernamePasswordAuthenticationToken authToken =

new UsernamePasswordAuthenticationToken(

userDetails, null, [Link]());

[Link](

new WebAuthenticationDetailsSource()

.buildDetails(request));

[Link]()

.setAuthentication(authToken);

[Link](request, response);

Updated SecurityConfig for JWT (Stateless):


@Configuration

@EnableWebSecurity

@RequiredArgsConstructor

public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthFilter;

private final UserDetailsService userDetailsService;

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http)

throws Exception {

http

.csrf(csrf -> [Link]())

.authorizeHttpRequests(auth -> auth

.requestMatchers("/api/auth/**").permitAll()

.requestMatchers("/v3/api-docs/**",

"/swagger-ui/**").permitAll()

.requestMatchers("/api/admin/**").hasRole("ADMIN")

.anyRequest().authenticated()

.sessionManagement(session -> session

.sessionCreationPolicy([Link])

.authenticationProvider(authenticationProvider())

.addFilterBefore(jwtAuthFilter,

[Link]);

return [Link]();

@Bean

public AuthenticationProvider authenticationProvider() {

DaoAuthenticationProvider provider = new DaoAuthenticationProvider();

[Link](userDetailsService);

[Link](passwordEncoder());
return provider;

@Bean

public AuthenticationManager authenticationManager(

AuthenticationConfiguration config) throws Exception {

return [Link]();

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

Auth DTOs and Controller:

// Request/Response DTOs

public class LoginRequest {

@NotBlank private String username;

@NotBlank private String password;

public class AuthResponse {

private String token;

private String username;

private List<String> roles;

// [Link]

@RestController

@RequestMapping("/api/auth")

@RequiredArgsConstructor

public class AuthController {


private final AuthenticationManager authenticationManager;

private final UserDetailsService userDetailsService;

private final JwtService jwtService;

private final UserService userService;

@PostMapping("/login")

public ResponseEntity<AuthResponse> login(

@Valid @RequestBody LoginRequest request) {

[Link](

new UsernamePasswordAuthenticationToken(

[Link](), [Link]()));

UserDetails userDetails =

[Link]([Link]());

String token = [Link](userDetails);

List<String> roles = [Link]().stream()

.map(GrantedAuthority::getAuthority)

.collect([Link]());

return [Link](

new AuthResponse(token, [Link](), roles));

@PostMapping("/register")

public ResponseEntity<UserDTO> register(

@Valid @RequestBody CreateUserDTO dto) {

User user = [Link](

[Link](), [Link](), [Link]());

return [Link]([Link])

.body([Link](user));

Testing JWT-secured endpoints (curl):


# Register

curl -X POST [Link] \

-H "Content-Type: application/json" \

-d '{"username":"john","password":"password123","email":"john@[Link]"}'

# Login -- get token

curl -X POST [Link] \

-H "Content-Type: application/json" \

-d '{"username":"john","password":"password123"}'

# Use token

curl [Link] \

-H "Authorization: Bearer <your-token-here>"

COMMON MISTAKES

Storing the JWT secret in code (use [Link] or env vars)


Using a short or weak secret key (must be at least 256 bits)
Not validating token expiry
Storing the token in an insecure location on the frontend

PRACTICAL EXERCISE

Add JWT dependencies


Implement JwtService for token generation and validation
Create JwtAuthenticationFilter
Update SecurityConfig for stateless authentication
Create AuthController with /login and /register
Test with curl or Postman
Add token expiry handling

WEEK 2 REVIEW CHECKPOINT


Before moving to Week 3, confirm you can:
[ ] Create and use DTOs with MapStruct mappers
[ ] Handle exceptions globally with @RestControllerAdvice
[ ] Return correct HTTP status codes for every scenario
[ ] Document APIs with Swagger/OpenAPI
[ ] Cache method results with Spring Cache + Caffeine
[ ] Write structured log messages at correct levels
[ ] Implement full JWT authentication (login -> token -> secured request)

KEY TAKEAWAYS

JWT is stateless -- the server stores no session data


Always validate both the signature and expiry
Use "Authorization: Bearer <token>" header
Disable CSRF for stateless REST APIs
Store the JWT secret securely (env variables, not hardcoded)

WEEK 3: MODERN FRONTEND &


INTEGRATION

DAY 15: REACT FUNDAMENTALS -- SETUP &


COMPONENTS

Topics To Learn

React Basics:

Component-based architecture
JSX syntax
Props (data passed in) and State (data owned internally)
Functional components (modern approach)
MENTAL MODEL: Think of React components like LEGO bricks. Each brick (component) has a
specific shape (props it accepts) and can be assembled into larger structures. You build complex
UIs by combining simple, reusable pieces.

CODE EXAMPLES
Setup with Vite:

npm create vite@latest my-app -- --template react

cd my-app

npm install

npm run dev

Basic Component:

// [Link]

function HelloWorld() {

return (

<div>

<h1>Hello, World!</h1>

<p>My first React component</p>

</div>

);

export default HelloWorld;

Component with Props:

// [Link]

function UserCard({ username, email, role, onDelete }) {

return (

<div className="card p-4 border rounded shadow-sm mb-3">

<h3 className="font-bold text-lg">{username}</h3>

<p className="text-gray-600">Email: {email}</p>


<span className="badge bg-blue-100 text-blue-800 px-2 py-1 rounded">

{role}

</span>

<button

onClick={() => onDelete(username)}

className="ml-2 text-red-500 hover:text-red-700"

>

Delete

</button>

</div>

);

export default UserCard;

State with useState:

import { useState } from 'react';

function Counter() {

const [count, setCount] = useState(0);

return (

<div className="text-center p-4">

<p className="text-2xl font-bold">Count: {count}</p>

<div className="flex gap-2 justify-center mt-2">

<button onClick={() => setCount(c => c + 1)}>+</button>

<button onClick={() => setCount(c => c - 1)}>-</button>

<button onClick={() => setCount(0)}>Reset</button>

</div>

</div>

);

List Rendering (always use key):


function UserList({ users }) {

if ([Link] === 0) {

return <p className="text-gray-500">No users found.</p>;

return (

<div>

<h2 className="text-xl font-bold mb-4">Users ({[Link]})</h2>

<ul className="space-y-2">

{[Link](user => (

<li key={[Link]}

className="p-3 bg-gray-50 rounded border">

<span className="font-medium">{[Link]}</span>

<span className="text-gray-500 ml-2">

-- {[Link]}

</span>

</li>

))}

</ul>

</div>

);

Conditional Rendering:

function UserStatus({ isLoggedIn, username }) {

return (

<div>

{isLoggedIn ? (

<p>Welcome back, <strong>{username}</strong>!</p>

) : (

<p>Please <a href="/login">log in</a> to continue.</p>

)}

</div>
);

PRACTICAL EXERCISE

Create a new React project with Vite


Build 5 reusable components: Header, Footer, Card, Button, UserList
Practice passing props between components
Implement a counter with useState
Add conditional rendering for empty states
Display a hardcoded list of users

RESOURCES

React Official Tutorial: [Link]


Vite Documentation: [Link]

KEY TAKEAWAYS

React uses component-based architecture for reusability


JSX mixes HTML with JavaScript
Props pass data down to children (read-only)
useState manages local component state
Always use key prop when rendering lists
Prefer functional components over class components

DAY 16: REACT HOOKS & EFFECTS

Topics To Learn

useEffect: useEffect lets you perform side effects -- fetching data, subscriptions, timers, and DOM
manipulation.

useEffect(callback, dependencyArray)

Empty array []: Runs once after first render (componentDidMount)


With deps [value]: Runs when value changes
No array: Runs after every render (avoid for API calls)

MENTAL MODEL: useEffect is React's way of saying "after rendering, also do this." The
dependency array tells React WHEN to re-run the effect.

CODE EXAMPLES
Fetching Data with useEffect:

import { useState, useEffect } from 'react';

function UserList() {

const [users, setUsers] = useState([]);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

const fetchUsers = async () => {

try {

setLoading(true);

const response = await fetch('/api/users');

if (![Link]) throw new Error('Failed to fetch users');

const data = await [Link]();

setUsers([Link]);

} catch (err) {

setError([Link]);

} finally {

setLoading(false);

};

fetchUsers();

}, []); // Empty array = run once on mount

if (loading) return <div>Loading...</div>;


if (error) return <div>Error: {error}</div>;

return (

<ul>

{[Link](user => (

<li key={[Link]}>{[Link]}</li>

))}

</ul>

);

useEffect with Dependencies (debounced search):

function UserSearch() {

const [query, setQuery] = useState('');

const [results, setResults] = useState([]);

useEffect(() => {

if ([Link] < 2) {

setResults([]);

return;

const timeoutId = setTimeout(async () => {

const res = await fetch(`/api/users/search?username=${query}`);

const data = await [Link]();

setResults(data);

}, 300); // Debounce -- wait 300ms after typing stops

// Cleanup: cancel previous timeout if query changes

return () => clearTimeout(timeoutId);

}, [query]);

return (

<div>
<input

value={query}

onChange={e => setQuery([Link])}

placeholder="Search users..."

/>

<ul>

{[Link](user => (

<li key={[Link]}>{[Link]}</li>

))}

</ul>

</div>

);

Custom Hook (reusable fetch logic):

// hooks/[Link]

import { useState, useEffect } from 'react';

function useFetch(url) {

const [data, setData] = useState(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

let cancelled = false;

const fetchData = async () => {

try {

setLoading(true);

const res = await fetch(url);

if (![Link]) throw new Error(`HTTP ${[Link]}`);

const json = await [Link]();

if (!cancelled) setData(json);

} catch (err) {
if (!cancelled) setError([Link]);

} finally {

if (!cancelled) setLoading(false);

};

fetchData();

return () => { cancelled = true; }; // Cleanup

}, [url]);

return { data, loading, error };

export default useFetch;

// Usage in any component:

function UserList() {

const { data, loading, error } = useFetch('/api/users');

if (loading) return <p>Loading...</p>;

if (error) return <p>Error: {error}</p>;

return (

<ul>

{data?.[Link](u => <li key={[Link]}>{[Link]}</li>)}

</ul>

);

Other Common Hooks:

import { useRef, useCallback, useMemo } from 'react';

// useRef -- access DOM elements

function FocusInput() {

const inputRef = useRef(null);


return (

<div>

<input ref={inputRef} type="text" />

<button onClick={() => [Link]()}>Focus</button>

</div>

);

// useMemo -- memoize expensive calculations

function UserStats({ users }) {

const activeCount = useMemo(

() => [Link](u => [Link]).length,

[users]

);

return <p>Active Users: {activeCount}</p>;

// useCallback -- memoize functions

function Parent() {

const handleDelete = useCallback((id) => {

// delete logic

}, []);

return <Child onDelete={handleDelete} />;

COMMON MISTAKES

Forgetting cleanup functions (causes memory leaks)


Putting async functions directly inside useEffect
Missing dependencies in the dependency array (stale closures)
Overusing useEffect -- prefer event handlers when possible

PRACTICAL EXERCISE

Create a component fetching users from your Spring Boot API


Show loading and error states
Implement debounced search with useEffect cleanup
Extract fetch logic into a custom useFetch hook
Use useMemo to compute user stats without re-computing every render

KEY TAKEAWAYS

useEffect is for side effects (fetching, subscriptions, timers)


The dependency array controls when the effect re-runs
Always return a cleanup function when needed
Custom hooks extract and reuse stateful logic
useMemo and useCallback optimize performance

DAY 17: REACT FORMS & FORM VALIDATION

Topics To Learn

Controlled vs Uncontrolled Components:

Controlled: React state is the single source of truth (preferred)


Uncontrolled: DOM handles state via ref

MENTAL MODEL: A controlled input is like a live transcript -- every keystroke immediately
updates your state. An uncontrolled input is like a notebook -- you only read it when you need to.

CODE EXAMPLES
Controlled Form -- Login:

import { useState } from 'react';

function LoginForm({ onSuccess }) {

const [formData, setFormData] = useState({

username: '',

password: ''

});
const [errors, setErrors] = useState({});

const [loading, setLoading] = useState(false);

const [apiError, setApiError] = useState('');

const handleChange = (e) => {

const { name, value } = [Link];

setFormData(prev => ({ ...prev, [name]: value }));

if (errors[name]) {

setErrors(prev => ({ ...prev, [name]: '' }));

};

const validate = () => {

const newErrors = {};

if (![Link]())

[Link] = 'Username is required';

else if ([Link] < 3)

[Link] = 'Username must be at least 3 characters';

if (![Link])

[Link] = 'Password is required';

else if ([Link] < 8)

[Link] = 'Password must be at least 8 characters';

return newErrors;

};

const handleSubmit = async (e) => {

[Link]();

setApiError('');

const validationErrors = validate();

if ([Link](validationErrors).length > 0) {

setErrors(validationErrors);

return;

}
try {

setLoading(true);

const res = await fetch('/api/auth/login', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: [Link](formData)

});

if (![Link]) {

const error = await [Link]();

throw new Error([Link] || 'Login failed');

const data = await [Link]();

[Link]('token', [Link]);

onSuccess(data);

} catch (err) {

setApiError([Link]);

} finally {

setLoading(false);

};

return (

<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6">

<h2 className="text-2xl font-bold mb-6">Login</h2>

{apiError && (

<div className="bg-red-100 text-red-700 p-3 rounded mb-4">

{apiError}

</div>

)}

<div className="mb-4">
<label className="block text-sm font-medium mb-1">

Username

</label>

<input

type="text"

name="username"

value={[Link]}

onChange={handleChange}

className={`w-full border rounded px-3 py-2 ${

[Link] ? 'border-red-500' : 'border-gray-300'

}`}

/>

{[Link] && (

<p className="text-red-500 text-sm mt-1">

{[Link]}

</p>

)}

</div>

<div className="mb-6">

<label className="block text-sm font-medium mb-1">

Password

</label>

<input

type="password"

name="password"

value={[Link]}

onChange={handleChange}

className={`w-full border rounded px-3 py-2 ${

[Link] ? 'border-red-500' : 'border-gray-300'

}`}

/>

{[Link] && (

<p className="text-red-500 text-sm mt-1">

{[Link]}
</p>

)}

</div>

<button

type="submit"

disabled={loading}

className="w-full bg-blue-600 text-white py-2 rounded

hover:bg-blue-700 disabled:opacity-50"

>

{loading ? 'Logging in...' : 'Login'}

</button>

</form>

);

PRACTICAL EXERCISE

Build a Login form with client-side validation


Build a Registration form with password confirmation
Show inline validation errors under each field
Show API errors at the top of the form
Disable the submit button while loading
Show a success message after registration

KEY TAKEAWAYS

Controlled components give React full control of form state


Always prevent default form submission with [Link]()
Validate both client-side (UX) and server-side (security)
Show inline errors next to each field
Disable the submit button while submitting to prevent double-clicks
DAY 18: REACT ROUTER -- CLIENT-SIDE
ROUTING

Topics To Learn

Why React Router?


In a Single Page Application (SPA), the browser never fully reloads. React Router intercepts
navigation events and renders the correct component based on the URL without a round trip to the
server.

CODE EXAMPLES
Install:

npm install react-router-dom

[Link] -- Route Setup:

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';

import HomePage from './pages/HomePage';

import LoginPage from './pages/LoginPage';

import RegisterPage from './pages/RegisterPage';

import UserListPage from './pages/UserListPage';

import UserDetailPage from './pages/UserDetailPage';

import NotFoundPage from './pages/NotFoundPage';

import Navbar from './components/Navbar';

import ProtectedRoute from './components/ProtectedRoute';

function App() {

return (

<BrowserRouter>

<Navbar />
<Routes>

<Route path="/" element={<HomePage />} />

<Route path="/login" element={<LoginPage />} />

<Route path="/register" element={<RegisterPage />} />

{/* Protected routes -- require authentication */}

<Route element={<ProtectedRoute />}>

<Route path="/users" element={<UserListPage />} />

<Route path="/users/:id" element={<UserDetailPage />} />

</Route>

{/* Catch all -- 404 */}

<Route path="*" element={<NotFoundPage />} />

</Routes>

</BrowserRouter>

);

export default App;

ProtectedRoute:

// components/[Link]

import { Navigate, Outlet } from 'react-router-dom';

function ProtectedRoute() {

const token = [Link]('token');

if (!token) return <Navigate to="/login" replace />;

return <Outlet />;

export default ProtectedRoute;

Navigation with hooks:


import { useNavigate, useParams, Link } from 'react-router-dom';

function LoginPage() {

const navigate = useNavigate();

const handleLogin = async (credentials) => {

const res = await fetch('/api/auth/login', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: [Link](credentials)

});

const data = await [Link]();

[Link]('token', [Link]);

navigate('/users'); // Redirect after login

};

return (

<div>

{/* login form */}

<p>Don't have an account? <Link to="/register">Register</Link></p>

</div>

);

// Route parameters -- /users/:id

function UserDetailPage() {

const { id } = useParams();

const { data: user, loading } = useFetch(`/api/users/${id}`);

if (loading) return <p>Loading...</p>;

return (

<div>

<h1>{user?.username}</h1>

<p>{user?.email}</p>
<Link to="/users">Back to Users</Link>

</div>

);

Navbar with active link styling:

import { NavLink } from 'react-router-dom';

function Navbar() {

const token = [Link]('token');

const handleLogout = () => {

[Link]('token');

[Link] = '/login';

};

return (

<nav className="bg-gray-800 text-white px-6 py-3 flex justify-between">

<span className="font-bold text-xl">MyApp</span>

<div className="flex gap-4 items-center">

<NavLink to="/"

className={({ isActive }) =>

isActive ? 'text-blue-400' : 'hover:text-gray-300'

}>

Home

</NavLink>

{token ? (

<>

<NavLink to="/users"

className={({ isActive }) =>

isActive

? 'text-blue-400'

: 'hover:text-gray-300'

}>
Users

</NavLink>

<button onClick={handleLogout}

className="text-red-400 hover:text-red-300">

Logout

</button>

</>

) : (

<NavLink to="/login"

className="bg-blue-600 px-4 py-1 rounded

hover:bg-blue-700">

Login

</NavLink>

)}

</div>

</nav>

);

PRACTICAL EXERCISE

Set up React Router in your project


Create pages: Home, Login, Register, UserList, UserDetail, NotFound
Implement protected routes that redirect to /login if unauthenticated
Add Navbar with NavLink (active styling)
Implement useNavigate to redirect after login
Use useParams to load user details by ID from your API

KEY TAKEAWAYS

React Router enables SPA navigation without page reloads


Routes and Route define URL-to-component mapping
Protected routes guard private pages
useNavigate programmatically redirects users
useParams extracts URL parameters
Use Link and NavLink instead of anchor tags

DAY 19: AXIOS & API INTEGRATION

Topics To Learn

Why Axios over Fetch?

Automatic JSON parsing


Request/response interceptors (inject tokens automatically)
Better error handling (rejects on 4xx/5xx)
Request cancellation
Simpler API

CODE EXAMPLES
Install:

npm install axios

Axios Instance with Interceptors:

// api/[Link]

import axios from 'axios';

const api = [Link]({

baseURL: '[Link]

timeout: 10000,

headers: { 'Content-Type': 'application/json' }

});

// Request interceptor -- automatically attach JWT

[Link](

(config) => {

const token = [Link]('token');


if (token) {

[Link] = `Bearer ${token}`;

return config;

},

(error) => [Link](error)

);

// Response interceptor -- handle 401 globally

[Link](

(response) => response,

(error) => {

if ([Link]?.status === 401) {

[Link]('token');

[Link] = '/login';

return [Link](error);

);

export default api;

API Service layer:

// api/[Link]

import api from './axiosConfig';

const userService = {

getAllUsers: (page = 0, size = 10, sortBy = 'id') =>

[Link]('/users', { params: { page, size, sortBy } }),

getUserById: (id) =>

[Link](`/users/${id}`),

createUser: (userData) =>


[Link]('/users', userData),

updateUser: (id, userData) =>

[Link](`/users/${id}`, userData),

deleteUser: (id) =>

[Link](`/users/${id}`),

searchUsers: (query) =>

[Link]('/users/search', { params: { username: query } })

};

export default userService;

// api/[Link]

import api from './axiosConfig';

const authService = {

login: (credentials) =>

[Link]('/auth/login', credentials),

register: (userData) =>

[Link]('/auth/register', userData),

logout: () => {

[Link]('token');

[Link] = '/login';

};

export default authService;

Using the service in a component:

import { useState, useEffect } from 'react';


import userService from '../api/userService';

function UserListPage() {

const [users, setUsers] = useState([]);

const [loading, setLoading] = useState(true);

const [error, setError] = useState('');

const [page, setPage] = useState(0);

const [totalPages, setTotalPages] = useState(0);

useEffect(() => {

const loadUsers = async () => {

try {

setLoading(true);

const res = await [Link](page, 10);

setUsers([Link]);

setTotalPages([Link]);

} catch (err) {

setError(

[Link]?.data?.message || 'Failed to load users');

} finally {

setLoading(false);

};

loadUsers();

}, [page]);

const handleDelete = async (id) => {

if (![Link]('Delete this user?')) return;

try {

await [Link](id);

setUsers(prev => [Link](u => [Link] !== id));

} catch (err) {

alert('Failed to delete user');

};
if (loading) return <div>Loading...</div>;

if (error) return <div className="text-red-500">{error}</div>;

return (

<div className="container mx-auto p-6">

<h1 className="text-2xl font-bold mb-6">Users</h1>

<div className="space-y-3">

{[Link](user => (

<div key={[Link]}

className="flex justify-between items-center

p-4 bg-white rounded shadow">

<div>

<p className="font-medium">{[Link]}</p>

<p className="text-gray-500 text-sm">

{[Link]}

</p>

</div>

<button

onClick={() => handleDelete([Link])}

className="text-red-500 hover:text-red-700">

Delete

</button>

</div>

))}

</div>

{/* Pagination Controls */}

<div className="flex justify-center gap-2 mt-6">

<button

disabled={page === 0}

onClick={() => setPage(p => p - 1)}

className="px-4 py-2 border rounded disabled:opacity-50">

Previous

</button>
<span className="px-4 py-2">

Page {page + 1} of {totalPages}

</span>

<button

disabled={page >= totalPages - 1}

onClick={() => setPage(p => p + 1)}

className="px-4 py-2 border rounded disabled:opacity-50">

Next

</button>

</div>

</div>

);

CORS Configuration in Spring Boot:

// [Link]

@Configuration

public class CorsConfig {

@Bean

public CorsConfigurationSource corsConfigurationSource() {

CorsConfiguration configuration = new CorsConfiguration();

[Link](

[Link]("[Link] // Vite dev server

[Link](

[Link]("GET", "POST", "PUT", "DELETE", "OPTIONS"));

[Link](

[Link]("Authorization", "Content-Type"));

[Link](true);

UrlBasedCorsConfigurationSource source =

new UrlBasedCorsConfigurationSource();

[Link]("/api/**", configuration);

return source;
}

Also update SecurityConfig:

[Link](cors -> [Link](corsConfigurationSource()))

COMMON MISTAKES

Hardcoding the base URL instead of using environment variables


Not handling 401 responses globally
Forgetting CORS configuration on the backend

PRACTICAL EXERCISE

Create an Axios instance with request interceptors for JWT


Add a 401 response interceptor that redirects to login
Create [Link] and [Link] service files
Build a UserListPage fetching paginated users
Implement delete with confirmation dialog
Connect your Login form to [Link]()

KEY TAKEAWAYS

Axios interceptors centralize token injection and error handling


Create a service layer to abstract API calls from components
Always handle loading and error states in the UI
Configure CORS on the Spring Boot backend for local development
Use environment variables for API base URLs
DAY 20: TAILWIND CSS -- UTILITY-FIRST
STYLING

Topics To Learn

What is Tailwind CSS?


Tailwind is a utility-first CSS framework. Instead of writing custom CSS, you compose small
utility classes directly in your HTML/JSX.

MENTAL MODEL: Traditional CSS is like tailoring a suit from scratch. Tailwind is like
assembling an outfit from a well-stocked wardrobe of perfectly cut individual pieces -- much
faster, and you can mix and match.

CODE EXAMPLES
Install and configure:

npm install -D tailwindcss postcss autoprefixer

npx tailwindcss init -p

[Link]:

export default {

content: [

"./[Link]",

"./src/**/*.{js,ts,jsx,tsx}",

],

theme: {

extend: {

colors: {

primary: '#3B82F6',

danger: '#EF4444'

}
},

},

plugins: [],

Add to src/[Link]:

@tailwind base;

@tailwind components;

@tailwind utilities;

Responsive Card Component:

function UserCard({ user, onEdit, onDelete }) {

return (

<div className="bg-white rounded-xl shadow-md p-6

hover:shadow-lg transition-shadow duration-200

border border-gray-100">

<div className="flex items-center gap-4">

{/* Avatar */}

<div className="w-12 h-12 rounded-full bg-blue-500

flex items-center justify-center

text-white font-bold text-lg flex-shrink-0">

{[Link][0].toUpperCase()}

</div>

{/* User Info */}

<div className="flex-1 min-w-0">

<h3 className="font-semibold text-gray-900 truncate">

{[Link]}

</h3>

<p className="text-gray-500 text-sm truncate">

{[Link]}

</p>
</div>

{/* Role badge */}

<span className={`px-3 py-1 rounded-full text-xs font-medium

${[Link]('ROLE_ADMIN')

? 'bg-purple-100 text-purple-800'

: 'bg-green-100 text-green-800'

}`}>

{[Link]('ROLE_ADMIN') ? 'Admin' : 'User'}

</span>

</div>

{/* Action Buttons */}

<div className="flex gap-2 mt-4 pt-4 border-t border-gray-100">

<button

onClick={() => onEdit(user)}

className="flex-1 py-2 px-4 text-sm font-medium

text-blue-600 bg-blue-50 rounded-lg

hover:bg-blue-100 transition-colors">

Edit

</button>

<button

onClick={() => onDelete([Link])}

className="flex-1 py-2 px-4 text-sm font-medium

text-red-600 bg-red-50 rounded-lg

hover:bg-red-100 transition-colors">

Delete

</button>

</div>

</div>

);

Responsive Grid Layout:


function UserGrid({ users }) {

return (

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3

xl:grid-cols-4 gap-4 p-4">

{[Link](user => (

<UserCard key={[Link]} user={user}

onEdit={u => [Link]('edit', u)}

onDelete={id => [Link]('delete', id)} />

))}

</div>

);

Reusable Button Component:

const variants = {

primary: 'bg-blue-600 text-white hover:bg-blue-700',

secondary: 'bg-gray-200 text-gray-700 hover:bg-gray-300',

danger: 'bg-red-600 text-white hover:bg-red-700',

outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50'

};

const sizes = {

sm: 'px-3 py-1.5 text-sm',

md: 'px-4 py-2 text-base',

lg: 'px-6 py-3 text-lg'

};

function Button({ children, variant = 'primary', size = 'md',

disabled = false, loading = false,

onClick, className = '' }) {

return (

<button

onClick={onClick}

disabled={disabled || loading}
className={`

${variants[variant]} ${sizes[size]}

rounded-lg font-medium transition-colors duration-150

disabled:opacity-50 disabled:cursor-not-allowed

flex items-center gap-2

${className}

`}

>

{loading && (

<span className="w-4 h-4 border-2 border-white

border-t-transparent rounded-full

animate-spin" />

)}

{children}

</button>

);

PRACTICAL EXERCISE

Install and configure Tailwind in your React project


Refactor UserList, UserCard, and Navbar with Tailwind
Build a responsive grid (1 col mobile, 3 cols desktop)
Create a reusable Button component with variants and sizes
Add hover transitions and focus states to interactive elements
Style your Login and Register forms

KEY TAKEAWAYS

Tailwind is utility-first -- compose classes instead of writing CSS


Use responsive prefixes (sm:, md:, lg:) for different screen sizes
Extract repeated class combinations into reusable components
Use transition and hover: for smooth interactive states
Configure [Link] to extend theme with brand colors
DAY 21: DOCKER BASICS

Topics To Learn

What is Docker?
Docker packages your application and all its dependencies into a container -- a lightweight,
portable, isolated environment that runs the same on any machine.

MENTAL MODEL: A container is like a shipping container. It holds exactly the right goods, is
sealed, and works the same on a truck, a ship, or a crane. Docker containers hold your app,
runtime, and dependencies -- and work the same on your laptop, a colleague's machine, or a cloud
server.

Key Concepts:

Image: A blueprint (read-only template)


Container: A running instance of an image
Dockerfile: Instructions to build an image
docker-compose: Orchestrates multiple containers together

CODE EXAMPLES
Backend Dockerfile:

# backend/Dockerfile

FROM eclipse-temurin:21-jdk-alpine AS build

WORKDIR /app

COPY [Link] .

COPY .mvn .mvn

COPY mvnw .

RUN ./mvnw dependency:go-offline -B

COPY src ./src

RUN ./mvnw package -DskipTests


# Runtime stage (smaller image)

FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

COPY --from=build /app/target/*.jar [Link]

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "[Link]"]

Frontend Dockerfile:

# frontend/Dockerfile

FROM node:20-alpine AS build

WORKDIR /app

COPY package*.json .

RUN npm ci

COPY . .

RUN npm run build

# Runtime: serve with Nginx

FROM nginx:alpine

COPY --from=build /app/dist /usr/share/nginx/html

COPY [Link] /etc/nginx/conf.d/[Link]

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

[Link]:

server {

listen 80;

root /usr/share/nginx/html;
index [Link];

location / {

try_files $uri $uri/ /[Link];

location /api/ {

proxy_pass [Link]

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

[Link]:

version: '3.8'

services:

db:

image: postgres:16-alpine

environment:

POSTGRES_DB: myapp_db

POSTGRES_USER: myapp_user

POSTGRES_PASSWORD: myapp_password

volumes:

- postgres_data:/var/lib/postgresql/data

ports:

- "5432:5432"

healthcheck:

test: ["CMD-SHELL", "pg_isready -U myapp_user"]

interval: 10s

timeout: 5s

retries: 5

backend:
build: ./backend

ports:

- "8080:8080"

environment:

SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp_db

SPRING_DATASOURCE_USERNAME: myapp_user

SPRING_DATASOURCE_PASSWORD: myapp_password

JWT_SECRET: your-super-secret-jwt-key-change-in-production

SPRING_JPA_HIBERNATE_DDL_AUTO: update

depends_on:

db:

condition: service_healthy

frontend:

build: ./frontend

ports:

- "3000:80"

depends_on:

- backend

volumes:

postgres_data:

Common Docker commands:

# Build and start all services

docker-compose up --build

# Start in background

docker-compose up -d

# View logs for a specific service

docker-compose logs -f backend

# Stop all services


docker-compose down

# Stop and remove volumes (full reset)

docker-compose down -v

# Shell into a running container

docker exec -it <container_name> sh

# List running containers

docker ps

Update [Link] to support env vars:

[Link]=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/myapp_db}

[Link]=${SPRING_DATASOURCE_USERNAME:myapp_user}

[Link]=${SPRING_DATASOURCE_PASSWORD:myapp_password}

[Link]=${JWT_SECRET:dev-secret-key}

[Link]=86400000

COMMON MISTAKES

Hardcoding credentials in [Link] (use .env files)


Using :latest image tags (pin to specific versions)
Not using multi-stage builds (leads to bloated images)
Forgetting health checks (dependent services start before DB is ready)

PRACTICAL EXERCISE

Write a Dockerfile for your Spring Boot application


Write a Dockerfile for your React frontend
Create [Link] with backend, frontend, and PostgreSQL
Run docker-compose up --build and verify everything works
Test the full application through the browser
Run docker-compose logs -f backend to watch live logs
WEEK 3 REVIEW CHECKPOINT
Before moving to Week 4, confirm you can:

[ ] Create and compose React components with props and state


[ ] Use useEffect to fetch data from your Spring Boot API
[ ] Build controlled forms with client-side validation
[ ] Set up React Router with protected routes
[ ] Use Axios with interceptors for JWT and error handling
[ ] Style components responsively with Tailwind CSS
[ ] Containerize the full stack with Docker and docker-compose

KEY TAKEAWAYS

Docker containers are portable, isolated, and reproducible


Multi-stage builds create small production images
docker-compose orchestrates multiple services together
Use environment variables for all credentials and config
Health checks prevent race conditions between services

WEEK 4: DEVOPS, ADVANCED


TOPICS & PORTFOLIO PROJECT

DAY 22: REDIS CACHING -- DISTRIBUTED


CACHING

Topics To Learn

Why Redis over In-Memory Cache (Caffeine)?

Caffeine (Day 12) Redis


Lives in-process (RAM) External server Lost on app restart Persistent across restarts Not shared
between instances Shared across app instances Best for single-instance apps Best for production
/scaled deployments

MENTAL MODEL: Caffeine is a sticky note on your desk -- fast, personal, gone when you go
home. Redis is a shared whiteboard in the office -- slightly slower to reach, but everyone sees the
same thing, and it's still there tomorrow.

CODE EXAMPLES
Add dependency:

<dependency>

<groupId>[Link]</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

[Link]:

[Link]=${REDIS_HOST:localhost}

[Link]=${REDIS_PORT:6379}

[Link]=${REDIS_PASSWORD:}

[Link]-to-live=600000

[Link]=redis

Redis Cache Configuration:

// [Link]

@Configuration

@EnableCaching

public class RedisCacheConfig {

@Bean

public RedisCacheManager cacheManager(RedisConnectionFactory factory) {

RedisCacheConfiguration defaultConfig = RedisCacheConfiguration

.defaultCacheConfig()
.entryTtl([Link](10))

.serializeKeysWith(

[Link](

new StringRedisSerializer()))

.serializeValuesWith(

[Link](

new GenericJackson2JsonRedisSerializer()))

.disableCachingNullValues();

Map<String, RedisCacheConfiguration> cacheConfigs = [Link](

"users", [Link]([Link](10)),

"products", [Link]([Link](30)),

"posts", [Link]([Link](5))

);

return [Link](factory)

.cacheDefaults(defaultConfig)

.withInitialCacheConfigurations(cacheConfigs)

.build();

Service with Redis Caching (same annotations as Caffeine):

@Service

public class ProductService {

@Cacheable(value = "products", key = "#id")

public Product findById(Long id) {

return [Link](id)

.orElseThrow(() ->

new ResourceNotFoundException("Product not found"));

@CachePut(value = "products", key = "#[Link]")


public Product update(Long id, UpdateProductDTO dto) {

Product product = findById(id);

[Link]([Link]());

[Link]([Link]());

return [Link](product);

@CacheEvict(value = "products", key = "#id")

public void delete(Long id) {

[Link](id);

RedisTemplate for manual operations:

@Service

@RequiredArgsConstructor

public class SessionService {

private final RedisTemplate<String, Object> redisTemplate;

public void storeToken(String email, String token) {

String key = "reset_token:" + email;

[Link]().set(key, token,

[Link](15));

public String getToken(String email) {

return (String) [Link]()

.get("reset_token:" + email);

public void deleteToken(String email) {

[Link]("reset_token:" + email);
}

Add Redis to [Link]:

redis:

image: redis:7-alpine

ports:

- "6379:6379"

command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

backend:

environment:

REDIS_HOST: redis

REDIS_PORT: 6379

depends_on:

- db

- redis

PRACTICAL EXERCISE

Add Redis dependency and configuration


Replace Caffeine cache with Redis in your application
Implement cache for product and user lookups
Use RedisTemplate to store a short-lived token
Add Redis to your docker-compose setup
Test that cache survives an application restart

KEY TAKEAWAYS

Redis is a distributed cache -- shared across multiple app instances


Spring Cache annotations work identically with Redis
Configure per-cache TTLs for freshness vs performance
Use RedisTemplate for manual, fine-grained cache operations
Redis also works as a message broker, session store, and rate limiter
DAY 23: RABBITMQ -- ASYNCHRONOUS
MESSAGING

Topics To Learn

What is RabbitMQ?
RabbitMQ is a message broker. Instead of calling a service directly, you publish a message to a
queue. The consumer processes it asynchronously.

MENTAL MODEL: Think of RabbitMQ like a post office. Your app drops a letter (message) in a
mailbox (queue). The post office (RabbitMQ) holds it until the recipient (consumer) is ready to
pick it up. The sender doesn't wait for the recipient to read the letter.

When to use messaging:

Sending emails after registration (don't make the user wait)


Processing image uploads in background
Sending notifications
Decoupling services in microservices architectures

CODE EXAMPLES
Add dependency:

<dependency>

<groupId>[Link]</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

[Link]:

[Link]=${RABBITMQ_HOST:localhost}

[Link]=${RABBITMQ_PORT:5672}
[Link]=${RABBITMQ_USERNAME:guest}

[Link]=${RABBITMQ_PASSWORD:guest}

RabbitMQ Configuration:

// [Link]

@Configuration

public class RabbitMQConfig {

public static final String EMAIL_QUEUE = "[Link]";

public static final String NOTIFICATION_QUEUE = "[Link]";

public static final String EXCHANGE = "[Link]";

public static final String EMAIL_ROUTING_KEY = "[Link]";

public static final String NOTIFICATION_ROUTING_KEY = "[Link]";

@Bean

public Queue emailQueue() {

return new Queue(EMAIL_QUEUE, true); // durable

@Bean

public Queue notificationQueue() {

return new Queue(NOTIFICATION_QUEUE, true);

@Bean

public TopicExchange exchange() {

return new TopicExchange(EXCHANGE);

@Bean

public Binding emailBinding(Queue emailQueue, TopicExchange exchange) {

return [Link](emailQueue)

.to(exchange).with(EMAIL_ROUTING_KEY);

}
@Bean

public MessageConverter messageConverter() {

return new Jackson2JsonMessageConverter();

@Bean

public AmqpTemplate amqpTemplate(ConnectionFactory factory) {

RabbitTemplate template = new RabbitTemplate(factory);

[Link](messageConverter());

return template;

Message DTO:

// messages/[Link]

public class EmailMessage {

private String to;

private String subject;

private String body;

private String type; // "WELCOME", "RESET_PASSWORD", "ORDER_CONFIRMATION"

public EmailMessage() {}

public EmailMessage(String to, String subject,

String body, String type) {

[Link] = to;

[Link] = subject;

[Link] = body;

[Link] = type;

// getters/setters

Producer:
// messaging/[Link]

@Service

@RequiredArgsConstructor

@Slf4j

public class MessageProducer {

private final AmqpTemplate amqpTemplate;

public void sendEmail(String to, String subject,

String body, String type) {

EmailMessage message = new EmailMessage(to, subject, body, type);

[Link](

[Link],

RabbitMQConfig.EMAIL_ROUTING_KEY,

message

);

[Link]("Email message queued for: {}", to);

public void sendWelcomeEmail(String to, String username) {

sendEmail(to, "Welcome to MyApp!",

"Hi " + username + ", thanks for registering!", "WELCOME");

Consumer:

// messaging/[Link]

@Component

@Slf4j

public class EmailConsumer {

@RabbitListener(queues = RabbitMQConfig.EMAIL_QUEUE)

public void processEmailMessage(EmailMessage message) {


[Link]("Processing email to: {} | Type: {}",

[Link](), [Link]());

try {

sendEmail([Link](),

[Link](),

[Link]());

[Link]("Email sent to: {}", [Link]());

} catch (Exception e) {

[Link]("Failed to send email to: {}", [Link](), e);

throw e;

private void sendEmail(String to, String subject, String body) {

// Replace with actual email service (JavaMailSender, SendGrid, etc.)

[Link]("SENDING EMAIL -> To: {} | Subject: {}", to, subject);

Integrate with UserService:

@Service

@RequiredArgsConstructor

public class UserService {

private final UserRepository userRepository;

private final PasswordEncoder passwordEncoder;

private final RoleRepository roleRepository;

private final MessageProducer messageProducer;

public User registerUser(String username, String password, String email) {

// ... validation and user creation ...

User saved = [Link](user);

// Non-blocking: send welcome email via queue


[Link](email, username);

return saved;

Add RabbitMQ to [Link]:

rabbitmq:

image: rabbitmq:3-management-alpine

ports:

- "5672:5672" # AMQP protocol

- "15672:15672" # Management UI ([Link]

environment:

RABBITMQ_DEFAULT_USER: guest

RABBITMQ_DEFAULT_PASS: guest

Access management dashboard at: [Link] (guest/guest)

PRACTICAL EXERCISE

Add Spring AMQP dependency


Configure queues, exchange, and bindings
Create a MessageProducer service
Create an EmailConsumer that processes messages
Trigger a welcome email on user registration
Add RabbitMQ to docker-compose
Watch messages flow through the management dashboard

KEY TAKEAWAYS

Message queues decouple producers from consumers


The sender doesn't wait -- fire and forget
Use messaging for slow operations (email, notifications, file processing)
RabbitMQ guarantees delivery -- messages persist until consumed
The management dashboard makes it easy to debug

DAY 24: SPRING PROFILES & DATABASE


MIGRATIONS [PREVIOUSLY MISSING]

Topics To Learn

Spring Profiles: Profiles let you have different configurations for different environments (dev, test,
prod) without changing code.

Flyway -- Database Migrations: Flyway tracks and applies database schema changes in a
versioned, repeatable, ordered way -- like Git for your database schema.

MENTAL MODEL: Flyway is to your database what Git commits are to your code. Every schema
change is a versioned migration script. You always know exactly what state the database is in, and
can reproduce it anywhere.

CODE EXAMPLES
[Link] (base):

[Link]=myapp

[Link]-in-view=false

[Link]:

[Link]=jdbc:h2:mem:devdb

[Link]-class-name=[Link]

[Link]-auto=create-drop

[Link]-sql=true

[Link]=DEBUG

[Link]=true

[Link]:
[Link]=${DATABASE_URL}

[Link]=${DATABASE_USER}

[Link]=${DATABASE_PASSWORD}

[Link]-auto=validate

[Link]-sql=false

[Link]=WARN

[Link]=8080

Activating profiles:

# Local dev

java -jar [Link] --[Link]=dev

# Production via env variable

SPRING_PROFILES_ACTIVE=prod java -jar [Link]

# In docker-compose

environment:

SPRING_PROFILES_ACTIVE: prod

Profile-specific beans:

@Configuration

public class EmailConfig {

@Bean

@Profile("prod")

public EmailService realEmailService() {

return new SmtpEmailService();

@Bean

@Profile({"dev", "test"})
public EmailService mockEmailService() {

return new LoggingEmailService();

Add Flyway:

<dependency>

<groupId>[Link]</groupId>

<artifactId>flyway-core</artifactId>

</dependency>

[Link]:

[Link]-auto=validate

[Link]=true

[Link]=classpath:db/migration

Migration scripts in src/main/resources/db/migration/:

-- V1__Create_users_table.sql

CREATE TABLE users (

id BIGSERIAL PRIMARY KEY,

username VARCHAR(50) UNIQUE NOT NULL,

password VARCHAR(255) NOT NULL,

email VARCHAR(100) UNIQUE NOT NULL,

enabled BOOLEAN DEFAULT TRUE,

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

);

-- V2__Create_roles_table.sql

CREATE TABLE roles (

id BIGSERIAL PRIMARY KEY,

name VARCHAR(50) UNIQUE NOT NULL


);

CREATE TABLE user_roles (

user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,

role_id BIGINT REFERENCES roles(id) ON DELETE CASCADE,

PRIMARY KEY (user_id, role_id)

);

-- V3__Seed_roles.sql

INSERT INTO roles (name) VALUES ('ROLE_USER'), ('ROLE_ADMIN')

ON CONFLICT DO NOTHING;

-- V4__Add_products_table.sql

CREATE TABLE products (

id BIGSERIAL PRIMARY KEY,

name VARCHAR(200) NOT NULL,

description TEXT,

price DECIMAL(10,2) NOT NULL,

stock INTEGER DEFAULT 0,

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

);

-- V5__Add_category_to_products.sql

ALTER TABLE products ADD COLUMN category VARCHAR(100);

CREATE INDEX idx_products_category ON products(category);

Naming convention: V{version}__{description}.sql

Always incrementing version numbers


Double underscore between version and description
NEVER modify an already-applied migration

COMMON MISTAKES

Editing an already-applied migration (checksum mismatch error)


Using ddl-auto=create-drop with Flyway (they conflict)
Not committing migrations to version control

PRACTICAL EXERCISE

Create [Link] and [Link]


Add Flyway and write migration scripts for all your tables
Create a profile-specific EmailService (real vs mock)
Run with --[Link]=dev
Verify Flyway creates the flyway_schema_history table
Add a new migration (add a column) and verify it applies automatically

KEY TAKEAWAYS

Profiles separate environment-specific configuration


Never hardcode credentials -- use environment variables in prod
Flyway provides version-controlled, reproducible database migrations
Never edit applied migrations -- always create a new version
Flyway automatically applies new migrations on application startup

DAY 25: CI/CD WITH GITHUB ACTIONS

Topics To Learn

What is CI/CD?

CI (Continuous Integration): Automatically build and test code on every push


CD (Continuous Deployment): Automatically deploy after tests pass

MENTAL MODEL: CI/CD is like having a quality control robot on your assembly line. Every
time you add a new part (code), the robot checks that the whole machine still works before it ships.

CODE EXAMPLES
.github/workflows/[Link]:
name: CI

on:

push:

branches: [ main, develop ]

pull_request:

branches: [ main ]

jobs:

backend-test:

runs-on: ubuntu-latest

services:

postgres:

image: postgres:16-alpine

env:

POSTGRES_DB: testdb

POSTGRES_USER: testuser

POSTGRES_PASSWORD: testpass

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 21

uses: actions/setup-java@v4

with:

java-version: '21'

distribution: 'temurin'
cache: 'maven'

- name: Run backend tests

working-directory: ./backend

run: ./mvnw test

env:

SPRING_PROFILES_ACTIVE: test

SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/testdb

SPRING_DATASOURCE_USERNAME: testuser

SPRING_DATASOURCE_PASSWORD: testpass

JWT_SECRET: test-secret-key-at-least-256-bits-long-for-tests

- name: Upload test report

if: always()

uses: actions/upload-artifact@v4

with:

name: backend-test-report

path: backend/target/surefire-reports/

frontend-test:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Set up [Link]

uses: actions/setup-node@v4

with:

node-version: '20'

cache: 'npm'

cache-dependency-path: frontend/[Link]

- name: Install dependencies

working-directory: ./frontend

run: npm ci
- name: Run tests

working-directory: ./frontend

run: npm test -- --watchAll=false

- name: Build frontend

working-directory: ./frontend

run: npm run build

docker-build:

runs-on: ubuntu-latest

needs: [backend-test, frontend-test]

if: [Link] == 'refs/heads/main'

steps:

- uses: actions/checkout@v4

- name: Log in to Docker Hub

uses: docker/login-action@v3

with:

username: ${{ secrets.DOCKERHUB_USERNAME }}

password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push backend image

uses: docker/build-push-action@v5

with:

context: ./backend

push: true

tags: |

${{ secrets.DOCKERHUB_USERNAME }}/myapp-backend:latest

${{ secrets.DOCKERHUB_USERNAME }}/myapp-backend:${{ [Link] }}

- name: Build and push frontend image

uses: docker/build-push-action@v5

with:
context: ./frontend

push: true

tags: |

${{ secrets.DOCKERHUB_USERNAME }}/myapp-frontend:latest

${{ secrets.DOCKERHUB_USERNAME }}/myapp-frontend:${{ [Link] }}

.github/workflows/[Link]:

name: Deploy to Production

on:

push:

branches: [ main ]

jobs:

deploy:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Deploy via SSH

uses: appleboy/ssh-action@v1

with:

host: ${{ secrets.SERVER_HOST }}

username: ${{ secrets.SERVER_USER }}

key: ${{ secrets.SSH_PRIVATE_KEY }}

script: |

cd /app

docker-compose pull

docker-compose up -d --no-build

docker image prune -f

JaCoCo test coverage in [Link]:


<plugin>

<groupId>[Link]</groupId>

<artifactId>jacoco-maven-plugin</artifactId>

<version>0.8.11</version>

<executions>

<execution>

<goals><goal>prepare-agent</goal></goals>

</execution>

<execution>

<id>report</id>

<phase>test</phase>

<goals><goal>report</goal></goals>

</execution>

<execution>

<id>check</id>

<goals><goal>check</goal></goals>

<configuration>

<rules>

<rule>

<limits>

<limit>

<counter>LINE</counter>

<value>COVEREDRATIO</value>

<minimum>0.80</minimum>

</limit>

</limits>

</rule>

</rules>

</configuration>

</execution>

</executions>

</plugin>

Setting up GitHub Secrets:

1.
1. Go to repo -> Settings -> Secrets and variables -> Actions
2. Add: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, JWT_SECRET,
SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY

PRACTICAL EXERCISE

Create .github/workflows/[Link] in your project


Configure it to run backend tests on every push
Add a step to build the frontend
Add Docker build step for the main branch only
Set up GitHub Secrets for sensitive values
Open a pull request and watch the CI pipeline run
Add JaCoCo coverage reporting

KEY TAKEAWAYS

CI runs tests automatically on every push


CD deploys automatically after tests pass
Store all secrets in GitHub Secrets -- never in YAML files
Use branch protection rules to require CI to pass before merging
Test in CI exactly as you would in production

DAYS 26-30: FINAL PORTFOLIO PROJECT --


E-COMMERCE PLATFORM
Overview: You now have all the tools. Over the final 5 days, build a complete, deployed e-
commerce application integrating everything you've learned.

DAY 26: PROJECT SETUP & BACKEND


FOUNDATION
Goals: Scaffold the project, set up the database schema, implement auth.
Project Structure:

ecommerce/ |-- backend/ | |-- src/main/java/com/ecommerce/ | | |-- config/ # Security, CORS,


OpenAPI, Cache, RabbitMQ | | |-- entity/ # User, Role, Product, Category, Order, Cart | | |-- dto/ #
All request/response DTOs | | |-- repository/ # JPA repositories | | |-- service/ # Business logic | | |--
controller/ # REST endpoints | | |-- security/ # JWT filter, UserDetailsService | | |-- messaging/ #
RabbitMQ producer/consumer | | |-- exception/ # Custom exceptions, GlobalExceptionHandler | |--
src/main/resources/ | | |-- db/migration/ # Flyway scripts | | |-- application*.properties | |-- Dockerfile
|-- frontend/ | |-- src/ | | |-- api/ # axiosConfig, services | | |-- components/ # Navbar, Button, Card,
Modal, Pagination | | |-- pages/ # Home, Login, Register, Products, Cart, Orders | | |-- hooks/ #
useFetch, useAuth, useCart | | |-- context/ # AuthContext, CartContext | |-- Dockerfile |-- docker-
[Link] |-- .github/workflows/[Link]

Database Schema (Flyway):

-- V1__Create_base_schema.sql

CREATE TABLE users (

id BIGSERIAL PRIMARY KEY,

username VARCHAR(50) UNIQUE NOT NULL,

password VARCHAR(255) NOT NULL,

email VARCHAR(100) UNIQUE NOT NULL,

enabled BOOLEAN DEFAULT TRUE,

created_at TIMESTAMP DEFAULT NOW()

);

CREATE TABLE roles (

id BIGSERIAL PRIMARY KEY,

name VARCHAR(50) UNIQUE NOT NULL

);

CREATE TABLE user_roles (

user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,

role_id BIGINT REFERENCES roles(id) ON DELETE CASCADE,

PRIMARY KEY (user_id, role_id)

);
CREATE TABLE categories (

id BIGSERIAL PRIMARY KEY,

name VARCHAR(100) UNIQUE NOT NULL,

description TEXT

);

CREATE TABLE products (

id BIGSERIAL PRIMARY KEY,

name VARCHAR(200) NOT NULL,

description TEXT,

price DECIMAL(10,2) NOT NULL,

stock INTEGER DEFAULT 0,

image_url VARCHAR(500),

category_id BIGINT REFERENCES categories(id),

created_at TIMESTAMP DEFAULT NOW()

);

CREATE TABLE orders (

id BIGSERIAL PRIMARY KEY,

user_id BIGINT REFERENCES users(id),

status VARCHAR(50) DEFAULT 'PENDING',

total_amount DECIMAL(10,2) NOT NULL,

created_at TIMESTAMP DEFAULT NOW()

);

CREATE TABLE order_items (

id BIGSERIAL PRIMARY KEY,

order_id BIGINT REFERENCES orders(id) ON DELETE CASCADE,

product_id BIGINT REFERENCES products(id),

quantity INTEGER NOT NULL,

price_at_purchase DECIMAL(10,2) NOT NULL

);

CREATE TABLE cart_items (

id BIGSERIAL PRIMARY KEY,


user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,

product_id BIGINT REFERENCES products(id),

quantity INTEGER NOT NULL,

UNIQUE (user_id, product_id)

);

-- V2__Seed_data.sql

INSERT INTO roles (name) VALUES ('ROLE_USER'), ('ROLE_ADMIN');

INSERT INTO categories (name, description)

VALUES ('Electronics', 'Gadgets and devices'),

('Clothing', 'Fashion and apparel'),

('Books', 'Physical and digital books');

Day 26 Tasks:

[ ] Create Spring Boot project with all dependencies


[ ] Write Flyway migration scripts
[ ] Implement all entities (User, Role, Product, Category, Order, CartItem)
[ ] Implement JWT auth (from Day 14)
[ ] Create all repositories
[ ] Write and pass unit tests for UserService

DAY 27: CORE BACKEND SERVICES & APIs


Goals: Implement all business logic and REST endpoints.

ProductService key methods:

@Service

@RequiredArgsConstructor

@Slf4j

public class ProductService {

private final ProductRepository productRepository;

private final ProductMapper productMapper;


@Cacheable(value = "products", key = "#[Link]")

public Page<ProductDTO> getAllProducts(Pageable pageable) {

return [Link](pageable)

.map(productMapper::toDTO);

@Cacheable(value = "products", key = "#id")

public ProductDTO getById(Long id) {

return [Link](id)

.map(productMapper::toDTO)

.orElseThrow(() ->

new ResourceNotFoundException("Product not found: " + id));

public Page<ProductDTO> search(String keyword,

Long categoryId,

BigDecimal minPrice,

BigDecimal maxPrice,

Pageable pageable) {

return productRepository

.findBySearchCriteria(keyword, categoryId,

minPrice, maxPrice, pageable)

.map(productMapper::toDTO);

@PreAuthorize("hasRole('ADMIN')")

@CacheEvict(value = "products", allEntries = true)

public ProductDTO create(CreateProductDTO dto) {

Product product = [Link](dto);

return [Link]([Link](product));

@PreAuthorize("hasRole('ADMIN')")

@CacheEvict(value = "products", key = "#id")


public ProductDTO update(Long id, UpdateProductDTO dto) {

Product product = [Link](id)

.orElseThrow(() ->

new ResourceNotFoundException("Product not found: " + id));

[Link](dto, product);

return [Link]([Link](product));

CartService:

@Service

@RequiredArgsConstructor

public class CartService {

private final CartItemRepository cartItemRepository;

private final ProductRepository productRepository;

private final OrderRepository orderRepository;

private final MessageProducer messageProducer;

public List<CartItemDTO> getCart(Long userId) {

return [Link](userId)

.stream().map(this::toDTO).collect([Link]());

@Transactional

public CartItemDTO addToCart(Long userId, Long productId, int quantity) {

Product product = [Link](productId)

.orElseThrow(() ->

new ResourceNotFoundException("Product not found"));

if ([Link]() < quantity)

throw new RuntimeException("Insufficient stock");

CartItem item = cartItemRepository


.findByUserIdAndProductId(userId, productId)

.map(existing -> {

[Link]([Link]() + quantity);

return existing;

})

.orElse(new CartItem(userId, productId, quantity));

return toDTO([Link](item));

@Transactional

public OrderDTO checkout(Long userId) {

List<CartItem> items = [Link](userId);

if ([Link]())

throw new RuntimeException("Cart is empty");

BigDecimal total = [Link]()

.map(i -> [Link]().getPrice()

.multiply([Link]([Link]())))

.reduce([Link], BigDecimal::add);

Order order = new Order(userId, total, "PENDING");

List<OrderItem> orderItems = [Link]()

.map(i -> new OrderItem(order, [Link](),

[Link](),

[Link]().getPrice()))

.collect([Link]());

[Link](orderItems);

[Link](order);

[Link](i -> {

Product p = [Link]();

[Link]([Link]() - [Link]());

[Link](p);

});
[Link](userId);

[Link](

[Link]().getEmail(), [Link](), total);

return [Link](order);

Day 27 Tasks:

[ ] Implement ProductService, CartService, OrderService


[ ] Create all REST controllers with proper status codes
[ ] Add Swagger documentation to all endpoints
[ ] Add Redis caching to ProductService
[ ] Send order confirmation via RabbitMQ
[ ] Write integration tests for product and cart endpoints

DAY 28: REACT FRONTEND


Goals: Build all pages, connect to backend, style with Tailwind.

AuthContext -- global auth state:

// context/[Link]

import { createContext, useContext, useState, useEffect } from 'react';

import authService from '../api/authService';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {

const [user, setUser] = useState(null);

const [loading, setLoading] = useState(true);


useEffect(() => {

const token = [Link]('token');

const username = [Link]('username');

if (token && username) setUser({ username, token });

setLoading(false);

}, []);

const login = async (credentials) => {

const res = await [Link](credentials);

[Link]('token', [Link]);

[Link]('username', [Link]);

setUser([Link]);

return [Link];

};

const logout = () => {

[Link]('token');

[Link]('username');

setUser(null);

};

return (

<[Link] value={{ user, login, logout, loading }}>

{children}

</[Link]>

);

export const useAuth = () => useContext(AuthContext);

Products page:

// pages/[Link]

import { useState, useEffect } from 'react';

import productService from '../api/productService';


import ProductCard from '../components/ProductCard';

import Pagination from '../components/Pagination';

import SearchBar from '../components/SearchBar';

function ProductsPage() {

const [products, setProducts] = useState([]);

const [page, setPage] = useState(0);

const [totalPages, setTotalPages] = useState(0);

const [search, setSearch] = useState('');

const [loading, setLoading] = useState(true);

useEffect(() => {

const load = async () => {

setLoading(true);

try {

const res = await [Link](page, 12, search);

setProducts([Link]);

setTotalPages([Link]);

} finally {

setLoading(false);

};

load();

}, [page, search]);

return (

<div className="container mx-auto px-4 py-8">

<div className="flex justify-between items-center mb-6">

<h1 className="text-3xl font-bold text-gray-900">Products</h1>

<SearchBar

value={search}

onChange={setSearch}

onSearch={() => setPage(0)}

placeholder="Search products..."

/>
</div>

{loading ? (

<div className="grid grid-cols-1 sm:grid-cols-2

lg:grid-cols-4 gap-4">

{[...Array(8)].map((_, i) => (

<div key={i}

className="h-64 bg-gray-200 rounded-xl

animate-pulse" />

))}

</div>

) : (

<div className="grid grid-cols-1 sm:grid-cols-2

lg:grid-cols-4 gap-4">

{[Link](product => (

<ProductCard key={[Link]} product={product} />

))}

</div>

)}

<Pagination

page={page}

totalPages={totalPages}

onPageChange={setPage}

/>

</div>

);

ProductCard:

// components/[Link]

import { Link } from 'react-router-dom';

import { useCart } from '../context/CartContext';


function ProductCard({ product }) {

const { addToCart } = useCart();

return (

<div className="bg-white rounded-xl shadow hover:shadow-lg

transition-shadow duration-200 overflow-hidden group">

<Link to={`/products/${[Link]}`}>

<div className="h-48 bg-gray-100 overflow-hidden">

<img

src={[Link] || '/[Link]'}

alt={[Link]}

className="w-full h-full object-cover

group-hover:scale-105 transition-transform"

/>

</div>

</Link>

<div className="p-4">

<h3 className="font-semibold text-gray-900 truncate">

{[Link]}

</h3>

<p className="text-gray-500 text-sm mt-1 line-clamp-2">

{[Link]}

</p>

<div className="flex items-center justify-between mt-4">

<span className="text-xl font-bold text-blue-600">

${[Link](2)}

</span>

<button

onClick={() => addToCart([Link], 1)}

disabled={[Link] === 0}

className="bg-blue-600 text-white px-3 py-1.5

rounded-lg text-sm hover:bg-blue-700

disabled:opacity-50

disabled:cursor-not-allowed

transition-colors">
{[Link] === 0 ? 'Out of Stock' : 'Add to Cart'}

</button>

</div>

</div>

</div>

);

Day 28 Tasks:

[ ] Create AuthContext and CartContext


[ ] Build ProductsPage with search and pagination
[ ] Build ProductDetailPage
[ ] Build CartPage with checkout button
[ ] Build OrderHistoryPage
[ ] Connect all forms (login, register) to auth context
[ ] Implement admin-only product management page

DAY 29: DOCKER, CI/CD & DEPLOYMENT


Goals: Containerize everything and set up the deployment pipeline.

Complete [Link]:

version: '3.8'

services:

db:

image: postgres:16-alpine

environment:

POSTGRES_DB: ${DB_NAME:-ecommerce_db}

POSTGRES_USER: ${DB_USER:-ecommerce_user}

POSTGRES_PASSWORD: ${DB_PASSWORD:-ecommerce_pass}

volumes:

- postgres_data:/var/lib/postgresql/data
healthcheck:

test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-ecommerce_user}"]

interval: 10s

retries: 5

redis:

image: redis:7-alpine

command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

healthcheck:

test: ["CMD", "redis-cli", "ping"]

interval: 10s

rabbitmq:

image: rabbitmq:3-management-alpine

environment:

RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest}

RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-guest}

ports:

- "15672:15672"

healthcheck:

test: rabbitmq-diagnostics -q ping

interval: 15s

retries: 5

backend:

build:

context: ./backend

dockerfile: Dockerfile

ports:

- "8080:8080"

environment:

SPRING_PROFILES_ACTIVE: prod

SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/${DB_NAME:-ecommerce_db}

SPRING_DATASOURCE_USERNAME: ${DB_USER:-ecommerce_user}

SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-ecommerce_pass}
REDIS_HOST: redis

RABBITMQ_HOST: rabbitmq

JWT_SECRET: ${JWT_SECRET}

depends_on:

db:

condition: service_healthy

redis:

condition: service_healthy

rabbitmq:

condition: service_healthy

frontend:

build:

context: ./frontend

dockerfile: Dockerfile

ports:

- "80:80"

depends_on:

- backend

volumes:

postgres_data:

.env file (NEVER commit this):

DB_NAME=ecommerce_db

DB_USER=ecommerce_user

DB_PASSWORD=supersecretpassword

JWT_SECRET=your-very-long-jwt-secret-at-least-256-bits

RABBITMQ_USER=appuser

RABBITMQ_PASS=apppassword

Add .env to .gitignore. Create a .[Link] with placeholder values to commit instead.

Free deployment options:


Railway ([Link] Deploy docker-compose directly. Easiest.
Render ([Link] Free tier for web services and PostgreSQL
[Link]: Good for Dockerized apps

Day 29 Tasks:

[ ] Write Dockerfiles for backend and frontend


[ ] Create production [Link]
[ ] Create .env and .[Link] files
[ ] Run full stack with docker-compose up --build locally
[ ] Set up GitHub Actions CI pipeline
[ ] Deploy to Railway or Render
[ ] Verify all endpoints work in production

DAY 30: FINAL POLISH, TESTING & LAUNCH


Goals: Complete the portfolio project with testing, documentation, and README.

Testing Checklist:

// Minimum required tests:

// 1. Unit tests (service layer)

// UserServiceTest -- register, login, validation

// ProductServiceTest -- CRUD, search, stock validation

// CartServiceTest -- add, remove, checkout

// OrderServiceTest -- creation, status updates

// 2. Integration tests (controller layer)

// AuthControllerTest -- register, login, invalid credentials

// ProductControllerTest -- get all, get by ID, search, create (admin)

// CartControllerTest -- add, update, remove, checkout

// OrderControllerTest -- get orders, get by ID

// 3. Security tests

@Test
@WithMockUser(roles = "USER")

void adminEndpoint_shouldReturn403_forRegularUser() throws Exception {

[Link](delete("/api/admin/users/1"))

.andExpect(status().isForbidden());

@Test

void protectedEndpoint_shouldReturn401_withoutToken() throws Exception {

[Link](get("/api/users"))

.andExpect(status().isUnauthorized());

[Link] template:

EcommerceApp -- Full-Stack Java


Developer Portfolio Project
A production-ready e-commerce platform built with Spring Boot and React.

Live Demo
[Link]

Test credentials:

User: user@[Link] / password123


Admin: admin@[Link] / admin123

Features

JWT authentication & authorization


Product catalog with search, filtering, and pagination
Shopping cart and checkout flow
Order history and management
Admin dashboard (product/user management)
Redis caching for product listings
Async email notifications via RabbitMQ
Full Docker containerization

Tech Stack
Backend: Java 21, Spring Boot 3, Spring Security, JWT, PostgreSQL, Redis, RabbitMQ, Flyway,
MapStruct, Swagger Frontend: React 18, React Router, Axios, Tailwind CSS DevOps: Docker,
docker-compose, GitHub Actions CI/CD

Running Locally

git clone [Link]

cd ecommerce-app

cp .[Link] .env

docker-compose up --build

Test Coverage
Backend: ~85% line coverage (JaCoCo)

API Documentation
[Link] (when running locally)

Day 30 Tasks:

[ ] Achieve >80% test coverage on backend services


[ ] Write integration tests for all major user flows
[ ] Final end-to-end test of complete user journey
[ ] Clean up code (remove TODOs, unused imports)
[ ] Write comprehensive README with setup instructions
[ ] Add screenshots to README
[ ] Update LinkedIn with project and tech stack
[ ] Push to GitHub with clean commit history

FINAL REVIEW CHECKPOINT


Before calling yourself a full-stack Java developer, confirm you can:

Backend:

[ ] Implement Spring Security with JWT (stateless)


[ ] Write unit tests with JUnit 5 + Mockito
[ ] Write integration tests with MockMvc
[ ] Design REST APIs with proper naming, status codes, and pagination
[ ] Use DTOs + MapStruct to separate API contracts from entities
[ ] Handle exceptions globally with @RestControllerAdvice
[ ] Document APIs with Swagger/OpenAPI
[ ] Cache with Redis using @Cacheable, @CachePut, @CacheEvict
[ ] Send async messages with RabbitMQ
[ ] Manage database migrations with Flyway
[ ] Use Spring Profiles for environment-specific config

Frontend:

[ ] Build reusable React components with props and state


[ ] Fetch data with Axios + interceptors (JWT injection, 401 redirect)
[ ] Handle loading, error, and empty states
[ ] Build controlled forms with client-side validation
[ ] Navigate with React Router (protected routes, useParams)
[ ] Style responsively with Tailwind CSS

DevOps:
[ ] Write Dockerfiles for backend (multi-stage) and frontend (Nginx)
[ ] Orchestrate multi-service apps with docker-compose
[ ] Set up CI/CD with GitHub Actions
[ ] Deploy to a cloud platform

CONCLUSION
Congratulations on completing this comprehensive 30-day roadmap!

WHAT YOU'VE BUILT AND LEARNED:

Spring Security -- authentication, authorization, JWT (stateless)


Testing -- JUnit 5, Mockito, MockMvc, JaCoCo coverage
API Design -- DTOs, MapStruct, validation, REST best practices
Documentation -- Swagger/OpenAPI
Caching -- Spring Cache with Caffeine and Redis
Messaging -- RabbitMQ for async background processing
Database -- JPA, Flyway migrations, Spring Profiles
Frontend -- React with hooks, routing, forms, Axios
Styling -- Tailwind CSS responsive design
DevOps -- Docker, docker-compose, GitHub Actions CI/CD
Portfolio -- A complete, deployed e-commerce application

NEXT STEPS:

1. Apply for junior/mid-level full-stack Java developer positions


2. Contribute to open-source (check "good first issue" labels)
3. Continue learning: Kubernetes, Spring Cloud, GraphQL, AWS/Azure/GCP
4. Practice LeetCode (focus on arrays, strings, hashmaps, trees)
5. Study system design (Grokking the System Design Interview)
6. Network on LinkedIn, attend local Java meetups

FINAL CHECKLIST:

[ ] Portfolio project is deployed and publicly accessible


[ ] GitHub shows consistent, meaningful commit history
[ ] LinkedIn updated with your tech stack and project
[ ] Resume highlights Java, Spring Boot, React, Docker
[ ] You can explain every line of your project code confidently
[ ] You've practiced common interview questions

Remember: Every expert was once a beginner. Stay consistent, stay curious, and never stop
building!

Good luck on your journey!

You might also like