Criar sistema web RESTful fullstack, com backend Spring Boot Kotlin (ORM hibernate JPA DAO) e frontend Vue.js TypeScript (vue router), com funções de CRUD (criar, listar, ver específico, editar e excluir) de livros.
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
runtimeOnly("com.mysql:mysql-connector-j")
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
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
server.port=8080
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
class WebConfig {
@Bean
fun corsConfigurer(): WebMvcConfigurer {
return object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8081")
.allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
}
}
}
}
package ubsocial.com.biblioteca.model.entity
import com.fasterxml.jackson.annotation.JsonFormat
import jakarta.persistence.*
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size
import java.time.LocalDate
@Entity
data class Livro(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long? = null,
@field:NotBlank
@field:Size(min = 1, max = 255, message = "Título precisa ter entre 1 e 255 caracteres")
@Column(length = 255)
val title: String = "",
@field:NotBlank
@field:Size(min = 1, max = 255, message = "Autor precisa ter entre 1 e 255 caracteres")
@Column(length = 255)
val author: String = "",
@field:NotNull
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
@Column(name = "published_date")
val published_date: LocalDate = LocalDate.now(),
@field:NotBlank
@field:Size(min = 13, max = 13, message = "ISBN precisa ter 13 caracteres")
@Column(unique = true)
val isbn: String = "",
val pages: Int = 0,
@field:Size(min = 1, max = 255, message = "Capa/Gênero precisa ter entre 1 e 255 caracteres")
@Column(length = 255)
val cover: String? = null,
@field:NotBlank
@field:Size(min = 1, max = 255, message = "Idioma precisa ter entre 1 e 255 caracteres")
@Column(length = 255)
val language: String = ""
)
package ubsocial.com.biblioteca.model.repositories
import org.springframework.data.jpa.repository.JpaRepository
import ubsocial.com.biblioteca.model.entity.Livro
interface LivroRepository : JpaRepository<Livro, Long>
package ubsocial.com.biblioteca.controller
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import ubsocial.com.biblioteca.model.entity.Livro
import ubsocial.com.biblioteca.model.repositories.LivroRepository
@RestController
@RequestMapping("/books")
class LivroResource(private val livroRepository: LivroRepository) {
@GetMapping
fun getAll(): List<Livro> = livroRepository.findAll()
@GetMapping("/{id}")
fun getById(@PathVariable id: Long): ResponseEntity<Livro> {
val livro = livroRepository.findById(id)
return if (livro.isPresent) ResponseEntity.ok(livro.get())
else ResponseEntity.notFound().build()
}
@PostMapping
fun create(@RequestBody @Valid livro: Livro): Livro = livroRepository.save(livro)
@PutMapping("/{id}")
fun update(@PathVariable id: Long, @RequestBody @Valid livro: Livro): ResponseEntity<Livro> {
val optional = livroRepository.findById(id)
if (optional.isEmpty) return ResponseEntity.notFound().build()
val updated = optional.get().copy(
title = livro.title,
author = livro.author,
published_date = livro.published_date,
isbn = livro.isbn,
pages = livro.pages,
cover = livro.cover,
language = livro.language
)
return ResponseEntity.ok(livroRepository.save(updated))
}
@DeleteMapping("/{id}")
fun delete(@PathVariable id: Long): ResponseEntity<Void> {
return if (livroRepository.existsById(id)) {
livroRepository.deleteById(id)
ResponseEntity.noContent().build()
} else {
ResponseEntity.notFound().build()
}
}
}
{
"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 install vue-router@4
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">
<div class="form-group">
<input type="text" class="form-control" v-model="book.title" placeholder="Título" required id="title"
name="title" />
</div>
<div class="form-group">
<input type="text" class="form-control" v-model="book.author" placeholder="Autor(a)" required id="author"
name="author" />
</div>
<div class="form-group">
<label for="published_date">Publicação:</label>
<input type="date" class="form-control" v-model="book.published_date" required id="published_date"
name="published_date" :max="new Date().toISOString().split('T')[0]" />
</div>
<div class="form-group">
<input type="text" class="form-control" v-model="book.isbn" placeholder="ISBN" required id="isbn"
name="isbn" pattern="^\d{13}$" />
</div>
<div class="form-group">
<input type="number" class="form-control" v-model="book.pages" placeholder="Páginas" required id="pages"
name="pages" min="1" />
</div>
<div class="form-group">
<input type="text" name="cover" class="form-control" v-model="book.cover" placeholder="Capa" required
id="cover" />
</div>
<div class="form-group">
<input type="text" class="form-control" v-model="book.language" placeholder="Idioma" required id="language"
name="language" />
</div>
<button type="button" class="btn btn-secondary" @click="$router.push('/books')">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.error(e);
});
},
},
});
</script>
<style></style>
<template>
<div v-if="currentBook.id">
<h4>Livro {{ currentBook.id }} - {{ currentBook.title }}</h4>
<form @submit.prevent="updateBook">
<div class="form-group">
<input type="text" class="form-control" v-model="currentBook.title" placeholder="Título" required
id="title" name="title" />
</div>
<div class="form-group">
<input type="text" class="form-control" v-model="currentBook.author" placeholder="Autor(a)" required
id="author" name="author" />
</div>
<div class="form-group">
<label for="published_date">Publicação:</label>
<input type="date" class="form-control" v-model="currentBook.published_date" required
id="published_date" name="published_date" :max="new Date().toISOString().split('T')[0]" />
</div>
<div class="form-group">
<input type="text" class="form-control" v-model="currentBook.isbn" placeholder="ISBN" required id="isbn"
name="isbn" pattern="^\d{13}$" />
</div>
<div class="form-group">
<input type="number" class="form-control" v-model="currentBook.pages" placeholder="Páginas" required
id="pages" name="pages" min="1" />
</div>
<div class="form-group">
<input type="text" name="cover" class="form-control" v-model="currentBook.cover" placeholder="Capa"
required id="cover" />
</div>
<div class="form-group">
<input type="text" class="form-control" v-model="currentBook.language" placeholder="Idioma" required
id="language" name="language" />
</div>
<button type="button" class="btn btn-secondary" @click="$router.push('/books')">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.error(e);
});
},
updateBook() {
BookDataService.update(this.currentBook.id, this.currentBook)
.then((response: ResponseData) => {
this.$router.push({ name: "books" });
})
.catch((e: Error) => {
console.error(e);
this.message = "Erro ao atualizar o livro: " + e.message;
});
},
deleteBook(id: number) {
BookDataService.delete(this.currentBook.id)
.then((response: ResponseData) => {
console.log(response.data);
this.$router.push({ name: "books" });
})
.catch((e: Error) => {
console.error(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.error(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.error(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>
<style></style>
Integrar (juntar) backend Spring com frontend Vue.js, para tornar sistema unificado.
Elaborado por Mateus Schwede
ubsocial.github.io