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:
@@ -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
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# known-accepted findings go here
|
||||||
+27
@@ -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)
|
||||||
|
"""
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
+30
@@ -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" "$@"
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
Generated
+4465
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>App</h1>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user