Nowoczesna aplikacja w Spring Boot i Angular – [część 1]
Nowoczesna aplikacja w Spring Boot i Angular – [część 1]
Z tego artykułu dowiesz się w jaki sposób zaprojektować nowoczesną aplikację internetową z użyciem Spring Boota i Angulara. Artykuł ten podzielony jest na 2 części. W pierwszej części napiszemy Rest API z użyciem Spring Boota, w drugiej części natomiast zajmiemy konsumowaniem napisanego API w oparciu o Angulara. Artykuł ten jest polskim opracowaniem i bazuje na oryginalnym artykule dostępnym tutaj – https://bezkoder.com/angular-10-crud-app/. Zaczynamy od wygenerowania nowego projektu Spring Boota dodając do pliku pom.xml następujące zależności:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>de.flapdoodle.embed</groupId> <artifactId>de.flapdoodle.embed.mongo</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> </dependencies>
Korzystamy z wbudowanej bazy danych mongodb zatem koniecznym było dodanie zależności:
<dependency> <groupId>de.flapdoodle.embed</groupId> <artifactId>de.flapdoodle.embed.mongo</artifactId> </dependency>
W pliku konfiguracyjnym application.properties dodajemy wpis:
spring.data.mongodb.database=javaleaderdb spring.data.mongodb.port=27017
Klasa modelu:
@Document(collection = "tutorials") @Data public class Tutorial { @Id private String id; private String title; private String description; private boolean published; public Tutorial() { } public Tutorial(String title, String description, boolean published) { this.title = title; this.description = description; this.published = published; } }
Klasa repozytorium:
public interface TutorialRepository extends MongoRepository<Tutorial, String> { List<Tutorial> findByTitleContaining(String title); List<Tutorial> findByPublished(boolean published); }
Implementacja API:
Tworzymy nową klase w której dodajemy wszystkie metody obsługujące API:
@CrossOrigin(origins = "http://localhost:8081") @RestController @RequestMapping("/api") public class TutorialController { @Autowired TutorialRepository tutorialRepository; // all api methods here }
- GET:
Metoda pobierająca z bazy danych wszystkie szkolenia lub jeśli przekazany jest tytuł szkolenia to zwracane jest szkolenie po tytule:
@GetMapping("/tutorials") public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) { try { List<Tutorial> tutorials = new ArrayList<Tutorial>(); if (title == null) tutorialRepository.findAll().forEach(tutorials::add); else tutorialRepository.findByTitleContaining(title).forEach(tutorials::add); if (tutorials.isEmpty()) { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } return new ResponseEntity<>(tutorials, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); } }
Metoda pobierająca z bazy konkretne szkolenie bazując na jego identyfikatorze:
@GetMapping("/tutorials/{id}") public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") String id) { Optional<Tutorial> tutorialData = tutorialRepository.findById(id); if (tutorialData.isPresent()) { return new ResponseEntity<>(tutorialData.get(), HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } }
Metoda pobierająca z bazy szkolenia w statusie opublikowanym:
@GetMapping("/tutorials/published") public ResponseEntity<List<Tutorial>> findByPublished() { try { List<Tutorial> tutorials = tutorialRepository.findByPublished(true); if (tutorials.isEmpty()) { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } return new ResponseEntity<>(tutorials, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } }
- CREATE:
Metoda zapisująca w bazie nowe szkolenie:
@PostMapping("/tutorials") public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) { try { Tutorial _tutorial = tutorialRepository.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), false)); return new ResponseEntity<>(_tutorial, HttpStatus.CREATED); } catch (Exception e) { return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); } }
- PUT:
Metoda aktualizująca szkolenie bazując na jego identyfikatorze:
@PutMapping("/tutorials/{id}") public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") String id, @RequestBody Tutorial tutorial) { Optional<Tutorial> tutorialData = tutorialRepository.findById(id); if (tutorialData.isPresent()) { Tutorial _tutorial = tutorialData.get(); _tutorial.setTitle(tutorial.getTitle()); _tutorial.setDescription(tutorial.getDescription()); _tutorial.setPublished(tutorial.isPublished()); return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } }
- DELETE:
Metoda usuwająca z bazy szkolenie bazując na jego identyfikatorze:
@DeleteMapping("/tutorials/{id}") public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") String id) { try { tutorialRepository.deleteById(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } }
Metoda usuwająca z bazy wszystkie dostępne szkolenia:
@DeleteMapping("/tutorials") public ResponseEntity<HttpStatus> deleteAllTutorials() { try { tutorialRepository.deleteAll(); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } }
Testy aplikacji:
mvn spring-boot:run
Wysyłamy kolejno zapytanie POST na adres:
http://localhost:8080/api/tutorials
z danymi:
{ "title":"Spring Boot + Angular", "description":"nowoczesna aplikacja webowa" } { "title":"Rest API", "description":"wprowadzenie do REST API" } { "title":"Angular", "description":"pisanie aplikacji frontendowych" }
Po tej operacji w bazie danych MongoDB będziemy mieli 3 rekordy co możemy zweryfikować wysyłając zapytanie GET na adres:
http://localhost:8080/api/tutorials
wynik:
{ "id": "5f6f36b22520be6f8bb299f5", "title": "Spring Boot + Angular", "description": "nowoczesna aplikacja webowa", "published": false } { "id": "5f6f37e12520be6f8bb299f6", "title": "Rest API", "description": "wprowadzenie do REST API", "published": false } { "id": "5f6f37e62520be6f8bb299f7", "title": "Angular", "description": "pisanie aplikacji frontendowych", "published": false }
Adresy dostępnych endpointów:
GET:
- http://localhost:8080/api/tutorials
- http://localhost:8080/api/tutorials/{id}
- http://localhost:8080/api/tutorials/published
POST:
- http://localhost:8080/api/tutorials
PUT:
- http://localhost:8080/api/tutorials/{id}
DELETE:
- http://localhost:8080/api/tutorials/{id}
- http://localhost:8080/api/tutorials
Mając tak przygotowane API możemy śmiało przystąpić do realizacji części drugiej – frontend.
Dzięki za post! Jeśli tylko mogę zasugerować starałbym się używać ifPresent oraz get na Optionalu możliwie rzadko a najlepiej wcale. Zwykle wtedy tak naprawdę if jest tym co potrzebujemy. Kod:
public ResponseEntity getTutorialById(@PathVariable(„id”) String id) {
Optional tutorialData = tutorialRepository.findById(id);
if (tutorialData.isPresent()) {
return new ResponseEntity(tutorialData.get(), HttpStatus.OK);
} else {
return new ResponseEntity(HttpStatus.NOT_FOUND);
}
}
można wtedy śmiało zamienić na :
public ResponseEntity getTutorialById(@PathVariable(„id”) String id) {
return tutorialRepository.findById(id)
.map(tutorial -> new ResponseEntity(tutorial, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity(HttpStatus.NOT_FOUND));
}
a z statycznymi metodami fabrykującymi OK(tutorial) oraz NOT_FOUND w klasie ResponseEntity mogło by to wyglądać końcowo tak:
public ResponseEntity getTutorialById(@PathVariable(„id”) String id) {
return tutorialRepository.findById(id)
.map(ResponseEntity::OK)
.orElseGet(ResponseEntity::NOT_FOUND);
}
Pozdrawiam
Hejka,
fajny wpis. Mam kilka uwag. Try catch, a później i tak zwracanie internal server error jest nadmiarowe raczej. Nie zwracałbym również no content dla pustej listy, lecz po prostu pustą listę ?.
Dzięki Arturze za uwagi!
Dzięki Paweł za uwagi!
Dlaczego mamy w pomx.xml dwa razy:
org.springframework.boot
spring-boot-starter-web
?
zbędne.