Creare un'API GraphQL in Java

Creare un'API GraphQL in Java

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 limita perPage; 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à.