TLDW logo

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...

Loading video analysis...