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.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-validation'
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
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");
}
};
}
}
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;
}
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;
}
}
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> {
}
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);
}
}
{
"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"
}
{
"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"
}
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
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");
import axios, { AxiosInstance } from "axios";
const apiClient: AxiosInstance = axios.create({
baseURL: "http://localhost:8080",
headers: {
"Content-type": "application/json",
},
});
export default apiClient;
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
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',
},
};
module.exports = {
devServer: {
port: 8081,
},
};
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;
<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>
<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>
<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>
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();
export default interface Book {
id: number;
title: string;
author: string;
published_date: Date;
isbn: string;
pages: number;
cover: string;
language: string;
}
export default interface ResponseData<T = any> {
data: T;
}
<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>
Integrar (juntar) backend Spring com frontend Vue.js, para tornar sistema unificado.
Elaborado por Mateus Schwede
ubsocial.github.io