创建 REST API 层
这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。
Create a REST API Layer#
Since our GUI is going to run in the browser, we need something for the browser
to talk to. There are other choices we could make, but REST is the most sensible
choice we can make for this app in 2019. So that's what we're going to do!
Rocket is a web framework for Rust that will make it easy
for us to quickly write a Rust program to serve our API.
Add Rocket to Cargo.toml#
We need to add Rocket to our app. Make your Cargo.toml's dependencies section
look like:
{{#include ../ch7-mytodo/Cargo.toml:dependencies}}
Run cargo build
to download and compile all the new dependencies.
Create the Backend Binary#
We'll write our backend in src/bin/backend.rs. Let's make a first rough pass:
{{#include ../ch7-mytodo/src/bin/backend-stub.rs}}
The first line enables a couple of features that Rocket's macros need. These are
experimental features, and this is part of why we need to build with Rust
nightly instead of stable.
Then we pull in Rocket's macros.
Then we've got the handler for our route. For the moment it just returns a
static string.
In our main
we create a Rocket instance, mount our handler, and start it.
Now if you cargo run --bin backend
it will compile everything and start our
backend listening on localhost port 8000. If you open your browser to
localhost:8000/tasks (or use curl
) you will see "this is a response".
Query the Database#
So far we've got a pretty lame API. It just spits out a static string. It's cool
that we can create a functioning web server from so little code, but we want to
return dynamic data -- info pulled from the database.
We've basically already written this code -- it's nearly the same as the
subcommand in our CLI program:
{{#include ../ch7-mytodo/src/bin/backend.rs:tasks_get}}
Run this (cargo will rebuild the changes for you), and refresh your browser --
you should see the task titles we inserted earlier. Great! But also, not so
great -- we might want to add more data to the tasks in the future as our app
gets popular. Like done-ness, due dates, and priority.
Just printing a bunch of lines of output is going to be tedious and error-prone
for our frontend to parse.
We can make it easier for our frontend and our backend to communicate
with each other if we use a standard data serialization format for our API, and
likewise if we use some standard tools to do it.
That format will be JSON. There are lots of tools for dealing with JSON, and
luckily for us one of them is part of Rocket. The other tool we need is the
excellent serde
framework for serializing and
deserializing data (in JSON and many other formats).
Serializing to JSON#
We need to add serde and JSON support from rocket_contrib
to our
Cargo.toml:
{{#include ../ch7a-mytodo/Cargo.toml}}
And then we need to use rocket_contrib and serde in backend.rs:
{{#include ../ch7a-mytodo/src/bin/backend.rs:use}}
Let's think about how we want to format our response. We could just send back an
array of tasks, where each task is an object with a title
key that has a
string value:
[
{ "title": "do the thing" },
{ "title": "get stuff done" }
]
One problem with this is that we have no uniform way to indicate an error. We'll
be better off if we're closer to complying with the [JSON API
spec](jsonapi.org), which requires an object at the top level, and a
data
key at that level -- something like this:
{
"data": [
{ "id": 1, "title": "do the thing" },
{ "id": 2, "title": "get stuff done" },
]
}
Note that this isn't strictly conforming with the JSON API spec because the
resource objects (the stuff in the data
array) aren't formatted properly. But
for the sake of keeping this exposition simple we're going to settle for being
non-compliant for now -- see the exercises for an approach to getting this done
the right way.
Now that we know what we want to return, let's put together a Rust structure to
represent it in backend.rs:
{{#include ../ch7a-mytodo/src/bin/backend.rs:response}}
Here we're using the Serialize derive-macro from serde for our struct. This
makes it so that we can magically get JSON out of it. However, if we try to
build this we get an error:
error[E0277]: the trait bound `mytodo::db::models::Task: serde::ser::Serialize`
is not satisfied
We can only Serialize a struct if all the things in the struct also implement
Serialize -- and Task doesn't do that... yet.
Can we fix it? YES WE CAN!
At the top of both lib.rs and backend.rs we need to enable serde macros:
{{#include ../ch7a-mytodo/src/lib.rs:serde}}
And then in db/models.rs we need to slap a Serialize on the Task struct:
{{#include ../ch7a-mytodo/src/db/models.rs:Task}}
Our handler function will use the Json type from rocket_contrib
-- note
that this is a different type than serde's Json type! We need to add a use
declaration for it at the top of backend.rs:
{{#include ../ch7a-mytodo/src/bin/backend.rs:json}}
With all of that in place we can modify our handler function to push the tasks
we get back from query_task
onto a response object, and then convert that to
Json on the way out:
{{#include ../ch7a-mytodo/src/bin/backend.rs:tasks_get}}
Run the backend, refresh your browser, and you should see json similar to the
sample above. (It won't be pretty-printed -- you can `curl --silent
localhost:8000/tasks/ | jq .` if you're into that.)
REST API Layer Wrap-Up#
We just built a functional REST API backend, and I don't know about you, but I
didn't even break a sweat. Of course, there are things we'd do differently in a
production app:
* testing, of both the unit and integration varieties
* documentation, from comments to docstrings to REST API user (developer) docs
* stricter conformance to JSON API
* API versioning
so that we don't have to do establish_connection
for every request, which
would be important under load
* make our response object use a parameterized type so that we can return
different object types as the API grows new features
In the next chapter we will place the final layer -- a browser-based UI based on
the Seed framework. But first -- some exercises!
REST API Layer Exercises#
These exercises are more challenging and will require consulting more external
documentation than in the last chapter.
Bring the API in conformance with the JSON API Spec#
Specifically, this part:
A resource object MUST contain at least the following top-level members:
* id
* type
Exception: The id member is not required when the resource object originates at
the client and represents a new resource to be created on the server.
In addition, a resource object MAY contain any of these top-level members:
* attributes: an attributes object representing some of the resource’s data.
To do this, create a new Task wrapper struct that contains the id, type, and
attributes, modify the JsonApiResponse struct to contain a vector of that
wrapper, and modify the loop around task_query
to create and push this type
onto the response.
Use Connection Pooling#
Read the Rocket documentation on [database connection
pooling](rocket.rs/v0.4/guide/state/#databa...) and implement it in
backend.rs.
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。