Skip to content

Commit f4fb69b

Browse files
Merge pull request #6 from JohanCodeForFun/test/add-swagger-ui2
Add OpenAPI documentation and Swagger UI configuration for Customer API
2 parents 7d00736 + 90ab6fa commit f4fb69b

File tree

5 files changed

+220
-8
lines changed

5 files changed

+220
-8
lines changed

server/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
<artifactId>spring-boot-starter-web</artifactId>
2727
</dependency>
2828

29+
<!-- SpringDoc OpenAPI (Swagger) for API documentation -->
30+
<dependency>
31+
<groupId>org.springdoc</groupId>
32+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
33+
<version>2.6.0</version>
34+
</dependency>
35+
2936
<dependency>
3037
<groupId>org.postgresql</groupId>
3138
<artifactId>postgresql</artifactId>

server/src/main/java/com/example/relationaldataaccess/Customer.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
package com.example.relationaldataaccess;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
@Schema(description = "Customer entity representing a customer in the system")
36
public class Customer {
7+
@Schema(description = "Unique identifier for the customer", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
48
private long id;
5-
private String firstName, lastName;
9+
10+
@Schema(description = "Customer's first name", example = "John", maxLength = 50, requiredMode = Schema.RequiredMode.REQUIRED)
11+
private String firstName;
12+
13+
@Schema(description = "Customer's last name", example = "Doe", maxLength = 50, requiredMode = Schema.RequiredMode.REQUIRED)
14+
private String lastName;
615

716
public Customer(long id, String firstName, String lastName) {
817
this.id = id;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.example.relationaldataaccess.config;
2+
3+
import java.util.List;
4+
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
import io.swagger.v3.oas.models.OpenAPI;
10+
import io.swagger.v3.oas.models.info.Contact;
11+
import io.swagger.v3.oas.models.info.Info;
12+
import io.swagger.v3.oas.models.info.License;
13+
import io.swagger.v3.oas.models.servers.Server;
14+
15+
@Configuration
16+
public class OpenApiConfig {
17+
18+
@Value("${cors.allowed-origins:http://localhost:5173}")
19+
private String allowedOrigins;
20+
21+
@Bean
22+
public OpenAPI customerManagementOpenAPI() {
23+
// Parse the first allowed origin for the server URL
24+
String serverUrl = allowedOrigins.split(",")[0].trim();
25+
26+
// For local development, use backend URL
27+
if (serverUrl.contains("localhost:5173") || serverUrl.contains("localhost:5174")) {
28+
serverUrl = "http://localhost:8080";
29+
}
30+
31+
Server server = new Server()
32+
.url(serverUrl)
33+
.description("Customer Management API Server");
34+
35+
Contact contact = new Contact()
36+
.name("Customer Management Team")
37+
.email("support@customerapp.com")
38+
.url("https://github.com/JohanCodeForFun/customer-management-aws-demo");
39+
40+
License license = new License()
41+
.name("MIT License")
42+
.url("https://opensource.org/licenses/MIT");
43+
44+
Info info = new Info()
45+
.title("Customer Management API")
46+
.description("""
47+
A comprehensive REST API for managing customer data with full CRUD operations.
48+
49+
## Features
50+
- **Customer CRUD Operations**: Create, Read, Update, Delete customers
51+
- **Search Functionality**: Search customers by name (case-insensitive)
52+
- **Health Monitoring**: System health and connectivity checks
53+
- **Input Validation**: Automatic sanitization and validation
54+
- **CORS Support**: Cross-origin resource sharing enabled
55+
56+
## Authentication
57+
Currently no authentication required. In production, implement proper authentication.
58+
59+
## Error Handling
60+
All endpoints return appropriate HTTP status codes and error messages.
61+
""")
62+
.version("1.0.0")
63+
.contact(contact)
64+
.license(license);
65+
66+
return new OpenAPI()
67+
.info(info)
68+
.servers(List.of(server));
69+
}
70+
}

server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,58 @@
11
package com.example.relationaldataaccess.controller;
22

3-
import com.example.relationaldataaccess.Customer;
3+
import java.util.List;
4+
45
import org.springframework.beans.factory.annotation.Autowired;
56
import org.springframework.http.ResponseEntity;
67
import org.springframework.jdbc.core.JdbcTemplate;
7-
import org.springframework.web.bind.annotation.*;
8+
import org.springframework.web.bind.annotation.CrossOrigin;
9+
import org.springframework.web.bind.annotation.DeleteMapping;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RequestParam;
16+
import org.springframework.web.bind.annotation.RestController;
817

9-
import java.util.List;
18+
import com.example.relationaldataaccess.Customer;
19+
20+
import io.swagger.v3.oas.annotations.Operation;
21+
import io.swagger.v3.oas.annotations.Parameter;
22+
import io.swagger.v3.oas.annotations.media.Content;
23+
import io.swagger.v3.oas.annotations.media.ExampleObject;
24+
import io.swagger.v3.oas.annotations.media.Schema;
25+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
26+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
27+
import io.swagger.v3.oas.annotations.tags.Tag;
1028

1129
@RestController
1230
@RequestMapping("/api/customers")
1331
@CrossOrigin(origins = "${cors.allowed-origins:http://localhost:5173,http://localhost:5174}")
32+
@Tag(name = "Customer Management", description = "REST API for managing customer data with full CRUD operations and search functionality")
1433
public class CustomerController {
1534

1635
@Autowired
1736
private JdbcTemplate jdbcTemplate;
1837

38+
@Operation(
39+
summary = "Get all customers",
40+
description = "Retrieve a complete list of all customers in the system, ordered by ID"
41+
)
42+
@ApiResponses(value = {
43+
@ApiResponse(
44+
responseCode = "200",
45+
description = "Successfully retrieved customers",
46+
content = @Content(
47+
mediaType = "application/json",
48+
schema = @Schema(implementation = Customer.class),
49+
examples = @ExampleObject(
50+
name = "Customer list example",
51+
value = "[{\"id\":1,\"firstName\":\"John\",\"lastName\":\"Doe\"},{\"id\":2,\"firstName\":\"Jane\",\"lastName\":\"Smith\"}]"
52+
)
53+
)
54+
)
55+
})
1956
@GetMapping
2057
public List<Customer> getAllCustomers() {
2158
return jdbcTemplate.query(
@@ -28,8 +65,32 @@ public List<Customer> getAllCustomers() {
2865
);
2966
}
3067

68+
@Operation(
69+
summary = "Get customer by ID",
70+
description = "Retrieve a specific customer by their unique identifier"
71+
)
72+
@ApiResponses(value = {
73+
@ApiResponse(
74+
responseCode = "200",
75+
description = "Customer found",
76+
content = @Content(
77+
mediaType = "application/json",
78+
schema = @Schema(implementation = Customer.class),
79+
examples = @ExampleObject(
80+
name = "Customer example",
81+
value = "{\"id\":1,\"firstName\":\"John\",\"lastName\":\"Doe\"}"
82+
)
83+
)
84+
),
85+
@ApiResponse(
86+
responseCode = "404",
87+
description = "Customer not found with the specified ID"
88+
)
89+
})
3190
@GetMapping("/{id}")
32-
public ResponseEntity<Customer> getCustomerById(@PathVariable Long id) {
91+
public ResponseEntity<Customer> getCustomerById(
92+
@Parameter(description = "Unique identifier of the customer", required = true, example = "1")
93+
@PathVariable Long id) {
3394
List<Customer> customers = jdbcTemplate.query(
3495
"SELECT id, first_name, last_name FROM customers WHERE id = ?",
3596
(rs, rowNum) -> new Customer(
@@ -47,8 +108,44 @@ public ResponseEntity<Customer> getCustomerById(@PathVariable Long id) {
47108
return ResponseEntity.ok(customers.get(0));
48109
}
49110

111+
@Operation(
112+
summary = "Create a new customer",
113+
description = "Create a new customer with the provided first name and last name. Names are automatically sanitized and validated for security."
114+
)
115+
@ApiResponses(value = {
116+
@ApiResponse(
117+
responseCode = "200",
118+
description = "Customer successfully created",
119+
content = @Content(
120+
mediaType = "application/json",
121+
schema = @Schema(implementation = Customer.class),
122+
examples = @ExampleObject(
123+
name = "Created customer example",
124+
value = "{\"id\":3,\"firstName\":\"John\",\"lastName\":\"Doe\"}"
125+
)
126+
)
127+
),
128+
@ApiResponse(
129+
responseCode = "400",
130+
description = "Invalid customer data provided (empty names, null data, etc.)"
131+
)
132+
})
50133
@PostMapping
51-
public Customer createCustomer(@RequestBody Customer customer) {
134+
public Customer createCustomer(
135+
@Parameter(description = "Customer data with firstName and lastName", required = true)
136+
@io.swagger.v3.oas.annotations.parameters.RequestBody(
137+
description = "Customer object with first name and last name",
138+
required = true,
139+
content = @Content(
140+
mediaType = "application/json",
141+
schema = @Schema(implementation = Customer.class),
142+
examples = @ExampleObject(
143+
name = "New customer example",
144+
value = "{\"firstName\":\"John\",\"lastName\":\"Doe\"}"
145+
)
146+
)
147+
)
148+
@RequestBody Customer customer) {
52149
// Input validation and sanitization
53150
if (customer == null) {
54151
throw new IllegalArgumentException("Customer data cannot be null");
@@ -99,7 +196,28 @@ public Customer createCustomer(@RequestBody Customer customer) {
99196
}
100197

101198
@DeleteMapping("/{id}")
102-
public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
199+
@Operation(
200+
summary = "Delete a customer",
201+
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."
202+
)
203+
@ApiResponses(value = {
204+
@ApiResponse(
205+
responseCode = "200",
206+
description = "Customer successfully deleted"
207+
),
208+
@ApiResponse(
209+
responseCode = "404",
210+
description = "Customer not found with the specified ID",
211+
content = @Content
212+
)
213+
})
214+
public ResponseEntity<Void> deleteCustomer(
215+
@Parameter(
216+
description = "The unique identifier of the customer to delete",
217+
required = true,
218+
example = "1"
219+
)
220+
@PathVariable Long id) {
103221
int rowsAffected = jdbcTemplate.update("DELETE FROM customers WHERE id = ?", id);
104222

105223
if (rowsAffected > 0) {

server/src/main/resources/application.properties

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,12 @@ server.port=${PORT:8080}
1414
logging.level.com.example.relationaldataaccess=${LOG_LEVEL:DEBUG}
1515

1616
# CORS Configuration
17-
cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:5174}
17+
cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:5174}
18+
19+
# OpenAPI/Swagger Configuration
20+
springdoc.api-docs.path=/api/docs
21+
springdoc.swagger-ui.path=/api/swagger-ui
22+
springdoc.swagger-ui.operationsSorter=method
23+
springdoc.swagger-ui.tagsSorter=alpha
24+
springdoc.swagger-ui.tryItOutEnabled=true
25+
springdoc.swagger-ui.filter=true

0 commit comments

Comments
 (0)