Recently, we've released a Node.js CLI package that allows you to quickly bootstrap an application that uses a Next.js frontend with a Rust backend that uses Axum, a popular Rust web framework with easy-to-use, uncomplicated syntax.
The app we'll be building will be a notes app with a login portal that can register users, as well as log in users and reset passwords and logged in users will be able to view, create, update and delete notes. This article will focus more on the Rust (backend) side and will assume that you have knowledge of using React.js/Next.js for your frontend.

The repo containing all the code can be found here.
Getting Started
We can simply get started by running the following command (note: Looking to bootstrap your frontend so you can focus on the backend? Feel free to skip to the frontend section):
Once you press enter, it should ask you for a name - feel free to enter any name you want here, and then it should start installing Rust automatically for you and bootstrap an application that uses Next.js (with Typescript because of the additional flag) as well as Rust for the backend, along with relevant npm commands to allow us to quickly get started with developing both the back and frontend. The framework we will be using for the backend is Axum, which is a highly performant, flexible framework with simple syntax and is highly compatible with tower_http
, which is another extremely strong library for creating middleware.
shuttle is a cloud development platform that simplifies the deployment of your apps. What makes it stand out is its "infrastructure-from-code" approach, allowing you to define your infrastructure directly in your code without the need for complicated consoles or external yaml/config files. This approach not only improves the clarity of your code but also provides compile-time assurance that you'll get what you asked for. Need a Postgres instance? Just add an annotation and you're good to go. shuttle also supports secrets (environment variables), static file folders and state persistence.
Next, we'll want to install sqlx-cli
which is a great command line app for managing our database migrations. We can install this by simply running the following command:
If we navigate to our backend directory in the project folder, we'll be able to create our database migrations by using sqlx migrate add schema
which will add a migrations folder (if you don't have one already) with a file that follows the naming convention of <timestamp>_schema.sql
because we named our migration "schema".
This SQL file should have the following:
As a side note, we'll be running these migrations automatically but if you want to run them manually, you can use sqlx migrate run --database-url <database-url>
. The reason why we can do this is that we've set up our SQL file to be idempotent - this simply means that if the table already exists, we won't attempt to create it again. We drop the sessions table to force users to log back in once the app re-uploads as their cookies won't work.
Now that we're set up, let's get started!
Frontend
For this app, we'll need several pages:
- Pages for logging in and registering
- A page for users to be able to reset forgotten passwords
- A dashboard page to show records
- Pages for editing and creating new records
You can clone the frontend-only example for this article by cloning it like below (note: If you skipped straight to this section, you'll need to make sure you have Rust, cargo-shuttle
and sqlx-cli
installed and create the migrations from the previous section):
The cloned repository will have an already pre-setup src
directory that looks like this:

The components folder contains two layout components that we nest our page components inside of and a modal for editing records that we use in the Dashboard index page. The pages folder includes the relevant page components we'll be using in our app (where the file name indicates the route).
We use TailwindCSS for the CSS, as well as using Zustand for easy, bare-bones state management that doesn't require much boilerplate.
When the user logs in, they should see something like this if there are any messages:

Once we build the backend, the user will be able to register, log in (using cookie session-based authentication) and view, create, edit and delete their own messages by using the frontend. Users will also be able to reset their password if they've forgotten it by entering their email.
Looking to make your own frontend? Feel free to consult the GitHub repo to check how the API calling and state management is set up.
Now that we're done with this part, we can move on to writing the backend!
Backend
If you navigate to the backend folder, you should see a single file called main.rs
with a function in it that creates a basic router with one function that returns "Hello, world!". We'll be using this file as the entry point for our application and then creating other files that we'll import call in our main function.
You'll want to make sure you have the following contents in your Cargo.toml file:
Once we're done with this, we will want to set up our main function so that we can use the shuttle_shared_db
and shuttle_secrets
crates to get a free shuttle-provisioned database and be able to use secrets, like so (as well as setting up a crude implementation of cookie-based session storage):
Now we can start creating our router! Let's make a file called router.rs
in the src
folder of our backend directory. We will put the bulk of our router code in here and then import the function we'll be using to make the final router into our main file once we're ready.
Let's open up our router.rs
file and create a function that returns a router with routes for registering and logging in:
As you can see, all we need to do is write the functions that we'll be using in our router and include them in the router. We can also use multiple request methods in one route by simply chaining the methods (more on this later on once we finish writing all of the routes).
As you can see, we hash the password, set up a query via SQLx to create a new user and then if it's successful, return a 402 Created status code - if it's not successful, return a 400 Bad Request status code to indicate that something's wrong.
Pattern matching is an extremely strong form of exhaustive error handling in Rust, and it comes in many forms: We can use if let else
and let else
, both of which utilise pattern matching as you will see later on.
As you can see, the requests simply take a JSON request body of whatever type we've decided to give it (so because we have given both a type of axum::Json
for the request body, it will only accept requests with a JSON request body of "username" and "password"). Structs used in this way must implement serde::Deserialize
as we need to be able to pull the data from JSON, as well as the JSON request argument itself being the final argument we pass into the route function.
You may notice we've used a struct called PrivateCookieJar
in our login request. This is simply a way to be able to automatically handle HTTP cookies without having to explicitly set headers for them - to be able to propagate any changes in them however, we need to set them as a return type and return the changes. When the user wants to access a protected route, all we need to do is grab the value from the cookie jar and validate it against the session IDs we've saved in our database. Because we're using a private cookie jar, any cookies saved on the client side will be encrypted with the key we've created in our initial struct which will generate a new key each time we start our app up.
Now that we've added a route to be able to log in, let's have a look at adding a route for logging out as well as some middleware for validating a session:
As you can see above - for the logout route we simply attempt to destroy the session and then return the cookie removal, and then for the validation route we attempt to get the session cookie and then make sure the cookie session is valid in our database.
Let's have a look at creating basic CRUD functionality for some records in our database. We'll want to make a struct that utilizes sqlx::FromRow
so that we can easily pull records from our database, like so:
Then we can simply just use sqlx::query_as
while typing the variable as a vector of the struct to get what we want, like so:
As you can see, all we need to do is simply use the query with our database connection while making sure the struct we've typed our return as has the sqlx::FromRow
derive macro on it. Using what we know here, we can also quite simply make our other routes like so:
Now we've created all of our basic functionality for our web app! However, we are missing one last thing before we combine all of our routes. What if a user wants to reset their password? Surely we should have a self-service route for that? Let's make that route now.
We'll also want to use a Secrets.toml
as well as Secrets.dev.toml
file at the Cargo.toml
level to add secrets that we'll need. We should use the following format for this:
Now our all of our apps are done, we should probably have a look at creating the router out of all of our apps. We can simply nest our routing and include the middleware by appending it to our protected routes, like so:
As you can see, we can create an API router by simply defining two routers, each with their own routes (one router with protected routes that will only run if the session is validated), and then simply returning a router that has a health check route, nests our two previous routes and then adds the CORS and app state to the router.
Our final router function can simply then look like this:
We will use this function in our initial entry point function in our main function (in lib.rs
) to generate the router, like so:
Note that for importing functions from files, you need to define them in your lib.rs
file if they're in the same file directory like above (use router
); this also applies to trying to import functions from one file into another file that is also not the main entrypoint file. This link explains it quite well if you need clarification.
Now we're done with the programming section! We can finally look at deploying.
Deployment
Deploying with shuttle, thankfully, is quite easy - you just need to run npm run deploy
in the root directory of your project and if there aren't any issues, you should be able to see that shuttle has launched your app and it will return a list of information about your deployment followed by the database connection string for your shuttle-provisioned database. If you need to find this database string again, you can run shuttle resource list --show-secrets
in the backend directory of your project and it will find it for you.
You may wish to run cargo fmt
and cargo clippy
before you deploy as any warnings or errors will appear while your web service is being built. If you don't have either of these components, you can use rustup component add rustfmt
and rustup component add clippy
respectively - both of these tools are a great addition to any Rust developer's toolbox and I would highly recommend using both of them.
Finishing Up
Thank you for reading my article! I hope this has given you some insight into how building a Rust webservice can be made easily and without hassle. Rust has evolved significantly in the past couple of years, making it much more approachable for new learners. If you've been hesitant to try it out, now is a great time to give it a go and see for yourself how powerful and user-friendly Rust can be.