Skip to content

Commit 6c8e790

Browse files
committed
feat(api)!: normalize endpoints, add Swagger annotations
1 parent 84704eb commit 6c8e790

3 files changed

Lines changed: 94 additions & 37 deletions

File tree

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,36 @@
1515
import org.springframework.web.bind.annotation.RestController;
1616
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
1717

18+
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
19+
import io.swagger.v3.oas.annotations.Operation;
20+
import io.swagger.v3.oas.annotations.info.Contact;
21+
import io.swagger.v3.oas.annotations.info.Info;
22+
import io.swagger.v3.oas.annotations.info.License;
23+
import io.swagger.v3.oas.annotations.media.Content;
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;
1828
import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO;
1929
import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService;
2030

2131
@RestController
32+
@Tag(name = "Books", description = "CRUD operations for books")
33+
@OpenAPIDefinition(
34+
info = @Info(
35+
title = "java.samples.spring.boot",
36+
version = "1.0",
37+
description = "🧪 Proof of Concept for a RESTful Web Service made with JDK 21 (LTS) and Spring Boot 3",
38+
contact = @Contact(
39+
name = "Github",
40+
url = "https://github.com/nanotaboada/java.samples.spring.boot"
41+
),
42+
license = @License(
43+
name = "MIT License",
44+
url = "https://opensource.org/licenses/MIT"
45+
)
46+
)
47+
)
2248
public class BooksController {
2349

2450
private final BooksService service;
@@ -31,14 +57,20 @@ public BooksController(BooksService service) {
3157
* HTTP POST
3258
* ----------------------------------------------------------------------------------------- */
3359

34-
@PostMapping("/book")
35-
public ResponseEntity<String> postBook(@RequestBody BookDTO bookDTO) {
60+
@PostMapping("/books")
61+
@Operation(summary = "Creates a new book")
62+
@ApiResponses(value = {
63+
@ApiResponse(responseCode = "201", description = "Created", content = @Content),
64+
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content),
65+
@ApiResponse(responseCode = "409", description = "Conflict", content = @Content)
66+
})
67+
public ResponseEntity<String> post(@RequestBody BookDTO bookDTO) {
3668
if (service.retrieveByIsbn(bookDTO.getIsbn()) != null) {
3769
return new ResponseEntity<>(HttpStatus.CONFLICT);
3870
} else {
3971
if (service.create(bookDTO)) {
4072
URI location = MvcUriComponentsBuilder
41-
.fromMethodName(BooksController.class, "getBook", bookDTO.getIsbn())
73+
.fromMethodName(BooksController.class, "getByIsbn", bookDTO.getIsbn())
4274
.build()
4375
.toUri();
4476
HttpHeaders httpHeaders = new HttpHeaders();
@@ -54,8 +86,14 @@ public ResponseEntity<String> postBook(@RequestBody BookDTO bookDTO) {
5486
* HTTP GET
5587
* ----------------------------------------------------------------------------------------- */
5688

57-
@GetMapping("/book/{isbn}")
58-
public ResponseEntity<BookDTO> getBook(@PathVariable String isbn) {
89+
@GetMapping("/books/{isbn}")
90+
@Operation(summary = "Retrieves a book by its ID")
91+
@ApiResponses(value = {
92+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json",
93+
schema = @Schema(implementation = BookDTO.class))),
94+
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
95+
})
96+
public ResponseEntity<BookDTO> getByIsbn(@PathVariable String isbn) {
5997
BookDTO bookDTO = service.retrieveByIsbn(isbn);
6098
if (bookDTO != null) {
6199
return new ResponseEntity<>(bookDTO, HttpStatus.OK);
@@ -65,7 +103,12 @@ public ResponseEntity<BookDTO> getBook(@PathVariable String isbn) {
65103
}
66104

67105
@GetMapping("/books")
68-
public ResponseEntity<List<BookDTO>> getAllBooks() {
106+
@Operation(summary = "Retrieves all books")
107+
@ApiResponses(value = {
108+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json",
109+
schema = @Schema(implementation = BookDTO[].class)))
110+
})
111+
public ResponseEntity<List<BookDTO>> getAll() {
69112
List<BookDTO> books = service.retrieveAll();
70113
return new ResponseEntity<>(books, HttpStatus.OK);
71114
}
@@ -74,8 +117,14 @@ public ResponseEntity<List<BookDTO>> getAllBooks() {
74117
* HTTP PUT
75118
* ----------------------------------------------------------------------------------------- */
76119

77-
@PutMapping("/book")
78-
public ResponseEntity<String> putBook(@RequestBody BookDTO bookDTO) {
120+
@PutMapping("/books")
121+
@Operation(summary = "Updates (entirely) a book by its ID")
122+
@ApiResponses(value = {
123+
@ApiResponse(responseCode = "204", description = "No Content", content = @Content),
124+
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content),
125+
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
126+
})
127+
public ResponseEntity<String> put(@RequestBody BookDTO bookDTO) {
79128
if (service.retrieveByIsbn(bookDTO.getIsbn()) != null) {
80129
if (service.update(bookDTO)) {
81130
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
@@ -91,8 +140,14 @@ public ResponseEntity<String> putBook(@RequestBody BookDTO bookDTO) {
91140
* HTTP DELETE
92141
* ----------------------------------------------------------------------------------------- */
93142

94-
@DeleteMapping("/book/{isbn}")
95-
public ResponseEntity<String> deleteBook(@PathVariable String isbn) {
143+
@DeleteMapping("/books/{isbn}")
144+
@Operation(summary = "Deletes a book by its ID")
145+
@ApiResponses(value = {
146+
@ApiResponse(responseCode = "204", description = "No Content", content = @Content),
147+
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content),
148+
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
149+
})
150+
public ResponseEntity<String> delete(@PathVariable String isbn) {
96151
if (service.retrieveByIsbn(isbn) != null) {
97152
if (service.delete(isbn)) {
98153
return new ResponseEntity<>(HttpStatus.NO_CONTENT);

src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService;
3232
import ar.com.nanotaboada.java.samples.spring.boot.test.BookDTOsBuilder;
3333

34-
@DisplayName("HTTP Verbs on Controller")
34+
@DisplayName("HTTP Methods on Controller")
3535
@WebMvcTest(BooksController.class)
3636
class BooksControllerTests {
3737

38+
private static final String PATH = "/books";
39+
3840
@Autowired
3941
private MockMvc mockMvc;
4042

@@ -49,7 +51,7 @@ class BooksControllerTests {
4951
* ----------------------------------------------------------------------------------------- */
5052

5153
@Test
52-
void givenHttpPostVerb_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusConflict()
54+
void givenPost_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusConflict()
5355
throws Exception {
5456
// Arrange
5557
BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid();
@@ -58,7 +60,7 @@ void givenHttpPostVerb_whenRequestBodyContainsExistingValidBook_thenShouldReturn
5860
.when(service.retrieveByIsbn(anyString()))
5961
.thenReturn(bookDTO); // Existing
6062
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
61-
.post("/book")
63+
.post(PATH)
6264
.content(content)
6365
.contentType(MediaType.APPLICATION_JSON);
6466
// Act
@@ -72,7 +74,7 @@ void givenHttpPostVerb_whenRequestBodyContainsExistingValidBook_thenShouldReturn
7274
}
7375

7476
@Test
75-
void givenHttpPostVerb_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusCreatedAndLocationHeader()
77+
void givenPost_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusCreatedAndLocationHeader()
7678
throws Exception {
7779
// Arrange
7880
BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid();
@@ -84,7 +86,7 @@ void givenHttpPostVerb_whenRequestBodyContainsNewValidBook_thenShouldReturnStatu
8486
.when(service.create(any(BookDTO.class)))
8587
.thenReturn(true);
8688
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
87-
.post("/book")
89+
.post(PATH)
8890
.content(content)
8991
.contentType(MediaType.APPLICATION_JSON);
9092
// Act
@@ -96,13 +98,13 @@ void givenHttpPostVerb_whenRequestBodyContainsNewValidBook_thenShouldReturnStatu
9698
// Assert
9799
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
98100
assertThat(response.getHeader(HttpHeaders.LOCATION)).isNotNull();
99-
assertThat(response.getHeader(HttpHeaders.LOCATION)).contains("/book/" + bookDTO.getIsbn());
101+
assertThat(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/" + bookDTO.getIsbn());
100102
verify(service, times(1)).retrieveByIsbn(anyString());
101103
verify(service, times(1)).create(any(BookDTO.class));
102104
}
103105

104106
@Test
105-
void givenHttpPostVerb_whenRequestBodyContainsInvalidBook_thenShouldReturnStatusBadRequest()
107+
void givenPost_whenRequestBodyContainsInvalidBook_thenShouldReturnStatusBadRequest()
106108
throws Exception {
107109
// Arrange
108110
BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid();
@@ -114,7 +116,7 @@ void givenHttpPostVerb_whenRequestBodyContainsInvalidBook_thenShouldReturnStatus
114116
.when(service.create(any(BookDTO.class)))
115117
.thenReturn(false);
116118
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
117-
.post("/book")
119+
.post(PATH)
118120
.content(content)
119121
.contentType(MediaType.APPLICATION_JSON);
120122
// Act
@@ -133,15 +135,15 @@ void givenHttpPostVerb_whenRequestBodyContainsInvalidBook_thenShouldReturnStatus
133135
* ----------------------------------------------------------------------------------------- */
134136

135137
@Test
136-
void givenHttpGetVerb_whenRequestParameterIdentifiesExistingBook_thenShouldReturnStatusOkAndTheBook()
138+
void givenGetByIsbn_whenRequestParameterIdentifiesExistingBook_thenShouldReturnStatusOkAndTheBook()
137139
throws Exception {
138140
// Arrange
139141
BookDTO bookDTO = BookDTOsBuilder.buildOneValid();
140142
Mockito
141143
.when(service.retrieveByIsbn(anyString()))
142144
.thenReturn(bookDTO); // Existing
143145
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
144-
.get("/book/{isbn}", bookDTO.getIsbn());
146+
.get(PATH + "/{isbn}", bookDTO.getIsbn());
145147
// Act
146148
MockHttpServletResponse response = mockMvc
147149
.perform(request)
@@ -157,15 +159,15 @@ void givenHttpGetVerb_whenRequestParameterIdentifiesExistingBook_thenShouldRetur
157159
}
158160

159161
@Test
160-
void givenHttpGetVerb_whenRequestParameterDoesNotIdentifyAnExistingBook_thenShouldReturnStatusNotFound()
162+
void givenGetByIsbn_whenRequestParameterDoesNotIdentifyAnExistingBook_thenShouldReturnStatusNotFound()
161163
throws Exception {
162164
// Arrange
163165
String isbn = "9781484242216";
164166
Mockito
165167
.when(service.retrieveByIsbn(anyString()))
166168
.thenReturn(null); // New
167169
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
168-
.get("/book/{isbn}", isbn);
170+
.get(PATH + "/{isbn}", isbn);
169171
// Act
170172
MockHttpServletResponse response = mockMvc
171173
.perform(request)
@@ -177,15 +179,15 @@ void givenHttpGetVerb_whenRequestParameterDoesNotIdentifyAnExistingBook_thenShou
177179
}
178180

179181
@Test
180-
void givenHttpGetVerb_whenRequestPathIsBooks_thenShouldReturnStatusOkAndCollectionOfBooks()
182+
void givenGetAll_whenRequestPathIsBooks_thenShouldReturnStatusOkAndCollectionOfBooks()
181183
throws Exception {
182184
// Arrange
183185
List<BookDTO> expected = BookDTOsBuilder.buildManyValid();
184186
Mockito
185187
.when(service.retrieveAll())
186188
.thenReturn(expected);
187189
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
188-
.get("/books");
190+
.get(PATH);
189191
// Act
190192
MockHttpServletResponse response = mockMvc
191193
.perform(request)
@@ -205,7 +207,7 @@ void givenHttpGetVerb_whenRequestPathIsBooks_thenShouldReturnStatusOkAndCollecti
205207
* ----------------------------------------------------------------------------------------- */
206208

207209
@Test
208-
void givenHttpPutVerb_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusNoContent()
210+
void givenPut_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusNoContent()
209211
throws Exception {
210212
// Arrange
211213
BookDTO bookDTO = BookDTOsBuilder.buildOneValid();
@@ -217,7 +219,7 @@ void givenHttpPutVerb_whenRequestBodyContainsExistingValidBook_thenShouldReturnS
217219
.when(service.update(any(BookDTO.class)))
218220
.thenReturn(true);
219221
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
220-
.put("/book")
222+
.put(PATH)
221223
.content(content)
222224
.contentType(MediaType.APPLICATION_JSON);
223225
// Act
@@ -232,7 +234,7 @@ void givenHttpPutVerb_whenRequestBodyContainsExistingValidBook_thenShouldReturnS
232234
}
233235

234236
@Test
235-
void givenHttpPutVerb_whenRequestBodyContainsExistingInvalidBook_thenShouldReturnStatusBadRequest()
237+
void givenPut_whenRequestBodyContainsExistingInvalidBook_thenShouldReturnStatusBadRequest()
236238
throws Exception {
237239
// Arrange
238240
BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid();
@@ -244,7 +246,7 @@ void givenHttpPutVerb_whenRequestBodyContainsExistingInvalidBook_thenShouldRetur
244246
.when(service.update(any(BookDTO.class)))
245247
.thenReturn(false);
246248
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
247-
.put("/book")
249+
.put(PATH)
248250
.content(content)
249251
.contentType(MediaType.APPLICATION_JSON);
250252
// Act
@@ -259,7 +261,7 @@ void givenHttpPutVerb_whenRequestBodyContainsExistingInvalidBook_thenShouldRetur
259261
}
260262

261263
@Test
262-
void givenHttpPutVerb_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound()
264+
void givenPut_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound()
263265
throws Exception {
264266
// Arrange
265267
BookDTO bookDTO = BookDTOsBuilder.buildOneValid();
@@ -268,7 +270,7 @@ void givenHttpPutVerb_whenRequestBodyContainsNewValidBook_thenShouldReturnStatus
268270
.when(service.retrieveByIsbn(anyString()))
269271
.thenReturn(null); // New
270272
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
271-
.put("/book")
273+
.put(PATH)
272274
.content(content)
273275
.contentType(MediaType.APPLICATION_JSON);
274276
// Act
@@ -286,7 +288,7 @@ void givenHttpPutVerb_whenRequestBodyContainsNewValidBook_thenShouldReturnStatus
286288
* ----------------------------------------------------------------------------------------- */
287289

288290
@Test
289-
void givenHttpDeleteVerb_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusNoContent()
291+
void givenDelete_whenRequestBodyContainsExistingValidBook_thenShouldReturnStatusNoContent()
290292
throws Exception {
291293
// Arrange
292294
BookDTO bookDTO = BookDTOsBuilder.buildOneValid();
@@ -297,7 +299,7 @@ void givenHttpDeleteVerb_whenRequestBodyContainsExistingValidBook_thenShouldRetu
297299
.when(service.delete(anyString()))
298300
.thenReturn(true);
299301
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
300-
.delete("/book/{isbn}", bookDTO.getIsbn());
302+
.delete(PATH + "/{isbn}", bookDTO.getIsbn());
301303
// Act
302304
MockHttpServletResponse response = mockMvc
303305
.perform(request)
@@ -310,7 +312,7 @@ void givenHttpDeleteVerb_whenRequestBodyContainsExistingValidBook_thenShouldRetu
310312
}
311313

312314
@Test
313-
void givenHttpDeleteVerb_whenRequestBodyContainsExistingInvalidBook_thenShouldReturnStatusBadRequest()
315+
void givenDelete_whenRequestBodyContainsExistingInvalidBook_thenShouldReturnStatusBadRequest()
314316
throws Exception {
315317
// Arrange
316318
BookDTO bookDTO = BookDTOsBuilder.buildOneInvalid();
@@ -321,7 +323,7 @@ void givenHttpDeleteVerb_whenRequestBodyContainsExistingInvalidBook_thenShouldRe
321323
.when(service.delete(anyString()))
322324
.thenReturn(false);
323325
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
324-
.delete("/book/{isbn}", bookDTO.getIsbn());
326+
.delete(PATH + "/{isbn}", bookDTO.getIsbn());
325327
// Act
326328
MockHttpServletResponse response = mockMvc
327329
.perform(request)
@@ -334,15 +336,15 @@ void givenHttpDeleteVerb_whenRequestBodyContainsExistingInvalidBook_thenShouldRe
334336
}
335337

336338
@Test
337-
void givenHttpDeleteVerb_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound()
339+
void givenDelete_whenRequestBodyContainsNewValidBook_thenShouldReturnStatusNotFound()
338340
throws Exception {
339341
// Arrange
340342
BookDTO bookDTO = BookDTOsBuilder.buildOneValid();
341343
Mockito
342344
.when(service.retrieveByIsbn(anyString()))
343345
.thenReturn(null); // New
344346
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
345-
.delete("/book/{isbn}", bookDTO.getIsbn());
347+
.delete(PATH + "/{isbn}", bookDTO.getIsbn());
346348
// Act
347349
MockHttpServletResponse response = mockMvc
348350
.perform(request)

src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
@DisplayName("CRUD Operations on Service")
3636
@ExtendWith(MockitoExtension.class)
37-
public class BooksServiceTests {
37+
class BooksServiceTests {
3838

3939
@Mock
4040
private BooksRepository repository;

0 commit comments

Comments
 (0)