Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties

### Local secrets ###
application-local.yaml

### Project ###
contacts.zip
contacts.json
Expand Down
1 change: 1 addition & 0 deletions .idea/dictionaries/project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .run/Docker Compose Up.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker Compose Up" type="ShConfigurationType" factoryName="Shell Script">
<option name="SCRIPT_TEXT" value="docker compose up -d" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>
3 changes: 2 additions & 1 deletion .run/Spring Development.run.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Spring Development" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="dev" />
<option name="ACTIVE_PROFILES" value="local" />
<module name="contacts" />
<option name="SPRING_BOOT_MAIN_CLASS" value="codes.yam.contacts.ContactsApplication" />
<method v="2">
<option name="Make" enabled="true" />
<option name="ToolBeforeRunTask" enabled="true" actionId="Tool_External Tools_Docker Compose Up + Wait" />
</method>
</configuration>
</component>
109 changes: 77 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,53 +21,90 @@ This app follows the architecture from [Hypermedia Systems](https://hypermedia.s
- Add, edit, and delete contact details
- Server-rendered HTML with Thymeleaf
- Partial page updates (no full reloads)
- H2 in-memory database with Spring Data JPA
- PostgreSQL + Spring Data JPA + Flyway migrations

## Development

### Quickstart
### Prerequisites

- Java 25
- Docker (for local Postgres)

### Setup

1. **Create `src/main/resources/application-local.yaml`** (gitignored — never committed):

```yaml
spring:
security:
user:
name: <your-username>
password: <your-password>
datasource:
url: jdbc:postgresql://localhost:5432/contacts
username: contacts
password: contacts
jpa:
hibernate:
ddl-auto: validate
sql:
init:
mode: always
thymeleaf:
cache: false
prefix: file:src/main/resources/templates/
devtools:
livereload:
enabled: true
restart:
additional-paths: src/main/resources/templates/
additional-exclude: "**/*.html"
web:
resources:
static-locations:
- file:src/main/resources/static/
- classpath:/static/
cache:
period: 0
chain:
cache: false
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

```bash
# Start dev server
./mvnw spring-boot:run
2. **Start Postgres:**

# Open in browser
open http://localhost:8080
```bash
./scripts/start-db.sh
```

### VS Code
3. **Run the app:**

1. **Install extensions** - open the Extensions panel, search `@recommended`, and install all workspace recommendations (Java Extension Pack, Spring Boot Extension Pack, Lombok, htmx attributes)
2. **Java 25 SDK** - VS Code will prompt to download a JDK if none is found; select Java 25 (if you have multiple JDKs, point `java.jdt.ls.java.home` in user settings to your Java 25 installation)
3. **Run** - use the **Run** task via `Ctrl+Shift+P → Tasks: Run Task → Run`, or press `Ctrl+Shift+P → Spring Boot Dashboard: Run` from the Spring Boot extension

**Tips:**

- Spring Boot DevTools (already included) enables hot reload - save a file and changes apply automatically
- Thymeleaf templates hot-reload without a restart
- H2 console is available at `http://localhost:8080/h2-console`
```bash
./mvnw spring-boot:run -Dspring-boot.run.profiles=local
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

### IntelliJ IDEA

1. **Open** - **File → Open**, select `pom.xml`, choose **"Open as Project"**
2. **Java 25 SDK** - **File → Project Structure → SDKs**, add Java 25 if missing (IntelliJ can download it)
3. **Lombok** - install the [Lombok plugin](https://plugins.jetbrains.com/plugin/6317-lombok) via **Settings → Plugins**, then enable annotation processing under **Settings → Build, Execution, Deployment → Compiler → Annotation Processors**
4. **Run** - use the included **Contacts** run configuration (`.run/Contacts.run.xml`) from the toolbar, or run `spring-boot:run` from the Maven tool window
4. **Create `application-local.yaml`** - see [Setup](#setup) above
5. **Run** - use the included **Development** compound run configuration from the toolbar — it starts Docker, waits for Postgres, then launches the app

**Tips:**

- Spring Boot DevTools (already included) enables hot reload - recompile with `Ctrl+F9` without restarting
- Enable **"Build project automatically"** (**Settings → Build, Execution, Deployment → Compiler**) and **"Allow auto-make to start even if developed application is currently running"** (**Settings → Advanced Settings**) for seamless reloads on save
- Thymeleaf templates hot-reload without a restart
- H2 console is available at `http://localhost:8080/h2-console`

### Scripts

| Command | Description |
| ------------------------ | ---------------- |
| `./mvnw spring-boot:run` | Start dev server |
| `./mvnw test` | Run tests |
| `./mvnw package` | Build jar |
| Command | Description |
| --------------------------------------------------------- | -------------------- |
| `./scripts/start-db.sh` | Start local Postgres |
| `docker compose down` | Stop local Postgres |
| `./mvnw spring-boot:run -Dspring-boot.run.profiles=local` | Start dev server |
| `./mvnw test` | Run tests |
| `./mvnw package` | Build jar |

## Project structure

Expand All @@ -81,6 +118,8 @@ src/
│ │ ├── ContactRepository.java # Spring Data JPA (search, pagination)
│ │ └── ContactService.java # Validation, slug generation, business logic
│ └── resources/
│ ├── db/migration/
│ │ └── V1__init.sql # Initial schema (Flyway)
│ ├── templates/
│ │ ├── layout.html # Base layout (Thymeleaf fragment)
│ │ ├── contacts/
Expand All @@ -97,13 +136,18 @@ src/
│ │ ├── styles.css
│ │ └── img/
│ │ └── spinning-circles.svg # Loading spinner
│ └── application.yaml
└── test/
└── java/codes/yam/contacts/
├── controller/
│ └── ContactControllerTest.java
└── service/
└── ContactServiceTest.java
│ ├── application.yaml # Production config (env var placeholders)
│ ├── application-local.yaml # Local overrides — gitignored, never committed
│ └── data.sql # Seed data (local only)
├── test/
│ └── java/codes/yam/contacts/
│ ├── controller/
│ │ └── ContactControllerTest.java
│ └── service/
│ └── ContactServiceTest.java
scripts/
└── start-db.sh # docker compose up + wait for Postgres readiness
docker-compose.yml # Local Postgres service
```

### Routes
Expand All @@ -130,7 +174,8 @@ src/
| Framework | [Spring Boot](https://spring.io/projects/spring-boot) 4.0.3 |
| Templating | [Thymeleaf](https://www.thymeleaf.org) |
| Hypermedia | [htmx](https://htmx.org) + [htmx-spring-boot-thymeleaf](https://github.com/wimdeblauwe/htmx-spring-boot) |
| Database | H2 (in-memory) + Spring Data JPA |
| Database | PostgreSQL + Spring Data JPA |
| Migrations | [Flyway](https://flywaydb.org) |
| Build | Maven |

## Branches
Expand Down
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
db:
image: postgres:17
environment:
POSTGRES_DB: contacts
POSTGRES_USER: contacts
POSTGRES_PASSWORD: contacts
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U contacts -d contacts"]
interval: 5s
timeout: 5s
retries: 10

volumes:
postgres_data:
19 changes: 16 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-h2console</artifactId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<artifactId>spring-boot-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -72,10 +80,15 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down
12 changes: 12 additions & 0 deletions scripts/start-db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
MAX_RETRIES=30
docker compose up -d
i=0
until docker compose exec -T db pg_isready -q; do
i=$((i + 1))
if [ "$i" -ge "$MAX_RETRIES" ]; then
echo "Postgres did not become ready after $MAX_RETRIES seconds" >&2
exit 1
fi
sleep 1
done
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
6 changes: 1 addition & 5 deletions src/main/java/codes/yam/contacts/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
Expand All @@ -14,11 +13,8 @@ public class SecurityConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http.authorizeHttpRequests(
auth -> auth.requestMatchers("/h2-console/**").permitAll().anyRequest().authenticated())
return http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"))
.build();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}
26 changes: 0 additions & 26 deletions src/main/resources/application-dev.yaml

This file was deleted.

8 changes: 6 additions & 2 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ spring:
thymeleaf:
cache: true
jpa:
defer-datasource-initialization: true
open-in-view: false
hibernate:
ddl-auto: validate
datasource:
url: jdbc:h2:mem:contacts;DB_CLOSE_DELAY=-1
url: ${DATABASE_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
security:
user:
name: ${APP_USERNAME}
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/db/migration/V1__init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE contact (
id UUID PRIMARY KEY,
slug VARCHAR(255),
first VARCHAR(255),
last VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(255)
);
31 changes: 31 additions & 0 deletions src/main/resources/db/seed/V2__seed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
INSERT INTO contact (id, slug, first, last, email, phone) VALUES
('550e8400-e29b-41d4-a716-446655440000', 'john-doe', 'John', 'Doe', 'john@example.com', '555-1234'),
('550e8400-e29b-41d4-a716-446655440001', 'jane-doe', 'Jane', 'Doe', 'jane@example.com', '555-5678'),
('550e8400-e29b-41d4-a716-446655440002', 'alice-smith', 'Alice', 'Smith', 'alice.smith@example.com', '555-1001'),
('550e8400-e29b-41d4-a716-446655440003', 'bob-johnson', 'Bob', 'Johnson', 'bob.johnson@example.com', '555-1002'),
('550e8400-e29b-41d4-a716-446655440004', 'carol-williams', 'Carol', 'Williams', 'carol.williams@example.com', '555-1003'),
('550e8400-e29b-41d4-a716-446655440005', 'david-brown', 'David', 'Brown', 'david.brown@example.com', '555-1004'),
('550e8400-e29b-41d4-a716-446655440006', 'eve-jones', 'Eve', 'Jones', 'eve.jones@example.com', '555-1005'),
('550e8400-e29b-41d4-a716-446655440007', 'frank-garcia', 'Frank', 'Garcia', 'frank.garcia@example.com', '555-1006'),
('550e8400-e29b-41d4-a716-446655440008', 'grace-miller', 'Grace', 'Miller', 'grace.miller@example.com', '555-1007'),
('550e8400-e29b-41d4-a716-446655440009', 'henry-davis', 'Henry', 'Davis', 'henry.davis@example.com', '555-1008'),
('550e8400-e29b-41d4-a716-44665544000a', 'iris-martinez', 'Iris', 'Martinez', 'iris.martinez@example.com', '555-1009'),
('550e8400-e29b-41d4-a716-44665544000b', 'jack-wilson', 'Jack', 'Wilson', 'jack.wilson@example.com', '555-1010'),
('550e8400-e29b-41d4-a716-44665544000c', 'karen-anderson', 'Karen', 'Anderson', 'karen.anderson@example.com', '555-1011'),
('550e8400-e29b-41d4-a716-44665544000d', 'leo-thomas', 'Leo', 'Thomas', 'leo.thomas@example.com', '555-1012'),
('550e8400-e29b-41d4-a716-44665544000e', 'mia-taylor', 'Mia', 'Taylor', 'mia.taylor@example.com', '555-1013'),
('550e8400-e29b-41d4-a716-44665544000f', 'noah-hernandez', 'Noah', 'Hernandez', 'noah.hernandez@example.com', '555-1014'),
('550e8400-e29b-41d4-a716-446655440010', 'olivia-moore', 'Olivia', 'Moore', 'olivia.moore@example.com', '555-1015'),
('550e8400-e29b-41d4-a716-446655440011', 'paul-jackson', 'Paul', 'Jackson', 'paul.jackson@example.com', '555-1016'),
('550e8400-e29b-41d4-a716-446655440012', 'quinn-martin', 'Quinn', 'Martin', 'quinn.martin@example.com', '555-1017'),
('550e8400-e29b-41d4-a716-446655440013', 'rachel-lee', 'Rachel', 'Lee', 'rachel.lee@example.com', '555-1018'),
('550e8400-e29b-41d4-a716-446655440014', 'sam-perez', 'Sam', 'Perez', 'sam.perez@example.com', '555-1019'),
('550e8400-e29b-41d4-a716-446655440015', 'tina-white', 'Tina', 'White', 'tina.white@example.com', '555-1020'),
('550e8400-e29b-41d4-a716-446655440016', 'ursula-harris', 'Ursula', 'Harris', 'ursula.harris@example.com', '555-1021'),
('550e8400-e29b-41d4-a716-446655440017', 'victor-clark', 'Victor', 'Clark', 'victor.clark@example.com', '555-1022'),
('550e8400-e29b-41d4-a716-446655440018', 'wendy-lewis', 'Wendy', 'Lewis', 'wendy.lewis@example.com', '555-1023'),
('550e8400-e29b-41d4-a716-446655440019', 'xavier-robinson', 'Xavier', 'Robinson', 'xavier.robinson@example.com', '555-1024'),
('550e8400-e29b-41d4-a716-44665544001a', 'yara-walker', 'Yara', 'Walker', 'yara.walker@example.com', '555-1025'),
('550e8400-e29b-41d4-a716-44665544001b', 'zane-hall', 'Zane', 'Hall', 'zane.hall@example.com', '555-1026'),
('550e8400-e29b-41d4-a716-44665544001c', 'amber-allen', 'Amber', 'Allen', 'amber.allen@example.com', '555-1027'),
('550e8400-e29b-41d4-a716-44665544001d', 'brian-young', 'Brian', 'Young', 'brian.young@example.com', '555-1028');
16 changes: 16 additions & 0 deletions src/test/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
spring:
flyway:
enabled: false
datasource:
url: jdbc:h2:mem:contacts;DB_CLOSE_DELAY=-1
jpa:
hibernate:
ddl-auto: create-drop
defer-datasource-initialization: true
sql:
init:
mode: always
security:
user:
name: admin
password: admin
Loading