Full Stack • Java • System Design • Cloud • AI Engineering

Web Services2026-06-17

Java RESTful Web Services

Master Java REST APIs with visual diagrams covering JAX-RS, Spring REST, HTTP methods, URI design, annotations, and complete implementation examples

RESTful Web Services in Java provide a standardized way to build scalable, stateless APIs. Understanding JAX-RS, Spring REST, and proper REST design principles is essential for modern Java development.

REST vs SOAP Comparison

graph TB
    subgraph "REST Architecture"
        A[REST API] --> B[Lightweight]
        A --> C[HTTP Methods]
        A --> D[Multiple Formats]
        A --> E[Stateless]
        
        C --> C1[GET POST PUT DELETE]
        D --> D1[JSON XML Plain Text]
    end
    
    subgraph "SOAP Architecture"
        F[SOAP API] --> G[Heavy XML]
        F --> H[WSDL Contract]
        F --> I[Complex]
        F --> J[Stateful Possible]
        
        H --> H1[Strict Schema]
        I --> I1[More Overhead]
    end
    
    style A fill:#4CAF50
    style F fill:#FF9800

Key Points:

  • REST: Lightweight, uses HTTP methods, supports multiple formats (JSON, XML)
  • SOAP: Heavy XML-based, requires WSDL, more complex protocol
  • Simplicity: REST is simpler to implement and consume
  • Performance: REST has less overhead, faster processing
  • Flexibility: REST supports multiple data formats, SOAP primarily XML

REST Benefits Code Example

// REST - Simple and Clean
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
    
    @GetMapping("/{id}")
    public ResponseEntity<Account> getAccount(@PathVariable Long id) {
        Account account = accountService.findById(id);
        return ResponseEntity.ok(account);
    }
    
    @PostMapping
    public ResponseEntity<Account> createAccount(@RequestBody Account account) {
        Account saved = accountService.save(account);
        return ResponseEntity.status(HttpStatus.CREATED).body(saved);
    }
}

// SOAP - More Complex (for comparison)
@WebService
public class AccountWebService {
    
    @WebMethod
    public Account getAccount(@WebParam(name = "id") Long id) {
        return accountService.findById(id);
    }
    
    @WebMethod
    public Account createAccount(@WebParam(name = "account") Account account) {
        return accountService.save(account);
    }
}

RESTful URI Design

graph LR
    A[Protocol<br/>https://] --> B[Domain<br/>api.example.com]
    B --> C[Port<br/>:8080]
    C --> D[App<br/>/accounting]
    D --> E[Version<br/>/v1]
    E --> F[Resource<br/>/accounts]
    F --> G[ID<br/>/123]
    G --> H[Sub-Resource<br/>/transactions]
    
    style F fill:#4CAF50
    style H fill:#FF9800

Key Points:

  • Nouns Not Verbs: Use nouns for resources (accounts, not getAccounts)
  • Plural Resources: Use plural for collections (/accounts not /account)
  • Hierarchical: Organize resources hierarchically
  • Versioning: Include API version in URL
  • HTTP Methods: Use HTTP verbs for actions (GET, POST, PUT, DELETE)

URI Design Examples

// Good REST URI Design
@RestController
@RequestMapping("/api/v1/accounting")
public class AccountingController {
    
    // GET all accounts (Collection)
    // GET /api/v1/accounting/accounts
    @GetMapping("/accounts")
    public List<Account> getAllAccounts() {
        return accountService.findAll();
    }
    
    // GET single account (Resource)
    // GET /api/v1/accounting/accounts/123
    @GetMapping("/accounts/{accountId}")
    public Account getAccount(@PathVariable Long accountId) {
        return accountService.findById(accountId);
    }
    
    // GET transactions for an account (Sub-resource collection)
    // GET /api/v1/accounting/accounts/123/transactions
    @GetMapping("/accounts/{accountId}/transactions")
    public List<Transaction> getAccountTransactions(
            @PathVariable Long accountId) {
        return transactionService.findByAccountId(accountId);
    }
    
    // GET specific transaction (Sub-resource)
    // GET /api/v1/accounting/accounts/123/transactions/567
    @GetMapping("/accounts/{accountId}/transactions/{txnId}")
    public Transaction getTransaction(
            @PathVariable Long accountId,
            @PathVariable Long txnId) {
        return transactionService.findById(accountId, txnId);
    }
    
    // POST create new account
    // POST /api/v1/accounting/accounts
    @PostMapping("/accounts")
    public ResponseEntity<Account> createAccount(
            @RequestBody Account account) {
        Account saved = accountService.save(account);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(saved.getId())
            .toUri();
        return ResponseEntity.created(location).body(saved);
    }
    
    // PUT update existing account
    // PUT /api/v1/accounting/accounts/123
    @PutMapping("/accounts/{accountId}")
    public Account updateAccount(
            @PathVariable Long accountId,
            @RequestBody Account account) {
        account.setId(accountId);
        return accountService.update(account);
    }
    
    // DELETE account
    // DELETE /api/v1/accounting/accounts/123
    @DeleteMapping("/accounts/{accountId}")
    public ResponseEntity<Void> deleteAccount(
            @PathVariable Long accountId) {
        accountService.delete(accountId);
        return ResponseEntity.noContent().build();
    }
    
    // Query parameters for filtering/searching
    // GET /api/v1/accounting/accounts/123/transactions?date=2026-01-01&status=COMPLETED
    @GetMapping("/accounts/{accountId}/transactions")
    public List<Transaction> searchTransactions(
            @PathVariable Long accountId,
            @RequestParam(required = false) LocalDate date,
            @RequestParam(required = false) String status) {
        return transactionService.search(accountId, date, status);
    }
}

HTTP Methods and Status Codes

sequenceDiagram
    participant Client
    participant API as REST API
    participant DB as Database
    
    Client->>API: GET /accounts/123
    API->>DB: SELECT
    DB-->>API: Account data
    API-->>Client: 200 OK + Account
    
    Client->>API: POST /accounts
    API->>DB: INSERT
    DB-->>API: New account
    API-->>Client: 201 Created + Location
    
    Client->>API: PUT /accounts/123
    API->>DB: UPDATE
    DB-->>API: Updated account
    API-->>Client: 200 OK + Account
    
    Client->>API: DELETE /accounts/123
    API->>DB: DELETE
    DB-->>API: Success
    API-->>Client: 204 No Content
    
    Client->>API: GET /accounts/999
    API->>DB: SELECT
    DB-->>API: Not found
    API-->>Client: 404 Not Found

Key Points:

  • GET: Retrieve resource(s), idempotent, safe, returns 200 OK
  • POST: Create new resource, not idempotent, returns 201 Created
  • PUT: Update existing resource, idempotent, returns 200 OK
  • DELETE: Remove resource, idempotent, returns 204 No Content
  • Status Codes: Use appropriate HTTP status codes (2xx, 4xx, 5xx)

HTTP Methods Implementation

@RestController
@RequestMapping("/api/accounts")
public class AccountRestController {
    
    @Autowired
    private AccountService accountService;
    
    // GET - Retrieve (Safe, Idempotent)
    @GetMapping("/{id}")
    public ResponseEntity<Account> getAccount(@PathVariable Long id) {
        return accountService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    // POST - Create (Not Idempotent)
    @PostMapping
    public ResponseEntity<Account> createAccount(
            @Valid @RequestBody Account account) {
        Account saved = accountService.save(account);
        
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(saved.getId())
            .toUri();
        
        return ResponseEntity.created(location).body(saved);
    }
    
    // PUT - Update (Idempotent)
    @PutMapping("/{id}")
    public ResponseEntity<Account> updateAccount(
            @PathVariable Long id,
            @Valid @RequestBody Account account) {
        
        if (!accountService.exists(id)) {
            return ResponseEntity.notFound().build();
        }
        
        account.setId(id);
        Account updated = accountService.update(account);
        return ResponseEntity.ok(updated);
    }
    
    // PATCH - Partial Update
    @PatchMapping("/{id}")
    public ResponseEntity<Account> patchAccount(
            @PathVariable Long id,
            @RequestBody Map<String, Object> updates) {
        
        return accountService.findById(id)
            .map(account -> {
                accountService.applyPatch(account, updates);
                Account updated = accountService.update(account);
                return ResponseEntity.ok(updated);
            })
            .orElse(ResponseEntity.notFound().build());
    }
    
    // DELETE - Remove (Idempotent)
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteAccount(@PathVariable Long id) {
        if (!accountService.exists(id)) {
            return ResponseEntity.notFound().build();
        }
        
        accountService.delete(id);
        return ResponseEntity.noContent().build();
    }
    
    // Exception handling
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation failed",
            ex.getBindingResult().getAllErrors()
        );
        
        return ResponseEntity.badRequest().body(error);
    }
}

JAX-RS Annotations

// Complete JAX-RS Example
@Path("/accounts")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AccountResource {
    
    @Inject
    private AccountService accountService;
    
    // GET all accounts
    @GET
    public Response getAllAccounts(
            @QueryParam("page") @DefaultValue("0") int page,
            @QueryParam("size") @DefaultValue("10") int size) {
        
        List<Account> accounts = accountService.findAll(page, size);
        return Response.ok(accounts).build();
    }
    
    // GET single account by ID
    @GET
    @Path("/{id}")
    public Response getAccount(@PathParam("id") Long id) {
        return accountService.findById(id)
            .map(account -> Response.ok(account).build())
            .orElse(Response.status(Response.Status.NOT_FOUND).build());
    }
    
    // POST create new account
    @POST
    public Response createAccount(
            @Valid Account account,
            @Context UriInfo uriInfo) {
        
        Account saved = accountService.save(account);
        
        URI location = uriInfo.getAbsolutePathBuilder()
            .path(String.valueOf(saved.getId()))
            .build();
        
        return Response.created(location).entity(saved).build();
    }
    
    // PUT update account
    @PUT
    @Path("/{id}")
    public Response updateAccount(
            @PathParam("id") Long id,
            @Valid Account account) {
        
        if (!accountService.exists(id)) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        
        account.setId(id);
        Account updated = accountService.update(account);
        return Response.ok(updated).build();
    }
    
    // DELETE account
    @DELETE
    @Path("/{id}")
    public Response deleteAccount(@PathParam("id") Long id) {
        if (!accountService.exists(id)) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        
        accountService.delete(id);
        return Response.noContent().build();
    }
    
    // Sub-resource for transactions
    @Path("/{accountId}/transactions")
    public TransactionResource getTransactionResource(
            @PathParam("accountId") Long accountId) {
        return new TransactionResource(accountId);
    }
    
    // Content negotiation - XML support
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_XML)
    public Response getAccountAsXml(@PathParam("id") Long id) {
        return accountService.findById(id)
            .map(account -> Response.ok(account).build())
            .orElse(Response.status(Response.Status.NOT_FOUND).build());
    }
    
    // Form parameters
    @POST
    @Path("/search")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response searchAccounts(
            @FormParam("name") String name,
            @FormParam("status") String status) {
        
        List<Account> accounts = accountService.search(name, status);
        return Response.ok(accounts).build();
    }
    
    // Header parameters
    @GET
    @Path("/{id}")
    public Response getAccountWithHeaders(
            @PathParam("id") Long id,
            @HeaderParam("Accept-Language") String language,
            @HeaderParam("Authorization") String authToken) {
        
        // Use headers for authentication, localization, etc.
        Account account = accountService.findById(id, language);
        return Response.ok(account).build();
    }
}

Spring REST Implementation

// Complete Spring REST Example
@RestController
@RequestMapping("/api/v1/accounts")
@Validated
public class AccountController {
    
    @Autowired
    private AccountService accountService;
    
    // GET with pagination and sorting
    @GetMapping
    public ResponseEntity<Page<Account>> getAllAccounts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy) {
        
        Pageable pageable = PageRequest.of(page, size, 
            Sort.by(sortBy).descending());
        Page<Account> accounts = accountService.findAll(pageable);
        
        return ResponseEntity.ok(accounts);
    }
    
    // GET with custom headers
    @GetMapping("/{id}")
    public ResponseEntity<Account> getAccount(
            @PathVariable Long id,
            @RequestHeader(value = "X-API-Version", defaultValue = "1") String version) {
        
        return accountService.findById(id)
            .map(account -> ResponseEntity.ok()
                .header("X-Account-Status", account.getStatus())
                .body(account))
            .orElse(ResponseEntity.notFound().build());
    }
    
    // POST with validation
    @PostMapping
    public ResponseEntity<Account> createAccount(
            @Valid @RequestBody AccountRequest request) {
        
        Account account = accountService.create(request);
        
        return ResponseEntity
            .created(URI.create("/api/v1/accounts/" + account.getId()))
            .body(account);
    }
    
    // Async processing
    @PostMapping("/async")
    public CompletableFuture<ResponseEntity<Account>> createAccountAsync(
            @Valid @RequestBody AccountRequest request) {
        
        return accountService.createAsync(request)
            .thenApply(account -> ResponseEntity
                .created(URI.create("/api/v1/accounts/" + account.getId()))
                .body(account));
    }
    
    // File upload
    @PostMapping("/{id}/documents")
    public ResponseEntity<Document> uploadDocument(
            @PathVariable Long id,
            @RequestParam("file") MultipartFile file) throws IOException {
        
        Document document = accountService.saveDocument(id, file);
        return ResponseEntity.ok(document);
    }
    
    // Global exception handling
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ResourceNotFoundException ex) {
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

// DTO classes
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AccountRequest {
    @NotBlank(message = "Account name is required")
    private String name;
    
    @Email(message = "Invalid email format")
    private String email;
    
    @Min(value = 0, message = "Balance must be positive")
    private BigDecimal balance;
}

@Data
@AllArgsConstructor
public class ErrorResponse {
    private int status;
    private String message;
    private LocalDateTime timestamp;
}

Thread Safety in REST

REST resources are thread-safe by default because:

  • New resource instance created per request
  • No shared state between requests
  • Stateless architecture
// Thread-safe REST resource
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
    
    @Autowired
    private AccountService accountService; // Singleton, thread-safe
    
    @GetMapping("/{id}")
    public Account getAccount(@PathVariable Long id) {
        // New instance per request - thread-safe
        return accountService.findById(id);
    }
}

Best Practices

  1. Use Nouns for Resources: /accounts not /getAccounts
  2. Plural for Collections: /accounts not /account
  3. HTTP Methods for Actions: Use GET, POST, PUT, DELETE appropriately
  4. Proper Status Codes: Return correct HTTP status codes
  5. Versioning: Include API version in URL (/v1/accounts)
  6. Pagination: Support pagination for large collections
  7. Filtering: Use query parameters for filtering
  8. Error Handling: Return consistent error responses