Making a Simple API

published 02/22/2022 • 3m reading time • 340 views

Warning

By the time you are reading this, the library versions used are likely very out of date.

In this article, I will show you how to make a simple JSON API with afire. This article assumes you have some rust experience already. If not check out the Rust Book here.

Planning

For this example I will be making an api to fetch random quotes. So, let’s find some quotes first. I found a quote dataset on kaggle, download it here.

Now to plan what API endpoints we want. To start, let’s keep it simple and use these endpoints:

  • Get a random quote
  • Get a quote by ID

Building

Setup

So first create a new cargo project and add the following dependencies to Cargo.toml. Also take the quotes we downloaded from before and add the quotes.json file to the project root.

[dependencies]
afire = "3.0.0-alpha.3"
anyhow = "1.0.86"
rand = "0.8.5"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0"

JSON Parsing

Let’s define a quote struct to store the author and quote text. I used renamed all of the fields to pascal case because that is what was used in the above dataset.

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Quote {
    quote: String,
    author: String,
}

Putting it Together

Now we will add to our main function to load the quotes from the file and parse them. Then we can start work on the Web Server.

Tip

When working with rust use statements, I usually organize them in the following order. With newlines separating them.

  • STD Imports
  • External Crate Imports
  • Internal Imports

Now read the quotes.json file as a string and pass it to serde_json::from_str to deserialize to a Vec<Quote>. Make sure to throw in some nice little print statements! These look cool and they can help with debugging; For example, my code loaded 48391 Quotes and if yours loaded any less, you now know to look into that.

use std::fs;

use anyhow::Result;

const QUOTE_FILE: &str = "quotes.json";

fn main() -> Result<()> {
    println!("[*] Starting");

    let raw_quotes = fs::read_to_string(QUOTE_FILE)?;
    let quotes = serde_json::from_str::<Vec<Quote>>(&raw_quotes)?;

    println!("[*] Loaded {} quotes", quotes.len());
    Ok(())
}

Web Server

First thing to do is to import afire::prelude::*, this automatically imports all the important things. Then we can create the server, add the (currently unimplemented) route and start the server.

let mut server = Server::<()>::new("localhost", 8080);

server.route(Method::GET, "/quote", |ctx| Ok(()));

println!(
    "[*] Starting Server {}:{}",
    server.ip.to_string(),
    server.port
);

server.run()?;

If you’re then the server and navigate to http://localhost:8080/quote you will see something like the following.

Internal Server Error :/
Error: not implemented

So let’s fix that!

Implementing the Route

So the first thing to do is to get / generate the index. We will use req.query.get("index") to try to get the index query parameter. This can be matched and parsed into an usize if found. If it’s not found, we can randomly pick an index with the rand crate.

Then index &quotes to get the quote of that index and craft a JSON response! Make sure to set the content type to Content::JSON.

This is the final route code I came up with:

server.route(Method::GET, "/quote", move |ctx| {
    let id = match ctx.req.query.get("index") {
        Some(i) => i.parse::<usize>().unwrap(),
        None => rand::thread_rng().gen_range(0..quotes.len()),
    };

    let quote = quotes.get(id).context("Index out of bounds")?;
    ctx.text(json!(quote)).content(Content::JSON).send()?;

    Ok(())
});

At this point, everything should be working. Go ahead, try it out!

  • GET /quote
  • GET /quote?index=100

Error Handler

This is an optional step, but I think It makes your API much cleaner. If there is an error in your route, for example, if someone tries to get index 99999999 afire will return an Internal Server Error.

We can make this error response use JSON to be more consistent with the rest of the API. Using server.error_handler(|ctx, err| ...); you can define basically a route to run on errors. Here is the new server definition with the error handler I created:

use afire::route::RouteError;

let mut server =
    Server::<()>::new("localhost", 8080).error_handler(|ctx: &Context<()>, err: RouteError| {
        ctx.status(500)
            .content(Content::JSON)
            .text(json!({ "error": err.to_string() }))
            .send()?;
        Ok(())
    });

Final Code

This is the code I had at the end of this.

Click to expand
use std::fs;

use afire::{prelude::*, route::RouteError};
use anyhow::Result;
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_json::json;

const QUOTE_FILE: &str = "quotes.json";

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Quote {
    quote: String,
    author: String,
}

fn main() -> Result<()> {
    println!("[*] Starting");

    let raw_quotes = fs::read_to_string(QUOTE_FILE)?;
    let quotes = serde_json::from_str::<Vec<Quote>>(&raw_quotes)?;

    println!("[*] Loaded {} quotes", quotes.len());

    let mut server =
        Server::<()>::new("localhost", 8080).error_handler(|ctx: &Context<()>, err: RouteError| {
            ctx.status(500)
                .content(Content::JSON)
                .text(json!({ "error": err.to_string() }))
                .send()?;
            Ok(())
        });

    server.route(Method::GET, "/quote", move |ctx| {
        let id = match ctx.req.query.get("index") {
            Some(i) => i.parse::<usize>().unwrap(),
            None => rand::thread_rng().gen_range(0..quotes.len()),
        };

        let quote = quotes.get(id).context("Index out of bounds")?;
        ctx.text(json!(quote)).content(Content::JSON).send()?;

        Ok(())
    });

    println!(
        "[*] Starting Server {}:{}",
        server.ip.to_string(),
        server.port
    );

    server.run()?;
    Ok(())
}

Conclusion

Hopefully you learned something from this. Rust is really a great language for API / backend development because of its speed and being compiled significantly reduces the possibility of errors. I use afire and rust for many of my web projects, including the api for this website!