One place for hosting & domains

      RESTful

      Build a RESTful API with Flask – The TDD Way: Part 2

      Introduction

      In Part 1 of this series, we learned how to create a RESTful API the TDD way. We covered writing tests and learned a lot about Flask. If you haven’t read Part 1, please do because this tutorial will build upon it.

      In this part of the series, we’ll learn how to authenticate and authorize users in our API.

      In this tutorial, we’ll talk about securing our API with token-based authentication and user authorization. We will integrate users into the API we built in Part 1.

      In order to get started, ensure your virtual environment is activated.

      We intend to allow bucketlists to be owned by users. For now, anyone can manipulate a bucketlist even if they did not create it. We’ve got to fix this security hole.

      How do we keep track of users, you ask? We define a model.

      
      
      from app import db
      from flask_bcrypt import Bcrypt
      
      class User(db.Model):
          """This class defines the users table """
      
          __tablename__ = 'users'
      
          
          id = db.Column(db.Integer, primary_key=True)
          email = db.Column(db.String(256), nullable=False, unique=True)
          password = db.Column(db.String(256), nullable=False)
          bucketlists = db.relationship(
              'Bucketlist', order_by='Bucketlist.id', cascade="all, delete-orphan")
      
          def __init__(self, email, password):
              """Initialize the user with an email and a password."""
              self.email = email
              self.password = Bcrypt().generate_password_hash(password).decode()
      
          def password_is_valid(self, password):
              """
              Checks the password against it's hash to validates the user's password
              """
              return Bcrypt().check_password_hash(self.password, password)
      
          def save(self):
              """Save a user to the database.
              This includes creating a new user and editing one.
              """
              db.session.add(self)
              db.session.commit()
      
      class Bucketlist(db.Model):
          """This class defines the bucketlist table."""
      
          __tablename__ = 'bucketlists'
      
          
          id = db.Column(db.Integer, primary_key=True)
          name = db.Column(db.String(255))
          date_created = db.Column(db.DateTime, default=db.func.current_timestamp())
          date_modified = db.Column(
              db.DateTime, default=db.func.current_timestamp(),
              onupdate=db.func.current_timestamp())
          created_by = db.Column(db.Integer, db.ForeignKey(User.id))
      
          def __init__(self, name, created_by):
              """Initialize the bucketlist with a name and its creator."""
              self.name = name
              self.created_by = created_by
      
          def save(self):
              """Save a bucketlist.
              This applies for both creating a new bucketlist
              and updating an existing onupdate
              """
              db.session.add(self)
              db.session.commit()
      
          @staticmethod
          def get_all(user_id):
              """This method gets all the bucketlists for a given user."""
              return Bucketlist.query.filter_by(created_by=user_id)
      
          def delete(self):
              """Deletes a given bucketlist."""
              db.session.delete(self)
              db.session.commit()
      
          def __repr__(self):
              """Return a representation of a bucketlist instance."""
              return "<Bucketlist: {}>".format(self.name)
      

      Here’s what we’ve done:

      • We imported Flask-Bcrypt extension to help us in hashing our passwords. You should never store passwords in plaintext.
      • We created a User model that represents the users table. It contains the email and password fields to capture the user’s credentials.
      • Since a user can own many bucketlists, we defined a One-to-Many relationship between the two tables. We defined this relationship by adding the db.relationship() function on the User table (parent table)
      • We added a foreign key on the child table (Bucketlist) referencing the User table. The foreign key has some arguments. The cascade="all, delete-orphan" will delete all bucketlists when a referenced user is deleted.
      • We hash the password by using generate_password_hash(password). This will make our users’ passwords be secure from dictionary and brute force attacks.
      • We refactored the get_all() method to get all the bucketlists for a given user.

      Don’t forget to install Flask-Bcrypt

      1. pip install flask-bcrypt

      Migrate them

      Migrate the changes we’ve just made to the database we initially created in Part 1 of the series.

      1. python manage.py db migrate
      2. python manage.py db upgrade

      Now we have a user table to keep track of registered users.

      Our app will have many tests from now on. It’s best practice to have a test folder that will house all our tests. We’ll create a folder called tests. Inside this folder, we’ll move our test_bucketlists.py file into it.

      Our directory structure should now look like this:

      1. ├── bucketlist
      2. ├── app
      3. │ ├── __init__.py
      4. │ └── models.py
      5. ├── instance
      6. │ ├── __init__.py
      7. │ └── config.py
      8. ├── manage.py
      9. ├── requirements.txt
      10. ├── run.py
      11. ├── tests
      12. │ └── test_bucketlist.py

      Also, we’ll edit the manage.py as follows:

      import os
      import unittest
      
      from flask_script import Manager
      from flask_migrate import Migrate, MigrateCommand
      from app import db, create_app
      
      
      app = create_app(config_name=os.getenv('APP_SETTINGS'))
      migrate = Migrate(app, db)
      
      manager = Manager(app)
      
      
      
      manager.add_command('db', MigrateCommand)
      
      
      
      @manager.command
      def test():
          """Runs the unit tests without test coverage."""
          tests = unittest.TestLoader().discover('./tests', pattern='test*.py')
          result = unittest.TextTestRunner(verbosity=2).run(tests)
          if result.wasSuccessful():
              return 0
          return 1
      
      
      if __name__ == '__main__':
          manager.run()
      

      The decorator on top of test() allows us to define a command called test. Inside the function, we load the tests from the tests folder using the TestLoader() class and then run them with TextTestRunner.run(). If it’s successful, we exit gracefully with a return 0.

      Let’s test it out on our terminal.

      1. python manage.py test

      The tests should fail. This is because we’ve not modified our code to work with the new changes in the model.

      From now on, we’ll use this command to run our tests.

      Token-based authentication is a security technique that authenticates users who attempt to login to a server using a security token provided by the server. Without the token, a user won’t be granted access to restricted resources. You can find more intricate details about token-based authentication here

      For us to implement this authentication, we’ll use a Python package called PyJWT. PyJWT allows us to encode and decode JSON Web Tokens (JWT).

      That being said, let’s install it:

      1. pip install PyJWT

      For our users to authenticate, the access token is going to be placed in the Authorization HTTP header in all our bucketlist requests.

      Here’s how the header looks like:

      Authorization:  "Bearer <The-access-token-is-here>"
      

      We’ll put the word Bearer before the token and separate them with a space character.

      Don’t forget the space in between the Bearer and the token.

      Encode and Decode the Token

      We need to create a way to encode the token before it’s sent to the user. We also need to have a way to decode the token when the user sends it via the Authorization header.

      In our model.py we’ll create a function inside our User model to generate the token and another one to decode it. Let’s add the following code:

      
      
      
      import jwt
      from datetime import datetime, timedelta
      
      class User(db.Model):
          """Maps to users table """
      
          __tablename__ = 'users'
      
          
          
          
      
          def __init__(self, email, password):
              
              
      
          def password_is_valid(self, password):
              
              
      
          def save(self):
              
              
      
          def generate_token(self, user_id):
              """ Generates the access token"""
      
              try:
                  
                  payload = {
                      'exp': datetime.utcnow() + timedelta(minutes=5),
                      'iat': datetime.utcnow(),
                      'sub': user_id
                  }
                  
                  jwt_string = jwt.encode(
                      payload,
                      current_app.config.get('SECRET'),
                      algorithm='HS256'
                  )
                  return jwt_string
      
              except Exception as e:
                  
                  return str(e)
      
          @staticmethod
          def decode_token(token):
              """Decodes the access token from the Authorization header."""
              try:
                  
                  payload = jwt.decode(token, current_app.config.get('SECRET'))
                  return payload['sub']
              except jwt.ExpiredSignatureError:
                  
                  return "Expired token. Please login to get a new token"
              except jwt.InvalidTokenError:
                  
                  return "Invalid token. Please register or login"
      

      The generate_token() takes in a user ID as an argument, uses jwt to create a token using the secret key, and makes it time-based by defining its expiration time. The token is valid for 5 minutes as specified in the timedelta. You can set it to your liking.

      The decode_token() takes in a token as an argument and checks whether the token is valid. If it is, it returns the user ID as the payload. It returns an error message if the token is expired or invalid.

      Don’t forget to import jwt and the datetime above.

      Our app is growing bigger. We’ll have to organize it into components. Flask uses the concept of Blueprints to make application components.

      Blueprints are simply a set of operations that can be registered on a given app. Think of it as an extension of the app that can address a specific functionality.

      We’ll create an authentication blueprint.
      This blueprint will focus on handling user registration and logins.

      Inside our /app directory create a folder and call it auth.

      Our auth folder should contain:

      • __init__.py file
      • views.py file

      In our auth/__init__.py file, initialize a blueprint.

      
      
      from flask import Blueprint
      
      
      auth_blueprint = Blueprint('auth', __name__)
      
      from . import views
      

      Then import the blueprint and register it at the bottom of the app/__init__.py, just before the return app line.

      
      
      
      
      def create_app(config_name):
        
        
        
      
        @app.route('/bucketlists/<int:id>', methods=['GET', 'PUT', 'DELETE'])
        def bucketlist_manipulation(id, **kwargs):
               
          
          
          ...
      
        
        from .auth import auth_blueprint
        app.register_blueprint(auth_blueprint)
      
        return app
      

      Testing should never be an afterthought. It should always come first.

      We’re going to add a new test file that will house all our tests for the authentication blueprint.
      It’ll test whether our API can handle user registration, user log in, and access-token generation.

      In our tests directory, create a file naming it test_auth.py. Write the following code in it:

      
      
      import unittest
      import json
      from app import create_app, db
      
      class AuthTestCase(unittest.TestCase):
          """Test case for the authentication blueprint."""
      
          def setUp(self):
              """Set up test variables."""
              self.app = create_app(config_name="testing")
              
              self.client = self.app.test_client
              
              self.user_data = {
                  'email': '[email protected]',
                  'password': 'test_password'
              }
      
              with self.app.app_context():
                  
                  db.session.close()
                  db.drop_all()
                  db.create_all()
      
          def test_registration(self):
              """Test user registration works correcty."""
              res = self.client().post('/auth/register', data=self.user_data)
              
              result = json.loads(res.data.decode())
              
              self.assertEqual(result['message'], "You registered successfully.")
              self.assertEqual(res.status_code, 201)
      
          def test_already_registered_user(self):
              """Test that a user cannot be registered twice."""
              res = self.client().post('/auth/register', data=self.user_data)
              self.assertEqual(res.status_code, 201)
              second_res = self.client().post('/auth/register', data=self.user_data)
              self.assertEqual(second_res.status_code, 202)
              
              result = json.loads(second_res.data.decode())
              self.assertEqual(
                  result['message'], "User already exists. Please login.")
      

      We’ve initialized our test with a test client for making requests to our API and some test data.
      The first test function test_registration() sends a post request to /auth/register and tests the response it gets. It ensures that the status code is 201, meaning we’ve successfully created a user.
      The second test function tests whether the API can only register a user once. Having duplicates in the database is bad for business.

      Now let’s run the tests using python manage.py test. The tests should fail.

      1. ----------------------------------------------------------------------
      2. raise JSONDecodeError("Expecting value", s, err.value) from None
      3. json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

      The reason our tests fail is simply because we lack the functionality they need to test. Let’s implement something that’ll make these two tests pass.

      Open up the views.py file and add the following code:

      
      
      from . import auth_blueprint
      
      from flask.views import MethodView
      from flask import make_response, request, jsonify
      from app.models import User
      
      class RegistrationView(MethodView):
          """This class registers a new user."""
      
          def post(self):
              """Handle POST request for this view. Url ---> /auth/register"""
      
              
              user = User.query.filter_by(email=request.data['email']).first()
      
              if not user:
                  
                  try:
                      post_data = request.data
                      
                      email = post_data['email']
                      password = post_data['password']
                      user = User(email=email, password=password)
                      user.save()
      
                      response = {
                          'message': 'You registered successfully. Please log in.'
                      }
                      
                      return make_response(jsonify(response)), 201
                  except Exception as e:
                      
                      response = {
                          'message': str(e)
                      }
                      return make_response(jsonify(response)), 401
              else:
                  
                  
                  response = {
                      'message': 'User already exists. Please login.'
                  }
      
                  return make_response(jsonify(response)), 202
      
      registration_view = RegistrationView.as_view('register_view')
      
      
      auth_blueprint.add_url_rule(
          '/auth/register',
          view_func=registration_view,
          methods=['POST'])
      

      Here’s what we have added:

      • We imported our blueprint together with Flask’s make_response (for returning our response) and jsonify (for encoding our data in JSON and adding an application/json header to the response)
      • We’ve defined a class-based view to handle registration by dispatching a POST request to our post() method.
      • Inside our post() method, we check if the user exists in our database. If they don’t, we create a new user and return a message to them notifying their successful registration.
        If the user exists they are reminded to log in.
      • Lastly, we used as_view() method to make our class-based view callable so that it can take a request and return a response. We then defined the URL for registering a user as /auth/register.

      Let’s run our tests once more. Only the AuthTestCase tests should pass. The bucketlist tests still fail because we haven’t modified the __init__.py code.

      1. test_already_registered_user (test_auth.AuthTestCase)
      2. Test that a user cannot be registered twice. ... ok
      3. test_registration (test_auth.AuthTestCase)
      4. Test user registration works correcty. ... ok
      5. Bucketlist failed tests fall here
      6. ----------------------------------------------------------------------

      Using Postman for auth/register

      We’ll test our registration functionality by making a request using Postman.

      But before we make the requests, ensure the API is up and running.

      1. python run.py development
       * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
       * Restarting with stat
       * Debugger is active!
       * Debugger PIN: 225-021-817
      

      Now you can make a POST request to localhost:5000/auth/register. Specify an email and a password of your choice to represent the user we are registering. Click send.

      A user will have to log in to gain access to our API. Currently, we are lacking this login functionality. Let’s start with some tests. We’ll add two more tests at the bottom of our test_auth.py as follows:

      
      class AuthTestCase(unittest.TestCase):
          """Test case for the authentication blueprint."""
      
          def setUp(self):
              
      
          def test_registration(self):
              
      
          def test_already_registered_user(self):
              
      
          def test_user_login(self):
              """Test registered user can login."""
              res = self.client().post('/auth/register', data=self.user_data)
              self.assertEqual(res.status_code, 201)
              login_res = self.client().post('/auth/login', data=self.user_data)
      
              
              result = json.loads(login_res.data.decode())
              
              self.assertEqual(result['message'], "You logged in successfully.")
              
              self.assertEqual(login_res.status_code, 200)
              self.assertTrue(result['access_token'])
      
          def test_non_registered_user_login(self):
              """Test non registered users cannot login."""
              
              not_a_user = {
                  'email': '[email protected]',
                  'password': 'nope'
              }
              
              res = self.client().post('/auth/login', data=not_a_user)
              
              result = json.loads(res.data.decode())
      
              
              
              self.assertEqual(res.status_code, 401)
              self.assertEqual(
                  result['message'], "Invalid email or password, Please try again")
      

      The test_user_login() function tests whether our API can successfully log in as a registered user. It also tests for the access token.

      The other test function test_non_registered_user_login() tests whether our API can restrict signing in to only registered users.

      Again, we’ll make the tests pass by implementing its functionality. Let’s create the login view.

      from . import auth_blueprint
      
      from flask.views import MethodView
      from flask import Blueprint, make_response, request, jsonify
      from app.models import User
      
      class RegistrationView(MethodView):
          """This class-based view registers a new user."""
          
          
      
      class LoginView(MethodView):
          """This class-based view handles user login and access token generation."""
      
          def post(self):
              """Handle POST request for this view. Url ---> /auth/login"""
              try:
                  
                  user = User.query.filter_by(email=request.data['email']).first()
      
                  
                  if user and user.password_is_valid(request.data['password']):
                      
                      access_token = user.generate_token(user.id)
                      if access_token:
                          response = {
                              'message': 'You logged in successfully.',
                              'access_token': access_token.decode()
                          }
                          return make_response(jsonify(response)), 200
                  else:
                      
                      response = {
                          'message': 'Invalid email or password, Please try again'
                      }
                      return make_response(jsonify(response)), 401
      
              except Exception as e:
                  
                  response = {
                      'message': str(e)
                  }
                  
                  return make_response(jsonify(response)), 500
      
      
      registration_view = RegistrationView.as_view('registration_view')
      login_view = LoginView.as_view('login_view')
      
      
      
      auth_blueprint.add_url_rule(
          '/auth/register',
          view_func=registration_view,
          methods=['POST'])
      
      
      
      auth_blueprint.add_url_rule(
          '/auth/login',
          view_func=login_view,
          methods=['POST']
      )
      

      Here, we’ve defined a class-based view just like we did in the registration section.

      It dispatches the POST request to the post() method as well. This is to capture the user credentials (email, password) when they log in. It checks whether the password given is valid, generates an access token for the user, and returns a response containing the token.

      We’ve also handled exceptions gracefully so that if one occurs, our API will continue running and won’t crush.

      Finally, we defined a URL for the login route.

      Logging in on Postman with auth/login

      Make a POST request. Input the email and password we specified for the user during registration. Click send. You should get an access token in the JSON response.

      Running the tests

      If you run the tests, you will notice that the login tests pass, but the bucketlist one still fails. It’s time to refactor these tests.

      First, we’ll create two helper functions for registering and signing in to our test user.

      
      class BucketlistTestCase(unittest.TestCase):
          """This class represents the bucketlist test case"""
      
          def setUp(self):
              """Set up test variables."""
         
         
      
          def register_user(self, email="[email protected]", password="test1234"):
              """This helper method helps register a test user."""
              user_data = {
                  'email': email,
                  'password': password
              }
              return self.client().post('/auth/register', data=user_data)
      
          def login_user(self, email="[email protected]", password="test1234"):
              """This helper method helps log in a test user."""
              user_data = {
                  'email': email,
                  'password': password
              }
              return self.client().post('/auth/login', data=user_data)
      
          
          
      
      
      if __name__ == "__main__":
          unittest.main()
      

      We do this so that when we want to register or log in as a test user (which is in all the tests), we don’t have to repeat ourselves. We’ll simply call the function and we are set.

      Next, we’ll define a way to get the access token and add it to the Authorization header in all our client requests. Here’s a code snippet of how we’re going to do it.

          def test_bucketlist_creation(self):
              """Test the API can create a bucketlist (POST request)"""
              
              self.register_user():
              result = self.login_user()
              
              access_token = json.loads(result.data.decode())['access_token']
      
              
              res = self.client().post(
                  '/bucketlists/',
                  headers=dict(Authorization="Bearer " + access_token),
                  data=self.bucketlist)
      

      We can now go ahead and refactor the whole test_bucketlist.py file. After refactoring all our requests, we should have something like this:

      import unittest
      import os
      import json
      from app import create_app, db
      
      class BucketlistTestCase(unittest.TestCase):
          """This class represents the bucketlist test case"""
      
          def setUp(self):
              """Define test variables and initialize app."""
              self.app = create_app(config_name="testing")
              self.client = self.app.test_client
              self.bucketlist = {'name': 'Go to Borabora for vacay'}
      
              
              with self.app.app_context():
                  
                  db.session.close()
                  db.drop_all()
                  db.create_all()
      
          def register_user(self, email="[email protected]", password="test1234"):
              user_data = {
                  'email': email,
                  'password': password
              }
              return self.client().post('/auth/register', data=user_data)
      
          def login_user(self, email="[email protected]", password="test1234"):
              user_data = {
                  'email': email,
                  'password': password
              }
              return self.client().post('/auth/login', data=user_data)
      
          def test_bucketlist_creation(self):
              """Test API can create a bucketlist (POST request)"""
              self.register_user()
              result = self.login_user()
              access_token = json.loads(result.data.decode())['access_token']
      
              
              res = self.client().post(
                  '/bucketlists/',
                  headers=dict(Authorization="Bearer " + access_token),
                  data=self.bucketlist)
              self.assertEqual(res.status_code, 201)
              self.assertIn('Go to Borabora', str(res.data))
      
          def test_api_can_get_all_bucketlists(self):
              """Test API can get a bucketlist (GET request)."""
              self.register_user()
              result = self.login_user()
              access_token = json.loads(result.data.decode())['access_token']
      
              
              res = self.client().post(
                  '/bucketlists/',
                  headers=dict(Authorization="Bearer " + access_token),
                  data=self.bucketlist)
              self.assertEqual(res.status_code, 201)
      
              
              res = self.client().get(
                  '/bucketlists/',
                  headers=dict(Authorization="Bearer " + access_token),
              )
              self.assertEqual(res.status_code, 200)
              self.assertIn('Go to Borabora', str(res.data))
      
          def test_api_can_get_bucketlist_by_id(self):
              """Test API can get a single bucketlist by using it's id."""
              self.register_user()
              result = self.login_user()
              access_token = json.loads(result.data.decode())['access_token']
      
              rv = self.client().post(
                  '/bucketlists/',
                  headers=dict(Authorization="Bearer " + access_token),
                  data=self.bucketlist)
      
              
              self.assertEqual(rv.status_code, 201)
              
              results = json.loads(rv.data.decode())
      
              result = self.client().get(
                  '/bucketlists/{}'.format(results['id']),
                  headers=dict(Authorization="Bearer " + access_token))
              
              self.assertEqual(result.status_code, 200)
              self.assertIn('Go to Borabora', str(result.data))
      
          def test_bucketlist_can_be_edited(self):
              """Test API can edit an existing bucketlist. (PUT request)"""
              self.register_user()
              result = self.login_user()
              access_token = json.loads(result.data.decode())['access_token']
      
              
              rv = self.client().post(
                  '/bucketlists/',
                  headers=dict(Authorization="Bearer " + access_token),
                  data={'name': 'Eat, pray and love'})
              self.assertEqual(rv.status_code, 201)
              
              results = json.loads(rv.data.decode())
      
              
              rv = self.client().put(
                  '/bucketlists/{}'.format(results['id']),
                  headers=dict(Authorization="Bearer " + access_token),
                  data={
                      "name": "Dont just eat, but also pray and love :-)"
                  })
              self.assertEqual(rv.status_code, 200)
      
              
              results = self.client().get(
                  '/bucketlists/{}'.format(results['id']),
                  headers=dict(Authorization="Bearer " + access_token))
              self.assertIn('Dont just eat', str(results.data))
      
          def test_bucketlist_deletion(self):
              """Test API can delete an existing bucketlist. (DELETE request)."""
              self.register_user()
              result = self.login_user()
              access_token = json.loads(result.data.decode())['access_token']
      
              rv = self.client().post(
                  '/bucketlists/',
                  headers=dict(Authorization="Bearer " + access_token),
                  data={'name': 'Eat, pray and love'})
              self.assertEqual(rv.status_code, 201)
              
              results = json.loads(rv.data.decode())
      
              
              res = self.client().delete(
                  '/bucketlists/{}'.format(results['id']),
                  headers=dict(Authorization="Bearer " + access_token),)
              self.assertEqual(res.status_code, 200)
      
              
              result = self.client().get(
                  '/bucketlists/1',
                  headers=dict(Authorization="Bearer " + access_token))
              self.assertEqual(result.status_code, 404)
      
      
      if __name__ == "__main__":
          unittest.main()
      

      We’ll refactor the methods that handle the HTTP requests for bucketlist creation and getting all the bucketlists. Open up /app/__init__.py file and edit as follows:

      
      
      
      from flask import request, jsonify, abort, make_response
      
      def create_app(config_name):
          from models import Bucketlist, User
      
          
          
          
      
          @app.route('/bucketlists/', methods=['POST', 'GET'])
          def bucketlists():
              
              auth_header = request.headers.get('Authorization')
              access_token = auth_header.split(" ")[1]
      
              if access_token:
             
                  user_id = User.decode_token(access_token)
                  if not isinstance(user_id, str):
                      
      
                      if request.method == "POST":
                          name = str(request.data.get('name', ''))
                          if name:
                              bucketlist = Bucketlist(name=name, created_by=user_id)
                              bucketlist.save()
                              response = jsonify({
                                  'id': bucketlist.id,
                                  'name': bucketlist.name,
                                  'date_created': bucketlist.date_created,
                                  'date_modified': bucketlist.date_modified,
                                  'created_by': user_id
                              })
      
                              return make_response(response), 201
      
                      else:
                          
                          bucketlists = Bucketlist.query.filter_by(created_by=user_id)
                          results = []
      
                          for bucketlist in bucketlists:
                              obj = {
                                  'id': bucketlist.id,
                                  'name': bucketlist.name,
                                  'date_created': bucketlist.date_created,
                                  'date_modified': bucketlist.date_modified,
                                  'created_by': bucketlist.created_by
                              }
                              results.append(obj)
      
                          return make_response(jsonify(results)), 200
                  else:
                      
                      message = user_id
                      response = {
                          'message': message
                      }
                      return make_response(jsonify(response)), 401
      

      We first added two imports: the User model and the make_response from Flask.

      In the bucketlist function, we check for the authorization header from the request and extract the access token. Then, we decoded the token using User.decode_token(token) to give us the payload. The payload is expected to be a user ID if the token is valid and not expired. If the token is not valid or expired, the payload will be an error message as a string.

      Create a bucketlist or two

      Copy the token and paste it to the header section, creating an Authorization header. Don’t forget to put the word Bearer before the token with a space separating them like this:

      Authorization:   "Bearer dfg32r22349r40eiwoijr232394029wfopi23r2.2342..."
      

      Make a POST request to localhost:5000/bucketlists/, specifying the name of the bucketlist. Click send.

      Get all bucketlists for a given user

      Ensure you’ve set the Authorization header just as we did for the POST request.

      Make a GET request to localhost:5000/bucketlists/ and retrieve all the bucketlists our user just created.

      We’ll refactor the PUT and DELETE functionality the same way we tackled the GET and POST.

      
      
      
      
      from flask import request, jsonify, abort, make_response
      
      def create_app(config_name):
          from models import Bucketlist, User
      
          
          
          
      
          @app.route('/bucketlists/', methods=['POST', 'GET'])
          def bucketlists():
              
              
      
          @app.route('/bucketlists/<int:id>', methods=['GET', 'PUT', 'DELETE'])
          def bucketlist_manipulation(id, **kwargs):
              
              auth_header = request.headers.get('Authorization')
              access_token = auth_header.split(" ")[1]
      
              if access_token:
                  
                  user_id = User.decode_token(access_token)
      
                  if not isinstance(user_id, str):
                      
                      
                      bucketlist = Bucketlist.query.filter_by(id=id).first()
                      if not bucketlist:
                          
                          
                          abort(404)
      
                      if request.method == "DELETE":
                          
                          bucketlist.delete()
                          return {
                              "message": "bucketlist {} deleted".format(bucketlist.id)
                          }, 200
      
                      elif request.method == 'PUT':
                          
                          name = str(request.data.get('name', ''))
      
                          bucketlist.name = name
                          bucketlist.save()
      
                          response = {
                              'id': bucketlist.id,
                              'name': bucketlist.name,
                              'date_created': bucketlist.date_created,
                              'date_modified': bucketlist.date_modified,
                              'created_by': bucketlist.created_by
                          }
                          return make_response(jsonify(response)), 200
                      else:
                          
                          response = {
                              'id': bucketlist.id,
                              'name': bucketlist.name,
                              'date_created': bucketlist.date_created,
                              'date_modified': bucketlist.date_modified,
                              'created_by': bucketlist.created_by
                          }
                          return make_response(jsonify(response)), 200
                  else:
                      
                      message = user_id
                      response = {
                          'message': message
                      }
                      
                      return make_response(jsonify(response)), 401
      
          
          from .auth import auth_blueprint
          app.register_blueprint(auth_blueprint)
      
          return app
      

      Running python manage.py test should now yield passing tests.

      1. test_already_registered_user (test_auth.AuthTestCase)
      2. Test that a user cannot be registered twice. ... ok
      3. test_non_registered_user_login (test_auth.AuthTestCase)
      4. Test non registered users cannot login. ... ok
      5. test_registration (test_auth.AuthTestCase)
      6. Test user registration works correcty. ... ok
      7. test_user_login (test_auth.AuthTestCase)
      8. Test registered user can login. ... ok
      9. test_api_can_get_all_bucketlists (test_bucketlist.BucketlistTestCase)
      10. Test API can get a bucketlist (GET request). ... ok
      11. test_api_can_get_bucketlist_by_id (test_bucketlist.BucketlistTestCase)
      12. Test API can get a single bucketlist by using it's id. ... ok
      13. test_bucketlist_can_be_edited (test_bucketlist.BucketlistTestCase)
      14. Test API can edit an existing bucketlist. (PUT request) ... ok
      15. test_bucketlist_creation (test_bucketlist.BucketlistTestCase)
      16. Test API can create a bucketlist (POST request) ... ok
      17. test_bucketlist_deletion (test_bucketlist.BucketlistTestCase)
      18. Test API can delete an existing bucketlist. (DELETE request). ... ok
      19. ----------------------------------------------------------------------
      20. Ran 9 tests in 1.579s
      21. OK

      Now let’s test to see if it works on Postman.

      Fire up the API using python run.py development

      Make a GET request for a single bucketlist to localhost:5000/bucketlists/2

      Feel free to play around with the PUT and DELETE functionality.

      We’ve covered quite a lot on securing our API. We went through defining a user model and integrating users into our API. We also covered token-based authentication and used an authentication blueprint to implement it.

      Even though our main focus is to write the code, we should not let testing be an afterthought.
      For us to improve on code quality, there have to be tests. Testing is the secret to increasing the agility of your product development. In everything project you do, put TTD first.

      If you’ve coded this to the end, you are awesome!

      Feel free to recommend this to friends and colleagues.

      Build a RESTful JSON API With Rails 5 – Part Three

      Introduction

      In part two of this tutorial, we added token-based authentication with JWT (JSON Web Tokens) to our todo API.

      In this final part of the series, we’ll wrap with the following:

      • Versioning
      • Serializers
      • Pagination

      When building an API whether public or internal facing, it’s highly recommended that you version it.
      This might seem trivial when you have total control over all clients. However, when the API is public-facing, you want to establish a contract with your clients. Every breaking change should be a new version. Convincing enough? Great, let’s do this!

      In order to version a Rails API, we need to do two things:

      1. Add a route constraint – this will select a version based on the request headers
      2. Namespace the controllers – have different controller namespaces to handle different versions.

      Rails routing supports advanced constraints. Provided an object that responds to matches?, you can control which controller handles a specific route.

      We’ll define a class ApiVersion that checks the API version from the request headers and routes to the appropriate controller module. The class will live in app/lib since it’s non-domain-specific.

      1. touch app/lib/api_version.rb

      Implement ApiVersion

      app/lib/api_version.rb

      class ApiVersion
        attr_reader :version, :default
      
        def initialize(version, default = false)
          @version = version
          @default = default
        end
      
        
        def matches?(request)
          check_headers(request.headers) || default
        end
      
        private
      
        def check_headers(headers)
          
          accept = headers[:accept]
          accept && accept.include?("application/vnd.todos.#{version}+json")
        end
      end
      

      The ApiVersion class accepts a version and a default flag on initialization. In accordance with Rails constraints, we implement an instance method matches?. This method will be called with the request object upon initialization.

      From the request object, we can access the Accept headers and check for the requested version or if the instance is the default version. This process is called content negotiation. Let’s add some more context to this.

      Content Negotiation

      REST is closely tied to the HTTP specification. HTTP defines mechanisms that make it possible to serve different versions (representations) of a resource at the same URI. This is called content negotiation.

      Our ApiVersion class implements server-driven content negotiation where the client (user agent) informs the server what media types it understands by providing an Accept HTTP header.

      According to the Media Type Specification, you can define your own media types using the vendor tree i.e., application/vnd.example.resource+json.

      The vendor tree is used for media types associated with publicly available products. It uses the “vnd” facet.

      Thus, we define a custom vendor media type application/vnd.todos.{version_number}+json giving clients the ability to choose which API version they require.

      Cool, now that we have the constraint class, let’s change our routing to accommodate this.

      Since we don’t want to have the version number as part of the URI (this is argued as an anti-pattern), we’ll make use of the module scope to namespace our controllers.

      Let’s move the existing todos and todo-items resources into a v1 namespace.

      config/routes

      Rails.application.routes.draw do
        
      
        
        scope module: :v1, constraints: ApiVersion.new('v1', true) do
          resources :todos do
            resources :items
          end
        end
      
      
        post 'auth/login', to: 'authentication#authenticate'
        post 'signup', to: 'users#create'
      end
      

      We’ve set the version constraint at the namespace level. Thus, this will be applied to all resources within it. We’ve also defined v1 as the default version; in cases where the version is not provided, the API will default to v1.

      In the event we were to add new versions, they would have to be defined above the default version since Rails will cycle through all routes from top to bottom searching for one that matches(till method matches? resolves to true).

      Next up, let’s move the existing todos and items controllers into the v1 namespace. First, create a module directory in the controllers folder.

      1. mkdir app/controllers/v1

      Move the files into the module folder.

      1. mv app/controllers/{todos_controller.rb,items_controller.rb} app/controllers/v1

      That’s not all, let’s define the controllers in the v1 namespace. Let’s start with the todos controller.

      app/controllers/v1/todos_controller.rb

      module V1
        class TodosController < ApplicationController
        
        end
      end
      

      Do the same for the items controller.

      app/controllers/v1/items_controller.rb

      module V1
        class ItemsController < ApplicationController
        
        end
      end
      

      Let’s fire up the server and run some tests.

      1. http :3000/auth/login email=[email protected] password=foobar
      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'
      1. http :3000/todos Accept:'application/vnd.todos.v2+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'

      In case we attempt to access a nonexistent version, the API will default to v1 since we set it as the default version. For testing purposes, let’s define v2.

      Generate a v2 todos controller

      1. rails g controller v2/todos

      Define the namespace in the routes.

      config/routes.rb

      Rails.application.routes.draw do
        
      
        
        scope module: :v2, constraints: ApiVersion.new('v2') do
          resources :todos, only: :index
        end
      
        scope module: :v1, constraints: ApiVersion.new('v1', true) do
          
        end
        
      end
      

      Remember, non-default versions have to be defined above the default version.

      Since this is test controller, we’ll define an index controller with a dummy response.

      app/controllers/v2/todos_controller.rb

      class V2::TodosController < ApplicationController
        def index
          json_response({ message: 'Hello there'})
        end
      end
      

      Note the namespace syntax, this is shorthand in Ruby to define a class within a namespace.
      Great, now fire up the server once more and run some tests.

      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0e...Lw2bYQbK0g'
      1. http :3000/todos Accept:'application/vnd.todos.v2+json' Authorization:'eyJ0e...Lw2bYQbK0g'

      Voila! Our API responds to version 2!

      At this point, if we wanted to get a todo and its items, we’d have to make two API calls.
      Although this works well, it’s not ideal.

      We can achieve this with serializers. Serializers allow for custom representations of JSON responses. Active model serializers make it easy to define which model attributes and relationships need to be serialized. In order to get todos with their respective items, we need to define serializers on the Todo model to include its attributes and relationships.

      First, let’s add active model serializers to the Gemfile:

      Gemfile

      
        gem 'active_model_serializers', '~> 0.10.0'
      
      

      Run bundle to install it:

      1. bundle install

      Generate a serializer from the todo model:

      1. rails g serializer todo

      This creates a new directory app/serializers and adds a new file todo_serializer.rb. Let’s define the todo serializer with the data that we want it to contain.

      app/serializers/todo_serializer.rb

      class TodoSerializer < ActiveModel::Serializer
        
        attributes :id, :title, :created_by, :created_at, :updated_at
        
        has_many :items
      end
      

      We define a whitelist of attributes to be serialized and the model association (only defined attributes will be serialized). We’ve also defined a model association to the item model, this way the payload will include an array of items. Fire up the server, let’s test this.

      1. http POST :3000/todos/1/items name='Listen to Don Giovanni' Accept:'application/vnd.todos.v1+json' Authorization:'ey...HnLw2bYQbK0g'
      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'ey...HnLw2bYQbK0g'

      This is great. One request to rule them all!

      Our todos API has suddenly become very popular. All of a sudden everyone has something to do. Our data set has grown substantially. To make sure the requests are still fast and optimized, we’re going to add pagination; we’ll give clients the power to say what portion of data they require.

      To achieve this, we’ll make use of the will_paginate gem.

      Let’s add it to the Gemfile:

      Gemfile

      
        gem 'will_paginate', '~> 3.1.0'
      
      

      Install it:

      1. bundle install

      Let’s modify the todos controller index action to paginate its response.

      app/controllers/v1/todos_controller.rb

      module V1
        class TodosController < ApplicationController
        
        
        def index
          
          @todos = current_user.todos.paginate(page: params[:page], per_page: 20)
          json_response(@todos)
        end
        
      end
      

      The index action checks for the page number in the request params. If provided, it’ll return the page data with each page having twenty records each. As always, let’s fire up the Rails server and run some tests.

      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'
      1. http :3000/todos page==1 Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'
      1. http :3000/todos page==2 Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'

      The page number is part of the query string. Note that when we request the second page, we get an empty array. This is because we don’t have more than 20 records in the database.

      Let’s seed some test data into the database.

      Add faker and install faker gem. Faker generates data at random.

      Gemfile

      
        gem 'faker'
      
      

      In db/seeds.rb let’s define seed data.

      db/seeds.rb

      
      50.times do
        todo = Todo.create(title: Faker::Lorem.word, created_by: User.first.id)
        todo.items.create(name: Faker::Lorem.word, done: false)
      end
      

      Seed the database by running:

      1. rake db:seed

      Awesome, fire up the server and rerun the HTTP requests. Since we have test data, we’re able to see data from different pages.

      Congratulations for making it this far! We’ve come a long way! We’ve gone through generating an API-only Rails application, setting up a test framework, using TDD to implement the todo API, adding token-based authentication with JWT, versioning our API, serializing with active model serializers, and adding pagination features.

      Having gone through this series, I believe you should be able to build a RESTful API with Rails 5. Feel free to leave any feedback you may have in the comments section below. If you found the tutorial helpful, don’t hesitate to hit that share button. Cheers!

      Using cURL with RESTful APIs


      In web programming, developers often have to interact with online databases. Many of these services provide a
      Representational State Transfer (REST) API that allows authorized users to read and write data. Fortunately, the
      cURL application allows users to easily access REST APIs from the command line. This guide discusses how to use cURL to interrogate RESTful APIs. It also explains how curl, the command-line utility, uses RESTful verbs, and how to inspect headers and add authorization to requests.

      An Introduction to Using cURL with RESTful APIs

      What is cURL?

      cURL stands for “Client URL” and is a data transfer application. It consists of two components, the libcurl client-side library and the curl command-line tool. cURL was originally designed to allow Linux IRC users to automate common tasks. However, it is now available for most operating systems and behaves similarly across platforms.

      Note

      cURL is the complete data transfer application, including the library, while curl is the command-line utility. The two terms are often used interchangeably. This guide mainly discusses the curl utility, which transmits commands directly to a remote REST API.

      curl uses the libcurl library and a simple URL-based syntax to transmit and receive data. It can be used as a stand-alone command line application, or inside scripts or web applications. The curl utility is common in embedded applications for vehicles, routers, printers, and audio-visual equipment. It is also used to access REST APIs and to test new APIs.

      The cURL application is:

      • free and open source.
      • portable across operating systems.
      • contains APIs or bindings for over 50 programming languages, including C/C++, Java, and Python.
      • thread safe.

      It also supports:

      • most transfer protocols and web technologies, including HTTP, FTP, SFTP, and SCP.
      • Ipv6 and dual-stack requests.
      • APIs or bindings for over 50 programming languages, including C/C++, Java, and Python.

      What is REST?

      REST is an architecture consisting of best practices and patterns for web development. It is a set of guidelines for developers rather than a true protocol. Websites and applications are considered RESTful if they follow REST principles. REST is now the industry-standard model for client-server interactions on the web, and most popular web services are only accessible through REST interfaces. The most important REST guidelines are as follows:

      • Client-server Architecture: Clients and servers are loosely coupled and communicate via an API.
      • Statelessness: Requests are independent and do not rely on the current state of the transaction.
      • Caches: Caches are used for better performance and increased security.
      • Layering: Additional features, such as security protocols, can be added to REST as a separate layer. For example, the user can be authenticated and then the request can be passed to another layer for processing.
      • Uniform interfaces: Clients use well-known URIs to request information. They must identify the specific resource to access and the format to use. The services are not customizable, so clients must use the official generic interface.

      REST principles are straightforward. Clients use a Uniform Resource Identifier (URI) to request information from a server. Inside the message, which is typically sent using HTTP, the client identifies the resources it wants. It can also specify a format for the reply. The server replies with the requested data, in JavaScript Object Notation (JSON), HTML, or XML. A REST request includes the following components:

      • An HTTP method indicating the requested operation, such as GET or PUT.
      • A header, including the media type the sender wants to receive. Some examples are text/css and image/gif.
      • The URI to the resource, including any optional parameters. A client can specify the URI using the formats example.com/products/137 or example.com/products/:id.

      The REST architecture is an industry standard because it offers many advantages. Some of its advantages are as follows:

      • It is scalable, fast, robust, and efficient. REST APIs do not use much bandwidth.
      • It is easy to understand and implement.
      • It promotes modular architecture and good design.
      • Clients and servers are fully decoupled. It is easier to make changes to the API or the internal design and is more secure.
      • It allows many different message formats.

      However, REST cannot process any requests based on the state of the transaction. It also does not guarantee reliability or include any security features. Client applications must implement these features.

      What are RESTful Verbs?

      REST interfaces allow for a fixed set of interactions. Taken together, these operations are known as the RESTful verbs or REST verbs. Each RESTful verb indicates an action on the client-side application.

      Each distinct operation is associated with a specific RESTFul verb and a range of possible status codes. A client like curl must include a RESTful verb inside the HTTP header for each request. The RESTful verbs correspond to the main create, read, update, and delete (CRUD) database operations.

      Here are the main RESTful verbs that allow curl to use a REST API:

      • POST: This RESTful verb creates a new resource on the server. If successful, the POST action returns code 201 for “Created” and provides a link to the new reference. Failure codes include 404 for “Not Found”, or a 409 conflict error if the item already exists.
      • GET: GET is used to retrieve information from the server. It can read an entire list or one specific item, and returns code 200 for “OK” if successful. If the item or collection cannot be found, the server returns code 404.
      • PUT: The PUT REST verb is used to update a specific item. The client must specify all attributes for the item. This method returns the status code 200 when the item is updated. The server returns either 404 for “Not Found” or 405 for “Method Not Allowed” if the update fails.
      • PATCH: This REST verb is similar to PUT. It modifies the item, but only contains the new changes, not the entire item. However, this verb is not considered safe from collisions. It is not recommended and is not used very much.
      • DELETE: The DELETE RESTful verb deletes an entry from the database, although it can also potentially delete the entire collection. It returns code 200 when successful, and code 404 or 405 otherwise.
      • OPTIONS: This verb fetches a list of all available operations.

      For almost all APIs, the POST, PUT, PATCH, and DELETE operations require server authentication. However, many servers allow anonymous GET operations for public data. If the server cannot authorize a user, it returns the failure code 401 for “Unauthorized”. Failure code 403, or “Forbidden”, is used if the client is not allowed to access the resource.

      Installing curl

      As of 2022, the most recent release of curl is version 7.83.0. curl usually comes pre-installed on Ubuntu and other Linux distributions. To see if curl is already installed, run the curl command with the -V flag for “version”. The local installation might not match the latest edition, but any recent release should be adequate.

      curl -V
      
      curl 7.68.0 (x86_64-pc-linux-gnu) libcurl/7.68.0 OpenSSL/1.1.1f zlib/1.2.11 brotli/1.0.9 libidn2/2.3.0 libpsl/0.21.0 (+libidn2/2.2.0) libssh/0.9.3/openssl/zlib nghttp2/1.40.0 librtmp/2.3
      Release-Date: 2020-01-08
      Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
      Features: AsynchDNS brotli GSS-API HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets

      If necessary, curl can be installed using apt install. Ensure the system is updated first.

      sudo apt install curl
      

      Documentation for curl can be found on the
      curl website. The source code can be found on the
      curl GitHub page.

      Command Line Options for curl

      To use curl from the command line, type curl and the URL to access.

      curl example.com
      

      By default, curl displays its output in the terminal window. However, the -o option redirects the output to a file.

      curl -o source.html example.com
      

      curl includes a wide range of options. To see a list of all options, use the --help option.

      curl --help
      

      Some of the most important options/flags are as follows:

      • -B: Use ASCII for text and transfer.
      • -C: Resume an interrupted transfer.
      • -d: Data for the HTTP POST or PUT commands.
      • -E: Use a client certificate file and optional password.
      • -F: Update a HTTP form request from a file.
      • -H: Pass a custom header to the server.
      • -K: Use a file for the configuration.
      • -m: Set a maximum time for the transfer.
      • -N: Disable buffering.
      • -o: Write the output to a file.
      • -s: Run in silent mode.
      • -u: Add a user name and password for the server.
      • -v: Verbose mode, for more details.
      • -X: Specifies the HTTP command to use.
      • -4: Use Ipv4 addresses.
      • -6: Use Ipv6 addresses.
      • -#: Display a progress bar. This is useful for large transfers.

      cURL vs wget

      The wget utility is a simpler alternative to curl. wget is a command-line only utility, while the full cURL application includes the libcurl library. This makes it capable of more complicated tasks.

      Some of the similarities and differences between curl and wget are as follows:

      • Both utilities can be used from the command line.
      • They can both use FTP and HTTP and support proxies and cookies.
      • Both curl and wget are free and open source utilities.
      • Both run on a large number of operating systems and are completely portable.
      • Both can transmit HTTP POST and GET requests.
      • wget can be used recursively while curl cannot.
      • wget can automatically recover from a broken transfer. curl must be restarted.
      • curl includes the powerful libcurl API.
      • curl supports more protocols, SSL libraries, and HTTP authentication methods.
      • curl is bidirectional and can do transfers in parallel.
      • curl supports many more security measures, different releases of HTTP, and dual stack IPv4/Ipv6 transfers.

      Either utility is fine for most simple HTTP requests and downloads. If you are familiar with only one of the tools and it is suitable for your requirements, continue to use it. However, wget is only a simple transfer utility. curl is a better all-purpose tool for heavy duty and professional use. See our guide
      How to Use wget to learn more about this pared-down alternative to curl.

      cURL Methods

      curl uses several HTTP commands to connect to remote REST APIs. These actions correspond to the different REST verbs. The syntax for RESTful requests is simple and straightforward and is similar to other curl requests. For thorough documentation on how to use curl, see the official
      curl documentation.

      To determine the URIs to use for each operation, consult the API documentation provided for the tool or service. As an example, the official
      GitHub REST API explains how to use the interface. When designing a REST interface, it is easy to test the API using curl.

      Note

      The following examples use example.com in the instructions. Substitute example.com with your own URI.

      GET

      The GET operation allows curl to receive information from a REST API. To use the GET RESTful verb, use the curl command followed by the name of the resource to access. The -X attribute and the name of the operation are not required because GET is the default HTTP operation.

      The output varies based on the server. It includes a status, which is set to success if the request is valid, the data, and an optional message. In this case, the client does not specify a format for the data, so the server responds using JSON. To see more information about the transfer, including the server options, append the -v (verbose) option to the command.

      curl https://example.com/api/2/employees
      
      {"status":"success","data":[{"id":1,"name":"Tom","age":60,"image":""},
      ...
      {"id":40,"name":"Linda","age":50,"image":""}],"message":"All records retrieved."}

      To see one particular entry, append the id of the entry to retrieve. In this example, only the information for employee 10 is returned from the server. The output is again in JSON format.

      curl https://example.com/api/2/employees/10
      
      {"status":"success","data":{"id":10,"name":"Julia","age":33,"image":""},"message":"Record retrieved."}

      POST

      The POST verb allows users to push data to a REST API and add new entries to the remote database. The data is specified as an argument for the -d option. The data should be in a format matching the request. In this case, the -H option informs the server the data is in application/json format. If a format is not specified, curl adds Content-Type: application/x-www-form-urlencoded to the HTTP header. This might cause problems on some servers.

      The server returns the new record, including the id of the new entry. The following command adds a new record to the application server.

      Note

      The curl command infers this is a POST operation based on the other details. But it is considered good practice to explicitly state the verb as part of the -X option.

      curl -d '{"name":"Jamie","age":"23","image":""}' -H 'Content-Type: application/json' -X POST https://example.com/api/2/create
      
      {"status":"success","data":{"name":"Jamie","age":"23","image":null,"id":5126},"message":"Record added."}

      This approach is fine for small amounts of data. To add multiple records, pass a file containing the information to the server. The filename can be indicated with a @ symbol followed by the file name, as follows:

      curl -d @data.json -H 'Content-Type: application/json' -X POST https://example.com/api/2/create
      

      PUT

      The RESTful verb PUT modifies an existing entry. This option works similarly to the POST option. The -d flag specifies the updated information for the record, and -H indicates the data format. However, the id of the record to update must be included as part of the URI. For a PUT command, the -X option must include the keyword.

      curl -d '{"name":"Jamie","age":"23","image":""}' -H 'Content-Type: application/json' -X PUT  https://example.com/api/2/update/31
      
      {"status":"success","data":{"name":"Jamie","age":"23","image":null},"message":"Record updated."}

      DELETE

      The DELETE operation removes a record from the database. It is one of the simpler REST verbs to use. As part of the -X option, include the DELETE verb and append the id of the record to delete to the URI. The data and header flags are not required for this operation.

      curl -X DELETE https://example.com/api/2/delete/31
      
      {"status":"success","data":"31","message":"Record deleted"}

      Viewing and Changing Headers with cURL

      In normal usage, curl only displays the most relevant information, not the entire HTTP request and response. To view all information, including the HTTP headers, add the -v option to any curl command to activate verbose mode.

      curl -v example.com
      
      * TCP_NODELAY set
      * Connected to example.com (2606:2800:220:1:248:1893:25c8:1946) port 80 (#0)
      > GET / HTTP/1.1
      > Host: example.com
      > User-Agent: curl/7.68.0
      > Accept: */*
      >
      * Mark bundle as not supporting multiuse
      < HTTP/1.1 200 OK
      < Age: 409433
      < Cache-Control: max-age=604800
      < Content-Type: text/html; charset=UTF-8
      < Date: Tue, 03 May 2022 16:40:30 GMT
      < Etag: "3147526947+ident"
      < Expires: Tue, 10 May 2022 16:40:30 GMT
      < Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
      < Server: ECS (bsa/EB20)
      < Vary: Accept-Encoding
      < X-Cache: HIT

      Any outgoing HTTP header in curl can be modified using the -H option. Some of the previous examples already demonstrated how to use this flag when setting the content-type. However, -H also allows users to modify any field in the header. The following example demonstrates how to turn off the user-agent field in the header. When the header is reviewed in verbose mode, the field is no longer present.

      curl -H "User-Agent:" http://example.com -v
      
      *   Trying 2606:2800:220:1:248:1893:25c8:1946:80...
      * TCP_NODELAY set
      * Connected to example.com (2606:2800:220:1:248:1893:25c8:1946) port 80 (#0)
      > GET / HTTP/1.1
      > Host: example.com
      > Accept: */*

      Authorization and Passwords with cURL

      Many REST APIs require the user to authenticate using a valid user name and password. The easiest way to provide this information is through the -u option of the curl command. Include the account name and password, separated by a :. The following example executes the GET RESTful verb using authentication.

      curl -u user:password https://example.com/api/2/employee/10
      

      Conclusion

      Although it is best known as a data transfer application, the cURL application can interact with REST APIs. It includes the curl command line utility and the fully-featured libcurl library. REST is a popular architecture for client-server applications. It decouples the two components and stresses modularity and efficiency. Information is exchanged through well-known URIs.

      Users can access REST APIs using the RESTful verbs, which correspond to the basic HTTP actions. curl can send all common HTTP commands to a REST API including GET, POST, PUT, and DELETE. The curl utility is straightforward to use. It has a few main options for data transmission, user authentication, and making header changes. For more information about curl, see the
      curl documentation.

      More Information

      You may wish to consult the following resources for additional information
      on this topic. While these are provided in the hope that they will be
      useful, please note that we cannot vouch for the accuracy or timeliness of
      externally hosted materials.



      Source link