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

Oleg Agapov
codeburst
Published in
8 min readOct 18, 2017

--

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:

https://github.com/oleg-agapov/flask-vue-spa

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 to 0
  • in methods section we have getRandomInt(min, max) function which will return a number from specified range, getRandom function will dispatch previous function and assign it value to randomNumber
  • after creation of component method getRandom will be invoked to initialize randomNumber
  • 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.

--

--