Questo articolo illustra un percorso chiaro per esporre un unico modello, Post
, tramite GraphQL in Java. Useremo Spring Boot con Spring for GraphQL e JPA. Toccheremo schema, resolver, paginazione “page-based”, ordinamento per publishedAt
, validazione, autorizzazione e CORS sullendpoint /graphql
. Lobiettivo è una base pulita, facile da estendere ad altri modelli.
Stack e organizzazione
- Runtime: Java 17+ (consigliato)
- Framework: Spring Boot + Spring for GraphQL
- Persistenza: Spring Data JPA (database a scelta; H2 per sviluppo)
- Validazione: Jakarta Validation
- Sicurezza (opzionale): Spring Security per popolare il SecurityContext
Schema GraphQL (SDL): tipo Post e operazioni
Posiziona lo schema in src/main/resources/graphql/schema.graphqls
. Manteniamo il perimetro al solo Post
, con paginazione e CRUD essenziali.
"""Rappresenta un contenuto pubblicato."""
type Post {
id: ID!
title: String!
body: String!
publishedAt: String
createdAt: String!
updatedAt: String!
}
"""Metadati di paginazione."""
type PaginatorInfo {
count: Int!
currentPage: Int!
lastPage: Int!
perPage: Int!
total: Int!
hasMorePages: Boolean!
}
"""Risultato paginato dei Post."""
type PostPaginator {
data: [Post!]!
paginatorInfo: PaginatorInfo!
}
type Query {
posts(page: Int = 1, perPage: Int = 10, sort: String = "desc"): PostPaginator!
post(id: ID!): Post
}
type Mutation {
createPost(title: String!, body: String!, publishedAt: String): Post!
updatePost(id: ID!, title: String, body: String, publishedAt: String): Post!
deletePost(id: ID!): Boolean!
}
Dipendenze
Aggiungi le dipendenze in Maven o Gradle.
<!-- pom.xml (estratto) -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Opzionale: sicurezza -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
// build.gradle (estratto)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'com.h2database:h2'
// opzionale
implementation 'org.springframework.boot:spring-boot-starter-security'
}
Configurazione base
Imposta il path dellendpoint e abilita una console H2 per lo sviluppo.
# src/main/resources/application.properties
spring.graphql.path=/graphql
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:devdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
spring.jpa.hibernate.ddl-auto=update
Entità JPA Post
Un'entità essenziale con timestamp gestiti da callback e ordinamento su publishedAt
.
// src/main/java/com/example/postapi/domain/Post.java
package com.example.postapi.domain;
import jakarta.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "posts", indexes = {
@Index(name = "idx_posts_published_at", columnList = "published_at")
})
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String title;
@Lob @Column(nullable = false)
private String body;
@Column(name = "published_at")
private Instant publishedAt;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@PrePersist
void onCreate() {
Instant now = Instant.now();
this.createdAt = now;
this.updatedAt = now;
}
@PreUpdate
void onUpdate() {
this.updatedAt = Instant.now();
}
// getters e setters
public Long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public Instant getPublishedAt() { return publishedAt; }
public void setPublishedAt(Instant publishedAt) { this.publishedAt = publishedAt; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
Repository
Usiamo Spring Data JPA per paginazione e ordinamento.
// src/main/java/com/example/postapi/repo/PostRepository.java
package com.example.postapi.repo;
import com.example.postapi.domain.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
// findAll(Pageable) già disponibile per paginare/ordinare
}
DTO per paginazione
Due semplici DTO per incapsulare lista e metadati; i nomi dei campi seguono lo schema GraphQL.
// src/main/java/com/example/postapi/graphql/dto/PaginatorInfo.java
package com.example.postapi.graphql.dto;
public class PaginatorInfo {
public int count;
public int currentPage;
public int lastPage;
public int perPage;
public long total;
public boolean hasMorePages;
}
// src/main/java/com/example/postapi/graphql/dto/PostPaginator.java
package com.example.postapi.graphql.dto;
import com.example.postapi.domain.Post;
import java.util.List;
public class PostPaginator {
public List<Post> data;
public PaginatorInfo paginatorInfo;
public PostPaginator(List<Post> data, PaginatorInfo paginatorInfo) {
this.data = data;
this.paginatorInfo = paginatorInfo;
}
}
Validazione input
Definiamo i payload delle mutation con Jakarta Validation.
// src/main/java/com/example/postapi/graphql/input/CreatePostInput.java
package com.example.postapi.graphql.input;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class CreatePostInput {
@NotBlank @Size(max = 255)
public String title;
@NotBlank
public String body;
// ISO-8601 opzionale
public String publishedAt;
}
// src/main/java/com/example/postapi/graphql/input/UpdatePostInput.java
package com.example.postapi.graphql.input;
import jakarta.validation.constraints.Size;
public class UpdatePostInput {
public Long id;
@Size(min = 1, max = 255)
public String title;
public String body;
public String publishedAt;
}
Resolver GraphQL (Controller)
Implementiamo le query e le mutation. La paginazione usa PageRequest
e lordinamento cade su publishedAt
in modalità asc/desc. Lautorizzazione può essere applicata con Spring Security (ad esempio annotazioni @PreAuthorize
) o controlli custom.
// src/main/java/com/example/postapi/graphql/PostGraphQLController.java
package com.example.postapi.graphql;
import com.example.postapi.domain.Post;
import com.example.postapi.repo.PostRepository;
import com.example.postapi.graphql.dto.PaginatorInfo;
import com.example.postapi.graphql.dto.PostPaginator;
import com.example.postapi.graphql.input.CreatePostInput;
import com.example.postapi.graphql.input.UpdatePostInput;
import jakarta.validation.Valid;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Optional;
@Controller
@Validated
public class PostGraphQLController {
private final PostRepository posts;
public PostGraphQLController(PostRepository posts) {
this.posts = posts;
}
@QueryMapping
public PostPaginator posts(@Argument Integer page, @Argument Integer perPage, @Argument String sort) {
int p = (page != null && page > 0) ? page : 1;
int pp = (perPage != null && perPage > 0 && perPage <= 100) ? perPage : 10;
boolean asc = "asc".equalsIgnoreCase(sort);
Sort s = Sort.by("publishedAt");
s = asc ? s.ascending() : s.descending();
Pageable pageable = PageRequest.of(p - 1, pp, s);
Page<Post> paged = posts.findAll(pageable);
PaginatorInfo info = new PaginatorInfo();
info.count = paged.getNumberOfElements();
info.currentPage = p;
info.lastPage = Math.max(1, paged.getTotalPages());
info.perPage = pp;
info.total = paged.getTotalElements();
info.hasMorePages = paged.hasNext();
return new PostPaginator(paged.getContent(), info);
}
@QueryMapping
public Post post(@Argument Long id) {
return posts.findById(id).orElse(null);
}
@MutationMapping
public Post createPost(@Argument @Valid CreatePostInput input) {
Post p = new Post();
p.setTitle(input.title);
p.setBody(input.body);
if (input.publishedAt != null && !input.publishedAt.isBlank()) {
try { p.setPublishedAt(Instant.parse(input.publishedAt)); }
catch (DateTimeParseException ignored) {}
}
return posts.save(p);
}
@MutationMapping
public Post updatePost(@Argument @Valid UpdatePostInput input) {
Post current = posts.findById(input.id).orElseThrow();
if (input.title != null) current.setTitle(input.title);
if (input.body != null) current.setBody(input.body);
if (input.publishedAt != null) {
current.setPublishedAt(input.publishedAt.isBlank() ? null : Instant.parse(input.publishedAt));
}
return posts.save(current);
}
@MutationMapping
public Boolean deletePost(@Argument Long id) {
Optional<Post> found = posts.findById(id);
if (found.isEmpty()) return false;
posts.deleteById(id);
return true;
}
}
CORS sull'endpoint /graphql
Consenti gli origin necessari e il metodo OPTIONS
per il preflight.
// src/main/java/com/example/postapi/config/WebConfig.java
package com.example.postapi.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/graphql")
.allowedOrigins("http://localhost:5173", "https://app.example.com")
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("content-type", "authorization")
.allowCredentials(true)
.maxAge(600);
}
}
Esempi dal punto di vista del client
Richiedi solo i campi necessari; una mutation che ritorna Boolean!
non ammette subselection.
query {
posts(page: 1, perPage: 5, sort: "desc") {
data { id title publishedAt }
paginatorInfo { currentPage lastPage perPage total hasMorePages count }
}
}
query {
post(id: 1) { id title body publishedAt }
}
mutation {
createPost(title: "Titolo", body: "Testo", publishedAt: "2025-01-01T00:00:00Z") {
id title publishedAt
}
}
mutation {
updatePost(id: 1, title: "Nuovo titolo") { id title updatedAt }
}
mutation {
deletePost(id: 1)
}
Note su validazione, autorizzazione, performance
- Validazione: usa Jakarta Validation su input e, se necessario, constraint custom.
- Autorizzazione: applica regole con Spring Security (es.
@PreAuthorize
) o controlli nel controller. - Performance: indicizza
published_at
e limitaperPage
; per flussi continui valuta una paginazione a cursori.
Conclusione
Con pochi file ottieni un'API GraphQL in Java centrata su Post
: schema chiaro, resolver snelli, paginazione e ordinamento prevedibili, validazione e CORS pronti per ambienti reali. Questa base si estende naturalmente ad altri modelli mantenendo coerenza e qualità.