Building a RESTful Blog APIs using python and flask - Part 1
To be a programmer is to develop a carefully managed relationship with error. There's no getting around it. You either make your accommodations with failure, or the work will become intolerable - Ellen Ullman
In this series, I'm going to take you through a very easy to learn path in creating RESTFUL API using Python and Flask micro-framework.
Let's get started with some background context
WHAT IS RESTFUL API?
A RESTful API(Application Programming Interface) is an approach based on REST(REpresentational Stateless Transfer) technology. REST is an architectural style and approach to communications in web services development that defines a set of constraint and properties based on HTTP protocols which include GET, POST, PUT and DELETE. I'm not going to dive deep into the details about REST in this article, but you can read more about REST here
WHAT IS PYTHON?
Python is a programming language that lets you work more quickly and integrate your systems more effectively.
WHAT IS FLASK?
Flask is a micro web framework written in Python. It is called a micro-framework because it does not require any specific libraries or tools.
What we'll build?
First, solve the problem. Then write the code - JOHN JONHSON
In this series, we're going to develop a blog RESTFul API service, which will allow all the four basic CRUD(Create, Read, Update and Delete) operations.
The service will have the following endpoints(An endpoint is a URL pattern used to communicate with an API);
- POST /api/v1/users - create a user(CREATE)
- GET /api/v1/users - get all registered users(READ)
- GET /api/v1/users/<user_id> - get a user(READ)
- GET /api/v1/users/me - get my info(READ)
- PUT /api/v1/users/me - update my account(UPDATE)
- DELETE /api/v1/users/me - Delete my account(DELETE)
- POST /api/v1/blogs - Create a blog post(CREATE)
- GET /api/v1/blogs - Get all blog post(READ)
- GET /api/v1/blogs/<blog_id> - Get a single blog post(READ)
- PUT /api/v1/blogs/<blog_id> - Update a blog post(UPDATE)
- DELETE /api/v1/blogs/<blog_id> - Delete a blog post(DELETE)
Pre-requisites
Make sure you have the following installed on your system
- Python3.x - We'll use python 3.XX for the blog service project
- PostgreSQL - We'll use postgreSQL relational database for this project
- Pipenv - we'll use pipenv to create and manage our project virtual environment and to also install and uninstall packages
Run the following commands to check if you have the above installed on your system
$ python --version
$ which psql
$ pipenv --version
You should see something similar to this
Setting up project virtual environment using pipenv
Install pipenv if you don't already have it installed on your system using pip or if you're using a mac system using brew
$ pip install pipenv
OR
$ brew install pipenv
let's create our project's directory and name it blog_api
open your terminal and run the following command to create blog_api directory
$ mkdir blog_api
change your working directory to the directory you just created above and run pipenv command to setup project virtual environment
$ cd blog_api
$ pipenv --three
running pipenv --three
will create a virtual environment if not already created using python3 - Read pipenv documentation here to learn more about how it works
NOTE: we will be using python 3.XX in this project
So you may ask, why do we need a virtual environment in Python? using a virtual environment for python projects allows us to have an isolated working copy of python which gives us the opportunity to work on a specific project without worry of affecting other projects.
Activate the project virtual environment with the following command
$ pipenv shell
Installing Project Dependencies
The following are the required package dependencies we'll use to develop our blog API service
- flask - Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions
- flask sqlalchemy - flask wrapper for Sqlalchemy, it adds Sqlalchemy support to flask apps
- psycopg2 - python postgresql adapter that provides some commands for simple CRUD queries and more on postgresql db
- flask-migrate - flask extension that handles SQLAlchemy database migration. Migration basically refers to the management of incremental, reversible changes to relational database schemas
- flask-script - provides an extension for managing external scripts in flask. This is needed in running our db migrations from the command line
- marshmallow - marshmallow is a library for converting complex datatypes to and from native Python datatypes. Simply put it is used for deserialization(converting data to application object) and serialization(converting application object to simple types).
- flask-bcrypt - we'll use this to hash our password before saving it in the db - of course, you don't want to save user's password directly into your db without hashing it
- pyjwt - python library to encode and decode JSON Web Tokens - We will use this token in verifying user's authenticity
Run the following command to install all the dependencies;
$ pipenv install flask flask-sqlalchemy psycopg2 flask-migrate flask-script marshmallow flask-bcrypt pyjwt
currently, your project directory should look like this
blog_api/
|-Pipfile
|-Pipfile.lock
Pipfile contains information about your app including all the app prod and dev dependencies
Your Pipfile
should contain the following;
File structures
Create the following file structure for your project
NOTE: Do not change Pipfile and Pipfile.lock, leave it as it is
blog_api/
|-src
|- __init__.py
|- shared
|- __init__.py
|- Authentication.py
|- models
|- __init__.py
|- UserModel.py
|- BlogpostModel.py
|- views
|- __init__.py
|- UserView.py
|- BlogpostView.py
|- app.py
|- config.py
|- manage.py
|- run.py
|- Pipfile
|- Pipfile.lock
Create app db
Let's create our db and name it blog_api_db
You can use createdb
command
$ createdb blog_api_db
OR create your db using any PostgreSQL client application e.g Postico, pgadmin etc.
Flask Environment Configuration
Now, let's set up the configurations that would be required by our application.
Add the following inside /src/config.py
What we did?
We created two classes Development
- which contains development environment configs and Production
- which contains production environment configs. For this project, I don't want to go too complex with these configurations, so I only specified DEBUG, TESTING, JWT_SECRET_KEY, and SQLALCHEMY_DATABASE_URI options, you can read more about handling flask configurations and others here.
For our blog service, there're some values we don't want to exposed to the outside world, one of it is JWT_SECRET_KEY
which we will use later to sign user's token. This should be a secret key and as such should only be known to us - for this reason and more those secret variables need to be set in our system environment. You'll notice we did JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY')
in our config file, we imported os
and use it to get JWT_SECRET_KEY
value from the system environment.
Run the following from your terminal to set system environment variables
$ export FLASK_ENV=development
$ export DATABASE_URL= postgres://name:password@houst:port/blog_api_db
$ export JWT_SECRET_KEY = hhgaghhgsdhdhdd
Note: change the DATABASE_URL
value to your database url, also change JWT_SECRET_KEY
value to whatever you want to use as your secret key, you'll need this later to sign in user's token.
Create App
let's create our app with a sample api endpoint, add the following in src/app.py
file
What we did?
create_app
function takes in env_name
(env_name is the name of the environment we want to run our flask app on, it can be testing, development or production), this is needed to load our environment configuration. Don't forget we had already set up our environment configuration object in config.pysrc/config.py
file. create_app
returns a new flask app object. We also setup a simple endpoint /
with HTTP GET
method, request to this endpoint will return Congratulations! Your first endpoint is working
.
Now that we have created our app, let create an entry point for our app
add the following code to /run.py
file in the project root directory. Create run.py file in the project root directory if you don't have it already.
What we did?
here we imported create_app()
function from src/app and call the function by passing our environment name(in this case "development"). Don't forget that create_app()
functions return a flask object, in this case we name the object app. Before we called create_app()
function, we added a condition that checks if the python file name is the same as the file main name, if they're the same we run our app by calling flask run()
method and if not we do nothing.
Now let's run our app
We can achieve this by running the below command from the terminal
$ python run.py
You should get something similar to below if all went well
* Serving Flask app "src.app" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 300-081-843
Note that the app is running on port 5000, that is the default port number from flask. You can change the default port number to whatever you want by passing port
parameter to the run()
method - in our run.py code just change app.run()
to app.run(port=<new_port_number_here>)
.
Now let see if our sample endpoint is working
run http://127.0.0.1:5000/
on your browser
If all went well you should see "Congratulations! Your first endpoint is working" from the browser
Database Models
A model determines the logical structures of a database. Simply put, it determines how tables would look like on the database. Models define how records can be manipulated or retrieved in the database.
We'll create two models for our project
- User Model and
- Blogpost Model
Our models class will inherit Model
object from flask-sqlalchemy
, Flask Sqlalchemy is a flask wrapper for Sqlalchemy
. Sqlalchemy is an ORM(Object Relational Mapper). An ORM is a database sql abstraction that makes it easy to carry out sql operations on relational database. With ORM, instead of writing raw sql queries(e.g to retrieve all rows from our User's table) we could do something like - model.query.all()
.
Add the following to src/models/__init__.py
#src/models/__init__.py
from flask_sqlalchemy import SQLAlchemy
# initialize our db
db = SQLAlchemy()
What we did?
Here we imported SQLAlchemy from flask_sqlalchemy and initialize our db
Next, let create User and Blogpost Model
User Model will have the following fields
* id
* name
* email
* password
* created_at
* modified_at
While Blogpost Model will have the following fields
* id
* title
* contents
* owner_id
* created_at
* modified_at
Add the following code to src/models/UserModel
What we did?
- We imported db instance from
src/models/__init__.py
- Our UserModel class inherited from
db.Model
- we named our table
users
-__tablename__ = 'users'
- We defined user's table columns and assigned rules to each column, we assigned primary_key rule to
id
, unique rule toemail
field, and set not null rule toemail, password and name
, those fields are compulsory. save()
method will be use to save users to our dbupdate()
method will be use to update our user's record on the dbdelete()
method will be use to delete record from the db- we added three additional static methods
get_all_users()
,get_one_user()
andget_user_by_email(v)
- as their names depicts to get all user from the db and to get a single user from the db using primary_key field and email field __repl__()
will return a printable representation of UserModel object, in this case we're only returning the id__init__()
is the class constructor and we used it set the class attributes
One more thing to add to our user's model, we need to hash user's password before saving it into the db. Why do we need to hash user's password? Because we don't want to save raw user's password to the db for security reason. This is where we will use flask-bcrypt
module we installed in Installing Project Dependencies
Add the following to /src/models/__init__.py
from flask_bcrypt import Bcrypt
#######
# existing code remains #
#######
bcrypt = Bcrypt()
What we did?
Here we imported Bcrypt
from flask_bcrypt
and intialize it in this src/models/__init__.py
file.
Next, we need to make some changes to password field in our UserModel
Add the following to src/models/UserModels.py
What we did?
We made changes to UserModel to allow hashing of user's password,
- We added two new methods to UserModel
__generate_hash()
andcheck_hash()
, we'll use__generate_hash()
to hash user's password before saving it into the db whilecheck_hash()
will be use later in our code to validate user's password during login.
Noticed we also setself.password = self.__generate_hash(data.get('password'))
in our class constructor, this will make sure we generate hash for passwords before saving them into db. We also added a condition to ourupdate()
method that checks a user wants to update their password and hash it if yes.
Now, let's create our blogpost model
copy the following to src/models/BlogpostModel.py
What we did?
- We imported our db instance from
src/models/__init__.py
- our BlogpostModel class inherited from
db.Model
- we named our table
blogposts
-__tablename__ = 'blogposts'
- We defined our blogpost's table columns and assigned rules to each column, we assigned primary_key rule to
id
, and set not null rule totitle, and contents
, of course we don't want those fields to be empty. save()
method will be used to save blogpost to our dbupdate()
method will be used to update our blogpost's record on the dbdelete()
method will be used to delete a record from blogpost's table on the db- we added two static method
get_all_blogposts()
andget_one_blogpost()
, as their names depicts to get all blogposts and to get a single blogpost from the db __repl__()
will return a printable representation of BlogpostModel object, in this case, we're only returning the id__init__()
is the class constructor and we use it set the class attributes
Now, we need to define the relationship between our two tables users
and blogposts
table. A user can have many blogpost, so the relationship between users table and blogposts table will be one-to-many
.
We also need to define our model's schema, remembered we installed marshmallow
from Installing Project Dependencies section, here is where we'll use it.
let's update our UserModel and BlogpostModel, add the following
UserModel
BlogpostModel
What we did?
- We added
owner_id
field toBlogpostModel
and we linked it to our users' table with a ForeignKey.blogposts
field was added toUserModel
with some rules that will allow us to get every blogposts owns by a user when querying UserModel.
Next,we need to wrap db
and bcrypt
with app
Update /src/app.py
with the following code
We import db
and bcrypt
from src/model
and initialized it
Migrations
Database migration refers to the management of incremental, reversible changes to relational database schemas. Simply put, migrations is a way of updating our db with the changes we made to our models. Since we had already set up our models, now we need to migrate our models changes to the db.
Add the following code to /manage.py
What we did?
We set up a migration script that will help in the maintenance of our migrations.
The Manager class keeps track of all the commands and handles how they are called from the command line. The MigrateCommand contains a set of migration commands such as init, migrate, upgrade
etc. we will use these commands now.
Next, let's run migrations initialization with db init
command
$ python manage.py db init
You should see the following after running the above script
Creating directory /<path>/blog_api/migrations ... done
Creating directory /<path>/blog_api/migrations/versions ... done
Generating /<path>/blog_api/migrations/script.py.mako ... done
Generating /<path>/blog_api/migrations/env.py ... done
Generating /<path>/python/blog_api/migrations/README ... done
Generating /<path>/blog_api/migrations/alembic.ini ... done
Please edit configuration/connection/logging settings in
'/<path>/blog_api/migrations/alembic.ini' before proceeding.
This will create a new migration sub-folder in the project directory
Now, run this script which will populate our migrations files and generate users and blogposts tables with our model changes
$ python manage.py db migrate
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added table 'blogposts'
Generating /Users/olawalequest/my_projects/python/blog_api/migrations/versions/e23b7256bd81_.py ... done
Next, apply the changes to the db by running the following command
$ python manage.py db upgrade
To confirm if the tables were created in the db, you can use psql
command, check this out
User API
Our User API will have all the four CRUD operation,
We'll start by creating create user
endpoint. When a user creates an account, we want to return back to the user a jwt token that will be used for any authentication request.
Let's jump back to our code
What we did?
- We imported
request
- this contains all the request info from the user including headers, body and other information,json
- to serialize JSON output,Response
- we need this to build our response object andBlueprint
- we need this to group our user resources together fromflask
. - We created a blueprint app that we'll use for all user resources and we name it
user_api
- We created
create()
withuser_api.route()
operator, with this operator we defined ourcreate
endpoint as/
andPOST
request type.create()
will only accept aPOST
request and returned back JWT token if the request was successful. We use thisUserModel.get_user_by_email(data.get('email'))
to check if the email that the user supplied already exist in our db, if the query returns an empty object, that means that email does not exist and will go ahead and save the user in the db. if the user does exist, we returned back an error message telling the user to use another email. Remembered we defined ouremail
field to be unique inUserModel
request.get_json()
to get the JSON object from the body of the request, we use thisuser_schema.load(req_data)
to vaidate and deserialize input json data from the user, remembered we definedUserSchema
class in our UserModel model.- We also imported
Auth
from.shared.Authentication
, we useAuth
to generate user's token and it will be use later to decode user's token.
Next, let define Auth
class.
What we did?
- we imported
jwt
- We set up
generate_token
static method that takes inuser_id
and use that in our payload to generates jwt token. we also set the tokenexp
date to be 1 day after it's creation. weJWT_SECRET_KEY
already set in our system environment sign the token withHS256
algorithm. - we use
decode_token()
static method to decode supplied user's token using the sameJWT_SECRET_KEY
we used in signing the token. The method checks and validates the token, we'll need this later in our project.
Next, let's set up another route to get all user's in the db. We want to do this in such a way that only registered user can get all users, meaning a user without an auth token can not access this route. A user can only get a token when they create an account(this will only happen once) and subsequent login to the service. To achieve this we need to set up a decorator
, let's call it auth_required
, we also need to set up user login route.
- auth_requred decorator
let's go back and edit ourAuthentication
class to includeauth_decorator
What we did?
-
We added a new static method
auth_required
toAuthentication
class and we wrapped it usingwraps
imported from pythonfunctools
. we set up a condition that checks ifapi-token
exist in the request header, a user will need to attached the token gotten from creating an account or the one gotten from logging in. If the token exist from the request header, we passed the token to decode_token method to validate the authenticity of the token, if the token is valid, we get the payload data which is the user_id and save it tog
,g
is a global variable in flask that is valid till we finished processing a request. We will use that later to get current user's information. We returned back to the user an error message if the token is not valid. -
login endpoint
We need to set up this here so that a user can get a token
What we did?
- We added a new method
login
and assigned/login
route with request typePOST
to user view.request.get_json()
was used to get the request body data and we passed it to ouruser_schema.load(req_data, partial=True)
to deserialized and validate the content of the data, we passed inpartial=True
to our schema since login does not requirename
field which is required if a user wants to create an account. Check here to read more about how mashmallow schema works. - We use
UserModel.get_user_by_email(data.get('email'))
to filter the user's table using the user email address and return an error message if the user does not exist. If the user does exist in the db, we added a condition to validate the supplied password with the user's saved hash pasword usinguser.check_hash(data.get('password'))
, remembered we addedcheck_hash()
method to our UserModel when we created it. If the password matches, then we generate and return a token to the user. The token will be used in any subsequent request.
Now, let jump back to creating the endpoint that will get all user's data on the db and only a user with a valid token will be able to access this route.
What we did?
- set up a new method
get_all
with endpoint/
and request methodGET
, we useUserModel.get_all_users()
to query user's table on the db and retured all user's data. We also attached@Auth.auth_required
decorator to validate the authenticity of the user making the request.
Finally, to complete our user APIs, let add get - GET
, update - PUT
and delete - DELETE
a single user endpoints
Note: A user can only update and delete their own account.
What we did?
- we added four more endpoints to get, update and delete the current user, and the fourth one to get a user information through their id.
Finally, let's register user_api
blueprint in app
, as it is currently, app
does not know that user_api
blueprint exist.
To register it, add the following to app.py
What we did?
We imported user_api
as user_blueprint
from .views.UserView
and registered the blueprint using app.register_blueprint(user_blueprint, url_prefix='/api/v1/users')
with prefix /api/v1/users
and the blueprint name.
RUN
let's run our code before we complete this part.
TO test the endpoint you can use POSTMAN or any other app
-
Create User POST
api/v1/users
-
Login POST
api/v1/users/login
-
Get Me GET
api/v1/users/me
Copy thejwt-token
returned from the login call and put it in this request headerapi-token
as key
-
Edit Me PUT
api/v1/users/me
using the same token as above
- Get all users GET
api/v1/users
using the same token
And finally, delete DELETE api/v1/users/me
CONCLUSION
We've covered quite a lot on how to create a simple RESTful API that has the four basic CRUD operation with Authentication using Flask. We learned about configuring our Flask environment, creating models, making and applying migrations to the DB, grouping resources using flask blueprint, validating the authenticity of a user using JWT token and we also complete setting up all our user's endpoints
We were able to set up the following endpoint;
- Create User - POST
api/v1/users
- Login User - POST
api/v1/users/login
- Get A User Info - GET
api/v1/users/<int:user_id>
- Get All users - GET
api/v1/users
- Get My Info - GET
api/v1/users/me
- Edit My Info - PUT
api/v1/users/me
- DELETE My Account - DELETE
api/v1/users/me
In Part 2 of this Series, we'll complete setting up blogpost endpoints. Click here to checkout the project on Github.
Feel free to drop your questions if any, or drop a comment, I'll be happy to respond to them.
you say :
we added three additional static methods get_all_users(), get_one_user() and get_user_by_email(v) - as their names depicts to get all user from the db and to get a single user from the db using primary_key field and email field
but your code does not ever show the definition for get_user_by_email.
These lines work, but it also looks like a typo:
ser_data = user_schema.dump(user).data
token = Auth.generate_token(ser_data.get(‘id’))
probably wanted ‘user_data’?
Is the line:
user_api = Blueprint(‘users’, name)
supposed to change to
user_api = Blueprint(‘user_api’, name)
in userView? Seems like a typo.
This line is also possibly a typo:
return ‘Congratulations! Your first endpoint is workin’
in app.py