diff --git a/server/pom.xml b/server/pom.xml
index df00ce6..9421e79 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -26,6 +26,13 @@
spring-boot-starter-web
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.6.0
+
+
org.postgresql
postgresql
diff --git a/server/src/main/java/com/example/relationaldataaccess/Customer.java b/server/src/main/java/com/example/relationaldataaccess/Customer.java
index 2c73e13..3e035c8 100644
--- a/server/src/main/java/com/example/relationaldataaccess/Customer.java
+++ b/server/src/main/java/com/example/relationaldataaccess/Customer.java
@@ -1,8 +1,17 @@
package com.example.relationaldataaccess;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "Customer entity representing a customer in the system")
public class Customer {
+ @Schema(description = "Unique identifier for the customer", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private long id;
- private String firstName, lastName;
+
+ @Schema(description = "Customer's first name", example = "John", maxLength = 50, requiredMode = Schema.RequiredMode.REQUIRED)
+ private String firstName;
+
+ @Schema(description = "Customer's last name", example = "Doe", maxLength = 50, requiredMode = Schema.RequiredMode.REQUIRED)
+ private String lastName;
public Customer(long id, String firstName, String lastName) {
this.id = id;
diff --git a/server/src/main/java/com/example/relationaldataaccess/config/OpenApiConfig.java b/server/src/main/java/com/example/relationaldataaccess/config/OpenApiConfig.java
new file mode 100644
index 0000000..5cd1ccd
--- /dev/null
+++ b/server/src/main/java/com/example/relationaldataaccess/config/OpenApiConfig.java
@@ -0,0 +1,70 @@
+package com.example.relationaldataaccess.config;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.servers.Server;
+
+@Configuration
+public class OpenApiConfig {
+
+ @Value("${cors.allowed-origins:http://localhost:5173}")
+ private String allowedOrigins;
+
+ @Bean
+ public OpenAPI customerManagementOpenAPI() {
+ // Parse the first allowed origin for the server URL
+ String serverUrl = allowedOrigins.split(",")[0].trim();
+
+ // For local development, use backend URL
+ if (serverUrl.contains("localhost:5173") || serverUrl.contains("localhost:5174")) {
+ serverUrl = "http://localhost:8080";
+ }
+
+ Server server = new Server()
+ .url(serverUrl)
+ .description("Customer Management API Server");
+
+ Contact contact = new Contact()
+ .name("Customer Management Team")
+ .email("support@customerapp.com")
+ .url("https://github.com/JohanCodeForFun/customer-management-aws-demo");
+
+ License license = new License()
+ .name("MIT License")
+ .url("https://opensource.org/licenses/MIT");
+
+ Info info = new Info()
+ .title("Customer Management API")
+ .description("""
+ A comprehensive REST API for managing customer data with full CRUD operations.
+
+ ## Features
+ - **Customer CRUD Operations**: Create, Read, Update, Delete customers
+ - **Search Functionality**: Search customers by name (case-insensitive)
+ - **Health Monitoring**: System health and connectivity checks
+ - **Input Validation**: Automatic sanitization and validation
+ - **CORS Support**: Cross-origin resource sharing enabled
+
+ ## Authentication
+ Currently no authentication required. In production, implement proper authentication.
+
+ ## Error Handling
+ All endpoints return appropriate HTTP status codes and error messages.
+ """)
+ .version("1.0.0")
+ .contact(contact)
+ .license(license);
+
+ return new OpenAPI()
+ .info(info)
+ .servers(List.of(server));
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java b/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java
index 2041f49..fd9cc66 100644
--- a/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java
+++ b/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java
@@ -1,21 +1,58 @@
package com.example.relationaldataaccess.controller;
-import com.example.relationaldataaccess.Customer;
+import java.util.List;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
-import java.util.List;
+import com.example.relationaldataaccess.Customer;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/customers")
@CrossOrigin(origins = "${cors.allowed-origins:http://localhost:5173,http://localhost:5174}")
+@Tag(name = "Customer Management", description = "REST API for managing customer data with full CRUD operations and search functionality")
public class CustomerController {
@Autowired
private JdbcTemplate jdbcTemplate;
+ @Operation(
+ summary = "Get all customers",
+ description = "Retrieve a complete list of all customers in the system, ordered by ID"
+ )
+ @ApiResponses(value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Successfully retrieved customers",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = Customer.class),
+ examples = @ExampleObject(
+ name = "Customer list example",
+ value = "[{\"id\":1,\"firstName\":\"John\",\"lastName\":\"Doe\"},{\"id\":2,\"firstName\":\"Jane\",\"lastName\":\"Smith\"}]"
+ )
+ )
+ )
+ })
@GetMapping
public List getAllCustomers() {
return jdbcTemplate.query(
@@ -28,8 +65,32 @@ public List getAllCustomers() {
);
}
+ @Operation(
+ summary = "Get customer by ID",
+ description = "Retrieve a specific customer by their unique identifier"
+ )
+ @ApiResponses(value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Customer found",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = Customer.class),
+ examples = @ExampleObject(
+ name = "Customer example",
+ value = "{\"id\":1,\"firstName\":\"John\",\"lastName\":\"Doe\"}"
+ )
+ )
+ ),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Customer not found with the specified ID"
+ )
+ })
@GetMapping("/{id}")
- public ResponseEntity getCustomerById(@PathVariable Long id) {
+ public ResponseEntity getCustomerById(
+ @Parameter(description = "Unique identifier of the customer", required = true, example = "1")
+ @PathVariable Long id) {
List customers = jdbcTemplate.query(
"SELECT id, first_name, last_name FROM customers WHERE id = ?",
(rs, rowNum) -> new Customer(
@@ -47,8 +108,44 @@ public ResponseEntity getCustomerById(@PathVariable Long id) {
return ResponseEntity.ok(customers.get(0));
}
+ @Operation(
+ summary = "Create a new customer",
+ description = "Create a new customer with the provided first name and last name. Names are automatically sanitized and validated for security."
+ )
+ @ApiResponses(value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Customer successfully created",
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = Customer.class),
+ examples = @ExampleObject(
+ name = "Created customer example",
+ value = "{\"id\":3,\"firstName\":\"John\",\"lastName\":\"Doe\"}"
+ )
+ )
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Invalid customer data provided (empty names, null data, etc.)"
+ )
+ })
@PostMapping
- public Customer createCustomer(@RequestBody Customer customer) {
+ public Customer createCustomer(
+ @Parameter(description = "Customer data with firstName and lastName", required = true)
+ @io.swagger.v3.oas.annotations.parameters.RequestBody(
+ description = "Customer object with first name and last name",
+ required = true,
+ content = @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = Customer.class),
+ examples = @ExampleObject(
+ name = "New customer example",
+ value = "{\"firstName\":\"John\",\"lastName\":\"Doe\"}"
+ )
+ )
+ )
+ @RequestBody Customer customer) {
// Input validation and sanitization
if (customer == null) {
throw new IllegalArgumentException("Customer data cannot be null");
@@ -99,7 +196,28 @@ public Customer createCustomer(@RequestBody Customer customer) {
}
@DeleteMapping("/{id}")
- public ResponseEntity deleteCustomer(@PathVariable Long id) {
+ @Operation(
+ summary = "Delete a customer",
+ description = "Deletes a customer by their unique ID. Returns 200 OK if the customer was successfully deleted, or 404 Not Found if no customer exists with the specified ID."
+ )
+ @ApiResponses(value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Customer successfully deleted"
+ ),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Customer not found with the specified ID",
+ content = @Content
+ )
+ })
+ public ResponseEntity deleteCustomer(
+ @Parameter(
+ description = "The unique identifier of the customer to delete",
+ required = true,
+ example = "1"
+ )
+ @PathVariable Long id) {
int rowsAffected = jdbcTemplate.update("DELETE FROM customers WHERE id = ?", id);
if (rowsAffected > 0) {
diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties
index f61aaf6..7e865c5 100644
--- a/server/src/main/resources/application.properties
+++ b/server/src/main/resources/application.properties
@@ -14,4 +14,12 @@ server.port=${PORT:8080}
logging.level.com.example.relationaldataaccess=${LOG_LEVEL:DEBUG}
# CORS Configuration
-cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:5174}
\ No newline at end of file
+cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:5174}
+
+# OpenAPI/Swagger Configuration
+springdoc.api-docs.path=/api/docs
+springdoc.swagger-ui.path=/api/swagger-ui
+springdoc.swagger-ui.operationsSorter=method
+springdoc.swagger-ui.tagsSorter=alpha
+springdoc.swagger-ui.tryItOutEnabled=true
+springdoc.swagger-ui.filter=true
\ No newline at end of file