- Creative Projects for Rust Programmers
- Carlo Milanesi
- 2554字
- 2021-06-18 19:02:00
Building a stub of a REST web service
The typical example of a REST service is a web service designed for uploading and downloading text files. As it would be too complex to understand, first we will look at a simpler project, the file_transfer_stub project, which mimics this service without actually doing anything on the filesystem.
You will see how an API of a RESTless web service is structured, without being overwhelmed by the details regarding the implementation of the commands.
In the next section, this example will be completed with the needed implementation, to obtain a working file-managing web app.
Running and testing the service
To run this service, it is enough to type the command cargo run in a console. After building the program, it will print Listening at address 127.0.0.1:8080 ..., and it will remain listening for incoming requests.
To test it, we need a web client. You can use a browser extension if you prefer, but in this chapter, the curl command-line utility will be used.
The file_transfer_stub service and the file_transfer service (we'll see them in the next section) have the same API, containing the following four commands:
- Download a file with a specified name.
- Upload a file with a specified name and specified contents.
- Upload a file with a specified name prefix and specified contents, obtaining the complete name as a response.
- Delete a file with a specified name.
Getting a resource using the GET method
To download a resource in the REST architecture, the GET method should be used. For these commands, the URL should specify the name of the file to download. No additional data should be passed, and the response should contain the contents of the file and the status code, which can be 200, 404, or 500:
- Type the following command into a console:
curl -X GET http://localhost:8080/datafile.txt
- In that console, the following mock line should be printed, and then the prompt should appear immediately:
Contents of the file.
- Meanwhile, on the other console, the following line should be printed:
Downloading file "datafile.txt" ... Downloaded file "datafile.txt"
This command mimics the request to download the datafile.txt file from the filesystem of the server.
- The GET method is the default one for curl, and hence you can simply type the following:
curl http://localhost:8080/datafile.txt
- In addition, you can redirect the output to any file by typing the following:
curl http://localhost:8080/datafile.txt >localfile.txt
So, we have now seen how our web service can be used by curl to download a remote file, to print it on the console, or to save it in a local file.
Sending a named resource to the server using the PUT method
To upload a resource in the REST architecture, either the PUT or POST methods should be used. The PUT method is used when the client knows where the resource should be stored, in essence, what will be its identifying key. If there is already a resource that has this key, that resource will be replaced by the newly uploaded resource:
- Type the following command into a console:
curl -X PUT http://localhost:8080/datafile.txt -d "File contents."
- In that console, the prompt should appear immediately. Meanwhile, on the other console, the following line should be printed:
Uploading file "datafile.txt" ... Uploaded file "datafile.txt"
This command mimics the request to send a file to the server, with the client specifying the name of that resource, so that if a resource with that name already exists, it is overwritten.
- You can use curl to send the data contained in a specified local file in the following way:
curl -X PUT http://localhost:8080/datafile.txt -d @localfile.txt
For these commands, the URI should specify the name of the file to upload and also the contents of the file, and the response should contain only the status code, which can be 200, 201 (Created), or 500. The difference between 200 and 201 is that in the first case, an existing file is overwritten, and in the second case, a new file is created.
So, we have now learned how our web service can be used by curl to upload a string into a remote file, while also specifying the name of the file.
Sending a new resource to the server using the POST method
In the REST architecture, the POST method is the one to use when it is the responsibility of the service to generate an identifier key for the new resource. Thus, the request does not have to specify it. The client can specify a pattern or prefix for the identifier, though. As the key is automatically generated and unique, there cannot be another resource that has the same key. The generated key should be returned to the client, though, because otherwise, it cannot reference that resource afterward:
- To upload a file with an unknown name, type the following command into the console:
curl -X POST http://localhost:8080/data -d "File contents."
- In that console, the text data17.txt should be printed, and then the prompt should appear. This text is the simulated name of the file, received from the server. Meanwhile, on the other console, the following line should be printed:
Uploading file "data*.txt" ... Uploaded file "data17.txt"
This command represents the request to send a file to the server, with the server specifying a new unique name for that resource so that no other resource will be overwritten.
For this command, the URI should not specify the full name of the file to upload, but only a prefix; of course, the request should also contain the contents of the file. The response should contain the complete name of the newly created file and the status code. In this case, the status code can only be 201 or 500, because the possibility of a file already existing is ruled out.
We have now learned how our web service can be used by curl to upload a string into a new remote file, leaving the task of inventing a new name for that file to the server. We have also seen that the generated filename is sent back as a response.
Deleting a resource using the DELETE method
In the REST architecture, to delete a resource, the DELETE method should be used:
- Type the following command into a console (don't worry—no file will be deleted!):
curl -X DELETE http://localhost:8080/datafile.txt
- After typing that command, the prompt should appear immediately. Meanwhile, in the server console, the following line should be printed:
Deleting file "datafile.txt" ... Deleted file "datafile.txt"
This command represents the request to delete a file from the filesystem of the server. For such a command, the URL should specify the name of the file to delete. No additional data needs to be passed, and the only response is the status code, which can be 200, 404, or 500. So, we have seen how our web service can be used by curl to delete a remote file.
As a summary, the possible status codes of this service are as follows:
- 200: OK
- 201: Created
- 404: Not Found
- 500: Internal Server Error
Also, the four commands of our API are as follows:

Sending an invalid command
Let's see the behavior of the server when an invalid command is received:
- Type the following command into a console:
curl -X GET http://localhost:8080/a/b
- In that console, the prompt should appear immediately. Meanwhile, in the other console, the following line should be printed:
Invalid URI: "/a/b"
This command represents the request to get the /a/b resource from the server, but, as our API does not permit this method of specifying a resource, the service rejects the request.
Examining the code
The main function contains the following statements:
HttpServer::new(|| ... )
.bind(server_address)?
.run()
The first line creates an instance of an HTTP server. Here, the body of the closure is omitted.
The second line binds the server to an IP endpoint, which is a pair composed of an IP address and an IP port, and returns an error if such a binding fails.
The third line puts the current thread in listening mode on that endpoint. It blocks the thread, waiting for incoming TCP connection requests.
The argument of the HttpServer::new call is a closure, shown here:
App::new()
.service(
web::resource("/{filename}")
.route(web::delete().to(delete_file))
.route(web::get().to(download_file))
.route(web::put().to(upload_specified_file))
.route(web::post().to(upload_new_file)),
)
.default_service(web::route().to(invalid_resource))
In this closure, a new web app is created, and then one call to the service function is applied to it. Such a function contains a call to the resource function, which returns an object on which four calls to the route function are applied. Lastly, a call to the default_service function is applied to the application object.
This complex statement implements a mechanism to decide which function to call based on the path and method of the HTTP request. In web programming parlance, such a kind of mechanism is named routing.
The request routing first performs pattern matching between the address URI and one or several patterns. In this case, there is only one pattern, /{filename}, which describes a URI that has an initial slash and then a word. The word is associated with the filename name.
The four calls to the route method proceed with the routing, based on the HTTP method (DELETE, GET, PUT, POST). There is a specific function for every possible HTTP method, followed by a call to the to function that has a handling function as an argument.
Such calls to route mean that the following applies:
- If the request method of the current HTTP command is DELETE, then such a request should be handled by going to the delete_file function.
- If the request method of the current HTTP command is GET, then such a request should be handled by going to the download_file function.
- If the request method of the current HTTP command is PUT, then such a request should be handled by going to the upload_specified_file function.
- If the request method of the current HTTP command is POST, then such a request should be handled by going to the upload_new_file function.
Such four handling functions, named handlers, must of course be implemented in the current scope. In actuality, they are defined, albeit interleaved with TODO comments, recalling what is missing to have a working application instead of a stub. Nevertheless, such handlers contain much functionality.
Such a routing mechanism can be read in English, in this way—for example, for a DELETE command:
After all of the patterns, there is the call to the default_service function that represents a catch-all pattern, typically to handle invalid URIs, such as /a/b in the previous example.
The argument of the catch-all statement—that is, web::route().to(invalid_resource), causes the routing to the invalid_resource function. You can read it as follows:
Now, let's see the handlers, starting with the simplest one, as follows:
fn invalid_resource(req: HttpRequest) -> impl Responder {
println!("Invalid URI: \"{}\"", req.uri());
HttpResponse::NotFound()
}
This function receives an HttpRequest object and returns something implementing the Responder trait. It means that it processes an HTTP request, and returns something that can be converted to an HTTP response.
This function is quite simple because it does so little. It prints the URI to the console and returns a Not Found HTTP status code.
The other four handlers get a different argument, though. It is the following: info: Path<(String,)>. Such an argument contains a description of the path matched before, with the filename argument put into a single-value tuple, inside a Path object. This is because such handlers do not need the whole HTTP request, but they need the parsed argument of the path.
Notice that we have one handler receiving an argument of the HttpRequest type, and the others receiving an argument of the Path<(String,)> type. This syntax is possible because the to function, called in the main function, expects as an argument a generic function, whose arguments can be of several different types.
All four handlers begin with the following statement:
let filename = &info.0;
Such a statement extracts a reference to the first (and only) field of the tuple containing the parameters resulting from the pattern matching of the path. This works as long as the path contained exactly one parameter. The /a/b path cannot be matched with the pattern, because it has two parameters. Also, the / path cannot be matched, because it has no parameters. Such cases end in the catch-all pattern.
Now, let's examine the delete_file function specifically. It continues with the following lines:
print!("Deleting file \"{}\" ... ", filename);
flush_stdout();
// TODO: Delete the file.
println!("Deleted file \"{}\"", filename);
HttpResponse::Ok()
It has two informational printing statements, and it ends returning a success value. In the middle, the actual statement to delete the file is still missing. The call to the flush_stdout function is needed to emit the text on the console immediately.
The download_file function is similar, but, as it has to send back the contents of the file, it has a more complex response, as illustrated in the following code snippet:
HttpResponse::Ok().content_type("text/plain").body(contents)
The object returned by the call to Ok() is decorated, first by calling content_type and setting text/plain as the type of the returned body, and then by calling body and setting the contents of the file as the body of the response.
The upload_specified_file function is quite simple, as its two main jobs are missing: getting the text to put in the file from the body of the request, and saving that text into the file, as illustrated in the following code block:
print!("Uploading file \"{}\" ... ", filename);
flush_stdout();
// TODO: Get from the client the contents to write into the file.
let _contents = "Contents of the file.\n".to_string();
// TODO: Create the file and write the contents into it.
println!("Uploaded file \"{}\"", filename);
HttpResponse::Ok()
The upload_new_file function is similar, but it should have another step that is still missing: to generate a unique filename for the file to save, as illustrated in the following code block:
print!("Uploading file \"{}*.txt\" ... ", filename_prefix);
flush_stdout();
// TODO: Get from the client the contents to write into the file.
let _contents = "Contents of the file.\n".to_string();
// TODO: Generate new filename and create that file.
let file_id = 17;
let filename = format!("{}{}.txt", filename_prefix, file_id);
// TODO: Write the contents into the file.
println!("Uploaded file \"{}\"", filename);
HttpResponse::Ok().content_type("text/plain").body(filename)
So, we have examined all of the Rust code of the stub of the web service. In the next section, we'll look at the complete implementation of this service.