scaffold: React 19 + Spring Boot 3.4 + PostgreSQL project

Sets up toolchain pinning, format/lint (Biome + Spotless), lefthook
pre-commit hooks, Docker Compose for Postgres, and trivial passing
tests for both stacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 00:52:57 +02:00
commit cc00c6c6ae
25 changed files with 4944 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Copy to .env — mise run setup does this automatically
DATABASE_URL=jdbc:postgresql://localhost:5432/app_dev
DATABASE_USERNAME=app
DATABASE_PASSWORD=app
LOG_LEVEL=INFO
+27
View File
@@ -0,0 +1,27 @@
# Java / Maven
target/
*.class
*.jar
*.war
*.ear
hs_err_pid*
# Spring Boot
*.log
# Node
frontend/node_modules/
frontend/dist/
frontend/.vite/
# IDE
.idea/
*.iml
.vscode/
# macOS
.DS_Store
# Environment — never commit real credentials
.env
!.env.example
+1
View File
@@ -0,0 +1 @@
# known-accepted findings go here
+27
View File
@@ -0,0 +1,27 @@
[tools]
java = "temurin-21"
maven = "3.9.9"
node = "22"
lefthook = "1.11.12"
gitleaks = "8.26.0"
[tasks.setup]
description = "Install deps and git hooks"
run = """
#!/bin/bash
set -e
npm --prefix frontend install
lefthook install
cp -n .env.example .env 2>/dev/null || true
"""
[tasks.check]
description = "Format-check, lint, and test"
run = """
#!/bin/bash
set -e
mvn -f backend/pom.xml spotless:check -q
(cd frontend && npx biome check src)
mvn -f backend/pom.xml test -q
(cd frontend && npx vitest run)
"""
+27
View File
@@ -0,0 +1,27 @@
# App
A full-stack web app: React 19 frontend talking to a Spring Boot 3.4 REST API backed by PostgreSQL 17.
The frontend is a single-page app; it is not server-side rendered. This is not a monorepo — it is
one project with two build roots (`frontend/` and `backend/`) under a single git repo.
**Stack:** Vite 6 + React 19 (TypeScript, strict) — Biome 1.9 for format + lint — Vitest + Testing Library for frontend tests.
Spring Boot 3.4 (Java 21) — Maven — Google Java Format via Spotless — JUnit 5 + AssertJ for backend tests.
PostgreSQL 17 via Docker Compose; Flyway owns all schema changes.
## Rules
- **Never suppress warnings** with `@SuppressWarnings`, `biome-ignore`, or `// noinspection`; fix the root cause.
- **All HTTP responses are validated with Zod** on the frontend before touching any field; never access raw `response.data` directly.
- **Flyway owns the schema** — never `ALTER TABLE` manually or let Hibernate generate DDL. Write a migration instead.
- **Logs via SLF4J only** on the backend (`LOG_LEVEL` env var controls root level); do not use `System.out.println`.
- **Transaction boundaries live in the service layer**, not controllers or repositories. No `@Transactional` on `@RestController`.
## Running locally
```bash
docker compose up -d # PostgreSQL on :5432
mvn -f backend/pom.xml spring-boot:run # backend on :8080
cd frontend && npm run dev # frontend on :5173 (proxies /api → :8080)
```
Verify everything at once: `mise run check` (requires mise; otherwise run the four commands in .mise.toml manually).
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+48
View File
@@ -0,0 +1,48 @@
# App
React 19 + Spring Boot 3.4 web app backed by PostgreSQL 17.
## Quick start
> **Requires a full JDK** (not just JRE) for Maven compilation. `mise install` below handles this.
> Without mise: `sudo apt install openjdk-21-jdk`.
```bash
# 1. Pin toolchain — installs Java 21 JDK, Node 22, Maven, lefthook, gitleaks
mise install
# 2. Install deps and git hooks (or run the four commands manually)
mise run setup
# 3. Start the database
docker compose up -d
```
Open two terminals:
```bash
# Backend → http://localhost:8080
mvn -f backend/pom.xml spring-boot:run
# Frontend → http://localhost:5173 (proxies /api → :8080)
cd frontend && npm run dev
```
## Verify everything
```bash
mise run check # format-check + lint + test (both stacks)
```
Without mise:
```bash
mvn -f backend/pom.xml spotless:check -q
cd frontend && npx biome check src
mvn -f backend/pom.xml test -q
cd frontend && npx vitest run
```
## Environment
`.env.example` documents all env vars. `mise run setup` copies it to `.env` automatically on first run.
+2
View File
@@ -0,0 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.tar.gz
wrapperVersion=3.3.2
Vendored Executable
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
# Minimal Maven Wrapper — downloads Apache Maven on first run and caches it.
set -e
MAVEN_VERSION="3.9.9"
MAVEN_CACHE="${HOME}/.m2/wrapper/dists/apache-maven-${MAVEN_VERSION}"
MAVEN_URL="https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/${MAVEN_VERSION}/apache-maven-${MAVEN_VERSION}-bin.tar.gz"
if [ ! -f "${MAVEN_CACHE}/bin/mvn" ]; then
MAVEN_TMP="${MAVEN_CACHE}.tmp.$$"
if mkdir "${MAVEN_CACHE}.lock" 2>/dev/null; then
echo "Downloading Apache Maven ${MAVEN_VERSION}..." >&2
mkdir -p "${MAVEN_TMP}"
if command -v curl > /dev/null 2>&1; then
curl -fsSL "${MAVEN_URL}" | tar -xz --strip-components=1 -C "${MAVEN_TMP}"
elif command -v wget > /dev/null 2>&1; then
wget -qO- "${MAVEN_URL}" | tar -xz --strip-components=1 -C "${MAVEN_TMP}"
else
rm -rf "${MAVEN_TMP}" "${MAVEN_CACHE}.lock"
echo "ERROR: curl or wget is required to download Maven" >&2
exit 1
fi
mv "${MAVEN_TMP}" "${MAVEN_CACHE}"
rm -rf "${MAVEN_CACHE}.lock"
else
while [ -d "${MAVEN_CACHE}.lock" ]; do sleep 0.2; done
fi
fi
exec "${MAVEN_CACHE}/bin/mvn" "$@"
+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>app</name>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Xlint:-processing</arg>
<arg>-Xlint:-serial</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.44.0</version>
<configuration>
<java>
<googleJavaFormat>
<version>1.23.0</version>
<style>GOOGLE</style>
</googleJavaFormat>
<removeUnusedImports/>
<trimTrailingWhitespace/>
<endWithNewline/>
</java>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,11 @@
package com.example.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,17 @@
spring:
application:
name: app
datasource:
url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/app_dev}
username: ${DATABASE_USERNAME:app}
password: ${DATABASE_PASSWORD:app}
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
flyway:
enabled: true
logging:
level:
root: ${LOG_LEVEL:INFO}
@@ -0,0 +1,12 @@
package com.example.app;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class ApplicationTest {
@Test
void applicationClassLoads() {
assertThat(Application.class.getSimpleName()).isEqualTo("Application");
}
}
+14
View File
@@ -0,0 +1,14 @@
services:
db:
image: postgres:17-alpine
ports:
- "5432:5432"
environment:
POSTGRES_DB: app_dev
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4465
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "frontend",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome check src",
"format": "biome format --write src"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"jsdom": "^25.0.1",
"typescript": "~5.7.2",
"vite": "^6.0.5",
"vitest": "^2.1.8"
}
}
+7
View File
@@ -0,0 +1,7 @@
export default function App() {
return (
<main>
<h1>App</h1>
</main>
);
}
+12
View File
@@ -0,0 +1,12 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>,
);
+10
View File
@@ -0,0 +1,10 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import App from "../src/App";
describe("App", () => {
it("renders a main element", () => {
render(<App />);
expect(screen.getByRole("main")).toBeDefined();
});
});
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
},
"include": ["src", "tests"]
}
+17
View File
@@ -0,0 +1,17 @@
/// <reference types="vitest" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:8080",
},
},
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./tests/setup.ts"],
},
});
+25
View File
@@ -0,0 +1,25 @@
pre-commit:
parallel: true
commands:
gitleaks:
run: gitleaks protect --staged --no-banner --redact
format-frontend:
glob: "frontend/src/**/*.{ts,tsx}"
run: cd frontend && npx biome format --write src/
stage_fixed: true
lint-frontend:
glob: "frontend/src/**/*.{ts,tsx}"
run: cd frontend && npx biome check src/
format-backend:
glob: "backend/src/**/*.java"
run: ./backend/mvnw -f backend/pom.xml spotless:apply -q
stage_fixed: true
lint-backend:
glob: "backend/src/**/*.java"
run: ./backend/mvnw -f backend/pom.xml spotless:check -q
pre-push:
commands:
test-frontend:
run: cd frontend && npx vitest run
test-backend:
run: ./backend/mvnw -f backend/pom.xml test -q