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.