factory

Muy buenas, hoy vamos a crear una API Rest con Spring Boot que nos va a proveer de información básica de hoteles y una aplicación frontend independiente con Angular 8 que ejecutará operaciones CRUD y consumirá ésta información.
Tenéis el código a vuestra disposición en un repositorio Git.

En primer lugar descargamos el boilerplate generado automáticamente con la herramienta Spring Initializr:

Dependencias

Además de las dependencias incluidas en el fichero generado con la herramienta Spring Initializr, añadiremos las dependencias de Swagger 2, que nos proporcionarán la documentación de la API:

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

Configuración

En el fichero application.properties añadiremos las siguientes parámetros:

spring.datasource.url=jdbc:h2:file:./hotel
spring.datasource.driverClassName=org.h2.Driver
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.username=sa
spring.datasource.password=sa
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false

spring.jpa.hibernate.ddl-auto=none
  • URL de la base de datos
  • Clase de acceso al Driver que nos proporciona conexión con la DB
  • Activación de consola Web
  • Ruta de acceso a consola Web
  • Nombre de usuario
  • Contraseña
  • Mostrar trazas de ejecución de sentencias SQL
  • Permitir acceso a externos
  • Activación de gestión automática de esquema por parte de Hibernate

Configuración de Swagger

Crearemos el fichero SwaggerConfiguration:

package com.juanmorschrott.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }
}

En ésta clase creamos una nueva instancia mediante el builder Docket y le indicamos que gestione cualquier petición y cualquier ruta respectivamente.

Schema

Creamos un fichero llamado schema.sql que se encarga de crear la tabla en nuestra base de datos.
Spring boot lee automáticamente los ficheros *.sql localizados dentro de la carpeta “resources”.

DROP TABLE IF EXISTS hotel;

CREATE TABLE hotel (
    id INTEGER NOT NULL AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    description VARCHAR(250) NOT NULL,
    price DOUBLE NOT NULL,
    PRIMARY KEY (id)
);

Cargaremos de datos la tabla creada con el fichero data.sql:

INSERT INTO hotel (name, description, price) 
VALUES ('Atenea', 'Hotel económico y confortable', 34.5);
INSERT INTO hotel (name, description, price) 
VALUES ('Kontiki', 'Gran calidad', 104.5);
INSERT INTO hotel (name, description, price) 
VALUES ('Apollo', 'Hotel económico y confortable', 34.54);
INSERT INTO hotel (name, description, price)
VALUES ('Oleander', 'Gran calidad', 104.5);
INSERT INTO hotel (name, description, price) 
VALUES ('Gran Playa', 'Hotel económico y confortable', 34.556);
INSERT INTO hotel (name, description, price) 
VALUES ('Riu Palace', 'Hotel económico y confortable', 34.5);
INSERT INTO hotel (name, description, price) 
VALUES ('Carioca', 'Gran calidad', 104.5);

Modelo

Crearemos un modelo que aún siendo sencillo, abarca varios tipos de variables. El Id seguirá la estrategia IDENTITY para generar valores de forma secuencial:

package com.juanmorschrott.api.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.math.BigDecimal;

@AllArgsConstructor
@Data
@Entity
@ToString
public class Hotel {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;
    private String description;
    private BigDecimal price;

    public Hotel() {}
}

Repositorio

Aprovecharemos la implementación CRUD que nos proporciona Spring Boot extendiendo nuestra interfaz con CrudRepository<T, ID>.
Ésta implementación tan simple, nos proporciona una gran cantidad de métodos para ejecutar operaciones CRUD (save, delete, update, etc).

package com.juanmorschrott.api.repository;

import com.juanmorschrott.api.model.Hotel;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface HotelRepository extends CrudRepository<Hotel, Long> {
    Optional<Hotel> findById(Long id);
    Optional<Hotel> findByName(String name);
}

He declarado dos métodos específicos para que realicen la búsqueda de Hotel por ID y por nombre.
Podemos crear búsquedas personalizadas nombrando los métodos “findBy[nombre de atributo]”. En caso de que queramos paginar, éste sería el sitio en el que lo configuraríamos. Tenéis un ejemplo sobre como hacerlo aquí .

Servicio

En nuestra capa de lógica de negocio haremos uso de nuestro repositorio. Aquí deberíamos implementar cualquier lógica que necesitemos:

package com.juanmorschrott.api.service;

import com.juanmorschrott.api.model.Hotel;
import com.juanmorschrott.api.repository.HotelRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class HotelServiceImpl implements HotelService {

    @Autowired
    private HotelRepository repository;

    public HotelServiceImpl() {}

    @Override
    public List<Hotel> list() {
        return (List<Hotel>) repository.findAll();
    }

    @Override
    public Hotel create(Hotel hotel) {
        return repository.save(hotel);
    }

    @Override
    public Hotel get(Long id) {
        return repository.findById(id).orElse(null);
    }

    @Override
    public Hotel getByName(String name) {
        return repository.findByName(name).orElse(null);
    }

    @Override
    public Hotel update(Hotel hotel) {
        return repository.save(hotel);
    }

    @Override
    public void delete(Long id) {
        Optional<Hotel> hotel = repository.findById(id);
        hotel.ifPresent(value -> repository.delete(value));
    }

}

Controlador

Y para completar la parte Backend, creamos el controlador que proporciona la API de nuestra aplicación:

package com.juanmorschrott.api.controller;

import com.juanmorschrott.api.model.Hotel;
import com.juanmorschrott.api.service.HotelServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@Api(value="hotels", description="Hotels simple REST API")
@CrossOrigin(origins = "http://localhost:4200")
@RestController
@RequestMapping(value = "/api/v1/hotels")
public class HotelController {

    private HotelServiceImpl service;

    @Autowired
    public HotelController(HotelServiceImpl service) {
        this.service = service;
    }

    @ApiOperation(value = "View a list of available hotels", response = ResponseEntity.class)
    @GetMapping
    public ResponseEntity<List<Hotel>> list() {
        return new ResponseEntity<>(service.list(), HttpStatus.OK);
    }

    @ApiOperation(value = "Creates a new Hotel", response = ResponseEntity.class)
    @PostMapping
    public ResponseEntity<Hotel> create(@RequestBody Hotel hotel) {
        Hotel created = service.create(hotel);
        if (created != null) {
            return new ResponseEntity<>(created, HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @ApiOperation(value = "Find an hotel", response = ResponseEntity.class)
    @GetMapping(value = "/{id}")
    public ResponseEntity get(@PathVariable Long id) {
        Hotel hotel = service.get(id);
        if (hotel != null) {
            return new ResponseEntity<>(hotel, HttpStatus.OK);
        } else {
            return new ResponseEntity("Hotel not found", HttpStatus.NOT_FOUND);
        }
    }

    @ApiOperation(value = "Updates an hotel", response = ResponseEntity.class)
    @PutMapping
    public ResponseEntity<Hotel> update(@RequestBody Hotel hotel) {
        Hotel updated = service.update(hotel);
        if (updated != null) {
            return new ResponseEntity<>(updated, HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @ApiOperation(value = "Delete an hotel", response = ResponseEntity.class)
    @DeleteMapping(value = "/{id}")
    public ResponseEntity delete(@PathVariable Long id) {
        try {
            service.delete(id);
            return new ResponseEntity(HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity(HttpStatus.NOT_FOUND);
        }
    }

}

Ejecución

Arrancaremos nuestro servicio accediendo a la carpeta raíz de nuestro proyecto y ejecutando el gestor de dependencias Maven embebido en el proyecto:

./mvnw spring-boot:run

Comprobación

Comprobaremos su correcto funcionamiento con una petición GET:

curl

Swagger

Podemos ver la documentación de nuestra API e interactuar con ella en la URL: http://localhost:8080/swagger-ui.html

swagger

Aplicación Angular

Crearemos la aplicación Frontend con el generador automático de Angular, para ello, necesitaremos tener instalado node.js. A continuación, instalaremos el CLI de Angular de manera global ejecutando:

npm install -g @angular/cli

Y ahora si, crearemos nuestra aplicación:

ng new frontend

Componente Raíz

Angular dispone de múltiples componentes (Servicios, ServiceWorker, Directivas, Modulos, etc).
En éste caso nos referimos a un elemento HTML al que nombraremos y añadiremos la lógica que queramos. Por ejemplo:

<mi-super-elemento></mi-super-elemento>

Éste era el objetivo fundamental por el que se creó Angular.js, poder crear cualquier elemento personalizado.

Nuestro primer componente representa el elemento HTML raíz de nuestro proyecto, que será incluido en nuestro index.html con el tag <app-root></app-root>

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Frontend';
}

Module

Un Modulo en Angular, es la clase que concentra un conjunto de componentes. Cómo su propio nombre indica, nos permite dividir una aplicación en módulos, facilitando su división.
Ésta es la clase module de nuestra aplicación:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { routing } from './app.routing';
import { AddHotelComponent } from './add-hotel/add-hotel.component';
import { EditHotelComponent } from './edit-hotel/edit-hotel.component';
import { ListHotelComponent } from './list-hotel/list-hotel.component';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HotelService } from './service/hotel.service';
import { DetailHotelComponent } from './detail-hotel/detail-hotel.component';

@NgModule({
  declarations: [
    AppComponent,
    AddHotelComponent,
    EditHotelComponent,
    ListHotelComponent,
    DetailHotelComponent
  ],
  imports: [
    BrowserModule,
    routing,
    ReactiveFormsModule,
    HttpClientModule
  ],
  providers: [HotelService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Router

Angular nos proporciona una potente librería para gestionar el enrutamiento.
Éstas serán nuestras rutas:

import { RouterModule, Routes } from '@angular/router';
import { AddHotelComponent } from './add-hotel/add-hotel.component';
import { ListHotelComponent } from './list-hotel/list-hotel.component';
import { EditHotelComponent } from './edit-hotel/edit-hotel.component';
import { DetailHotelComponent } from './detail-hotel/detail-hotel.component';

const routes: Routes = [
  { path: 'add-hotel', component: AddHotelComponent },
  { path: 'list-hotel', component: ListHotelComponent },
  { path: 'edit-hotel', component: EditHotelComponent },
  { path: 'hotel-detail', component: DetailHotelComponent },
  { path : '', component : ListHotelComponent}
];

export const routing = RouterModule.forRoot(routes);

Como podéis ver, se trata de un array de objetos que contienen el path y el componente a mostrar al navegar a dicho path.

Modelo

Es la representación de nuestro modelo de datos. Representa la clase y los tipos de las entidades con las que vamos a trabajar, en definitiva es la base de nuestra aplicación.

export class Hotel {
  id: number;
  name: string;
  description: string;
  price: number;
}

Servicio

Para Angular, un servicio es un componente encargado de ejecutar una lógica concreta.
El framework cargará una instancia singleton al arrancar que será accesible desde toda la aplicación, conservando la información durante todo su ciclo de vida.
Generalmente se suelen utilizar para conexiones HTTP o simplemente contener código reutilizable.

Crearemos nuestro HotelService ejecutando:

ng generate service

Que contendrá los métodos necesarios para comunicarnos con nuestra API:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Hotel } from '../model/hotel.model';

@Injectable()
export class HotelService {
  
  constructor(private http: HttpClient) { }

  uri = 'http://localhost:8080/api/v1/hotels/';

  getHotels() {
    return this.http.get<Hotel[]>(this.uri);
  }

  getHotelById(id: number) {
    return this.http.get(this.uri + id);
  }

  createHotel(hotel: Hotel) {
    return this.http.post(this.uri, hotel);
  }

  updateHotel(hotel: Hotel) {
    return this.http.put(this.uri, hotel);
  }

  deleteHotel(id: number) {
    return this.http.delete(this.uri + id);
  }
}

Componentes

Al igual que nuestro servicio, generaremos el resto de componentes mediante el CLI.

ng generate component

Por brevedad mostraremos el componente que hará un renderizado del listado de todos los hoteles proporcionados por la API. Disponéis del resto del código en el repositorio Git.

Plantilla HTML:

<div class="col-md-12">
  <h2> Hotel Details</h2>
  <hr/>
  <button class="btn btn-danger" (click)="addHotel()"> Add Hotel</button>
  <hr/>

  <table class="table table-striped">
    <thead>
    <tr>
      <th class="hidden">Id</th>
      <th>Name</th>
      <th>Description</th>
      <th>Mean Room Price</th>
      <th>Action</th>
    </tr>
    </thead>
    <tbody>
    <tr *ngFor="let hotel of hotels">
      <td class="hidden">{{hotel.id}}</td>
      <td>{{hotel.name}}</td>
      <td>{{hotel.description}}</td>
      <td>{{hotel.price | number | currency:'EUR'}}</td>
      <td>
        <button class="btn btn-danger" (click)="deleteHotel(hotel)"> Delete</button>
        <button class="btn btn-danger" (click)="editHotel(hotel)" style="margin-left: 20px;"> Edit</button>
        <button class="btn btn-info" (click)="hotelDetails(hotel)" style="margin-left: 10px"> Details</button>
      </td>
    </tr>
    </tbody>
  </table>
</div>

Componente ListHotel:

import {Component, OnInit} from '@angular/core';
import {Hotel} from '../model/hotel.model';
import {Router} from '@angular/router';
import {HotelService} from '../service/hotel.service';

@Component({
  selector: 'app-list-hotel',
  templateUrl: './list-hotel.component.html',
  styleUrls: ['./list-hotel.component.css']
})
export class ListHotelComponent implements OnInit {

  hotels: Hotel[];

  constructor(private router: Router, private hotelService: HotelService) {
  }

  ngOnInit() {
    this.hotelService.getHotels()
      .subscribe(data => {
        console.log(data);
        this.hotels = data;
      });
  }

  deleteHotel(hotel: Hotel): void {
    this.hotelService.deleteHotel(hotel.id)
      .subscribe(data => {
        this.hotels = this.hotels.filter(u => u !== hotel);
      });
  }

  editHotel(hotel: Hotel): void {
    localStorage.removeItem('editHotelId');
    localStorage.setItem('editHotelId', hotel.id.toString());
    this.router.navigate(['edit-hotel']);
  }

  addHotel(): void {
    this.router.navigate(['add-hotel']);
  }

  hotelDetails(hotel: Hotel) {
    localStorage.removeItem('hotelDetailId');
    localStorage.setItem('hotelDetailId', hotel.id.toString());
    this.router.navigate(['hotel-detail']);
  }

}

Como podéis ver, el componente puede aplicar lógica a objetos enlazados con la plantilla, ejecutada a través de eventos.

Resultado

Vamos a probar nuestro sistema.

En primer lugar arrancamos la Api. Accederemos al directorio raíz y ejecutaremos:

mvnw spring-boot:run

A continuación, desde la carpeta raíz de nuestra aplicación Frontend ejecutaremos:

ng serve

Tras lo cual abriremos un navegador Web, conectándonos a la URL http://localhost:4200 .

Conclusión

Hemos hecho un recorrido paso por paso para crear una aplicación Full Stack sencilla pero completa.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Translate