Having Trouble Making a RESTful API with Flask-RestX: "No operations defined in spec!" and "404"s

4.9k Views Asked by At

In summary, I have been following the flask restx tutorials to make an api, however none of my endpoints appear on the swagger page ("No operations defined in spec!") and I just get 404 whenever I call them

I created my api mainly following this https://flask-restx.readthedocs.io/en/latest/scaling.html

I'm using python 3.8.3 for reference.

A cut down example of what I'm doing is as follows.

My question in short is, what am I missing? Currently drawing blank on why this doesn't work.

Directory Structure

project/
  - __init__.py
  - views/
    - __init__.py
    - test.py
manage.py
requirements.txt

File Contents

requirements.txt

Flask-RESTX==0.2.0
Flask-Script==2.0.6

manage.py

from flask_script import Manager

from project import app


manager = Manager(app)


if __name__ == '__main__':
    manager.run()

project/init.py

from flask import Flask

from project.views import api


app = Flask(__name__)

api.init_app(app)

project/views/init.py

from flask_restx import Api, Namespace, fields


api = Api(
    title='TEST API',
    version='1.0',
    description='Testing Flask-RestX API.'
)

# Namespaces
ns_test = Namespace('test', description='a test namespace')

# Models
custom_greeting_model = ns_test.model('Custom', {
    'greeting': fields.String(required=True),
})

# Add namespaces
api.add_namespace(ns_test)

project/views/test.py

from flask_restx import Resource

from project.views import ns_test, custom_greeting_model


custom_greetings = list()


@ns_test.route('/')
class Hello(Resource):

    @ns_test.doc('say_hello')
    def get(self):
        return 'hello', 200


@ns_test.route('/custom')
class Custom(Resource):

    @ns_test.doc('custom_hello')
    @ns_test.expect(custom_greeting_model)
    @ns_test.marshal_with(custom_greeting_model)
    def post(self, **kwargs):
        custom_greetings.append(greeting)
        pos = len(custom_greetings) - 1

        return [{'id': pos, 'greeting': greeting}], 200

How I'm Testing & What I Expect

So going to the swagger page, I expect the 2 endpoints defined to be there, but I just see the aforementioned error.

Just using Ipython in a shell, I've tried to following calls using requests and just get back 404s.

import json
import requests as r

base_url = 'http://127.0.0.1:5000/'
response = r.get(base_url + 'api/test')
response
response = r.get(base_url + 'api/test/')
response
data = json.dumps({'greeting': 'hi'})
response = r.post(base_url + 'test/custom', data=data)
response
data = json.dumps({'greeting': 'hi'})
response = r.post(base_url + 'test/custom/', data=data)
response
1

There are 1 best solutions below

0
On BEST ANSWER

TL;DR

I made a few mistakes in my code and test:

  1. Registering api before declaring the routes.
  2. Making a wierd assumption about how the arguments would be passed to the post method.
  3. Using a model instead of request parser in the expect decorator
  4. Calling the endpoints in my testing with an erroneous api/ prefix.

In Full

I believe it's because I registered the namespace on the api before declaring any routes.

My understanding is when the api is registered on the app, the swagger documentation and routes on the app are setup at that point. Thus any routes defined on the api after this are not recognised. I think this because when I declared the namespace in the views/test.py file (also the model to avoid circular referencing between this file and views/__init__.py), the swagger documentation had the routes defined and my tests worked (after I corrected them).

There were some more mistakes in my app and my tests, which were

Further Mistake 1

In my app, in the views/test.py file, I made a silly assumption that a variable would be made of the expected parameter (that I would just magically have greeting as some non-local variable). Looking at the documentation, I learnt about the RequestParser, and that I needed to declare one like so

from flask_restx import reqparse

# Parser
custom_greeting_parser = reqparse.RequestParser()
custom_greeting_parser.add_argument('greeting', required=True, location='json')

and use this in the expect decorator. I could then retrieve a dictionary of the parameters in my post method. with the below

...
    def post(self):
        args = custom_greeting_parser.parse_args()
        greeting = args['greeting']
        ...

The **kwargs turned out to be unnecessary.

Further Mistake 2

In my tests, I was calling the endpoint api/test, which was incorrect, it was just test. The corrected test for this endpoint is

Corrected test for test endpoint

import json
import requests as r

base_url = 'http://127.0.0.1:5000/'

response = r.get(base_url + 'test')
print(response)
print(json.loads(response.content.decode()))

Further Mistake 3

The test for the other endpoint, the post, I needed to include a header declaring the content type so that the parser would "see" the parameters, because I had specified the location explictily as json. Corrected test below.

Corrected test for test/custom endpoint

import json
import requests as r

base_url = 'http://127.0.0.1:5000/'

data = json.dumps({'greeting': 'hi'})
headers = {'content-type': 'application/json'}
response = r.post(base_url + 'test/custom', data=data, headers=headers)
print(response)
print(json.loads(response.content.decode()))

Corrected Code

For the files with incorrect code.

views/init.py

from flask_restx import Api

from project.views.test import ns_test


api = Api(
    title='TEST API',
    version='1.0',
    description='Testing Flask-RestX API.'
)


# Add namespaces
api.add_namespace(ns_test)

views/test.py

from flask_restx import Resource, Namespace, fields, reqparse


# Namespace
ns_test = Namespace('test', description='a test namespace')

# Models
custom_greeting_model = ns_test.model('Custom', {
    'greeting': fields.String(required=True),
    'id': fields.Integer(required=True),
})

# Parser
custom_greeting_parser = reqparse.RequestParser()
custom_greeting_parser.add_argument('greeting', required=True, location='json')


custom_greetings = list()


@ns_test.route('/')
class Hello(Resource):

    @ns_test.doc('say_hello')
    def get(self):
        return 'hello', 200


@ns_test.route('/custom')
class Custom(Resource):

    @ns_test.doc('custom_hello')
    @ns_test.expect(custom_greeting_parser)
    @ns_test.marshal_with(custom_greeting_model)
    def post(self):
        args = custom_greeting_parser.parse_args()
        greeting = args['greeting']

        custom_greetings.append(greeting)
        pos = len(custom_greetings) - 1

        return [{'id': pos, 'greeting': greeting}], 200