Single Page Apps with Vue.js and Flask: JWT Authentication

Single Page Apps with Vue.js and Flask: JWT Authentication

JWT Authentication

Welcome to the sixth installment to this multi-part tutorial series on full-stack web development using Vue.js and Flask. In this post I will be demonstrating a way to use JSON Web Token (JWT) authentication.

The code for this post can be found on my GitHub account under the branch SixthPost.

Series Content

  1. Seup and Getting to Know VueJS
  2. Navigating Vue Router
  3. State Management with Vuex
  4. RESTful API with Flask
  5. AJAX Integration with REST API
  6. JWT Authentication (you are here)
  7. Deployment to a Virtual Private Server

Basic Introduction to JWT Authentication

Similar to some of the other posts in this series I will not be going into significant details on the theory of how JWT works. Instead I will be taking a pragmatic approach and demonstrating its implementation specifics using the technologies of interest within Flask and Vue.js. If you are interested in gaining a deeper understanding of JWTs I refer you to Scott Robinson's excellent post here on StackAbuse, where he explains the low-level details of the technique.

In the basic sense a JWT is an encoded JSON object used to convey information between two systems which is composed of a header, a payload, and a signature in the form of [HEADER].[PAYLOAD].[SIGNATURE] all contained in the HTTP header as "Authorization: Bearer [HEADER].[PAYLOAD].[SIGNATURE]". The process starts with the client (requesting system) authenticating with the server (a service with a desired resource) which generates a JWT that is only valid for a specific amount of time. The server then returns this as a signed and encoded token for the client to store and use for verification in later communications.

JWT authentication works quite well for SPA applications like the one being built out in this series and have gained significant popularity among developers implementing them.

Implementing JWT Authentication in the Flask RESTful API

On the Flask side of things I will be using the Python package PyJWT to handle some of the particulars around creating, parsing, and validating JWTs.

(venv) $ pip install PyJWT

With the PyJWT package installed I can move on to implementing the pieces necessary for authentication and verification in the Flask application. To start, I will give the application the ability to create new registered users which will be represented by a User class. As with all the other classes in this application the User class will reside in the models.py module.

First item to do is to import a couple of functions, generate_password_hash and check_password_hash from the werkzeug package's security module which I will use to generate and verify hashed passwords. There is no need to install this package as it comes with Flask automatically.

"""
models.py
- Data classes for the surveyapi application
"""

from datetime import datetime
from flask_sqlalchemy import SQLAlchemy

from werkzeug.security import generate_password_hash, check_password_hash

db = SQLAlchemy()

Directly below the above code I define the User class, which inherits from the SQLAlchemy Model class similar to the others defined in earlier posts. This User class needs to contain an auto generated integer primary key class field called id then two string fields called email and password with the email configured to be unique. I also give this class a relationship field to associate any surveys the user may create. On the other side of this equation I added a creator_id foreign key to the Survey class to link users to surveys they create.

I override the __init__(...) method so that I can hash the password upon instantiating a new User object. After that I give it the class method, authenticate, to query a user by email and check that the supplied password hash matches the one stored in the database. If they match I return the authenticated user. Last but not least I tacked on a to_dict() method to help with serializing user objects.

"""
models.py
- Data classes for the surveyapi application
"""

#
# omitting imports and what not
#

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)
    surveys = db.relationship('Survey', backref="creator", lazy=False)

    def __init__(self, email, password):
        self.email = email
        self.password = generate_password_hash(password, method='sha256')

    @classmethod
    def authenticate(cls, **kwargs):
        email = kwargs.get('email')
        password = kwargs.get('password')
        
        if not email or not password:
            return None

        user = cls.query.filter_by(email=email).first()
        if not user or not check_password_hash(user.password, password):
            return None

        return user

    def to_dict(self):
        return dict(id=self.id, email=self.email)

class Survey(db.Model):
    __tablename__ = 'surveys'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    questions = db.relationship('Question', backref="survey", lazy=False)
    creator_id = db.Column(db.Integer, db.ForeignKey('users.id'))

    def to_dict(self):
      return dict(id=self.id,
                  name=self.name,
                  created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                  questions=[question.to_dict() for question in self.questions])

Next up is to generate a new migration and update the database with it to pair the User Python class with a users SQLite database table. To do this I run the following commands in the same directory as my manage.py module.

(venv) $ python manage.py db migrate
(venv) $ python manage.py db upgrade

Ok, time to hop over to the api.py module and implement the functionality to register and authenticate users along with verification functionality to protect the creation of new surveys. After all, I don't want any nefarious web bots or other bad actors polluting my awesome survey app.

To start I add the User class to the list of imports from the models.py module towards the top of the api.py module. While I'm in there I will go ahead and add a couple of other imports I will be using later.

"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

from functools import wraps
from datetime import datetime, timedelta

from flask import Blueprint, jsonify, request, current_app

import jwt

from .models import db, Survey, Question, Choice, User

Now that I have all the tools I need imported I can implement a set of register and login view functions in the api.py module.

I will begin with the register() view function which expects an email and password to be sent along in JSON in the body of the POST request. The user is simply created with whatever is given for the email and password and I merrily return a JSON response (which isn't necessarily the best approach, but it will work for the moment).

"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/register/', methods=('POST',))
def register():
    data = request.get_json()
    user = User(**data)
    db.session.add(user)
    db.session.commit()
    return jsonify(user.to_dict()), 201

Cool. The backend is able to create new users eager to create gobs of surveys so, I better add some functionality to authenticate them and let them get on with creating their surveys.

The login function uses the User.authenticate(...) class method to try to find and authenticate a user. If the user matching the given email and password is found then the login function progresses to create a JWT token, otherwise None is returned, resulting in the login function to return a "failure to authenticate" message with the appropriate HTTP status code of 401.

I create the JWT token using PyJWT (as jwt) by encoding a dictionary containing the following:

  • sub - the subject of the jwt, which in this case is the user's email
  • iat - the time the jwt was issued at
  • exp - is the moment the jwt should expire, which is 30 minutes after issuing in this case
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/login/', methods=('POST',))
def login():
    data = request.get_json()
    user = User.authenticate(**data)

    if not user:
        return jsonify({ 'message': 'Invalid credentials', 'authenticated': False }), 401

    token = jwt.encode({
        'sub': user.email,
        'iat':datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(minutes=30)},
        current_app.config['SECRET_KEY'])
    return jsonify({ 'token': token.decode('UTF-8') })

The encoding process utilizes the value of the BaseConfig class's SECRET_KEY property defined in config.py and held in the current_app's config property once the Flask app is created.

Next up I would like to break up the GET and POST functionality that currently resides in a poorly named view function called fetch_survey(...) shown below in its original state. Instead, I will let fetch_surveys(...) be solely in charge of fetching all surveys when requesting "/api/surveys/" with a GET request. Survey creation, on the other hand, which happens when the same URL is hit with a POST request, will now reside in a new function called create_survey(...).

So this...

"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/surveys/', methods=('GET', 'POST'))
def fetch_surveys():
    if request.method == 'GET':
        surveys = Survey.query.all()
        return jsonify([s.to_dict() for s in surveys])
    elif request.method == 'POST':
        data = request.get_json()
        survey = Survey(name=data['name'])
        questions = []
        for q in data['questions']:
            question = Question(text=q['question'])
            question.choices = [Choice(text=c) for c in q['choices']]
            questions.append(question)
        survey.questions = questions
        db.session.add(survey)
        db.session.commit()
        return jsonify(survey.to_dict()), 201

becomes this...

"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/surveys/', methods=('POST',))
def create_survey(current_user):
    data = request.get_json()
    survey = Survey(name=data['name'])
    questions = []
    for q in data['questions']:
        question = Question(text=q['question'])
        question.choices = [Choice(text=c) for c in q['choices']]
        questions.append(question)
    survey.questions = questions
    survey.creator = current_user
    db.session.add(survey)
    db.session.commit()
    return jsonify(survey.to_dict()), 201


@api.route('/surveys/', methods=('GET',))
def fetch_surveys():
    surveys = Survey.query.all()
    return jsonify([s.to_dict() for s in surveys])

The real key now is to protect the create_survey(...) view function so that only authenticated users can create new surveys. Said another way, if a POST request is made against "/api/surveys" the application should check to make sure that it is being done by a valid and authenticated user.

In comes the handy Python decorator! I will use a decorator to wrap the create_survey(...) view function which will check that the requester contains a valid JWT token in its header and turn away any requests that do not. I will call this decorator token_required and implement it above all the other view functions in api.py like so:

"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

def token_required(f):
    @wraps(f)
    def _verify(*args, **kwargs):
        auth_headers = request.headers.get('Authorization', '').split()

        invalid_msg = {
            'message': 'Invalid token. Registeration and / or authentication required',
            'authenticated': False
        }
        expired_msg = {
            'message': 'Expired token. Reauthentication required.',
            'authenticated': False
        }

        if len(auth_headers) != 2:
            return jsonify(invalid_msg), 401

        try:
            token = auth_headers[1]
            data = jwt.decode(token, current_app.config['SECRET_KEY'])
            user = User.query.filter_by(email=data['sub']).first()
            if not user:
                raise RuntimeError('User not found')
            return f(user, *args, **kwargs)
        except jwt.ExpiredSignatureError:
            return jsonify(expired_msg), 401 # 401 is Unauthorized HTTP status code
        except (jwt.InvalidTokenError, Exception) as e:
            print(e)
            return jsonify(invalid_msg), 401

    return _verify

The primary logic of this decorator is to:

  1. Ensure it contains the "Authorization" header with a string that looks like a JWT token
  2. Validate that the JWT is not expired, which PyJWT takes care of for me by throwing a ExpiredSignatureError if it is no longer valid
  3. Validate that the JWT is a valid token, which PyJWT also takes care of by throwing a InvalidTokenError if it is not valid
  4. If all is valid then the associated user is queried from the database and returned to the function the decorator is wrapping

Now all that remains is to add the decorator to the create_survey(...) method like so:

"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other functions
#

@api.route('/surveys/', methods=('POST',))
@token_required
def create_survey(current_user):
    data = request.get_json()
    survey = Survey(name=data['name'])
    questions = []
    for q in data['questions']:
        question = Question(text=q['question'])
        question.choices = [Choice(text=c) for c in q['choices']]
        questions.append(question)
    survey.questions = questions
    survey.creator = current_user
    db.session.add(survey)
    db.session.commit()
    return jsonify(survey.to_dict()), 201

Implementing JWT Authentication in Vue.js SPA

With the back-end side of the authentication equation complete I now need to button up the client side by implementing JWT authentication in Vue.js. I start by creating a new module within the app called "utils'' within the src directory and placing an index.js file inside of the utils folder. This module will contain two things:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

  1. An event bus which I can use to send messages around the application when certain things happen, like failed authentication in the event of a expired JWT
  2. A function to check a JWT to see if it is still valid or not

These two things are implemented like so:

// utils/index.js

import Vue from 'vue'

export const EventBus = new Vue()

export function isValidJwt (jwt) {
  if (!jwt || jwt.split('.').length < 3) {
    return false
  }
  const data = JSON.parse(atob(jwt.split('.')[1]))
  const exp = new Date(data.exp * 1000) // JS deals with dates in milliseconds since epoch
  const now = new Date()
  return now < exp
}

The EventBus variable is just an instance of the Vue object. I can utilize the fact that the Vue object has both an $emit and a pair of $on / $off methods, which are used to emit events as well as register and unregister to events.

The isValid(jwt) function is what I will use to determine if a user is authenticated based on the information in the JWT. Recall from the earlier basic explanation of JWTs that a standard set of properties reside in an encoded JSON object of the form "[HEADER].[PAYLOAD].[SIGNATURE]". For example, say I have the following JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw

I can decode the middle body section to inspect its contents using the following JavaScript:

const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw'
const tokenParts = token.split('.')
const body = JSON.parse(atob(tokenParts[1]))
console.log(body)   // {sub: "[email protected]", iat: 1522326732, exp: 1522328532}

Here the contents of the token body are sub, representing the email of the subscriber, iat, which is issued at timestamp in seconds, and exp, which is the time in which the token will expire as seconds from epoch (the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z)). As you can see I am using the exp value in the isValidJwt(jwt) function to determine if the JWT is expired or not.

Next up is to add a couple of new AJAX functions to make calls to the Flask REST API to register new users and login existing ones, plus I will need to modify the postNewSurvey(...) function to include a header containing a JWT.


// api/index.js

//
// omitting stuff ... skipping to the bottom of the file
//

export function postNewSurvey (survey, jwt) {
  return axios.post(`${API_URL}/surveys/`, survey, { headers: { Authorization: `Bearer: ${jwt}` } })
}

export function authenticate (userData) {
  return axios.post(`${API_URL}/login/`, userData)
}

export function register (userData) {
  return axios.post(`${API_URL}/register/`, userData)
}

Ok, now I can put these things to use in the store to manage the state required to provide proper authentication functionality. To begin I import EventBus and the isValidJwt(...) function from the utils module as well as the two new AJAX functions from the api module. Then add a definition of a user object and a jwt token string in the store's state object like so:

// store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

// imports of AJAX functions will go here
import { fetchSurveys, fetchSurvey, saveSurveyResponse, postNewSurvey, authenticate, register } from '@/api'
import { isValidJwt, EventBus } from '@/utils'

Vue.use(Vuex)

const state = {
  // single source of data
  surveys: [],
  currentSurvey: {},
  user: {},
  jwt: ''
}

//
// omitting all the other stuff below
//

Next, I need to add in a couple of action methods which will call either the register(...) or authenticate(...) AJAX functions we just defined. I name the one responsible for authenticating a user login(...), which calls the authenticate(...) AJAX function and when it returns a successful response containing a new JWT, it commits a mutation I will name setJwtToken, which needs to be added to the mutations object. In the event of an unsuccessful authentication request I chain a catch method to the promise chain to catch the error and use the EventBus to emit an event notifying any subscribers that authentication failed.

The register(...) action method is quite similar to login(...), in fact, it actually utilizes login(...). I am also showing a small modification to the submitNewSurvey(...) action method that passes the JWT token as an additional parameter to the postNewSurvey(...) AJAX call.

const actions = {
  // asynchronous operations

  //
  // omitting the other action methods...
  //

  login (context, userData) {
    context.commit('setUserData', { userData })
    return authenticate(userData)
      .then(response => context.commit('setJwtToken', { jwt: response.data }))
      .catch(error => {
        console.log('Error Authenticating: ', error)
        EventBus.$emit('failedAuthentication', error)
      })
  },
  register (context, userData) {
    context.commit('setUserData', { userData })
    return register(userData)
      .then(context.dispatch('login', userData))
      .catch(error => {
        console.log('Error Registering: ', error)
        EventBus.$emit('failedRegistering: ', error)
      })
  },
  submitNewSurvey (context, survey) {
    return postNewSurvey(survey, context.state.jwt.token)
  }
}

As mentioned previously, I need to add a new mutation that explicitly sets the JWT and the user data.

const mutations = {
  // isolated data mutations

  //
  // omitting the other mutation methods...
  //

  setUserData (state, payload) {
    console.log('setUserData payload = ', payload)
    state.userData = payload.userData
  },
  setJwtToken (state, payload) {
    console.log('setJwtToken payload = ', payload)
    localStorage.token = payload.jwt.token
    state.jwt = payload.jwt
  }
}

The last thing that I would like to do in the store is to add a getter method that will be called in a couple of other places in the app which will indicate whether the current user is authenticated or not. I accomplish this by calling the isValidJwt(jwt) function from the utils module within the getter like so:

const getters = {
  // reusable data accessors
  isAuthenticated (state) {
    return isValidJwt(state.jwt.token)
  }
}

Ok, I am getting close. I need to add a new Vue.js component for a login / register page in the application. I create a file called Login.vue in the components directory. In the template section I give it two input fields, one for an email, which will serve as the username, and another for the password. Below them are two buttons, one for logging in if you are already a registered user and another for registering.

<!-- components/Login.vue -->
<template>
  <div>
    <section class="hero is-primary">
      <div class="hero-body">
        <div class="container has-text-centered">
          <h2 class="title">Login or Register</h2>
          <p class="subtitle error-msg">{{ errorMsg }}</p>
        </div>
      </div>
    </section>
    <section class="section">
      <div class="container">
        <div class="field">
          <label class="label is-large" for="email">Email:</label>
          <div class="control">
            <input type="email" class="input is-large" id="email" v-model="email">
          </div>
        </div>
        <div class="field">
          <label class="label is-large" for="password">Password:</label>
          <div class="control">
            <input type="password" class="input is-large" id="password" v-model="password">
          </div>
        </div>

        <div class="control">
          <a class="button is-large is-primary" @click="authenticate">Login</a>
          <a class="button is-large is-success" @click="register">Register</a>
        </div>

      </div>
    </section>

  </div>
</template>

Obviously this component will need some local state associated with a user as indicated by my use of v-model in the input fields, so I add that in the component's data property next. I also add an errorMsg data property which will hold any messages emitted by the EventBus in the event of failed registration or authentication. To utilize the EventBus I subscribe to the failedRegistering and failedAuthentication events in the mounted Vue.js component lifecycle stage, and unregister them in the beforeDestroy stage. Another thing to note is the usage of @click event handlers being called upon clicking the Login and Register buttons. Those are to be implemented as component methods, authenticate() and register().

<!-- components/Login.vue -->
<script>
export default {
  data () {
    return {
      email: '',
      password: '',
      errorMsg: ''
    }
  },
  methods: {
    authenticate () {
      this.$store.dispatch('login', { email: this.email, password: this.password })
        .then(() => this.$router.push('/'))
    },
    register () {
      this.$store.dispatch('register', { email: this.email, password: this.password })
        .then(() => this.$router.push('/'))
    }
  },
  mounted () {
    EventBus.$on('failedRegistering', (msg) => {
      this.errorMsg = msg
    })
    EventBus.$on('failedAuthentication', (msg) => {
      this.errorMsg = msg
    })
  },
  beforeDestroy () {
    EventBus.$off('failedRegistering')
    EventBus.$off('failedAuthentication')
  }
}
</script>

Ok, now I just need to let the rest of the application know that the Login component exists. I do this by importing it in the router module and defining its route. While I'm in the router module I need to make an additional change to the NewSurvey component's route to guard its access to only authenticated users as show below:

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Survey from '@/components/Survey'
import NewSurvey from '@/components/NewSurvey'
import Login from '@/components/Login'
import store from '@/store'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    }, {
      path: '/surveys/:id',
      name: 'Survey',
      component: Survey
    }, {
      path: '/surveys',
      name: 'NewSurvey',
      component: NewSurvey,
      beforeEnter (to, from, next) {
        if (!store.getters.isAuthenticated) {
          next('/login')
        } else {
          next()
        }
      }
    }, {
      path: '/login',
      name: 'Login',
      component: Login
    }
  ]
})

It's worth mentioning here that I am utilizing vue-router's route guard beforeEnter to check to see if the current user is authenticated via the isAuthenticated getter from the store. If isAuthenticated returns false then I redirect the application to the login page.

With the Login component coded up and its route defined I can provide access to it via a router-link component in the Header component within components/Header.vue. I conditionally show either the link to the NewSurvey component or the Login component by utilizing the isAuthenticated store getter once more within a computed property in the Header component referenced by v-if directives like so:

<!-- components/Header.vue -->
<template>
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
  <div class="navbar-menu">
    <div class="navbar-start">
      <router-link to="/" class="navbar-item">
        Home
      </router-link>
      <router-link v-if="isAuthenticated" to="/surveys" class="navbar-item">
        Create Survey
      </router-link>
      <router-link v-if="!isAuthenticated" to="/login" class="navbar-item">
        Login / Register
      </router-link>
    </div>
  </div>
</nav>
</template>

<script>
export default {
  computed: {
    isAuthenticated () {
      return this.$store.getters.isAuthenticated
    }
  }
}
</script>

<style>

</style>

Excellent! Now I can finally fire up the dev servers for the Flask app and the Vue.js app and test to see if I can register and login a user.

I start the Flask dev server first.

(venv) $ python appserver.py

Then the webpack dev server to compile and serve the Vue.js app.

$ npm run dev

In my browser I visit http://localhost:8080 (or whatever port the webpack dev server indicates) and make sure the navbar now displays "Login / Register" in the place of "Create Survey" like shown below:

Next I click on the "Login / Register" link and fill out the inputs for an email and password then click register to make sure it functions as expected and I get redirected back to the home page and see the "Create Survey" link displayed instead of the "Login / Register" one that was there before registering.

Alright, my work is largely done. The only thing left to do is to add a little error handling to the submitSurvey(...) Vue.js method of the NewSurvey component to handle the event where a token happens to expire while the user is creating a new survey like so:

<script>
import NewQuestion from '@/components/NewQuestion'

export default {
  components: { NewQuestion },
  data () {
    return {
      step: 'name',
      name: '',
      questions: []
    }
  },
  methods: {

    //
    // omitting other methods
    //

    submitSurvey () {
      this.$store.dispatch('submitNewSurvey', {
        name: this.name,
        questions: this.questions
      })
        .then(() => this.$router.push('/'))
        .catch((error) => {
          console.log('Error creating survey', error)
          this.$router.push('/')
        })
    }
  }
}
</script>

Resources

Want to learn more about the various frameworks used in this article? Try checking out some of the following resources for a deeper dive in to using Vue.js or building back-end APIs in Python:

Conclusion

In this post I demonstrated how to implement JWT authentication in the survey application using Vue.js and Flask. JWT is a popular and robust method for providing authentication within SPA applications, and I hope after reading this post you feel comfortable using these technologies to secure your applications. However, I do recommend visiting Scott's StackAbuse article for a deeper understanding of the how and why of JWT's work.

As always, thanks for reading and don't be shy about commenting or critiquing below.

Last Updated: July 20th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Adam McQuistanAuthor

I am both passionate and inquisitive about all things software. My background is mostly in Python, Java, and JavaScript in the areas of science but, have also worked on large ecommerce and ERP apps.

Course

Data Visualization in Python with Matplotlib and Pandas

# python# pandas# matplotlib

Data Visualization in Python with Matplotlib and Pandas is a course designed to take absolute beginners to Pandas and Matplotlib, with basic Python knowledge, and...

David Landup
David Landup
Details

Getting Started with AWS in Node.js

Build the foundation you'll need to provision, deploy, and run Node.js applications in the AWS cloud. Learn Lambda, EC2, S3, SQS, and more!

Ā© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms