I started my Wasm journey this summer with Golang and a browser and then with Node.js, tweeting every baby step of my progress. And that's how I got Connor Hicks' attention; This was the moment where he advised me to look at Atmo. Hmmm π€ Ok, this is Rust, it won't be so easy: so far, my experience in Rust is limited to a simple "Hello World". But challenge accepted. π So don't judge my writing style in Rust.
Surprisingly, my first contact with Atmo was easy, and my first conclusion was that thanks to the Runnable SDK, it has never been easier to develop Wasm with Rust.
My first "RESTful API" in Rust
I'm French, so every time I go to Paris with English, American or German, co-workers, they ask me to handle dinner. I made a list for that https://gitlab.com/-/snippets/1835049, so why not use it to do my baby steps with Atmo?
I proudly present to you my first (very static) Atmo service:
use suborbital::runnable::*;
use serde_json::json;
struct Restaurants{}
impl Runnable for Restaurants {
fn run(&self, _: Vec<u8>) -> Result<Vec<u8>, RunErr> {
let restaurants = json!([
{"name": "Le Ciel", "address": "17 rue Alexandre Dumas, 75011 Paris", "phone": "(33)664 441 416"},
{"name": "A La Renaissance", "address": "87 rue de la Roquette, 75011 Paris", "phone": "(33)143 798 309"},
{"name": "La Cave de l'Insolite", "address": "30 rue de la Folie MΓ©ricourt, 75011 Paris", "phone": "(33)153 360 833"}
]);
suborbital::resp::content_type("application/json; charset=utf-8");
Ok(restaurants.to_string().as_bytes().to_vec())
}
}
The latest version of Atmo brings PostgreSQL support, among other things, so it's an opportunity to build something a little more serious.
Preparing the environment
With this blog post (or tutorial), what I want to achieve, is to create a Restaurants CRUD API with Atmo by using PostgreSQL. I'll use Docker Compose to set up my development environment. At the root of your project, create a docker-compose.yml
file with this content:
services:
postgres:
image: postgres:latest
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=password
- APP_DB_USER=docker
- APP_DB_PASS=docker
- APP_DB_NAME=docker
volumes:
- ./db:/docker-entrypoint-initdb.d/
ports:
- 5432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U docker"]
interval: 5s
timeout: 5s
retries: 5
atmo-server:
image: suborbital/atmo:latest
container_name: atmo-server
entrypoint: atmo
environment:
- ATMO_HTTP_PORT=8080
ports:
- 8080:8080
depends_on:
postgres:
condition: service_healthy
volumes:
- ./restaurants:/home/atmo
We'll run two services: postgres
to store our data and atmo-server
that will load and start the Runnables. We need to write a script to create and populate the database. So, create a db
directory, add it a 01-init.sh
file with the below content:
#!/bin/bash
set -e
export PGPASSWORD=$POSTGRES_PASSWORD;
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER $APP_DB_USER WITH PASSWORD '$APP_DB_PASS';
CREATE DATABASE $APP_DB_NAME;
GRANT ALL PRIVILEGES ON DATABASE $APP_DB_NAME TO $APP_DB_USER;
\connect $APP_DB_NAME $APP_DB_USER
BEGIN;
CREATE TABLE IF NOT EXISTS restaurants (
id SERIAL PRIMARY KEY,
name varchar(250) NOT NULL,
address varchar(250) NOT NULL,
phone varchar(250) NOT NULL
);
INSERT INTO restaurants(name, address, phone) VALUES ('Le Ciel', '17 rue Alexandre Dumas, 75011 Paris', '(33)664 441 416');
INSERT INTO restaurants(name, address, phone) VALUES ('A La Renaissance', '87 rue de la Roquette, 75011 Paris', '(33)143 798 309');
INSERT INTO restaurants(name, address, phone) VALUES ('La Cave de lβInsolite', '30 rue de la Folie MΓ©ricourt, 75011 Paris', '(33)153 360 833');
SELECT * FROM restaurants;
COMMIT;
EOSQL
Now, we are ready to code!
The READ API
We will write the code to read the restaurants' list in the database and provide a JSON payload.
As prerequisites, you need the Subo CLI, it's easy to install:
brew tap suborbital/subo
brew install subo
- Brew works for both Mac and Linux, otherwise download it directly: https://github.com/suborbital/subo/releases
- Remark: you'll find a ready to use Gitpod project with all the source code and nothing to install here https://gitlab.com/k33g_org/discovering-atmo/atmo-postgresql. Just follow the instructions of the
README.md
file.
Generate the "Restaurants" project and code the first service
With subo
the Suborbital CLI, we'll generate the working project and our first Runnable:
subo create project restaurants
cd restaurants
rm -rf helloworld # we don't need it
subo create runnable readall
You should get this project structure:
βββ restaurants
βββ Directive.yaml
βββ Dockerfile
βββ readall
β βββ src
β βββ lib.rs
Defining the database connection and the query
Edit Directive.yaml
like below to create a database connection, a SQL query, and a route to call our new function:
identifier: com.suborbital.restaurants
appVersion: v0.1.0
atmoVersion: v0.4.0
connections:
database:
type: postgresql
connectionString: postgresql://docker:docker@postgres:5432/docker
queries:
- name: "SelectAllRestaurants"
query: |-
SELECT * FROM restaurants
handlers:
- type: request
resource: /restaurants
method: GET
steps:
- fn: readall
Executing the query
Edit readall/src/lib.rs
and update the dependencies and the run
function like below:
use suborbital::runnable::*;
use suborbital::db;
struct Readall{}
impl Runnable for Readall {
fn run(&self, _: Vec<u8>) -> Result<Vec<u8>, RunErr> {
suborbital::resp::content_type("application/json; charset=utf-8");
match db::select("SelectAllRestaurants", Vec::new()) {
Ok(result) => Ok(result),
Err(e) => {
Err(RunErr::new(500, e.message.as_str()))
}
}
}
}
the
db::select("SelectAllRestaurants", Vec::new())
command will execute the"SelectAllRestaurants"
query defined inDirective.yaml
Time to build and run the "readall" service
To build the bundle with the runnable, type the below commands:
cd restaurants
subo build .
This will generate a zip file (runnables.wasm.zip
):
βββ restaurants
βββ Directive.yaml
βββ Dockerfile
βββ runnables.wasm.zip π
βββ readall
β βββ src
β βββ lib.rs
Then to create the database and serve the bundle, at the root of the project, run this command: docker-compose up
and wait a few seconds. Once the database created, populate and started, you should get in your terminal an output similar to this one:
postgres_1 | 2021-11-23 05:34:04.302 UTC [1] LOG: database system is ready to accept connections
atmo-server | {"log_message":"(I) loaded bundle from ./runnables.wasm.zip","timestamp":"2021-11-23T05:34:09.902641098Z","level":3,"app":{"atmo_version":"0.4.2"}}
atmo-server | {"log_message":"(I) starting Atmo ...","timestamp":"2021-11-23T05:34:09.912903046Z","level":3,"app":{"atmo_version":"0.4.2"}}
atmo-server | {"log_message":"(I) serving on :8080","timestamp":"2021-11-23T05:34:09.912945469Z","level":3,"app":{"atmo_version":"0.4.2"}}
Executing the "readall" service
Remark: I use to run HTTPie CLI instead of
curl
command
To call our READ API, just type:
http http://localhost:8080/restaurants
And if everything is ok, you should get this:
[
{
"address": "17 rue Alexandre Dumas, 75011 Paris",
"id": 1,
"name": "Le Ciel",
"phone": "(33)664 441 416"
},
{
"address": "87 rue de la Roquette, 75011 Paris",
"id": 2,
"name": "A La Renaissance",
"phone": "(33)143 798 309"
},
{
"address": "30 rue de la Folie MΓ©ricourt, 75011 Paris",
"id": 3,
"name": "La Cave de lβInsolite",
"phone": "(33)153 360 833"
}
]
Easy! π
Add a new "READ API": get a restaurant by id
Before adding CREATE
, UPDATE
, DELETE
, we'll create a readone
service to get a restaurant record by its id
:
cd restaurants
subo create runnable readone
Then add a new query "SelectOneRestaurant"
with a parameter to Directive.yaml
queries:
- name: "SelectAllRestaurants"
query: |-
SELECT * FROM restaurants
- name: "SelectOneRestaurant"
query: |-
SELECT * FROM restaurants
WHERE id = $1
And a new route to call the readone
service:
handlers:
- type: request
resource: /restaurants
method: GET
steps:
- fn: readall
- type: request
resource: /restaurants/:key
method: GET
steps:
- fn: readone
Now, edit readone/src/lib.rs
and update the dependencies and the run
function like below:
use suborbital::runnable::*;
use suborbital::db;
use suborbital::req;
use suborbital::db::query;
struct Readone{}
impl Runnable for Readone {
fn run(&self, _: Vec<u8>) -> Result<Vec<u8>, RunErr> {
suborbital::resp::content_type("application/json; charset=utf-8");
let key = req::url_param("key");
let mut query_args: Vec<query::QueryArg> = Vec::new();
query_args.push(query::QueryArg::new("id", key.as_str()));
match db::select("SelectOneRestaurant", query_args) {
Ok(result) => Ok(result),
Err(e) => {
Err(RunErr::new(500, e.message.as_str()))
}
}
}
}
Some explanations:
let key = req::url_param("key");
allow to get the id when we callhttp http://localhost:8080/restaurants/<id>
- To pass the
id
as a paramater of the SQL query we use a vector ofQueryArg
:query_args.push(query::QueryArg::new("id", key.as_str()));
Then, build again with subo build .
and serve with docker-compose up
and finally query the readone
service with http http://localhost:8080/restaurants/2
(to get the second restaurant) and you'll get:
[
{
"address": "87 rue de la Roquette, 75011 Paris",
"id": 2,
"name": "A La Renaissance",
"phone": "(33)143 798 309"
}
]
So, nothing really complicated and you'll learn how to use queries with parameters.
The CREATE, UPDATE, DELETE API
The remaining services are not more complicated, so I give you the source code and a few necessary explanations.
Create a restaurant
Create the runnable:
cd restaurants
subo create runnable create
The query:
- name: "AddRestaurant"
query: |-
INSERT INTO restaurants (name, address, phone)
VALUES ($1, $2, $3)
The route:
- type: request
resource: /restaurants
method: POST # π don't forget to change the method
steps:
- fn: create
The Rust source code of the Runnable:
This time:
- We use the
serde
library to deserialize an instance of typeRestaurant
from a JSON string:let restaurant: Restaurant = serde_json::from_str(&in_string).unwrap();
- We use the
db::insert
method to execute the query
use suborbital::runnable::*;
use suborbital::db;
use suborbital::db::query;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Restaurant {
name: String,
address: String,
phone: String
}
struct Create{}
impl Runnable for Create {
fn run(&self, input: Vec<u8>) -> Result<Vec<u8>, RunErr> {
suborbital::resp::content_type("application/json; charset=utf-8");
let in_string = String::from_utf8(input).unwrap();
// read the JSON message
let restaurant: Restaurant = serde_json::from_str(&in_string).unwrap();
// add parameters
let mut query_args: Vec<query::QueryArg> = Vec::new();
query_args.push(query::QueryArg::new("name", restaurant.name.as_str()));
query_args.push(query::QueryArg::new("address", restaurant.address.as_str()));
query_args.push(query::QueryArg::new("phone", restaurant.phone.as_str()));
match db::insert("AddRestaurant", query_args) {
Ok(result) => Ok(result),
Err(e) => {
Err(RunErr::new(500, e.message.as_str()))
}
}
}
}
π don't forget to add the
serde
dependency toCargo.toml
:
[dependencies] suborbital = '0.13.0' serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"
Use the create service like that:
http POST http://localhost:8080/restaurants \
name="Au Nouveau Nez" \
address="104 Rue Saint-Maur, 75011 Paris" \
phone="(33)143 550 230"
And check the creation with:
http http://localhost:8080/restaurants
Update a restaurant
Create the runnable:
cd restaurants
subo create runnable update
The query:
- name: "UpdateRestaurant"
query: |-
UPDATE restaurants
SET name = $1, address = $2, phone = $3
WHERE id = $4
The route:
- type: request
resource: /restaurants
method: PUT # π don't forget to change the method
steps:
- fn: update
The Rust source code of the Runnable:
This time:
- We use the
db::update
method to execute the query - We deserialize an instance of type
Restaurant
:let restaurant: Restaurant = serde_json::from_str(&in_string).unwrap();
- We get the id of the restaurant to be updated:
let key = req::url_param("key");
use suborbital::runnable::*;
use suborbital::req;
use suborbital::db;
use suborbital::db::query;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Restaurant {
name: String,
address: String,
phone: String
}
struct Update{}
impl Runnable for Update {
fn run(&self, input: Vec<u8>) -> Result<Vec<u8>, RunErr> {
suborbital::resp::content_type("application/json; charset=utf-8");
let in_string = String::from_utf8(input).unwrap();
let restaurant: Restaurant = serde_json::from_str(&in_string).unwrap();
let key = req::url_param("key");
let mut query_args: Vec<query::QueryArg> = Vec::new();
query_args.push(query::QueryArg::new("name", restaurant.name.as_str()));
query_args.push(query::QueryArg::new("address", restaurant.address.as_str()));
query_args.push(query::QueryArg::new("phone", restaurant.phone.as_str()));
query_args.push(query::QueryArg::new("id", key.as_str()));
match db::update("UpdateRestaurant", query_args) {
Ok(result) => Ok(result),
Err(e) => {
Err(RunErr::new(500, e.message.as_str()))
}
}
}
}
Use the update service like that:
http PUT http://localhost:8080/restaurants/4 \
name="Au Nouveau Nez π·" \
address="104 Rue Saint-Maur, 75011 Paris" \
phone="(33)143 550 230"
π
4
is theid
value of the restaurant
And check the update with:
http http://localhost:8080/restaurants/4
You should get:
[
{
"address": "104 Rue Saint-Maur, 75011 Paris",
"id": 4,
"name": "Au Nouveau Nez π·",
"phone": "(33)143 550 230"
}
]
Delete a restaurant
And finally to complete the CRUD API, the delete
service π
Create the runnable:
cd restaurants
subo create runnable delete
The query:
- name: "DeleteRestaurant"
query: |-
DELETE FROM restaurants
WHERE id = $1
The route:
- type: request
resource: /restaurants
method: DELETE # π don't forget to change the method
steps:
- fn: delete
The Rust source code of the Runnable:
use suborbital::runnable::*;
use suborbital::req;
use suborbital::db;
use suborbital::db::query;
struct Delete{}
impl Runnable for Delete {
fn run(&self, _: Vec<u8>) -> Result<Vec<u8>, RunErr> {
suborbital::resp::content_type("application/json; charset=utf-8");
let key = req::url_param("key");
let mut query_args: Vec<query::QueryArg> = Vec::new();
query_args.push(query::QueryArg::new("id", key.as_str()));
match db::delete("DeleteRestaurant", query_args) {
Ok(result) => Ok(result),
Err(e) => {
Err(RunErr::new(500, e.message.as_str()))
}
}
}
}
Use the delete service like that:
http DELETE http://localhost:8080/restaurants/4
So, that's it for this time. You can find all the source code in this project: https://gitlab.com/k33g_org/discovering-atmo/atmo-postgresql, and if you open it with Gitpod, you don't need to install anything to start experimenting with Atmo.