Guides/ Java

Spring Boot with PostgreSQL and JPA

Create a Java API project with Spring Boot, PostgreSQL, Maven, JPA, JUnit 5, and optional Spring libraries using Better Fullstack.

Updated 2026-05-12

spring-bootpostgresjpajava

Use this stack when you want a Java API with the Spring ecosystem and relational persistence.

npm create better-fullstack@latest my-spring-api -- \
  --ecosystem java \
  --java-web-framework spring-boot \
  --java-build-tool maven \
  --database postgres \
  --java-orm spring-data-jpa \
  --java-testing-libraries junit5

What this creates

  • A Java project using Spring Boot.
  • Maven as the build tool.
  • PostgreSQL as the database option.
  • JPA for persistence.
  • JUnit 5 for testing.

Generated shape

The scaffold is a conventional Spring Boot service with wrapper scripts, source sets, a health controller, and JPA-backed sample user endpoints. The package name is derived from the project name under com.example.

my-spring-api/
├── mvnw
├── .mvn/
├── pom.xml
├── src/main/java/com/example/myspringapi/
│   ├── Application.java
│   ├── controller/
│   │   ├── HealthController.java
│   │   └── UserController.java
│   ├── domain/
│   │   └── AppUser.java
│   ├── repository/
│   │   └── AppUserRepository.java
│   └── service/
│       └── AppUserService.java
├── src/main/resources/
│   └── application.yml
└── src/test/java/com/example/myspringapi/
    └── ApplicationTests.java

The generated local JPA datasource uses an H2 file database in PostgreSQL compatibility mode. Treat --database postgres as the target database selection and switch the datasource URL, driver, credentials, and JDBC dependency for a real PostgreSQL environment.

When to choose it

Choose Spring Boot when you want a conventional Java backend with a large ecosystem, common enterprise integrations, and predictable application structure.

Choose this guide when you want a minimal Spring Boot API with persistence but not security and migration tooling on day one. Choose the secure Spring guide when every endpoint should start behind Spring Security and integration testing with Testcontainers is part of the baseline.

Decision pointMinimal Spring JPASecure Spring API
Build toolMavenGradle
SecurityNot selectedSpring Security selected
Database changesHibernate ddl-auto local loopFlyway migration path
TestsJUnit 5JUnit 5 plus Testcontainers
Best forLearning, prototypes, internal CRUDProduction-oriented API baseline

Representative snippets

The generated domain model is a standard JPA entity. Add fields here when they belong to the persisted aggregate.

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.Instant;

@Entity
public class AppUser {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String email;
  private String displayName;
  private Instant createdAt = Instant.now();

  protected AppUser() {
  }

  public AppUser(String email, String displayName) {
    this.email = email;
    this.displayName = displayName;
  }
}

Repositories stay small because Spring Data JPA derives common queries from method names.

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AppUserRepository extends JpaRepository<AppUser, Long> {
  Optional<AppUser> findByEmail(String email);
}

The controller exposes GET /users and POST /users through the service layer.

@RestController
@RequestMapping("/users")
public class UserController {
  private final AppUserService userService;

  public UserController(AppUserService userService) {
    this.userService = userService;
  }

  @GetMapping
  public List<UserResponse> listUsers() {
    return userService.findAll().stream().map(UserResponse::from).toList();
  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public UserResponse createUser(@RequestBody CreateUserRequest request) {
    AppUser createdUser = userService.create(request.email(), request.displayName());
    return UserResponse.from(createdUser);
  }
}

Migrations and tests

This minimal stack does not select Flyway or Liquibase. Hibernate can keep the local H2 schema moving during early development, but add a migration library before multiple environments depend on the database shape.

./mvnw test
./mvnw spring-boot:run
./mvnw package

When adding production PostgreSQL, move from local defaults to environment-driven datasource settings:

spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate

Compatibility notes

  • --java-orm spring-data-jpa requires Spring Boot and a Java build tool.
  • Flyway and Liquibase are tied to the Spring Data JPA path in Better Fullstack.
  • This guide uses --java-build-tool maven; use Maven wrapper commands unless you intentionally regenerate with Gradle.
  • The local generated datasource and dependency are H2 even though this stack targets PostgreSQL. Add the PostgreSQL JDBC driver and override spring.datasource.* before deploying against a managed Postgres database.

Deployment notes

Build the jar with Maven, provide Java 21 or newer, and run the application with environment-specific datasource settings.

./mvnw package
java -jar target/*.jar

For production, add migrations, add the PostgreSQL JDBC driver, configure connection pooling through Spring Boot datasource properties, and avoid relying on ddl-auto: update. Expose /health through your platform health check.

Troubleshooting

SymptomCheck
App starts with H2 instead of PostgresOverride spring.datasource.* in environment-specific config.
POST /users accepts incomplete inputAdd --java-libraries spring-validation in a future scaffold or add Bean Validation manually.
JPA entities do not appearKeep entities under the generated application package so component scanning finds them.
Maven commands failUse ./mvnw, not a globally installed Maven with a different version.

Tradeoffs

Spring Boot is heavier than minimal API frameworks, but it pays off when you need established conventions, library depth, and long-lived backend maintainability.

Next steps