-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve "basics" docs #152
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Thanks for taking the time to update the docs ❤️
docs/quickstart/basics.rst
Outdated
@@ -159,20 +156,25 @@ The ``response_body_schema`` (previously ``marshal_schema``) argument of ``Handl | |||
|
|||
In the case of a ``marshmallow.Schema``, that schema is used to ``dump`` the return value of the handler function. | |||
|
|||
In the case of a dictionary mapping integers to ``marshmallow.Schemas``, the integers are interpreted as status codes, and the handler function must return a tuple of ``(response_body, status_code)``: | |||
In the case of a dictionary mapping integers to ``marshmallow.Schemas``, the integers are interpreted as status codes, and the handler function must return a tuple like ``(response_body, status_code)``, | |||
or like ``(response_body, status_code, headers)`` to also include custom headers: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: I could be wrong, but I believe that we still assume a non-tuple response has a 200 status_code. If that is the case we might want to also document that as a valid response here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed another revision based on this comment. Ended up making what I think are a few other nice improvements to these docs, which I'll call out separately. Thanks for the suggestion!
@@ -19,7 +19,8 @@ Let's take a look at a very basic API using Flask-Rebar: | |||
|
|||
|
|||
class TodoSchema(ResponseSchema): | |||
id = fields.Integer() | |||
id = fields.Integer(dump_only=True) | |||
description = fields.String(required=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally like having different schema for create and get, but this pattern is also nice so I am not sure what we should recommend to beginners.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check out the latest revision (which actually includes a little more explanation in the docs) and let me know what you think.
When the data for the same type of object is for some reason very different in request bodies compared to response bodies, it makes sense to use different schemas. But in the simpler case (which is maybe more common?) where the only difference is that some fields should only be read-only by users, since they're managed entirely by the database or application, then I think using a single Schema with those fields marked as dump_only
is a perfect fit. Common examples in my experience are autogenerated id
s and automatically maintained updated_at
and created_at
timestamps, which can all be maintained by a SQL database automatically.
FWIW, this is the pattern I use in the class I've been teaching to beginners and it's been working really well. (For the past year I've been teaching a monthly class at Two Sigma on how to build REST APIs with Flask-Rebar, and by the time they leave the class, students have built a full working CRUD app to manage (you guessed it) their Todo list. 🤣)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely much better with the new comments, I am not sure about the form (explanation before/after code vs inline comments) but that can be improved later.
My pleasure, @airstandley! Note, all Travis jobs passed except one, but that looks like an ephemeral networking failure during the |
|
||
|
||
``RequestSchema`` is an extension of ``marshmallow.Schema`` that throws an internal server error if an object is missing a required field. It's usage is optional - a normal Marshmallow schema will also work. | ||
|
||
This request schema is passed to ``request_body_schema``, and the handler will now call `marshmallow.Schema.load <http://marshmallow.readthedocs.io/en/latest/api_reference.html#marshmallow.Schema.load>`_ on the request body decoded as JSON. A 400 error with a descriptive error will be returned if validation fails. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RequestSchema is still important for marshmallow 2 if I am not mistaken, otherwise additional fields are not rejected. What I think we should do in V2 is a single Schema class that includes the RequestSchema and ResponseSchema, but I will open an issue for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #152 (comment)
from flask import Flask | ||
from flask_rebar import Rebar | ||
from flask_rebar import ResponseSchema |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I've never used ResponseSchema
because I've always been the one in full control of my data, and have automated tests that make sure the data I keep in my database stays consistent (validation-wise) with the data my API promises to return. In the interest of keeping this example as basic as possible, while still being as useful as possible, I thought it's better to save Rebar's special RequestSchema/ResponseSchema
for a later section, in return for demonstrating the other stuff I added here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to agree on that one but I think we should move toward #161
) | ||
def get_todo(id): | ||
... | ||
return {'id': id} | ||
todo = _get_todo_or_404(id) # Some helper function that queries our database. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
app = Flask(__name__) | ||
rebar.init_app(app) | ||
|
||
if __name__ == '__main__': | ||
app.run() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(app.run()
has been deprecated/discouraged for several years, and it's just as easy to show best practice here instead, so I fixed that too while I was in here.)
@@ -60,38 +78,49 @@ Request Body Validation | |||
|
|||
.. code-block:: python | |||
|
|||
from flask_rebar import RequestSchema |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(FWIW, I haven't yet hit a case where I've wanted Rebar's RequestSchema
; in all my use cases so far, I've preferred "being liberal in what I accept and conservative in what I send", at least as far as unrecognized fields are concerned.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put it everywhere since I prefer a break fast strategy with a REST API. For example, if for some reason you have a breaking change in the API, you don't want to have clients sending requests thinking that all went well and it applied some changes but in reality it didn't (and no alert since its a 200).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(To be clear, I'm by no means saying it's always wrong to fail hard for unrecognized fields, just that it's definitely not always right. Maybe more citation needed, so just did a quick search and immediately turned up something interesting: https://opensource.zalando.com/restful-api-guidelines/#109 – this topic is deep!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this website, thanks for sharing. I think we should try to encourage some of it's principles while allowing divergences because they are quite strict and hard to meet for a lot of people.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So looking with fresh eyes today... That API Guidelines link I posted last night is saying that the Robustness Principle applies on the client side, but on the server side, it doesn't!:
Unknown input fields in payload or URL should not be ignored; servers should provide error feedback to clients via an HTTP 400 response code.
This is not what I thought was best practice, so I'll definitely be doing more research on this! If this API Guidelines doc is correct, then it's quite a validation of flask_rebar
's RequestSchema
, and I'll correct my understanding, my code, and my teaching accordingly!
In the meantime, the current revision of this PR currently is still removing the use of RequestSchema
for the example request_body_schema
s. That means going from conforming to these guidelines to not conforming to them. I really don't want to be responsible for, in the process of trying to improve the docs' demonstration of best practices, actually making them worse in this particular way.
At the same time, I'm very keen to see #161 happen, because I do think that having two different schemas (request and response) for the same type of object is not always necessary, and when it isn't, as in this Todo example, it adds unnecessary complexity to our docs. Shall we wait for #161 to be resolved so we can incorporate the outcome into these changes before we merge them?
Thank you @Sytten for prompting this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can wait, I will try to prioritize this weekend of monday. Though I am not sure if we want to only do that in V2 or backport it to V1.
|
||
|
||
class CreateTodoSchema(RequestSchema): | ||
description = fields.String(required=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Now reusing TodoSchema
for both requests and responses)
@twosigmajab This is fantastic! It's a huge improvement. However I'm against removing all references to |
|
||
export FLASK_APP=todos.py | ||
flask run | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should split this into two sections, "Creating a Flask App with Rebar" and "Registering a Handler"? As is this portion on running the app feels out of place and I think makes the lines that follow a bit confusing.
Is this still needed? can we make sure it is up to date for flask-rebar v2.0? Otherwise, i would like to close it to start clearing out the backlog. |
I think there are still some good improvements here that would be beneficial to merge, but I no longer work at Two Sigma, nor with Flask-Rebar, and at this point I would have to hand this off to the maintainers (or another external contributor). Hopefully someone is still interested in making these improvements. |
TodoSchema
andCreateTodoSchema
, so thatTodoSchema
actually includes the description (not just the id), and makeid
adump_only
field, so that there's no need for a separate schema for thecreate_todo
endpoint'srequest_body_schema
.create_todo
to actually userequest_body_schema
.Location
header increate_todo
's 201 Created response, in compliance with the HTTP spec ("...the [new resource's] location being... the content of theLocation
header").I actually implemented a real Todo list example app with Flask-Rebar using Flask-SQLAlchemy, which these changes are based on. Figured I'd finally try to contribute back some best practices I'm following that Flask-Rebar's docs aren't currently demonstrating.