-1

I am building a dictionary app with Flask where users can add new words, I am trying to request the word from the word input , I am having issues with the POST request, the error I am receiving on my terminal is this:

        line 50, in add_word                   
        word = req['word']                                                                          
        keyError:'word'

and this is how I wrote the code in my app.py file:

@app.route('/word', methods= ['POST'])
def add_word():
    req = request.get_json()
    word = req['word']
    meaning = req['meaning']
    conn = mysql.get_db()
    cur = conn.cursor()
    cur.execute('insert into word(word, meaning) VALUES (%s, %s)',(word, meaning))
    conn.commit()
    cur.close()

    return json.dumps("success")

here is the json in my JavaScript file, I am posting to my flask app:

     $('#word-form').submit(function() {
    let word = $('word').val();
    let meaning = $('meaning').val();
    

    $.ajax({
        url: '/word',
        type: 'POST',
        dataType: 'json',
        data : JSON.stringify({
            'word': word,
            'meaning': meaning
        }),
        contentType: 'application/json, charset = UTF-8',
        success: function(data) {
            location.reload();
        },
        error: function(err) {
            console.log(err);
        }
    })

here is the Html page:

    <div class="div col-md-2 sidenav">
    <a href="#" id="word-index" class="side-active">All words</a>
    <a href="#" id="word-add">Add New</a>
    <div>
        <form action="javascript:0" id="word-form">
            <div class="form-group">
                <label for="word">Word:</label>
                <input type="text" 
                class="form-control" 
                name="word"
                id="word"
                placeholder="Type in the word here:"
                required>
            </div>
            <div class="form-group">
                <label for="Meaning">Meaning:</label>
                <textarea class="form-control" id="meaning" 
                 placeholder="enter the meaning here: " required></textarea> 
            </div>
            <button type="submit" class="btn btn-primary btn-block btn-lg" id="submit">Submit</button>
            <button type="button" class="btn btn-warning btn-block btn-lg" id="cancel">Cancel</button>
        </form>
    </div>
</div>
<div class="div col-md-10 main">
    <table style="border: 2px;">
        <thead>
            <tr>
                <th>SN</th>
                <th>Word</th>
                <th>Meaning</th>
                <th></th>
                <th></th>
            </tr>
        </thead>

        <tbody>
            {% for word in words %}
            <tr>
                <td>{{ loop.index }}</td>
                <td>{{ word['word'] }}</td>
                <td>{{ word['meaning'] }}</td>
                <td><button class="btn btn-sm btn-success btn-block edit" id="{{word['id']}}">Edit</button></td>
                <td><button class="btn btn-sm btn-danger btn-block delete" id="{{word['id']}}">Delete</button></td>
            </tr>
            {% else %}
            <tr>
                 <td colspan="3">The dictionary has no words at the moment, please come bay later</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</div>
nelsoneep
  • 1
  • 2
  • Show how you're posting the JSON. Clearly, the JSON doesn't have the `word` key in it. Solution: add the `word` key to the JSON post. If the POST payload doesn't look like `{"word": "foo", "meaning": "bar"}`, there's the problem. – ggorlen Jul 23 '22 at 15:28
  • please review the json i wrote. thanks – nelsoneep Jul 23 '22 at 17:55
  • Great, thanks. Can you add the HTML? It should be a complete, runnable example. – ggorlen Jul 23 '22 at 18:05
  • i have posted the html page – nelsoneep Jul 24 '22 at 08:54
  • Thanks. What happens from the client's perspective when you submit your form? Are you returning false from the handler to prevent the default form submission action (page refresh)? – ggorlen Jul 24 '22 at 14:44

1 Answers1

0

Things seem to be in a confused state in the client code, and potentially the application architecture in general.

There are two general approaches to designing web apps that impacts how you create routes and build requests. One approach is the AJAX-based single page app architecture that loads one HTML skeleton, then uses JS (jQuery here) to make AJAX requests to a JSON API and injects the response data into the page using JS instead of page refreshes and navigations. Since you don't have a client-side router, this doesn't qualify as a SPA, but it's worth understanding to provide context on your design.

On the other hand, you can use HTML form submissions (<form action="/path/to/resource" method="POST">) and render_template to display new pages with a browser refresh for all form submissions.

The code here is somewhere in the middle, which is potentially fine (you can use AJAX for certain events and form submissions but mostly rely on full-navigation templates for routes). But it's important to be clear on the request-response workflow you're adopting so the design makes sense and can be debugged.

Here are few oddities/inconsistencies in your current design:

  • return json.dumps("success") is not really JSON as it seems like you want it to be--use jsonify and a dictionary or list, e.g. jsonify({"status": "success"}; it's customary for JSON routes to return JSON responses if they aren't rendering templates or redirecting.
  • The client ignores the JSON response and calls location.reload. If you're just planning on reloading and you have no special client processing to do, there's not much point in using AJAX or JSON here--just submit the form to the backend and redirect to the template or static HTML page you want to show next. No client-side JS involved. Redirect to an error page or render a template with errors shown on the form on error.
  • Links with href="#" are poor practice. Better to use buttons if you're adding JS to these handlers and you don't want them to trigger navigation. This is semantically more appropriate and doesn't hijack the URL.
  • <form action="javascript:0" id="word-form"> looks like it's trying to prevent the form submission, but all this does is replace the page content with the character "0". I can't imagine how this is useful or desirable. Submitting a form to a JSON route can produce the error you're seeing--another sign of confusion about which architecture you're following. Use event.preventDefault() (add the event parameter to the callback to .submit()) to prevent the form submission from refreshing the page.

After you've prevented the page refresh, you can debug the AJAX request.

When a route is complaining about missing keys, consider that objects with keys pointing to undefined disappear when serialized as JSON (undefined is not a thing in JSON):

const word = undefined;
const foo = 42;
const bar = "baz";

console.log({word, foo, bar}); /* =>
{
  "word": undefined,
  "foo": 42,
  "bar": "baz"
}
*/

console.log(JSON.stringify({
  word,
  foo,
  bar,
})); // => {"foo":42,"bar":"baz"}

If you add a console.log to see if your values are there (or print the JSON on the backend route before indexing into it), these values aren't defined:

let word = $('word').val();
let meaning = $('meaning').val();
console.log(word, meaning); // <-- undefined, undefined

Why? The reason is that these selectors are missing the # symbol prefix to denote an id. Without it, jQuery looks for <word></word> and <meaning></meaning> HTML elements that don't exist.

Change these lines to:

const word = $('#word').val();
const meaning = $('#meaning').val();

and now your request body should be ready to send.

Next problem: $.ajax's dataType key specifies the response type, not the request type. Use dataType: "json" to specify the appropriate request header to trigger the Flask handler to JSON parse the request body.

After these changes, things should work, with the caveat that it might be time for a rethink of your overall design, or at least a redesign of this route workflow.

A word of advice: work slowly and test all of your assuptions at each step. The code here shows many errors that are hard to debug because they're stacked on top of each other. Isolate and validate each behavior in your app. For example, when adding the jQuery submit handler and collecting the form values, print them to make sure they're actually there as you expected.

In case you're stuck, here's minimal, complete, runnable code you can reference.

app.py:

from flask import (
    Flask, jsonify, render_template, request, url_for
)


app = Flask(__name__)


@app.route("/")
def index():
    return render_template("index.html")


@app.post("/words/")
def words():
    payload = request.get_json()
    word = payload.get("word")
    meaning = payload.get("meaning")

    if word is None or meaning is None:
        return (jsonify({
            "error": "missing required keys `word` or `meaning`"
        }), 400)

    # handle db operation and report failure as above

    return jsonify({"status": "success"})


if __name__ == "__main__":
    app.run(debug=True)

templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
  <form id="word-form">
    <div>
      <label for="word">Word:</label>
      <input
        name="word"
        id="word"
        placeholder="Type in the word here:"
        required
      >
    </div>
    <div>
      <label for="meaning">Meaning:</label>
      <textarea 
        name="meaning"
        id="meaning"
        placeholder="enter the meaning here: "
        required
      ></textarea> 
    </div>
    <button type="submit">Submit</button>
  </form>

  <script>
$('#word-form').submit(function (e) {
  e.preventDefault();
  const word = $('#word').val();
  const meaning = $('#meaning').val();
  console.log(word, meaning);

  $.ajax({
    url: "{{url_for('words')}}",
    type: "POST",
    dataType: "json",
    contentType: "application/json",
    data: JSON.stringify({word, meaning}),
    success: function (data) {
      console.log(data);
    },
    error: function (err) {
      console.error(err);
    }
  });
});
  </script>
</body>
</html>

See also: How to get POSTed JSON in Flask?


Here are a few additional notes that are somewhat tangential to the main issue but have to be mentioned.

You have <label for="Meaning"> but no name="meaning" element to shift focus to when clicked.

It's another antipattern to put ids on everything promiscuously. Only add ids to elements when they must have one because you're using it for something specific. Prefer classes, especially for styling.

On the backend, the code here is unsafe:

req = request.get_json()
word = req['word']
meaning = req['meaning']

If your client gives a bad request with missing values, you should detect that and return a 400/422 response (or similar) rather than crashing.

For example (from the above code snippet):

req = request.get_json()
word = req.get("word")
meaning = req.get("meaning")

if word is None or meaning is None:
    return (jsonify({
        "error": "missing required keys `word` or `meaning`"
    }), 400)

Similarly, don't assume the database operation will succeed. Always check for errors and return an appropriate response to the client.

Resources are usually plural, not singular: words, users, posts.

ggorlen
  • 44,755
  • 7
  • 76
  • 106