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.

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

6 Comments

  1. 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

  2. marcin warycha - javaleader.pl 6 października 2020 at 14:35

    Dzięki Paweł za uwagi!

  3. Dlaczego mamy w pomx.xml dwa razy:

    org.springframework.boot
    spring-boot-starter-web

    ?

Leave a comment

Your email address will not be published.


*