
Building a Vue CRUD App with a FastAPI
CRUD operations are essential to many web applications. In this article, we will walk you through the process of building a CRUD app using Vue for the front end and FastAPI for the back end, showing how these frameworks integrate to create a smooth and efficient full-stack application.
Prerequisites
- Node.js
- Python 3.10
- MySQL
Setup Vue project
npm create vite@4.4.0 view -- --template vue
cd view
npm install vue-router@4 axios
Vue project structure
├─ index.html
├─ public
│ └─ css
│ └─ style.css
└─ src
├─ App.vue
├─ components
│ └─ product
│ ├─ Create.vue
│ ├─ Delete.vue
│ ├─ Detail.vue
│ ├─ Edit.vue
│ ├─ Index.vue
│ └─ Service.js
├─ http.js
├─ main.js
└─ router.js
Vue project files
main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
This main.js
file is the entry point for a Vue.js application. It sets up and mounts the app with routing by importing the root component and router, creating the app instance, and configuring it with the router before mounting it to the #app
element.
App.vue
<template>
<router-view />
</template>
<script>
export default {
name: 'App'
}
</script>
This App.vue
file defines the root component of a Vue.js application. It uses a <router-view />
router.js
import { createWebHistory, createRouter } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/product'
},
{
path: '/product',
name: 'product',
component: () => import('./components/product/Index.vue')
},
{
path: '/product/create',
component: () => import('./components/product/Create.vue')
},
{
path: '/product/:id/',
component: () => import('./components/product/Detail.vue')
},
{
path: '/product/edit/:id/',
component: () => import('./components/product/Edit.vue')
},
{
path: '/product/delete/:id/',
component: () => import('./components/product/Delete.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
This router.js
file configures routing for a Vue.js application. It sets up routes for various product-related views, including listing, creating, editing
, and deleting
products, as well as a default redirect to the product list. The router uses `createWebHistory` for HTML5 history mode and exports the configured router instance.
http.js
import axios from 'axios'
let http = axios.create({
baseURL: 'http://localhost:5122/api',
headers: {
'Content-type': 'application/json'
}
})
export default http
The http.js
file configures and exports an Axios instance with a centralized base URL, which is a standard practice for managing API endpoints and default headers set to application/json
.
Create.vue
<template>
<div class="container">
<div class="row">
<div class="col">
<form method="post" @submit.prevent="create()">
<div class="row">
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_name">Name</label>
<input id="product_name" name="Name" class="form-control" v-model="product.name" maxlength="50" />
</div>
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_price">Price</label>
<input id="product_price" name="Price" class="form-control" v-model="product.price" type="number" />
</div>
<div class="col-12">
<router-link class="btn btn-secondary" to="/product">Cancel</router-link>
<button class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import Service from './Service'
export default {
name: 'ProductCreate',
data() {
return {
product: {}
}
},
methods: {
create() {
Service.create(this.product).then(() => {
this.$router.push('/product')
}).catch((e) => {
alert(e.response.data)
})
}
}
}
</script>
This Create.vue
component provides a form for adding a new product with fields for name and price. On submission, it calls a create
method to save the product and redirects to the product list upon success. It also includes a cancel button to navigate back to the list and handles errors with an alert.
Delete.vue
<template>
<div class="container">
<div class="row">
<div class="col">
<form method="post" @submit.prevent="this.delete()">
<div class="row">
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_id">Id</label>
<input readonly id="product_id" name="Id" class="form-control" :value="product.id" type="number" required />
</div>
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_name">Name</label>
<input readonly id="product_name" name="Name" class="form-control" :value="product.name" maxlength="50" />
</div>
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_price">Price</label>
<input readonly id="product_price" name="Price" class="form-control" :value="product.price" type="number" />
</div>
<div class="col-12">
<router-link class="btn btn-secondary" to="/product">Cancel</router-link>
<button class="btn btn-danger">Delete</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import Service from './Service'
export default {
name: 'ProductDelete',
data() {
return {
product: {}
}
},
mounted() {
this.get()
},
methods: {
get() {
return Service.delete(this.$route.params.id).then(response => {
this.product = response.data
})
},
delete() {
Service.delete(this.$route.params.id, this.product).then(() => {
this.$router.push('/product')
}).catch((e) => {
alert(e.response.data)
})
}
}
}
</script>
The Delete.vue
component provides a form for deleting a product, with read-only fields for the product's. The component fetches the product details when mounted. The form calls a delete
method to remove the product and redirects to the product list upon success.
Detail.vue
<template>
<div class="container">
<div class="row">
<div class="col">
<form method="post">
<div class="row">
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_id">Id</label>
<input readonly id="product_id" name="Id" class="form-control" :value="product.id" type="number" required />
</div>
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_name">Name</label>
<input readonly id="product_name" name="Name" class="form-control" :value="product.name" maxlength="50" />
</div>
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_price">Price</label>
<input readonly id="product_price" name="Price" class="form-control" :value="product.price" type="number" />
</div>
<div class="col-12">
<router-link class="btn btn-secondary" to="/product">Back</router-link>
<router-link class="btn btn-primary" :to="`/product/edit/${product.id}`">Edit</router-link>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import Service from './Service'
export default {
name: 'ProductDetail',
data() {
return {
product: {}
}
},
mounted() {
this.get()
},
methods: {
get() {
return Service.get(this.$route.params.id).then(response => {
this.product = response.data
})
}
}
}
</script>
The Detail.vue
component displays detailed information about a product. It features read-only fields for the product's. The component fetches product details when mounted. It includes a "Back" button to navigate to the product list and an "Edit" button to navigate to the product's edit page.
Edit.vue
<template>
<div class="container">
<div class="row">
<div class="col">
<form method="post" @submit.prevent="edit()">
<div class="row">
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_id">Id</label>
<input readonly id="product_id" name="Id" class="form-control" v-model="product.id" type="number" required />
</div>
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_name">Name</label>
<input id="product_name" name="Name" class="form-control" v-model="product.name" maxlength="50" />
</div>
<div class="mb-3 col-md-6 col-lg-4">
<label class="form-label" for="product_price">Price</label>
<input id="product_price" name="Price" class="form-control" v-model="product.price" type="number" />
</div>
<div class="col-12">
<router-link class="btn btn-secondary" to="/product">Cancel</router-link>
<button class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import Service from './Service'
export default {
name: 'ProductEdit',
data() {
return {
product: {}
}
},
mounted() {
this.get()
},
methods: {
get() {
return Service.edit(this.$route.params.id).then(response => {
this.product = response.data
})
},
edit() {
Service.edit(this.$route.params.id, this.product).then(() => {
this.$router.push('/product')
}).catch((e) => {
alert(e.response.data)
})
}
}
}
</script>
The Edit.vue
component provides a form for editing an existing product. It includes fields for the product's. The component fetches the product details when mounted and updates the product on form submission. It also features a "Cancel" button to navigate back to the product list and a "Submit" button to save the changes.
Index.vue
<template>
<div class="container">
<div class="row">
<div class="col">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product">
<td class="text-center">{{product.id}}</td>
<td>{{product.name}}</td>
<td class="text-center">{{product.price}}</td>
<td class="text-center">
<router-link class="btn btn-secondary" :to="`/product/${product.id}`" title="View"><i class="fa fa-eye"></i></router-link>
<router-link class="btn btn-primary" :to="`/product/edit/${product.id}`" title="Edit"><i class="fa fa-pencil"></i></router-link>
<router-link class="btn btn-danger" :to="`/product/delete/${product.id}`" title="Delete"><i class="fa fa-times"></i></router-link>
</td>
</tr>
</tbody>
</table>
<router-link class="btn btn-primary" to="/product/create">Create</router-link>
</div>
</div>
</div>
</template>
<script>
import Service from './Service'
export default {
name: 'ProductIndex',
data() {
return {
products: []
}
},
mounted() {
this.get()
},
methods: {
get() {
Service.get().then(response => {
this.products = response.data
}).catch(e => {
alert(e.response.data)
})
}
}
}
</script>
The Index.vue
component displays a table of products with columns for ID, name, and price. It fetches the list of products when mounted and populates the table. Each product row includes action buttons for viewing, editing, and deleting the product. There is also a "Create" button for adding new products.
Service.js
import http from '../../http'
export default {
get(id) {
if (id) {
return http.get(`/products/${id}`)
}
else {
return http.get('/products' + location.search)
}
},
create(data) {
if (data) {
return http.post('/products', data)
}
else {
return http.get('/products/create')
}
},
edit(id, data) {
if (data) {
return http.put(`/products/${id}`, data)
}
else {
return http.get(`/products/${id}`)
}
},
delete(id, data) {
if (data) {
return http.delete(`/products/${id}`)
}
else {
return http.get(`/products/${id}`)
}
}
}
The Service.js
file defines API methods for handling product operations. It uses an http
instance for making requests:
get(id)
Retrieves a single product by ID or all products if no ID is provided.create(data)
Creates a new product with the provided data or fetches the creation form if no data is provided.edit(id, data)
Updates a product by ID with the provided data or fetches the product details if no data is provided.delete(id, data)
Deletes a product by ID or fetches the product details if no data is provided.
style.css
.container {
margin-top: 2em;
}
.btn {
margin-right: 0.25em;
}
The CSS adjusts the layout by adding space above the container and spacing out buttons horizontally.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
The HTML serves as the main entry point for a Vue application. It includes Bootstrap for styling and Font Awesome for icons. The application will render within a div
with the ID app
.
Setup FastAPI project
pip install fastapi sqlalchemy pymysql uvicorn python-dotenv
Create a testing database named "example" and execute the database.sql file to import the table and data.
FastAPI Project structure
├─ .env
└─ app
├─ db.py
├─ main.py
├─ models
│ └─ product.py
├─ routers
│ └─ product.py
├─ schemas
│ └─ product.py
└─ __init__.py
__init__.py
is used to mark the directory as a Python package, allowing it to be recognized as a module that can contain submodules and facilitate imports.
FastAPI Project files
.env
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=example
DB_USER=root
DB_PASSWORD=
This file holds the configuration details for connecting to the database.
db.py
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
load_dotenv()
url = f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_DATABASE')}"
engine = create_engine(url)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
The db.py
module sets up the database connection for a FastAPI application using SQLAlchemy. It loads database credentials from environment variables, creates a SQLAlchemy engine for a MySQL database, and establishes a session factory for database interactions. The get_db
function provides a database session that can be used in dependency injection, ensuring proper session management by closing the session after use.
models\product.py
from sqlalchemy import *
from app.db import Base
class Product(Base):
__tablename__ = "Product"
id = Column(INTEGER, primary_key=True)
name = Column(VARCHAR)
price = Column(DECIMAL)
The models\product.py
module defines a SQLAlchemy model for the Product
table, mapping its columns for the product's ID, name, and price. It extends the Base
class from the db
module, enabling easy interaction with product data in the database.
schemas\product.py
from pydantic import BaseModel
from decimal import Decimal
class ProductCreate(BaseModel):
name: str
price: Decimal
class ProductUpdate(BaseModel):
name: str
price: Decimal
The schemas/product.py
module defines Pydantic models for validating and serializing product data in a FastAPI application. It includes two classes: ProductCreate
, which is used for creating new products by specifying the name and price, and ProductUpdate
, which is designed for updating existing product information. Both classes ensure that the provided data adheres to the expected types.
routers\product.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db import get_db
from app.models.product import Product
from app.schemas.product import ProductCreate, ProductUpdate
router = APIRouter()
@router.get("/products")
def index(db: Session = Depends(get_db)):
return db.query(Product).all()
@router.get("/products/{id}")
def get(id: int, db: Session = Depends(get_db)):
return db.query(Product).filter(Product.id == id).first()
@router.post("/products")
def create(payload: ProductCreate, db: Session = Depends(get_db)):
product = Product(**payload.model_dump())
db.add(product)
db.commit()
db.refresh(product)
return product
@router.put("/products/{id}")
def update(id: int, payload: ProductUpdate, db: Session = Depends(get_db)):
product = db.query(Product).filter(Product.id == id).first()
product.name = payload.name
product.price = payload.price
db.commit()
db.refresh(product)
return product
@router.delete("/products/{id}")
def delete(id: int, db: Session = Depends(get_db)):
db.query(Product).filter(Product.id == id).delete()
db.commit()
The routers/product.py
module defines a FastAPI router for managing product-related API endpoints. It provides the following functionalities:
index
function returns a list of all products from the database.get
function fetches a specific product by its ID.create
function adds a new product to the database using the data provided in theProductCreate
schema.update
function modifies an existing product's details based on its ID and the providedProductUpdate
data.delete
function removes a product from the database by its ID.
All operations depend on a database session obtained through the get_db
dependency, ensuring proper session management for each request.
main.py
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from app.routers.product import router
app = FastAPI()
app.include_router(router, prefix="/api")
@app.get("/")
async def read_index():
return FileResponse("app/static/index.html")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1")
The main.py
module serves as the entry point for a FastAPI application. It initializes the FastAPI app and includes a router for product-related endpoints under the /api
prefix. The module also defines a root endpoint that serves an index.html
file from the static directory. CORS middleware is added to allow requests from any origin, enabling cross-origin resource sharing. Finally, the application is configured to run with Uvicorn when executed directly, listening on 127.0.0.1
.
Run projects
Run Vue project
npm run dev
Run FastAPI project
uvicorn app.main:app
Open the web browser and goto http://localhost:5173
You will find this product list page.

Testing
Click the "View" button to see the product details page.

Click the "Edit" button to modify the product and update its details.

Click the "Submit" button to save the updated product details.

Click the "Create" button to add a new product and input its details.

Click the "Submit" button to save the new product.

Click the "Delete" button to remove the previously created product.

Click the "Delete" button to confirm the removal of this product.

Conclusion
In conclusion, we have learned how to create a basic Vue project using Single-File Components (SFC) to build views and manage application routing. By setting up a FastAPI server as the backend and using SQLAlchemy for database operations, we've developed a dynamic front-end that integrates seamlessly with a robust backend, providing a solid foundation for building modern, full-stack web applications.
Source code: https://github.com/stackpuz/Example-CRUD-Vue-3-FastAPI
Create a Vue CRUD App in Minutes: https://stackpuz.com