Full Spring Boot + PostgreSQL CRUD app — copy, paste, and run
This is a single end-to-end reference for a minimal Item resource: Create, Read, List, Update, Delete, talking to PostgreSQL with Spring Data JPA and a JSON REST API. Copy the files into a folder structure as below, then run PostgreSQL and the app with the run section at the bottom.
What you get
POST /api/items— createGET /api/items— list allGET /api/items/{id}— get onePUT /api/items/{id}— full updateDELETE /api/items/{id}— deleteapplication.ymlusing env for URL/user/password- JPA can create/alter tables in
devwhenddl-autoisupdate(use Flyway for real production)
1) Folder layout
Create a Maven project (or spring init with Web + JPA + PostgreSQL + Lombok + Validation) and use this tree:
demo-service/
pom.xml
src/main/java/com/example/demoservice/
DemoServiceApplication.java
exception/NotFoundException.java
model/Item.java
repository/ItemRepository.java
service/ItemService.java
dto/CreateItemRequest.java
dto/ItemResponse.java
dto/UpdateItemRequest.java
web/ItemController.java
web/RestExceptionHandler.java
src/main/resources/
application.ymlPackage — com.example.demoservice (change the folder path if you rename the package; keep it consistent in every file).
2) pom.xml
Replace groupId / artifactId if you use your own coordinates. This uses Java 17 and Spring Boot 3.2+ (adjust parent version in a real file if you generate from start.spring.io).
<?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.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-service</name>
<description>CRUD with PostgreSQL</description>
<properties>
<java.version>17</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.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>3) src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: demo-service
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/demo}
username: ${DB_USER:demo}
password: ${DB_PASSWORD:demo}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: ${JPA_DDL:update}
show-sql: false
open-in-view: false
properties:
hibernate:
format_sql: truespring.jpa.hibernate.ddl-auto: update— in dev, Hibernate can create/alter tables. For production usevalidate(ornone) and Flyway migrations; do not rely onupdatethere.
4) DemoServiceApplication.java
package com.example.demoservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DemoServiceApplication.class, args);
}
}5) model/Item.java
package com.example.demoservice.model;
import jakarta.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "items")
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(name = "created_at", nullable = false)
private Instant createdAt = Instant.now();
protected Item() {}
public Item(String name) {
this.name = name;
}
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Instant getCreatedAt() { return createdAt; }
}6) repository/ItemRepository.java
package com.example.demoservice.repository;
import com.example.demoservice.model.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ItemRepository extends JpaRepository<Item, Long> {
}7) DTOs
dto/CreateItemRequest.java
package com.example.demoservice.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CreateItemRequest(
@NotBlank @Size(max = 200) String name
) {}dto/UpdateItemRequest.java
package com.example.demoservice.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record UpdateItemRequest(
@NotBlank @Size(max = 200) String name
) {}dto/ItemResponse.java
package com.example.demoservice.dto;
import com.example.demoservice.model.Item;
import java.time.Instant;
public record ItemResponse(Long id, String name, Instant createdAt) {
public static ItemResponse from(Item e) {
return new ItemResponse(e.getId(), e.getName(), e.getCreatedAt());
}
}8) exception/NotFoundException.java (add before the service; used by it)
package com.example.demoservice.exception;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}9) service/ItemService.java
package com.example.demoservice.service;
import com.example.demoservice.exception.NotFoundException;
import com.example.demoservice.model.Item;
import com.example.demoservice.repository.ItemRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ItemService {
private final ItemRepository itemRepository;
public ItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
@Transactional
public Item create(String name) {
return itemRepository.save(new Item(name.trim()));
}
@Transactional(readOnly = true)
public List<Item> listAll() {
return itemRepository.findAll();
}
@Transactional(readOnly = true)
public Item getById(Long id) {
return itemRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Item not found: " + id));
}
@Transactional
public Item updateName(Long id, String newName) {
Item item = getById(id);
item.setName(newName.trim());
return item;
}
@Transactional
public void delete(Long id) {
if (!itemRepository.existsById(id)) {
throw new NotFoundException("Item not found: " + id);
}
itemRepository.deleteById(id);
}
}10) web/ItemController.java
package com.example.demoservice.web;
import com.example.demoservice.dto.*;
import com.example.demoservice.model.Item;
import com.example.demoservice.service.ItemService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/items")
public class ItemController {
private final ItemService itemService;
public ItemController(ItemService itemService) {
this.itemService = itemService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ItemResponse create(@RequestBody @Valid CreateItemRequest body) {
return ItemResponse.from(itemService.create(body.name()));
}
@GetMapping
public List<ItemResponse> list() {
return itemService.listAll().stream().map(ItemResponse::from).toList();
}
@GetMapping("/{id}")
public ItemResponse get(@PathVariable Long id) {
return ItemResponse.from(itemService.getById(id));
}
@PutMapping("/{id}")
public ItemResponse update(@PathVariable Long id, @RequestBody @Valid UpdateItemRequest body) {
return ItemResponse.from(itemService.updateName(id, body.name()));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
itemService.delete(id);
}
}11) web/RestExceptionHandler.java (map 404 to HTTP)
package com.example.demoservice.web;
import com.example.demoservice.exception.NotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
import java.util.stream.Collectors;
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Map<String, String>> notFound(NotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> badRequest(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", msg));
}
}12) Optional: one-command PostgreSQL with Docker
docker run --name pg-demo -e POSTGRES_USER=demo -e POSTGRES_PASSWORD=demo -e POSTGRES_DB=demo -p 5432:5432 -d postgres:16Then either export env in your shell:
export DB_URL=jdbc:postgresql://localhost:5432/demo
export DB_USER=demo
export DB_PASSWORD=demo
export JPA_DDL=updateOr leave defaults in application.yml (they already match the Docker command above, except ensure username/password/db are demo).
Create DB manually (without Docker) example:
createdb -U postgres demo
psql -U postgres -c "CREATE USER demo WITH PASSWORD 'demo';"
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE demo TO demo;"(Tune to your local Postgres; match application.yml.)
13) Build and run
cd demo-service
./mvnw -DskipTests package
java -jar target/demo-service-0.0.1-SNAPSHOT.jarOr during development:
./mvnw spring-boot:runHealth check — open or curl:
http://localhost:8080/api/items— should return[]the first time.
Try CRUD (use curl or Postman):
# Create
curl -s -X POST http://localhost:8080/api/items -H "Content-Type: application/json" -d '{"name":"Alpha"}'
# List
curl -s http://localhost:8080/api/items
# Get by id (replace 1)
curl -s http://localhost:8080/api/items/1
# Update
curl -s -X PUT http://localhost:8080/api/items/1 -H "Content-Type: application/json" -d '{"name":"Alpha renamed"}'
# Delete
curl -s -X DELETE http://localhost:8080/api/items/114) Notes
- Lombok is in
pom.xmlfor optional use; the Java above does not use Lombok annotations, so the project compiles as-is. You can remove the Lombok dependency if you like. - If you do not have the Maven wrapper (
mvnw), usemvn packageandmvn spring-boot:runinstead, or add the wrapper withmvn -N io.takari:maven:wrapper(once). - For production: switch
ddl-autotovalidate, add Flyway migrations, use strong DB credentials from a secret store, and do not expose the DB to the public internet.