Projeto biblioteca fullstack Spring Boot e Vue.js

Criação de projeto de biblioteca fullstack Spring Boot e Vue.js

Resumo em construção
Voltar

Material complementar:

  • Conteúdo no YouTube: Acesse em breve
  • Conteúdo no GitHub: Acesse
  • Curso gratuito e com certificado na Udemy (Biblioteca fullstack: Spring Boot + Vue.js + Postman + Bootstrap): Acesse em breve

Conceito:

Criar sistema web RESTful fullstack, com backend Spring Boot (ORM hibernate JPA DAO) e frontend Vue.js, com funções de CRUD (criar, listar, ver específico, editar e exluir) de livros.


Backend (Spring Boot):

  1. Pré-requisitos:
    • JDK (e path);
    • Gradle (e path);
    • MySQL;
    • Postman web e Postman desktop agent.
  2. Criar projeto Spring Boot:
    • Em Spring Initializr, selecione as dependências:
      • Project: Gradle - Groovy
      • Project Metadata
        • Group: ubsocial.com
        • Artifact: biblioteca
        • Name: biblioteca (igual no Artifact)
        • Package name: ubsocial.com.biblioteca
        • Java: versão do JDK instalado na máquina
      • Dependencies:
        • Spring Web
      • Generate (projeto será gerado, abra-o no editor)
  3. Criar banco de dados MySQL: CREATE DATABASE biblioteca;
  4. 'build.gradle', em 'dependencies', adicionar novas dependencies:
    
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    
  5. 'src/main/resources/application.properties':
    
    spring.application.name=biblioteca
    spring.jpa.hibernate.ddl-auto=update
    spring.datasource.url=jdbc:mysql://localhost:3306/biblioteca
    spring.datasource.username=root
    spring.datasource.password=
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.jackson.serialization.fail-on-empty-beans=false
    server.error.include-message=always
    server.error.include-binding-errors=always
    
  6. Dentro do pacote principal do projeto (ubsocial.com.biblioteca), criar pacote "config": ficará ubsocial.com.biblioteca.config
  7. Dentro do pacote "config", criar classe WebConfig para conexão com frontend Vue.js:
    
    package ubsocial.com.biblioteca.config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebConfig {
    
        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurer() {
    
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**")
                            .allowedOrigins("http://localhost:8081")
                            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
                }
            };
        }
    }
    
  8. Dentro do pacote principal do projeto (ubsocial.com.biblioteca), criar pacote "model": ficará ubsocial.com.biblioteca.model
  9. Dentro do pacote "model", criar pacote de entidades "entity": ficará model.entity
  10. Dentro do pacote "model.entity", criar Model (entidade) Livro:
    
    package ubsocial.com.biblioteca.model.entity;
    import java.util.Date;
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    import jakarta.persistence.Temporal;
    import jakarta.persistence.TemporalType;
    
    @Entity
    public class Livro {
        
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    
        private String title;
        private String author;
        
        @Temporal(TemporalType.DATE)
        private Date published_date;
    
        private String isbn;
        private int pages;
        private String cover;
        private String language;
    }
    
  11. Na mesma pasta de "gradlew", executar projeto: ".\gradlew bootRun"
    • Verificar no banco de dados, se table Livro foi criada.
  12. Complementar model (entidade) Livro, com novos códigos:
    
    package ubsocial.com.biblioteca.model.entity;
    import java.util.Date;
    import com.fasterxml.jackson.annotation.JsonFormat;
    import jakarta.persistence.Column;
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    import jakarta.persistence.Temporal;
    import jakarta.persistence.TemporalType;
    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    @Entity
    public class Livro {
    
        public Livro() {
        }
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    
        @NotBlank // NotBlank também é automaticamente NotNull
        @Size(min = 1, max = 255, message = "Título precisa ter entre 1 e 255 caracteres")
        @Column(length = 255)
        private String title;
    
        @NotBlank
        @Size(min = 1, max = 255, message = "Autor precisa ter entre 1 e 255 caracteres")
        @Column(length = 255)
        private String author;
        
        @NotNull
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
        @Temporal(TemporalType.DATE)
        private Date published_date;
    
        @NotBlank
        @Column(unique = true)
        @Size(min = 13, max = 13, message = "ISBN precisa ter 13 caracteres")
        private String isbn;
        
        private int pages;
        
        @Size(min = 1, max = 255, message = "Gênero precisa ter entre 1 e 255 caracteres")
        @Column(length = 255)
        private String cover;
    
        @NotBlank
        @Size(min = 1, max = 255, message = "Idioma precisa ter entre 1 e 255 caracteres")
        @Column(length = 255)
        public String language;
    
        public Long getId() {
            return id;
        }
        
        public void setId(Long id) {
            this.id = id;
        }
        
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public String getAuthor() {
            return author;
        }
    
        public void setAuthor(String author) {
            this.author = author;
        }
    
        public Date getPublishedDate() {
            return published_date;
        }
    
        public void setPublishedDate(Date published_date) {
            this.published_date = published_date;
        }
    
        public String getIsbn() {
            return isbn;
        }
    
        public void setIsbn(String isbn) {
            this.isbn = isbn;
        }
    
        public int getPages() {
            return pages;
        }
    
        public void setPages(int pages) {
            this.pages = pages;
        }
    
        public String getCover() {
            return cover;
        }
    
        public void setCover(String cover) {
            this.cover = cover;
        }
    
        public String getLanguage() {
            return language;
        }
    
        public void setLanguage(String language) {
            this.language = language;
        }
    }
    
  13. No pacote "model", criar pacote "repositories": ficará model.repositories
  14. No pacote "repositories", criar interface LivroRepository:
    
    package ubsocial.com.biblioteca.model.repositories;
    import ubsocial.com.biblioteca.model.entity.Livro;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface LivroRepository extends JpaRepository<Livro, Long> {
    }
    
  15. Pacote principal "ubsocial.com.biblioteca", criar pacote "controller"
  16. Pacote "controller", criar classe LivroResource:
    
    package ubsocial.com.biblioteca.controller;
    import java.util.List;
    import java.util.Optional;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.PutMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import jakarta.validation.Valid;
    import ubsocial.com.biblioteca.model.entity.Livro;
    import ubsocial.com.biblioteca.model.repositories.LivroRepository;
    
    @RestController
    @RequestMapping("/books")
    public class LivroResource {
        private LivroRepository livroRepository;
    
        public LivroResource(LivroRepository livroRepository) {
            this.livroRepository = livroRepository;
        }
    
        @GetMapping
        public List<Livro> get() {
            return livroRepository.findAll();
        }
    
        @GetMapping("/{id}")
        public ResponseEntity<Livro> get(@PathVariable Long id) {
            Optional<Livro> optional = livroRepository.findById(id);
            if(!optional.isPresent()) {
                return new ResponseEntity<Livro>(HttpStatus.NOT_FOUND);
            }
            return new ResponseEntity<Livro>(optional.get(), HttpStatus.OK);
        }
    
        @PostMapping
         public Livro create(@RequestBody @Valid Livro livro) {
            return livroRepository.save(livro);
        }
    
        @PutMapping("/{id}")
        public ResponseEntity<Livro> update(@PathVariable Long id, @RequestBody @Valid Livro livro) {
            Optional<Livro> optional = livroRepository.findById(id);
            if(!optional.isPresent()) {
                return new ResponseEntity<Livro>(HttpStatus.NOT_FOUND);
            }
            Livro livroAux = optional.get();
            livroAux.setTitle(livro.getTitle());
            livroAux.setAuthor(livro.getAuthor());
            livroAux.setPublishedDate(livro.getPublishedDate());
            livroAux.setIsbn(livro.getIsbn());
            livroAux.setPages(livro.getPages());
            livroAux.setCover(livro.getCover());
            livroAux.setLanguage(livro.getLanguage());
            livroRepository.save(livroAux);
            return new ResponseEntity<Livro>(livroAux, HttpStatus.OK);
        }
    
        @DeleteMapping("/{id}")
        public ResponseEntity<Livro> delete(@PathVariable Long id) {
            if(!livroRepository.existsById(id)) {
                return new ResponseEntity<Livro>(HttpStatus.NOT_FOUND);
            }
            livroRepository.deleteById(id);
            return new ResponseEntity<Livro>(HttpStatus.NO_CONTENT);
        }
    }
    
  17. Na mesma pasta de "gradlew", executar projeto: ".\gradlew bootRun"
  18. Teste Postman (create):
    • URL: http://localhost:8080/books
    • Método: POST
    • Body raw (JSON):
      
      {
          "title": "O Senhor dos Anéis",
          "author": "J.R.R. Tolkien",
          "published_date": "1954-07-29",
          "isbn": "9780261102385",
          "pages": 1178,
          "cover": "Ficção",
          "language": "Português"
      }
      
    • Enviar (Send)
  19. Teste Postman (get):
  20. Teste Postman (get específico):
  21. Teste Postman (update):
    • URL: http://localhost:8080/books/1
    • Método: PUT
    • Body raw (JSON):
      
      {
          "title": "O Senhor dos Anéis - Edição Atualizada",
          "author": "J.R.R. Tolkien",
          "published_date": "1954-07-29",
          "isbn": "9780261102385",
          "pages": 1178,
          "cover": "Ficção",
          "language": "Português"
      }
      
    • Enviar (Send)
  22. Teste Postman (delete):

Frontend (Vue.js):

  1. Pré-requisitos:
    • Instalar nodejs (npm);
    • Instalar Vue: npm install -g @vue/cli vue-router@4
  2. Comandos para criar projeto Vue.js:
    
    vue create biblioteca-frontend
    -  Escolher features manualmente: Choose Vue version, Babel, Linter / Formatter e TypeScript
    – Use class-style component syntax? No
    – Use Babel alongside TypeScript? Yes
    – Pick a linter / formatter config: Prettier
    – Pick additional lint features: Lint on save
    – Where do you prefer placing config files? In dedicated config files
    – Save this for future project? No
    
    cd biblioteca-frontend
    npm i bootstrap jquery popper.js axios
    npm run serve
    
  3. src/main.ts:
    
    import { createApp } from "vue";
    import App from "@/App.vue";
    import router from "@/router";
    import "bootstrap";
    import "bootstrap/dist/css/bootstrap.min.css";
    
    createApp(App).use(router).mount("#app");
    
  4. src/http-common.ts:
    
    import axios, { AxiosInstance } from "axios";
    
    const apiClient: AxiosInstance = axios.create({
        baseURL: "http://localhost:8080",
        headers: {
            "Content-type": "application/json",
        },
    });
    
    export default apiClient;
    
  5. src/shims-vue.d.ts:
    
    declare module "*.vue" {
        import { DefineComponent } from "vue";
        const component: DefineComponent<{}, {}, any>;
        export default component;
    }
    
  6. .eslintrc.js:
    
    module.exports = {
        root: true,
        env: {
            node: true,
        },
        extends: [
            "plugin:vue/vue3-essential",
            "eslint:recommended",
            "@vue/typescript/recommended",
            //"plugin:prettier/recommended",
        ],
        parserOptions: {
            ecmaVersion: 2020,
        },
        rules: {
            '@typescript-eslint/no-explicit-any': 'off',
            '@typescript-eslint/ban-types': 'off',
            '@typescript-eslint/no-unused-vars': 'warn',
        },
    };
    
  7. vue.config.js:
    
    module.exports = {
        devServer: {
            port: 8081,
        },
    };
    
  8. src/router.ts:
    
    import { createWebHistory, createRouter } from "vue-router";
    import { RouteRecordRaw } from "vue-router";
    
    const routes: Array<RouteRecordRaw> = [
        {
            path: "/",
            alias: "/books",
            name: "books",
            component: () => import("./components/BooksList.vue"),
        },
        {
            path: "/books/:id",
            name: "book-details",
            component: () => import("./components/BookDetails.vue"),
        },
        {
            path: "/add",
            name: "add",
            component: () => import("./components/AddBook.vue"),
        },
    ];
    
    const router = createRouter({
        history: createWebHistory(),
        routes,
    });
    
    export default router;
    
  9. src/components/AddBook.vue:
    
    <template>
        <h3>Novo livro:</h3>
        <form @submit.prevent="saveBook">
            <input type="text" class="form-control" placeholder="Título" id="title" required v-model="book.title"
                name="title" />
            <div class="form-group">
                <input type="text" class="form-control" placeholder="Autor(a)" id="author" required v-model="book.author"
                    name="author" />
            </div>
            <div class="form-group">
                <label for="published_date">Publicação:</label>
                <input type="date" class="form-control" id="published_date" :max="new Date().toISOString().split('T')[0]"
                    required v-model="book.published_date" name="published_date" />
            </div>
            <div class="form-group">
                <input type="text" class="form-control" placeholder="ISBN" pattern="^\d{13}$" id="isbn" required
                    v-model="book.isbn" name="isbn" />
            </div>
            <div class="form-group">
                <input type="number" class="form-control" placeholder="Páginas" id="pages" min="1" required
                    v-model="book.pages" name="pages" />
            </div>
            <div class="form-group">
                <input type="text" class="form-control" placeholder="Capa" id="cover" v-model="book.cover" name="cover" />
            </div>
            <div class="form-group">
                <input type="text" class="form-control" placeholder="Idioma" id="language" required v-model="book.language"
                    name="language" />
            </div>
            <button type="button" @click="$router.push('/books')" class="btn btn-secondary">Cancelar</button>
            <button type="submit" class="btn btn-success">Adicionar</button>
        </form>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import BookDataService from "@/services/BookDataService";
    import Book from "@/types/Book";
    import ResponseData from "@/types/ResponseData";
    
    export default defineComponent({
        name: "add-book",
        data() {
            return {
                book: {
                    title: "",
                    author: "",
                    published_date: new Date(),
                    isbn: "",
                    pages: 0,
                    cover: "",
                    language: "",
                } as Book,
            };
        },
        methods: {
            saveBook() {
                let data = {
                    title: this.book.title,
                    author: this.book.author,
                    published_date: this.book.published_date,
                    isbn: this.book.isbn,
                    pages: this.book.pages,
                    cover: this.book.cover,
                    language: this.book.language,
                };
    
                BookDataService.create(data)
                    .then((response: ResponseData) => {
                        this.book.id = response.data.id;
                        this.$router.push({ name: "books" });
                    })
                    .catch((e: Error) => {
                        console.log(e);
                    });
            },
        },
    });
    </script>
    
    <style></style>
    
  10. src/components/BookDetails.vue:
    
    <template>
        <div v-if="currentBook.id">
            <h4>Livro {{ currentBook.id }} - {{ currentBook.title }}</h4>
            <form @submit.prevent="updateBook">
                <input type="text" class="form-control" placeholder="Título" id="title" required v-model="currentBook.title"
                    name="title" />
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="Autor(a)" id="author" required
                        v-model="currentBook.author" name="author" />
                </div>
                <div class="form-group">
                    <label for="published_date">Publicação:</label>
                    <input type="date" class="form-control" id="published_date"
                        :max="new Date().toISOString().split('T')[0]" required v-model="currentBook.published_date"
                        name="published_date" />
                </div>
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="ISBN" pattern="^\d{13}$" id="isbn" required
                        v-model="currentBook.isbn" name="isbn" />
                </div>
                <div class="form-group">
                    <input type="number" class="form-control" placeholder="Páginas" id="pages" min="1" required
                        v-model="currentBook.pages" name="pages" />
                </div>
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="Capa" id="cover" v-model="currentBook.cover"
                        name="cover" />
                </div>
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="Idioma" id="language" required
                        v-model="currentBook.language" name="language" />
                </div>
                <button type="button" @click="$router.push('/books')" class="btn btn-secondary">Cancelar</button>
                <button type="submit" class="btn btn-warning">Confirmar</button>
            </form>
            <p>{{ message }}</p>
        </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import BookDataService from "@/services/BookDataService";
    import Book from "@/types/Book";
    import ResponseData from "@/types/ResponseData";
    
    export default defineComponent({
        name: "BookDetails",
        data() {
            return {
                currentBook: {} as Book,
                message: "",
            };
        },
        methods: {
            getBook(id: any) {
                BookDataService.get(id)
                    .then((response: ResponseData) => {
                        this.currentBook = response.data;
                    })
                    .catch((e: Error) => {
                        console.log(e);
                    });
            },
    
            updateBook() {
                BookDataService.update(this.currentBook.id, this.currentBook)
                    .then((response: ResponseData) => {
                        this.$router.push({ name: "books" });
                    })
                    .catch((e: Error) => {
                        console.log(e);
                        this.message = "Erro ao adicionar livro";
                    });
            },
    
            deleteBook(id: number) {
                BookDataService.delete(this.currentBook.id)
                    .then((response: ResponseData) => {
                        console.log(response.data);
                        this.$router.push({ name: "books" });
                    })
                    .catch((e: Error) => {
                        console.log(e);
                    });
            },
        },
        mounted() {
            this.message = "";
            this.getBook(this.$route.params.id);
        },
    });
    </script>
    
    <style></style>
    
  11. src/components/BooksList.vue:
    
    <template>
            <h4>Lista de livros:</h4>
    
            <input type="text" class="form-control mb-3" placeholder="Pesquisar" v-model="searchTitle">
    
            <ul>
                <li :class="{ active: index === currentIndex }" v-for="(book, index) in filteredBooks" :key="book.id">
                    {{ book.title }}
                    <button class="btn btn-primary btn-sm" @click="setActiveBook(book, index)">Ver</button>
                    <router-link :to="'/books/' + book.id" class="btn btn-warning btn-sm">Editar</router-link>
                    <button class="btn btn-danger btn-sm" @click="deleteBook(book.id)">Excluir</button>
                </li>
            </ul>
    
            <div v-if="currentBook.id">
                <h4>Livro {{ currentBook.id }}:</h4>
                <p>Título: {{ currentBook.title }}</p>
                <p>Autor(a): {{ currentBook.author }}</p>
                <p>Publicação: {{ currentBook.published_date }}</p>
                <p>ISBN: {{ currentBook.isbn }}</p>
                <p>Páginas: {{ currentBook.pages }}</p>
                <p>Capa: {{ currentBook.cover }}</p>
                <p>Idioma: {{ currentBook.language }}</p>
                <router-link :to="'/books/' + currentBook.id" class="btn btn-warning btn-sm">Editar</router-link>
                <button @click="deleteBook(currentBook.id)" class="btn btn-danger btn-sm">Excluir</button>
            </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import BookDataService from "@/services/BookDataService";
    import Book from "@/types/Book";
    import ResponseData from "@/types/ResponseData";
    
    export default defineComponent({
        name: "books-list",
        data() {
            return {
                books: [] as Book[],
                currentBook: {} as Book,
                currentIndex: -1,
                searchTitle: "",
            };
        },
        computed: {
            filteredBooks(): Book[] {
                return this.books.filter(book =>
                    book.title.toLowerCase().includes(this.searchTitle.toLowerCase())
                );
            },
        },
        methods: {
            retrieveBooks() {
                BookDataService.getAll()
                    .then((response: ResponseData) => {
                        this.books = response.data;
                    })
                    .catch((e: Error) => {
                        console.log(e);
                    });
            },
    
            refreshList() {
                this.retrieveBooks();
                this.currentBook = {} as Book;
                this.currentIndex = -1;
            },
    
            setActiveBook(book: Book, index = -1) {
                this.currentBook = book;
                this.currentIndex = index;
            },
    
            deleteBook(id: number) {
                BookDataService.delete(id)
                    .then((response: ResponseData) => {
                        this.refreshList();
                    })
                    .catch((e: Error) => {
                        console.log(e);
                    });
            },
        },
        mounted() {
            this.retrieveBooks();
        },
    });
    </script>
    
    <style></style>
    
  12. src/services/BookDataService.ts:
    
    import http from "@/http-common";
    import Book from "@/types/Book";
    import ResponseData from "@/types/ResponseData";
    
    class BookDataService {
        getAll(): Promise<ResponseData> {
            return http.get("/books");
        }
    
        get(id: number): Promise<ResponseData> {
            return http.get(`/books/${id}`);
        }
    
        create(data: Partial<Book>): Promise<ResponseData> {
            return http.post("/books", data);
        }
    
        update(id: number, data: Partial<Book>): Promise<ResponseData> {
            return http.put(`/books/${id}`, data);
        }
    
        delete(id: number): Promise<ResponseData> {
            return http.delete(`/books/${id}`);
        }
    }
    
    export default new BookDataService();
    
  13. src/types/Book.ts:
    
    export default interface Book {
        id: number;
        title: string;
        author: string;
        published_date: Date;
        isbn: string;
        pages: number;
        cover: string;
        language: string;
    }
    
  14. src/types/ResponseData.ts:
    
    export default interface ResponseData<T = any> {
        data: T;
    }
    
  15. src/App.vue:
    
    <template>
        <div id="app">
            <h1>Livraria UB Social</h1>
            <router-link to="/" class="btn btn-primary">Home</router-link>
            <router-link to="/add" class="btn btn-success">Adicionar livro</router-link>
            <router-view />
        </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    
    export default defineComponent({
        name: "App",
    });
    </script>
    

Integração do sistema:

Integrar (juntar) backend Spring com frontend Vue.js, para tornar sistema unificado.

  1. Pasta do projeto backend Spring Boot, executar servidor Gradle: .\gradlew bootRun
  2. Pasta do projeto frontend Vue.js, executar servidor Vue.js: npm run serve

Elaborado por Mateus Schwede
ubsocial.github.io