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
- Use Nouns for Resources: /accounts not /getAccounts
- Plural for Collections: /accounts not /account
- HTTP Methods for Actions: Use GET, POST, PUT, DELETE appropriately
- Proper Status Codes: Return correct HTTP status codes
- Versioning: Include API version in URL (/v1/accounts)
- Pagination: Support pagination for large collections
- Filtering: Use query parameters for filtering
- Error Handling: Return consistent error responses