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