Making a Simple API
published 02/22/2022 • 3m reading time • 340 viewsWarning
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 "es
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!