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_ate 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à.