Full-stack single page application with Vue.js and Flask

In this tutorial I would like to show you how to connect Vue.js single page application with Flask back-end.
Basically, there is no problem if you want to just use Vue.js library with Flask templates. Well, actually the one obvious problem is that Jinja (template engine) uses double curly braces for rendering stuff as well as Vue.js, but there is a nice workaround explained here.
I wanted a bit different case. What if I need a single page application built with Vue.js (using single page components, vue-router
in HTML5 history mode and other good features) and served over Flask web server? In few words this should works as follows:
- Flask serves my
index.html
which contains my Vue.js app - during front-end development I use Webpack with all the cool features it provides
- Flask has API endpoints I can access from my SPA
- I can access API endpoints even while I run Node.js for front-end development
Sounds interesting? Let’s do this.
Full source code you can find here:
Client-side
For generating basic Vue.js app I will use vue-cli. If you haven’t installed it yet just run:
$ npm install -g vue-cli
Client-side and back-end code will be split to different folders. To initialize front-end part run following:
$ mkdir flaskvue
$ cd flaskvue
$ vue init webpack frontend
Go through installation wizard. My setup is:
- Vue build — Runtime only
- Install vue-router? — Yes
- Use ESLint to lint your code? — Yes
- Pick an ESLint preset — Standard
- Setup unit tests with Karma + Mocha? — No
- Setup e2e tests with Nightwatch? — No
Next:
$ cd frontend
$ npm install# after installation
$ npm run dev
You should starting setup of Vue.js application. Let’s start with adding some pages.
Add Home.vue
and About.vue
to frontend/src/components
folder. For now make them very simple, like this:
// Home.vue<template>
<div>
<p>Home page</p>
</div>
</template>
and
// About.vue<template>
<div>
<p>About</p>
</div>
</template>
We will use them to correctly recognize our current location (according to address bar). Now we need to change frontend/src/router/index.js
file in order to render our new components:
import Vue from 'vue'
import Router from 'vue-router'const routerOptions = [
{ path: '/', component: 'Home' },
{ path: '/about', component: 'About' }
]const routes = routerOptions.map(route => {
return {
...route,
component: () => import(`@/components/${route.component}.vue`)
}
})Vue.use(Router)export default new Router({
routes,
mode: 'history'
})
If you try to enter localhost:8080
and localhost:8080/about
you should see corresponding pages.

We are almost ready to build a project in order to create a bundle with static assets. Before that let’s redefine output directory for them. In frontend/config/index.js
find next settings
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
and change them to
index: path.resolve(__dirname, '../../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../../dist'),
So /dist
folder with html/css/js bundle will have the same level as /frontend
. Now you can run $ npm run build
to create a bundle.

Back-end
For Flask server I’ll use python version 3.6. Inside root /flaskvue
folder create new sub-folder for back-end code and initialize virtual environment there:
$ mkdir backend
$ cd backend
$ virtualenv -p python3 venv
To enable virtual environment run (on macOs):
$ source venv/bin/activate
For activation in Windows use this docs.
Under virtual environment install Flask with:
(venv) pip install Flask
Now let’s write code for Flask server. Create a file run.py
in root directory:
(venv) cd ..
(venv) touch run.py
Add next code to this file:
from flask import Flask, render_templateapp = Flask(__name__,
static_folder = "./dist/static",
template_folder = "./dist")@app.route('/')
def index():
return render_template("index.html")
This code is slightly differs from Flask starter “Hello world” code. The major difference is that we specify static and templates folder to point to /dist
folder with our front-end bundle. To run Flask server run in root folder:
(venv) FLASK_APP=run.py FLASK_DEBUG=1 flask run
This will start a web server on localhost:5000
. FLASK_APP
points to server startup file, FLASK_DEBUG=1
will run it in debug mode. If everything is correct you’ll see familiar Home page you’ve done on in Vue.
Meanwhile you’ll face an error if try to enter /about
page. Flask throws an error saying requested URL was not found. Indeed, because we use HTML5 history mode in vue-router
we need to configure our web server to redirect all routes to index.html
. It’s easy to do in Flask. Modify existing route to following:
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
return render_template("index.html")
Now URL localhost:5000/about
will be redirected to index.html
and vue-router
will handle it within itself.
Adding 404 page
Because we have a catch-all route inside our web server it is difficult now to catch 404 errors as Flask will redirect all requests to index.html
(even for non-existing pages). So we need to handle unknown routes inside Vue.js application. Of course all work can be done inside our router file.
In frontend/src/router/index.js
add next line:
const routerOptions = [
{ path: '/', component: 'Home' },
{ path: '/about', component: 'About' },
{ path: '*', component: 'NotFound' }
]
Here path '*'
is a wildcard for vue-router
so it means any other route except those we defined above. Now we need to additional create NotFound.vue
file in /components
folder. I’ll do it very simple:
// NotFound.vue<template>
<div>
<p>404 - Not Found</p>
</div>
</template>
Now run front-end server again with npm run dev
and try to enter some meaningless address like localhost:8080/gljhewrgoh
. You should see our “Not Found” message.
Adding API endpoint
The very last example of my Vue.js/Flask tutorial will be creation of API on server side and dispatching it on client-side. I’ll create a simple endpoint which will return a random number from 1 to 100.
Open run.py
and add:
from flask import Flask, render_template, jsonify
from random import *app = Flask(__name__,
static_folder = "./dist/static",
template_folder = "./dist")@app.route('/api/random')
def random_number():
response = {
'randomNumber': randint(1, 100)
}
return jsonify(response)@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
return render_template("index.html")
First I imported random
library and jsonify
function from Flask library. Then I added new route /api/random
to return JSON like this:
{
"randomNumber": 36
}
You can test this route by navigating to localhost:5000/api/random
.
At this point server-side work is done. Time to show this on client-side. I’ll change Home.vue
component to show my random number:
At this stage I just emulate random number generation process on client-side. So, this component works like this:
- on initialization variable
randomNumber
is equal to0
- in
methods
section we havegetRandomInt(min, max)
function which will return a number from specified range,getRandom
function will dispatch previous function and assign it value torandomNumber
- after creation of component method
getRandom
will be invoked to initializerandomNumber
- on button click event we will dispatch
getRandom
method to get new number
Now on home page you should see our random number generated by front-end. Let’s connect it to back-end.
For that purpose I will use axios library. It allows us to make HTTP requests and return JavaScript Promise
with JSON answer. Let’s install it:
(venv) cd frontend
(venv) npm install --save axios
Open Home.vue
again and add a few changes to <script>
section:
import axios from 'axios'methods: {
getRandom () {
// this.randomNumber = this.getRandomInt(1, 100)
this.randomNumber = this.getRandomFromBackend()
},
getRandomFromBackend () {
const path = `http://localhost:5000/api/random`
axios.get(path)
.then(response => {
this.randomNumber = response.data.randomNumber
})
.catch(error => {
console.log(error)
})
}
}
At the top we need to import axios library. Then there is a new method getRandomFromBackend
which will use axios to asynchronously reach API and retrieve the result. And finally, method getRandom
now should use getRandomFromBackend
function to get a random value.
Save a file, go to browser, run a dev server again, refresh localhost:8080
and… You should see an error in console and no random value. But don’t worry, everything is working. We got CORS error which means that our Flask server API by default is closed to other web-servers (in our case it’s Node.js server running our Vue.js app). If you create a bundle with npm run build
and open localhost:5000
(so Flask server) you will see working application. But it’s not very convenient to create a bundle every time you made some changes to client-side application.
Let’s use CORS plugin for Flask which will allow us to create a rules for API accesses. Plugin is called flask-cors , let’s install it:
(venv) pip install -U flask-cors
You can read documentation on better explanation of what ways you have to enable CORS on your server. I’ll use resource specific method and apply {“origins”: “*”}
to all /api/*
routes (so everyone can use my /api
endpoints). In run.py
:
from flask_cors import CORSapp = Flask(__name__,
static_folder = "./dist/static",
template_folder = "./dist")
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
With that change in place you can call Flask APIs right from front-end development server.
Update:
Actually, there is no need for CORS extension if you will serve static files through Flask. Thanks to Carson Gee for this trick.
The idea is next. If the application is in debug mode it will just proxying our front-end server. Otherwise (in production) serve the static files. Here is how we can do that:
Simple and elegant. Magic ✨!
Now you have a full-stack application built with your favorite technologies.


Afterword
In the end I want to say a few words on how you can improve in this solution.
First of all get use CORS extension only if you want to give access to your API endpoints for external servers. Otherwise just use a trick with proxying front-end development server.
Another improvement will avoiding hard coded API routes on client side. Maybe you need to think of some dictionary with API endpoints. So when you’ll change you API route all you need to do is just to refresh a dictionary. Front-end will still have a valid endpoint.
Usually during development you will have at least two terminal windows: one for Flask and another for Vue.js. In production you’ll get rid of running separate Node.js server for Vue.
Source code: https://github.com/oleg-agapov/flask-vue-spa
Thank you for reading!
If you like this tutorial and would like to donate me a few bucks💰 you can do it to my paypal account.