Creating an Axum Web Server in Rust is easy!
By Flo Woelki
Summary
## Key takeaways - **Axum Router Chains Routes Ergonomically**: We are chaining route calls to define the API endpoints and each route maps a URL path and an HTTP method to a specific handler function that will just process the request. [01:24], [01:47] - **Centralize Errors in APIError Enum**: We are doing this because I want to centralize the API errors in one enum and each variant just represents a class of error you want to expose from handlers. This just keeps the handlers really clean and obviously error handling consistent as well. [05:24], [05:48] - **IntoResponse Turns Errors to HTTP**: Axum uses the into response trait to turn return values into actual HTTP responses. By implementing into response for API error, any handler that returns result API error automatically knows how to turn an API error into a proper HTTP response. [06:42], [07:12] - **Path Extractor Handles Dynamic IDs**: This ID is just a dynamic path parameter that we are going to extract and Axum just knows to capture whatever value is in that part of the URL and make it available to the handler itself via the path extractor. If the value cannot be parsed for whatever reason, Axum will just automatically reject the request with a 400 bad request error. [13:26], [14:35] - **Tower oneshot Tests Without Network**: This oneshot function just comes from the tower service extension trait and it just allows us to send a single request directly to our app's router and get responses back. It's also quite clear that this is much more reliable and faster for testing than starting a real web server and then sending the HTTP request over the network, so with this oneshot function, there's no network overhead at all. [17:22], [17:58]
Topics Covered
- Axum Chains Routes Ergonomically
- Tokio Macro Hides Async Setup
- Enums Centralize API Errors
- IntoResponse Turns Errors to JSON
- Oneshot Tests Without Network
Full Transcript
Creating a web server in Rust can be relatively easy, especially when using web frameworks such as Axom. If you
don't know, Axom is a web framework in Rust specifically that just lets you to create a web server with a focus on ergonomics and modularity. Therefore, in
this video, we are going to check out Axim and learn how to basically use it to create a simple web server. So, let's
go. Okay, I think it should be quite clear that we do have Axim installed here and obviously we do have a new Rust project. Additionally, I will also add a
project. Additionally, I will also add a few tests here to just test the basic functionality of our really simple web server. But for now, let's just get
server. But for now, let's just get started with Axom. So, what we're going to have is we are going to have a new function here which we will call create app. And this will actually return a app
app. And this will actually return a app or it will actually set up the router for us. So, what we're going to say is
for us. So, what we're going to say is let app equals to this, right? Let's
just create this function here by saying fn and then create app and it will actually return a router. Now this
router is the XM router type or more specifically it is actually the router strct and then in here we can create our router by just saying router and then new and that's basically it. And from
here on we can just create our route. So
for instance we can say dot route and then we can define a path which could be /halth for instance and then we say get which is also a function of axim and here we just say health check which we
will also create in a minute here. Okay,
here what we're actually doing is we are chaining route calls to define the API endpoints and each route maps a URL path and an HTTP method to a specific handler
function that will just process the request. Right? In this case, the path
request. Right? In this case, the path is actually /halth. Then the HTTP method is get and then the handler function is health check which we will not call. We
will just pass in to this get function here. Now clearly the same functions
here. Now clearly the same functions just exist with post and patch for instance right so we could also say post here but we can also say patch for instance or delete right it really
depends on what HTTP method you want to use in this case we're just going to use get hopefully this kind of makes sense okay let's just create this health check function here and what we're going to do
is we are going to say async and then fn health check and then we are going to return something in a minute but for now This should be the basic definition here. Let's just get back to the main
here. Let's just get back to the main function because we still need to complete this main function. And in here we are going to bind a TCP listener on port 3000 on all interfaces. And we are
going to do this through basically using Tokyo. And then we say net and then we
Tokyo. And then we say net and then we use the TCP listener here. And then we say bind which will just bind the TCP listener to a specific address. And here
we're going to use 0.0.0.0.
And then with the port 30,000 then we say await right cuz we want to await the creation of this TCP listener or of this binding for the TCP listener and then we
just use expect here and let's just say fail to bind TCP listener. So we do have an error here because a weight is used inside a non async function. So we do have to mark this function as async. And
then we get another error because we do not have any sort of asynchronous runtime. But we can simply fix this
runtime. But we can simply fix this issue by just saying Tokyo and then main. And this macro just starts the
main. And this macro just starts the async runtime. So we can use async await
async runtime. So we can use async await basically everywhere we want. Tokyo in
this case is just the most popular async runtime in the Rust ecosystem. And this
macro specifically just transforms our async fn main into a regular main function and sets up all the necessary Tokyo runtime components behind the
scenes. So we don't actually have to
scenes. So we don't actually have to take care of all the setup here. Okay.
Now with that in mind, all the errors should be gone. And what we can say now is just a simple print line here. And
then we say server running on. And then
we use local host with the port 3000.
Now obviously Axim has not started this server yet. We only have kind of a TCP
server yet. We only have kind of a TCP listener that is just bound to the port 3000 in this case. And we do have a router. But the web server of Axim is
router. But the web server of Axim is not started yet. And we can actually serve this server by just saying axom and then surf. And then it expects a listener and a make service. Now the
listener is simply just this TCP listener here. And the make service can
listener here. And the make service can be for instance our app which will be the router. So we will just say app
the router. So we will just say app here. Now then we can easily say await
here. Now then we can easily say await and then we will also have some basic error handling. So we just say expect
error handling. So we just say expect here and then we say fail to start server. Quite simple. And now we have a
server. Quite simple. And now we have a really simple AXM server running. So
here again this AXM serve just connects the TCP listener to our app and starts handling requests. And this way just
handling requests. And this way just runs the server until it is shut down.
Okay, let's just start implementing this health check route here. Now before we actually do this, I want to have some centralizing API errors in one enum. So
what we can easily do is just define an enm up here and then we can say API error for instance and then we can define our variance here. So we can say not found. We can say invalid input
not found. We can say invalid input which will just contain data which will be a string. And then we also define an API error which is the internal error.
You can obviously extend this API error enum here but I'm going to leave it as it is right here with just three variants. Okay, we are doing this
variants. Okay, we are doing this because like I said before, I want to centralize the API errors in one enim and each variant just represents a class of error you want to expose from
handlers. This just keeps the handlers
handlers. This just keeps the handlers really clean and obviously error handling consistent as well. Now an enm is just a custom type that can be one of several possible values which we will
call variance and in this case the variants are not found, invalid input and internal error. What we will also do is we are going to define the derive macro up here and we are going to say
debug. Now the debug trait overall just
debug. Now the debug trait overall just allows us to print these errors for debugging. And now a quick explanation
debugging. And now a quick explanation for these three variants here. The not
found is just basically an error when a resource does not exist. So a four error. Now the invalid input variant
error. Now the invalid input variant here. This is kind of a special variant
here. This is kind of a special variant because it also carries data with it. In
this case a string containing a specific error message. Right? In this case, the
error message. Right? In this case, the variant just means a bad request. And
then we also have the internal error which should be clear which is just a 500 error. Okay, beautiful. Now let's
500 error. Okay, beautiful. Now let's
just continue with the into response implementation here. So what we can say
implementation here. So what we can say is imple into response and then we say for API error. Now let me quickly explain what is actually going on here.
Now Axim uses the into response trade to turn return values into actual HTTP responses. And by implementing into
responses. And by implementing into response for API error, any handler that returns result API error automatically knows how to turn an API error into a
proper HTTP response. Now, as already said, into response is just a trait and a trait is pretty similar to an interface in other languages. Here
specifically, we're just providing the specific implementation of this trait for our custom API error type. And yes,
we can implement trades and methods for enims, which is absolutely wonderful.
But before we continue with that, thank you so much Savala for sponsoring this video. Savala is the true all-in-one
video. Savala is the true all-in-one platform as a service that kind of allows you to deploy apps, databases, and static sites without dealing with complex infrastructure. The best part is
complex infrastructure. The best part is that there are no annoying artificial limits, so no restrictions on parallel builds, no caps on team members, and of course, you only pay for what you use.
They run on Google Kubernetes Engine across 25 regions with Cloudflare's global network for speed and reliability. Additionally, they provide
reliability. Additionally, they provide support from real human developers who understand your problems which can be incredibly helpful. Furthermore, the
incredibly helpful. Furthermore, the developer experience is just seamless.
You can push to Git, receive automatic builds, enjoy instant previews, and benefit from one-click templates.
Whether you are building the next big app or just want reliable hosting for your site projects, Savala handles it all and scales with you. They are
offering new users $50 in free credits to get started. So, feel free to check out the link in the description. Again,
thank you so much to Savala for sponsoring this video. Now, let's just get back to the code. Okay, what we get here is now an error because the into respawns function is missing. So what we
can say is just into respawns and then it returns a respawns or an XM respawns here. Now what we can say is we can just
here. Now what we can say is we can just say match self and this will then return a status and an error message here. Now
match in this case is pretty similar to a simple switch but there are some key differences. In this case self refers to
differences. In this case self refers to the specific instance of the enum that this function is being called on. So
that really means that we can say API error and then we can match or differentiate implementations or return values for each individual variant here.
So we can use not found and then in this case we just return status code not found. Now for the error message
found. Now for the error message specifically we are just going to say data not found and then we are going to say two string here and then we can obviously match the other variants as
well. So we can use the API error and
well. So we can use the API error and then invalid input. Then we use message here. I'm going to explain what is going
here. I'm going to explain what is going on in a minute. And then we can return the status code bad request and also the message. Right? So this ARM just runs
message. Right? So this ARM just runs when we have an invalid input and destructures the variant which just means that it pulls out the string data
inside it and binds it to the variable msg. So we can actually use it as return
msg. So we can actually use it as return value here or just do other things with it. Now this specifically is called
it. Now this specifically is called pattern dstructuring. So it pulls out
pattern dstructuring. So it pulls out the string value stored inside the invalid input variant. So then in the end we can use this value. All right.
Quite cool. Let's just do the same thing here with the internal error API error variant here. And then we're going to
variant here. And then we're going to return the internal server error status code. And then we're going to say for
code. And then we're going to say for the error message internal server error.
Okay. Quite cool. We now have a status and an error message. So let's just use it. So what we can say is we can just
it. So what we can say is we can just use the body here. So let body. So we're
going to create an HTTP response body here. And then we're going to say JSON
here. And then we're going to say JSON from axom JSON. And then we're going to use the JSON macro here fromert. And in
here we are going to say error and then error message. Right? So this specific
error message. Right? So this specific JSON axom extractor just sets the header content type and then application/json automatically and serializes the web
data into JSON format. Right? Quite
cool. And then in the end what we can do is just return the pupil status and body. And then we need to transform this
body. And then we need to transform this pupil into a respawn. So we just say into respawns here, right? And that's
basically it. So what we can do now is we can implement a health check function here or the health endpoint. Now again
this is just a tiny endpoint that tells us if the server is up and running. We
are going to define the return type here as imple and then into respawns which just means that anything that can be turned into a respawns will be returned
here and here we are just going to return JSON right remember the axim extractor here and then we are using thisert macro here again and then we're going to say status okay for instance
and then we have also message server is running right quite simple I'm not going to over complicate things here with this specific health check and remember we are only going to use this API error
enim for actual errors. What you could also do is kind of centralize this format here, right, with the status and message in this case to just have one format for all the API responses. But in
this case, I think it should be fine for now. Okay, let's just create another
now. Okay, let's just create another endpoint here and we are going to call this list users and this will be obviously an asynchronous function. And
here what I want to accomplish with this list users function is just to simulate a failure to just demonstrate error handling. So it will return a result and
handling. So it will return a result and this result will be JSON and then value and then also API error. Now this result is just a standard Rust enum used for
operations that can either succeed or fail. And we do have two variants here
fail. And we do have two variants here which is okay and it contains the success value and the error which is contains the error value. And we are going to possibly implement this in the
future here. For now, we are just going
future here. For now, we are just going to return an error here with an internal API error. So we just say error here and
API error. So we just say error here and then we use API error internal error and AXM will actually catch this and use the into response implementation to generate
the JSON response which is just quite handy and let's just define this list users function or the endpoint here. So
what we can do is just route and then we use / users and here this will be a get http method and then we will just use the handler function list users right
and there we go we've defined another route for our router let's just define one more route which we will call / users and then we will use this id here
and it will call another get http method which will be the get user function. Now
this ID is just a dynamic path parameter that we are going to extract and Axim just knows to capture whatever value is in that part of the URL and make it just
available to the handler itself via the path extractor which we are going to use right now by defining this get user function. So we can use async fn get
function. So we can use async fn get user here and this will obviously return the same thing. So result and then JSON and then value and then API error and
then we do have this weird syntax here.
So path id and then this will be of type path and then U32.
Now path extractor just pulls the ID from the URL and passes it to U32. And
this specific dstructuring syntax here this path ID and then path U32 just extracts the inner ID directly. And if
the value cannot be passed for whatever reason, XM will just automatically reject the request with a 400 bad request error. Okay. Now what we can do
request error. Okay. Now what we can do is just to have a basic check here is we can check if the ID is greater than 100.
And for now we are just going to return an error and then we're going to say API error not found. And then if the ID is below that we will just return okay and then JSON. And then we will use JSON
then JSON. And then we will use JSON again here. And then we will have the ID
again here. And then we will have the ID which will be the ID and also the name will be for instance user. Okay. And
that was basically it. And if we now run cargo run everything should work fine.
Right. So if we hit the health endpoint here we see that the server is running and we get a stat that is okay. Now
let's just get a few users. So we are trying to just use / users here and we do get an error internal server error here which is as expected and this is the format we actually use in our into
response implementation and then we are trying to get the user with the ID 14.
So this works as expected as well and if we want to find a user with an ID above 100 we actually see the error data not found. Now you can make these things
found. Now you can make these things more dynamic but I will do a few more videos just with this specific setup here. So we probably will refactor a few
here. So we probably will refactor a few things but let's just add a few tests here to actually verify the functionality of our XM server and what we're going to do is we are going to say
mod tests here and then we are going to use cfg so the cfg macro and then test right so they are only compiled when we are running the tests here and the goal
is really that we spin up the router in memory and then send synthetic requests through it so let's just define our first test here so we are going to say
async Fn and then test health check. Now
because we do use Tokyo here, we also have to mark this as a Tokyo test, right? Quite simple. And then let's just
right? Quite simple. And then let's just define the router by just saying create app. Now instead of using this import
app. Now instead of using this import here, we just could use for instance use super and then this star, which just means that it brings all the items from the parent module. So basically our main
code into the scope of this test module.
So what we can do now is construct a get request to /halth and we can actually do this by saying let request and then we're going to say request builder and then we define the URI which will be /
health and then we just use an empty body. So body empty and then we say
body. So body empty and then we say unwrap here. Right? So we use body empty
unwrap here. Right? So we use body empty since the get has no body and also we do not really set the HTTP method explicitly here because request builder just defaults to get in this case. And
then we need to send the request into the router without opening a real port here. And we can actually do this by
here. And we can actually do this by just using app and then oneshot. Right?
This will be a function of the XM router. And here we use the request and
router. And here we use the request and then we sayawaway.unrap unwrap and this will actually return a respawn. So what
is actually going on with this oneshot function now oneshot just returns a respawns we can actually inspect. It's
important to note here that this oneshot function just comes from the tower service extension trait and it just allows us to send a single request directly to our app's router and get
respawns back. And this is all done in
respawns back. And this is all done in memory which is really handy. Therefore,
it's also quite clear that this is much more reliable and faster for testing than starting a real web server and then sending the HTTP request over the
network. So with this oneshot function,
network. So with this oneshot function, there's no network overhead at all. Now
then what we can do is just assert equal here and we can use respawns status. So
we expect a 200 okay status here and then we use status code and then okay.
Now let's just read the body here as well. So we say let body and then
well. So we say let body and then respawns.colctawait.
respawns.colctawait.
ununwrap right. So we read the full response body here into memory and then pass it as JSON. And to pass it as JSON we will actually useert and then JSON
and then from slice and then we use the reference to body to bytes. And then we also say unwrap here and this will return a JSON value. So we will say JSON
and then define value right here. Now
this overall just converts the HTTP response bite into a structured JSON value. And also this two bytes function
value. And also this two bytes function converts the body into a bytes object.
And then we take a reference from that because from slice does not want to own this. Okay. And then we can just verify
this. Okay. And then we can just verify the shape and content of the JSON here.
So we can say assert equal and then JSON status. And then we say okay. And let's
status. And then we say okay. And let's
just do this with the message here as well. And then we say server is running.
well. And then we say server is running.
Now this should work as expected. Let's
just do the same thing with the API error into response. So we might want to test this as well. So let's just define a new Tokyo test here. And then we say
async function test API error into response. And then we're going to define
response. And then we're going to define a simple vector where each individual element is just a pupil which contains our error and the HTTP status we expect
from the into response implementation.
So we can say test cases here and then we define a new vector and in here we're going to define a pupil where we are going to define the API error which will be not found and the status code will be
not found. And we're going to do the
not found. And we're going to do the same thing for the internal error here.
Right? And then we are also missing the invalid input. So we can say API error
invalid input. So we can say API error invalid input. And here we are just
invalid input. And here we are just going to say bad data for instance to string. And then we have the status code
string. And then we have the status code bad request. Okay, quite cool. Now we've
bad request. Okay, quite cool. Now we've
defined the test cases here and what we can do is we can just use a simple for loop. So we say for error and then
loop. So we say for error and then expected status in the test cases and what we're going to do here is for each individual case we are going to turn the
error into a response and then just check the status code. So what we can say is let response and then error into response. Right? Again we are testing
response. Right? Again we are testing here the into response functionality to really test the API errors we've defined inside our enim and then we can say
assert equal response status and then the expected status right and that's basically it now we've defined successfully two tests so let's just run these tests here by just saying cargo
test and as we can see both tests actually pass which is really cool now that should be it for now and in the future we will actually extend this web server If you are curious about learning
how to build a TCP echoserver from scratch in Rust, feel free to check out this video here. Anyway, thank you so much for watching. Have a lovely day and bye-bye.
Loading video analysis...