Skip to content

06. Integration

Now we will integrate frontend with the backend. But before this I will create a folder in the src of frontend with name utils there I will keep creating utility functions for our frontend.

In this utility folder we create helpers for our application.

Let's start with Registration in our application.

Register

Create register component in the frontend app

js filename=src/views/RegisterPage.vue

<template>
  <FormCompo>
    <template v-slot:form>
      <h5 class="card-title">Sign up</h5>
      <form @submit.prevent="submitForm">
        <div class="mb-3">
          <label class="form-label">Email address</label>
          <input type="email" v-model="email" class="form-control" required>
          <div v-if="message" class="alert alert-warning">
            {{ message }}
          </div>
        </div>
        <div class="mb-3">
          <label class="form-label">Your Name</label>
          <input type="text" v-model="name" class="form-control" required>
        </div>
        <div class="mb-3">
          <label class="form-label">Password</label>
          <input type="password" v-model="password" class="form-control">
        </div>
        <div class="mb-3">
          <select class=" input is-large" v-model="role" required>
            <option class="input is-large" value="user">User</option>
            <option class="input is-large" value="manager">Manager</option>
          </select>
        </div>
        <button type="submit" class="btn btn-outline-primary">Sign up</button>
      </form>
    </template>
  </FormCompo>
</template>

<script>

import FormCompo from '../components/FormCompo.vue';
import home from '../utils/navigation.js';
export default {
  name: 'LoginPage',
  components: {
    FormCompo
  },
  data() {
    return {
      email: '',
      name: '',
      password: '',
      role: '',
      message: ''
    }
  },
  methods: {
    home() {
      home(this.$store, this.$route, this.$router);
    },
    async submitForm() {
      try {
        const response = await fetch('http://127.0.0.1:5000/signup', {
          method: 'POST',
          mode: 'cors',
          credentials: 'include',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            "email": this.email,
            "name": this.name,
            "password": this.password,
            "role": this.role
          }),
        });
        if (response.ok) {
          const data = await response.json();
          alert("User created successfully");
          if (this.$route.path != '/login') {
            this.$router.push('/login')
            this.home()
          }
        } else {
          const data = await response.json();
          throw new Error(data.error);
        }
      } catch (error) {
        alert(error);
      }
    }
  }
}
</script>

Here we are using a parent FormCompo which we will define next. This FormCompo will be the basic UI of a general form which we will use to submit data from client to server.

Note

will prevent to submit the html form, since we want to submit json data through api not html form submission. Now we will setup the backend so that we can start creating and updating Categories and Products by making requests to the API.

<template v-slot:form> is the placeholder which we have created in the parent FormCompo.vue template for dynamic templating.

The submitForm() is the api call method which will be called when the form is submitted.

Now let's create the FormCompo.vue file.

vue filename=src/components/FormCompo.vue

<template>
    <div class="row justify-content-center m-3">
        <div class="card bg-light" style="width: 18rem;">
            <div class="card-body">
                <div class="d-flex justify-content-end">
                    <!-- Cross button to close the card -->
                    <button
                        type="button"
                        class="btn-close"
                        aria-label="Close"
                        @click="home"
                    ></button>
                </div>
                <slot name="form"></slot>
            </div>
        </div>
    </div>
</template>

<script type="module">
import home from "../utils/navigation";
export default {
    name: "FormCompo",
    methods: {
        home() {
            home(this.$store, this.$route, this.$router);
        },
    },
};
</script>

You might have notice the home method in the FormCompo component, this is the method which will be called when the cross button is clicked.

Let's also create the helper function.

js filename=src/utils/navigation.js

function home(store, route, router) {
    const role = store.state.authenticatedUser.role;
    const targetRoute =
        role === "admin"
            ? "/admin"
            : role === "manager"
            ? "/manager"
            : role === "user"
            ? "/user"
            : "/";
    if (route.path !== targetRoute) {
        router.push(targetRoute);
    }
}

export default home;

Login

vue filename=src/views/LoginPage.vue

<template>
    <FormCompo>
        <template v-slot:form>
            <h5 class="card-title">Sign In</h5>
            <form @submit.prevent="submitForm">
                <div class="mb-3">
                    <label for="exampleInputEmail1" class="form-label"
                        >Email address</label
                    >
                    <input
                        type="email"
                        v-model="email"
                        class="form-control"
                        id="exampleInputEmail1"
                    />
                    <div v-if="message" class="alert alert-warning">
                        {{ message }}
                    </div>
                </div>
                <div class="mb-3">
                    <label for="exampleInputPassword1" class="form-label"
                        >Password</label
                    >
                    <input
                        type="password"
                        v-model="password"
                        class="form-control"
                        id="exampleInputPassword1"
                    />
                </div>
                <button type="submit" class="btn btn-outline-primary">
                    Login
                </button>
            </form>
        </template>
    </FormCompo>
</template>

<script>
import FormCompo from "../components/FormCompo.vue";
export default {
    name: "LoginPage",
    components: {
        FormCompo,
    },
    data() {
        return {
            email: "",
            name: "",
            password: "",
            role: "",
            remember: "",
            message: "",
        };
    },

    methods: {
        closeCard() {
            this.$router.push("/");
        },
        async submitForm() {
            try {
                // create operation
                const response = await fetch(
                    "http://127.0.0.1:5000/api/login",
                    {
                        method: "POST",
                        mode: "cors",
                        credentials: "include",
                        headers: {
                            "Content-Type": "application/json",
                        },
                        body: JSON.stringify({
                            email: this.email,
                            password: this.password,
                        }),
                    }
                );
                if (!response.ok) {
                    const data = await response.json();
                    console.log(data);
                    throw new Error(data.error);
                } else {
                    const data = await response.json();
                    localStorage.setItem("jwt", data.access_token);
                    localStorage.setItem("role", data.role);
                    console.log(data);
                    this.$store.commit("setAuthenticatedUser", data);
                    if (data.role === "admin") {
                        this.$router.push("/admin");
                    } else if (data.role === "manager") {
                        this.$router.push("/manager");
                    } else if (data.role === "user") {
                        this.$router.push("/user");
                    }
                }
            } catch (error) {
                alert(error);
            }
        },
    },
};
</script>

We will store the access token in localstorage.

Note: v-model is two way binding, which means that the value of the input field will be updated in the data object, and the data object will be updated in the input field.

js filename=static/store/index.js

const store = createStore({
  state: {
    products: [],
    categories: [],
    authenticatedUser: "" // [tl! add]
  },
  getters: {
    // write your code here
  },
  mutations: {
    ...
    setAuthenticatedUser: (state, user) => {
      state.authenticatedUser = user;
    },
  },
  actions: {
    // write your code here
  },
});

export default store;

We have created a variable to store the user data in the store.

Now we will to create CRUD api calls for category and product.

Create Category

js filename=static/components/CreateCatCompo.js

<template>
  ...
</template>

<script type="module">
import FormCompo from '../components/FormCompo.vue'; // [tl! add]
import { addCategory } from '../services/apiServices'; // [tl! add]
import home from '../utils/navigation'; // [tl! add]
export default {
  name: 'CreateCatCompo',
  components: {
    FormCompo //component binding
  },
  data() {
    return {
      name: '',
      message: ''
    }
  },
  methods: {
    home() {
      home(this.$store, this.$route, this.$router);
    },
    async addcategory() {
      /* write your code here */ // [tl! add:start]
            try {
        const response = await addCategory(this.name);
        this.$store.commit('addCat', response.data.resource);
        this.home();
      } catch (error) {
        console.error(error);
      } // [tl! add:end]
    }
  }
}
</script>

Here you can see that we are importing the addCategory function from the apiServices file and using it to add a new category.

Create Product

js filename=static/components/CreateProCompo.js

<template>
  ...
</template>

<script type="module">
import FormCompo from './FormCompo.vue'; // [tl! add]
import { addProduct } from '../services/apiServices';  // [tl! add]
export default {
  name: 'CreateProCompo',
  components: {
    FormCompo
  },
  data() {
    return {
      ...
    }
  },
  methods: {
    // write your code here
    ...
    async addProduct() { // [tl! add:start]
      try {
        const response = await addProduct(this.name, this.quantity, this.manufacture, this.expiry, this.rpu, this.unit,
          this.description, this.image, this.category_id);
        if (this.$store.state.authenticatedUser.role === 'admin') {
          this.$store.commit('addProduct', data.resource)
        }
        else {
          this.$store.commit('addNoti', data.resource)
        }
      } catch (error) {
        console.error(error);
      }
    }// [tl! add:end]
  }
}
</script>

Update Category

js filename=static/components/EditCatCompo.js

<template>
    ...
</template>

<script>
export default {
import FormCompo from './FormCompo.vue'; //[tl! add:start]
import home from '../utils/navigation';
import { updateCategory, fetchCategory, deleteCategory } from '../services/apiServices'; //[tl! add:end]
  name: 'EditCatCompo',
  components: {
    FormCompo
  },
  data() {
    return {
      name: '',
      message: ''
    }
  },
  methods: {
    // write your code here
    async fetchcategory() { //[tl! add:start]
      try {
        const data = await fetchCategory(this.$route.params.id);
        console.log(data);
        this.name = data.name;
      } catch (error) {
        console.error(error);
      }
    },
    async updatecategory() {
      if (confirm("Are you sure?")) {
        try {
          const data = await updateCategory(this.$route.params.id, this.name);
          if (this.$store.state.authenticatedUser.role === 'admin') {
            this.$store.commit('updateCategory', data.resource);
          }
          else {
            this.$store.commit('addNoti', data.resource)
          }
          this.home();
        } catch (error) {
          console.error(error);
        }
      }
    },
    async deletecategory() {
      if (confirm("Are you sure?")) {
        try {
          const data = await deleteCategory(this.$route.params.id);
          if (this.$store.state.authenticatedUser.role === 'admin') {
            this.$store.commit('deleteCategory', data.resource.id);
          }
          else {
            this.$store.commit('addNoti', data.resource)
          }
          this.home();
        } catch (error) {
          console.error(error);
        }
      }
    }//[tl! add:start]
  },
  mounted() {
    this.fetchcategory()
  }
}
</script>

Update Category

js filename=static/components/EditProCompo.js

<template>
  ...
</template>

<script type="module">
import FormCompo from './FormCompo.vue'; // [tl! add]
import { updateProduct, fetchProduct, deleteProduct } from '../services/apiServices'; // [tl! add]
export default {
  name: 'EditProCompo',
  components: {
    FormCompo
  },
  data() {
    return {
      ...
    }
  },
  methods: {
    handleFileUpload(event) {
      this.product.image = event.target.files[0];
    },
    async fetchproduct() { // [tl! add: start]
      try {
        const data = await fetchProduct(this.$route.params.id);
        console.log(data);
        this.product = data;
      } catch (error) {
        console.error(error);
      }
    },
    async updateproduct() {
      if (confirm("Are you sure?")) {
        try {
          const data = await updateProduct(this.$route.params.id, this.name, this.quantity, this.manufacture, this.expiry, this.rpu, this.unit, this.description, this.image, this.category_id);
          if (this.$store.state.authenticatedUser.role === 'admin') {
            this.$store.commit('updateProduct', data.resource);
          }
          else {
            this.$store.commit('addNoti', data.resource)
          }
        } catch (error) {
          console.error(error);
        }
      }
    },
    async deleteproduct() {
      if (confirm("Are you sure?")) {
        try {
          const data = await deleteProduct(this.$route.params.id);
          if (this.$store.state.authenticatedUser.role === 'admin') {
            this.$store.commit('deleteProduct', data.resource.id);
          }
          else {
            this.$store.commit('addNoti', data.resource)
          }
        } catch (error) {
          console.error(error);
        }
      }
    }// [tl! add: end]
  },
  mounted() {
    // write your code here
  }
}
</script>

Now we will create a file name apiServices and add the following code:

js filename=static/services/apiServices.js

const API_BASE = 'http://127.0.0.1:5000';

function getHeader() {
  return {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${localStorage.getItem('jwt')}`
  };
}

async function addCategory(name) {
  return await fetch(`${API_BASE}/add/cat`, {
    method: 'POST',
    credentials: 'include',
    headers: getHeader(),
    body: JSON.stringify({ name })
  }).then(handleResponse);
}

async function addProduct(name, quantity, manufacture, expiry, rpu, unit, description, image, category_id) {
  const formData = new FormData();
  formData.append('name', name);
  formData.append('quantity', quantity);
  formData.append('manufacture', manufacture);
  formData.append('expiry', expiry);
  formData.append('rpu', rpu);
  formData.append('unit', unit);
  formData.append('description', description);
  formData.append('image', image);
  formData.append('category_id', category_id);
  return await fetch(`${API_BASE}/add/product`, {
    method: 'POST',
    credentials: 'include',
    headers: getHeader(),
    body: formData
  }).then(handleResponse);
}
async function fetchCategory(id) {
  return await fetch(`${API_BASE}/update/category/${id}`, {
    method: 'GET',
    credentials: 'include',
    headers: getHeader()
  }).then(handleResponse);
}

async function updateCategory(id, name) {
  return await fetch(`${API_BASE}/update/category/${id}`, {
    method: 'PUT',
    credentials: 'include',
    headers: getHeader(),
    body: JSON.stringify({ name })
  }).then(handleResponse);
}

async function fetchProduct(id) {
  return await fetch(`${API_BASE}/update/product/${id}`, {
    method: 'GET',
    credentials: 'include',
    headers: getHeader()
  }).then(handleResponse);
}

async function updateProduct(id, name, quantity, manufacture, expiry, rpu, unit, description, image, category_id) {
  const formData = new FormData();
  formData.append('name', name);
  formData.append('quantity', quantity);
  formData.append('manufacture', manufacture);
  formData.append('expiry', expiry);
  formData.append('rpu', rpu);
  formData.append('unit', unit);
  formData.append('description', description);
  formData.append('image', image);
  formData.append('category_id', category_id);
  return await fetch(`${API_BASE}/update/product/${id}`, {
    method: 'PUT',
    credentials: 'include',
    headers: getHeader(),
    body: formData
  }).then(handleResponse);
}

async function deleteCategory(id) {
  return await fetch(`${API_BASE}/update/category/${id}`, {
    method: 'DELETE',
    credentials: 'include',
    headers: getHeader()
  }).then(handleResponse);
}

export {
  addCategory,
  addProduct,
  fetchCategory,
  updateCategory,
  deleteCategory,
  fetchProduct,
  updateProduct,
  deleteProduct,
};

Now we have successfully created the all api services in this apiServices.js file.

CORS Configuration

Since we have created two separate applications. Frontend and Backend both are running on different origins with different ports. By default browser does not allow to make a cross site request. It is like a application cannot make request to different origins. This is a security measure. So we need to configure the browser to allow cross site request.

Note You can read more about CORS heredeveloper.mozilla.org/en-US/docs/Web/HTTP/CORS.

Backend configuration for cross site request.

python filename=main.py

...

def create_app():
    app = Flask(__name__)
    CORS(app, origins=['http://localhost:5173'], supports_credentials=True)
    return app


if __name__ == "__main__":
    app.run()

supports_credentials=True is used to allow the browser to send cookies in the request. This is required if you want to use authentication with JWT or session cookies.

Testing it out

Now you can start the backend and frontend and test all the features whatever we have implemented so far.

To start the backend run the following command:

python3 main.py

To start the frontend run the following command:

npm run dev

Let's commit the change using the following command.

git add .
git commit -m "database integrated"

Continue to start more-features...