Deploying a Next.js front-end with a Rust API, in one go

A look at implementing Next.js in Rust API using Shuttle.

·

8 min read

As large tech companies are starting to take serious consideration into investing in Rust, more and more uses are being found for a language that prioritises safety, blazing speed and efficiency. Google's KataOS and Amazon's new Linux distribution Bottlerocket are evident examples of this. However, a more practical application that also benefits would be web servers written in Rust.

In this article, we'll be discussing how to implement a Javascript front end (specifically Next.js for this article, but you could use any framework in place) with a Rust API back-end with no hassle and deploying through shuttle, which is an open-source project aimed at bringing Rust to the native cloud through creating a Rust-native cloud development platform and has a generous free tier. They recently did a workshop aimed at Javascript developers looking to get into Rust, which you can find here.

Before we start though, let's have a look at why you would want to use Rust. We've already said before that Rust is said to prioritise safety, blazing speed and efficiency but what does that mean in terms of building a back end? We could interpret this as the following:

  • Schemas are practically already built into Rust because you're required to define what the response/request type should look like before you can use it in a route (although complex validation like regex will still be left up to you).

  • API response time will be extremely quick because of how quick Rust code is.

  • If you use Typescript for your back end, you'll appreciate not having to mess around with configs/bundlers before you even get started.

Without any ado, let's get started. You can click here to check out a live deployment of what we'll be making.

Starting Out

To start with, let's create a Next.js project. If you're just looking to explore and want to quickly scaffold a page to be able to move onto the deployment itself, you can use the following command:

npx create-next-app

If you want to use a pre-made Next.js example so you can see how this would look like in a real-world example, you could use git clone:

git clone https://github.com/joshua-mo-143/nextjs-rust-example.git

Once you've created (or cloned) a front-end that you'd like to use, it's time to get to work on the back-end API.

Setting up Rust

Getting started with Rust is very easy. You can install it on Linux or WSL (Windows Subsystems for Linux) by using the following command:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

If you're on Windows and don't have WSL, you can find the install page here.

However you install it, you'll also get Rust's package manager called Cargo, which is like NPM for Rust. Cargo allows you to install Rust's packages which are called "crates".

For the back end part, because we'll be serving the web server through shuttle, we will need to install their CLI which we can do with the following command:

cargo install cargo-shuttle

The installation may take a while depending on your Internet connection, so feel free to grab a drink while you wait. You will also want to log onto their website here through GitHub and make sure you have your API key as you will need to log in on the CLI with your key before you can make any projects.

Once the installation is done, you can start a shuttle project with the following command below (run this in your Next.js project at the packages.json level):

cargo shuttle init

This will then prompt you to input the name you want to use for your project. Once you've chosen your name and directory it'll ask you what framework you want to use - we'll be using Axum, which is easy to use and build on with uncomplicated syntax. For this project, our API will be named "shuttle-next" but feel free to change it as required.

Let's take a look at the generated files:

// Cargo.toml - this file is used by Cargo to manage dependencies, your file entrypoint/where your main functions are, etc.
[package]
name = "shuttle-next" // name of your package
version = "0.1.0" // the version of your package
edition = "2021"
publish = false

[dependencies] //installed packages
shuttle-runtime = "0.24.0"
shuttle-axum =  "0.24.0"
tokio = "1.28.2"
axum = "0.6.18"
// imports go here
use axum::{routing::get, Router};
use sync_wrapper::SyncWrapper;

// an async function that simply returns "Hello, world!" when you hit the endpoint
async fn hello_world() -> &'static str {
    "Hello, world!"
}

// this is a proc macro, which allows the Shuttle service to work
#[shuttle_runtime::main]
async fn axum() -> shuttle_axum::ShuttleAxum {
// set up a router that sets a new route of "/hello" that uses the hello_world function as above
    let router = Router::new().route("/hello", get(hello_world));
// starts server if the result is OK
    Ok(router.into())
}

Now that we've generated all the files we need to get the service running, let's get into how to integrate our front end into our web server. You'll want to use cargo build to compile all of the dependency packages first and then use cargo shuttle run to run the local deployment.

Setting up our API

Rust is typically typecasted as an extremely difficult language to learn, and not without reason. However, thankfully Axum already has SPA support built-in and shuttle has a crate to allow us to easily add a static folder. However, we'll need to install some new dependencies, so let's quickly get those installed:

// navigate to your Rust directory first before installing
cargo add tower-http --features fs
cargo add shuttle-static-folder

Now all you need to do is to replace the code in your lib.rs file like so:

use axum::{Router};
use sync_wrapper::SyncWrapper;
use std::path::PathBuf;
use axum_extra::routing::SpaRouter;
use tower_http::services::{ServeFile, ServeDir};

#[shuttle_service::main]
async fn axum(#[shuttle_static_folder::StaticFolder(folder = "out")] 
public_folder: PathBuf)
    -> shuttle_service::ShuttleAxum {

    // initialise the router using the spa router function
    // the path given resolves to "/out" because of the arg passed in
    let router = Router::new()
        .nest_service(
                "/", ServeDir::new(public_folder).not_found_service(
        ServeFile::new(public_folder.join("index.html")));

    Ok(sync_wrapper)
}

Now the web server is pretty much done, but we still need to add the actual static files. Let's have a look at doing that below.

Integrating our front and back end

At the moment, we currently have our front end and our back end in the same repo which makes things very simple (the Rust project at this point should be a subfolder of the root directory in your Next.js project), but there are currently two problems:

  • Next.js doesn't automatically export HTML files by default which you need on the Rust end, so we'll need to quickly set that up (as well as one-line deploy)

  • There's no reason for you to keep having to build assets just to work on the back end, so we need to set up our scripts to deploy in one command as well as run both back and front-end servers for easy development.

Thankfully, both of these issues are fairly easy to solve.

To ensure that Next can export HTML files, add the following to your next.config.js file:

module.exports = {
// -- your other config options if you have any 
images: {
    unoptimized: true
  },
  trailingSlash: true,
// -- your other config options if you have any 
}

This will allow Next to export HTML files, which is great for us as the API will need the HTML files to be able to display the web pages.

Now we can create a "deploy" command to use next export and a new output directory and then deploy to shuttle, like so:

// Build command is written separately from the deploy command in case of debugging if the deploy process breaks
// feel free to use npm run or another relevant package manager if not using yarn
"scripts": {
// ... your other package.json scripts
"build": next build && next export -o shuttle-next/out/
"deploy": npm run build && cargo shuttle deploy --working-directory shuttle-next
// ... your other package.json scripts
}

Now that that issue has been sorted, we can quickly set up both the back and front end development servers locally in one command by using an npm package called concurrently, which you can download using the relevant command:

// use the relevant package manager installer to install as a dev dependency
npm i -D concurrently
yarn add -D concurrently

Then you can simply update your dev script like below:

"scripts": {
// ... your other package.json scripts
"dev": "concurrently new \"next dev\" \"cargo shuttle run --working-directory <name-of-your-shuttle-project>\"",
// ... your other package.json scripts
}

Running "npm run dev" should now boot up a local server at http://localhost:8000 for your web server and http://localhost:3000 for your Next.js front end. This will allow both parts of your project to run and should save some headaches if you are trying to call the API from your front end and it's not working, as well as saving some space in your terminal tabs.

Finishing Up

Assuming there are no issues, you should be able to deploy your Rust server to shuttle with npm run deploy and it should allow you to view your deployed web server with your front-end assets, as well as any routes you've decided to add. That brings us to the end of this article - I hope you've gotten a better understanding of how Rust can be applied easily and without hassle in a web development context. The starter example in this tutorial is fairly barebones, but expanding on it is fairly simple and will be covered in future articles.

Thank you for reading! If you are looking for more information on how you can get the most out of shuttle, you can find their docs here which include tutorials on how you can build various web services in Rust like a link shortener as well as basic JWT authentication.