Aller au contenu
Java 8 min de lecture

Développement Java & APIs : Spring Boot, REST et intégration SI en pratique

Ce que recouvre une mission de développement Java API REST avec Spring Boot : architecture, gestion des erreurs, sécurité JWT, intégration SI, tests et déploiement. Guide concret pour DSI et chefs de projet.

JavaSpring BootAPI RESTSpring SecurityMicroservicesIntégration SI

Le développement d’une API Java avec Spring Boot est souvent présenté comme simple. Et il l’est — pour la partie “ça marche en local”. Ce qui prend du temps dans un vrai projet : la sécurité, la gestion des erreurs, l’intégration avec les systèmes existants, les tests, et le déploiement. Voici ce que ça implique concrètement.

Ce que Spring Boot apporte réellement

Spring Boot n’est pas juste un framework — c’est un point de vue sur comment structurer une application Java. Il prend des décisions par défaut (embedded Tomcat, configuration auto, Starter Dependencies) qui accélèrent le démarrage mais peuvent surprendre si on ne connaît pas les conventions.

Configuration d’un projet Spring Boot API :

<!-- pom.xml — structure de base -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.2</version>
</parent>

<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-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

Architecture en couches : pourquoi c’est important

Une API Java bien structurée en couches évite la “big ball of mud” :

Controller (HTTP) → Service (métier) → Repository (données)
     ↕                    ↕                    ↕
   DTOs            Domain Models          Entities JPA

Controller — uniquement du HTTP :

@RestController
@RequestMapping("/api/v1/commandes")
@RequiredArgsConstructor
public class CommandeController {

    private final CommandeService commandeService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public CommandeResponse creer(@Valid @RequestBody CommandeRequest request,
                                  @AuthenticationPrincipal Jwt jwt) {
        String userId = jwt.getSubject();
        CommandeDto commande = commandeService.creer(request, userId);
        return CommandeResponse.from(commande);
    }

    @GetMapping("/{id}")
    public CommandeResponse findById(@PathVariable UUID id,
                                     @AuthenticationPrincipal Jwt jwt) {
        return CommandeResponse.from(commandeService.findById(id, jwt.getSubject()));
    }
}

Service — logique métier isolée :

@Service
@Transactional
@RequiredArgsConstructor
public class CommandeServiceImpl implements CommandeService {

    private final CommandeRepository commandeRepository;
    private final ProduitRepository produitRepository;
    private final PrixService prixService;
    private final CommandeMapper mapper;
    private final ApplicationEventPublisher eventPublisher;

    @Override
    public CommandeDto creer(CommandeRequest request, String userId) {
        // Validation métier (au-delà de la validation syntaxique du Controller)
        validerDisponibiliteProduits(request.lignes());
        
        Commande commande = new Commande();
        commande.setUserId(userId);
        commande.setStatut(StatutCommande.EN_ATTENTE);
        
        List<LigneCommande> lignes = request.lignes().stream()
            .map(l -> creerLigne(l, commande))
            .toList();
        
        commande.setLignes(lignes);
        commande.calculerTotal();
        
        Commande savedCommande = commandeRepository.save(commande);
        
        // Event pour les side-effects (email, stock, analytics)
        eventPublisher.publishEvent(new CommandeCreeeEvent(savedCommande.getId()));
        
        return mapper.toDto(savedCommande);
    }
}

Sécurité : JWT avec Spring Security

La sécurité est souvent ajoutée en fin de projet — erreur classique. Elle doit être conçue dès le début.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/produits/**").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))
            )
            .build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthoritiesClaimName("roles");
        authoritiesConverter.setAuthorityPrefix("ROLE_");
        
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }
}

Intégration SI : patterns récurrents

L’intégration avec un SI existant (ERP, CRM, PIM) représente souvent 40-60% du temps sur un projet d’API. Trois patterns reviennent systématiquement :

Pattern 1 — Appel HTTP synchrone avec résilience :

@Service
@RequiredArgsConstructor
public class ErpClient {
    
    private final RestTemplate restTemplate;
    
    // Circuit breaker avec Resilience4j
    @CircuitBreaker(name = "erp", fallbackMethod = "getPrixFallback")
    @Retry(name = "erp")
    @TimeLimiter(name = "erp")
    public PrixDto getPrix(String sku) {
        return restTemplate.getForObject(
            "{url}/prix/{sku}", 
            PrixDto.class,
            erpBaseUrl, sku
        );
    }
    
    private PrixDto getPrixFallback(String sku, Exception ex) {
        log.warn("ERP indisponible pour SKU {}, utilisation du cache", sku, ex);
        return prixCache.getOrDefault(sku, PrixDto.prixDefaut());
    }
}

Pattern 2 — File de messages pour les événements critiques :

// Quand l'API distante peut être indisponible
@Component
@RequiredArgsConstructor
public class CommandeEventListener {

    private final JmsTemplate jmsTemplate;
    
    @EventListener
    @Async
    public void onCommandeCreee(CommandeCreeeEvent event) {
        CommandeMessage message = new CommandeMessage(event.getCommandeId());
        
        jmsTemplate.convertAndSend("commandes.nouvelles", message);
        // Le système downstream consomme quand il est disponible
    }
}

Pattern 3 — Transformation et mapping de données hétérogènes :

@Component
public class ErpProduitMapper {
    
    // L'ERP a son propre modèle de données — on mappe vers notre domaine
    public Produit fromErpResponse(ErpProduitDto erp) {
        return Produit.builder()
            .sku(erp.getCodeProduit())
            .nom(erp.getLibelle() != null ? erp.getLibelle() : erp.getReference())
            .prixHT(parsePrix(erp.getPrixVente()))
            .categorieId(mapCategorie(erp.getFamille(), erp.getSousFamille()))
            .actif(!"I".equals(erp.getStatut())) // Statut ERP : A=Actif, I=Inactif
            .build();
    }
    
    private BigDecimal parsePrix(String prixString) {
        // L'ERP envoie le prix en centimes en string...
        return new BigDecimal(prixString).divide(BigDecimal.valueOf(100), 2, HALF_UP);
    }
}

Tests : la partie qu’on bâcle à tort

Tests unitaires des services (Mockito) :

@ExtendWith(MockitoExtension.class)
class CommandeServiceTest {
    
    @Mock CommandeRepository commandeRepository;
    @Mock PrixService prixService;
    @InjectMocks CommandeServiceImpl commandeService;
    
    @Test
    void creerCommande_produitIndisponible_leveException() {
        // Given
        CommandeRequest request = CommandeRequest.builder()
            .lignes(List.of(new LigneRequest("SKU-001", 5)))
            .build();
        
        when(prixService.isDisponible("SKU-001", 5)).thenReturn(false);
        
        // When / Then
        assertThatThrownBy(() -> commandeService.creer(request, "user-123"))
            .isInstanceOf(ProduitIndisponibleException.class)
            .hasMessageContaining("SKU-001");
        
        verify(commandeRepository, never()).save(any());
    }
}

Tests d’intégration sur les endpoints (Testcontainers) :

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class CommandeControllerIT {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
        .withDatabaseName("testdb");
    
    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
    }
    
    @Test
    void creerCommande_retourne201AvecLocation() {
        // Test avec une vraie base de données PostgreSQL
        // (pas un mock H2 qui ne se comporte pas pareil)
    }
}

Estimation d’un projet API Java

À titre indicatif, les volumes que je rencontre :

Type de besoinEstimation
API CRUD simple (1 entité, JWT inclus)3-5 jours
API avec 5-8 endpoints + intégration 1 SI10-15 jours
Microservice avec messaging (Kafka/JMS)8-12 jours
Refactoring / reprise d’une API existanteAudit (2j) + selon findings
API complète (10+ endpoints, 2+ intégrations SI, tests > 70%)20-30 jours

Ces estimations varient selon la complexité métier, la documentation des APIs tierces, et la disponibilité des environnements de test.


Si vous avez un besoin d’API Java REST — nouveau développement, intégration SI, optimisation ou reprise de code existant — décrivez votre contexte pour qu’on cadre ensemble l’intervention.

Amine MEGDICHE

Amine MEGDICHE

Développeur AEM & Java Full Stack — Freelance depuis 2013

Vous avez un projet sur ces sujets ?

Envoyez-moi un message pour qualifier votre besoin, vos contraintes techniques et le bon format d'intervention.

Cadrer mon besoin