# Tuono tutorial This tutorial is meant for giving you a sneak peek of the framework and is intended to evolve along the development - be sure to have installed the latest version. The first part is about the project setup and the base knowledge needed to work with tuono. The actual tutorial starts at [Tutorial introduction](#tutorial-introduction). > If you have already installed the tuono CLI you can download the tutorial source with `tuono new tuono-tutorial --template tutorial` ## Table of Content * [CLI Installation](#cli-installation) * [Project scaffold](#project-scaffold) * [Start the dev environment](#start-the-dev-environment) * [The “/” route](#the--route) * [Tutorial introduction](#tutorial-introduction) * [Fetch all the pokemons](#fetch-all-the-pokemons) * [Create a stand-alone component](#create-a-stand-alone-component) * [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route) * [Error handling](#error-handling) * [Conclusion](#conclusion) ## CLI Installation The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal: ```bash $ cargo install tuono ``` To check that is correctly installed run: ```bash $ tuono --version ``` Run `tuono -h` to see all the available commands. ## Project scaffold To setup a new fresh project you just need to run the following command: ```bash $ tuono new tuono-tutorial ``` Get into the project folder and install the dependencies with: ```bash $ pnpm install ``` Open it with your favourite code editor. The project will have the following structure: ``` ├── package.json ├── public ├── src │ ├── routes │ └── styles ├── Cargo.toml ├── README.md └── tsconfig.json ``` **public/**: put here all the files you want to be public **src/routes/**: All the files in this folder are considered routes. All the routes are server side rendered out of the box. To add server side capabilities just create a rust file with the same name as the route (i.e. `about.tsx` → `about.rs`). **src/styles/**: In this folder there is the `global.css` file that stores all the global styles. For the rest of the project you can use CSS modules (⚠️ CSS modules on routes are forbidden). ## Start the dev environment To start the development environment you just need to run the following command within the project folder: ```bash $ tuono dev ``` The first time might take a little bit because it will install all the rust’s dependencies. All the other execution will be pretty quick! > 💡 The `tuono dev` development script is currently under strong optimization improvements. In case you face any error delete the cache `.tuono` folder and run it again! Then open [`http://localhost:3000/`](http://localhost:3000/) on the browser. ## The “/” route All the `index.tsx` files represent the folder root page (i.e. `src/routes/posts/index.tsx` is [`http://localhost:3000/posts`](http://localhost:3000/posts) as well as `src/routes/posts.tsx`). The file `index.rs` represents the server side capabilities for the index route. On this file you can: - Passing server side props - Changing http status code - Redirect/Rewrite to a different route (Available soon) ## Tutorial introduction Now that we have some knowledge about the project structure let’s start the real tutorial. The goal is to use the [PokeAPI](https://pokeapi.co/docs/v2) to list all the pokemons of the first generation (the best one btw) and then reserve a dynamic page for each one separately. ## Fetch all the pokemons To start let’s fetch all of them in the root page; since we want to render them on the server side we gonna need to implement the logic in the `index.rs` file. Clear the `index.rs` file and paste: ```rust // src/routes/index.rs use serde::{Deserialize, Serialize}; use tuono_lib::{Props, Request, Response}; const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151"; #[derive(Debug, Serialize, Deserialize)] struct Pokemons { results: Vec, } #[derive(Debug, Serialize, Deserialize)] struct Pokemon { name: String, url: String, } #[tuono_lib::handler] async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response { return match fetch.get(ALL_POKEMON).send().await { Ok(res) => { let data = res.json::().await.unwrap(); Response::Props(Props::new(data)) } Err(_err) => Response::Props(Props::new("{}")), }; } ``` Now the pokemons are correctly fetched and hydrated on the client side so we can actually use them. Clear the `index.tsx` file and paste: ```tsx // src/routes/index.tsx import type { TuonoProps } from "tuono"; interface Pokemon { name: string } interface IndexProps { results: Pokemon[] } export default function IndexPage({ data, }: TuonoProps): JSX.Element { if (!data?.results) { return <>; } return ( <>
Crates Npm

TUONO

    {data.results.map((pokemon) => { return pokemon.name; })}
); } ``` Refresh now the browser! A bit ugly but all the pokemons are finally printed on screen! ## Create a stand-alone component Let’s then create the button needed for displaying the list of pokemons. Create the following file `src/components/PokemonLink.tsx` and fill the content with: ```tsx // src/components/PokemonLink.tsx import { Link } from "tuono"; interface Pokemon { name: string } export default function PokemonLink({ pokemon, id, }: { pokemon: Pokemon; id: number; }): JSX.Element { return ( {pokemon.name} ); } ``` Now that the link is done let’s import it in the `index.tsx` file ```diff // src/routes/index.tsx ++ import PokemonLink from '../components/PokemonLink' // ...
    -- {pokemons.map((pokemon) => { -- return pokemon.name; ++ {pokemons.map((pokemon, i) => { ++ return ; })}
// ... ``` Now the links work. Clicking on any of them we get redirected to the 404 page because we haven’t yet implemented the `pokemons/[pokemon]` page. As previously said CSS modules are enabled out of the box so let’s make those links a little bit nicer. Create alongside the `PokemonLink.tsx` component the CSS module `PokemonLink.module.css` and copy the following content into it: ```css /* src/components/PokemonLink.module.css */ .link { width: 100%; max-width: 216px; position: relative; background: white; margin-bottom: 10px; border: solid #f0f0f0 1px; text-decoration: none; color: black; padding: 5px 5px 5px 15px; border-radius: 10px; display: flex; justify-content: space-between; transition: 0.2s; align-items: center; } .link:hover { box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; } .link img { width: 70px; background: white; border-radius: 50%; } ``` > 💡 SASS is supported out of the box. Just install the processor in the devDependencies `pnpm i -D sass` and run again `tuono dev` Then import the styles into the `PokemonLink` component as following: ```diff // src/components/PokemonLink.tsx import { Link } from "tuono"; import type { Pokemon } from "./../types/pokemon"; ++ import styles from './PokemonLink.module.css' export default function PokemonLink({ pokemon, id, }: { pokemon: Pokemon; id: number; }): JSX.Element { return ( -- ++ {pokemon.name} ); } ``` ## Create the `/pokemons/[pokemon]` route The homepage is ready. We have the full list of pokemons and they are all links. Great! Now we want to make those links actually pointing to a real page. Let’s create the dynamic route. Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`. These two will handle every requests that points to `http://localhost:3000/pokemons/bulbasaur..mew`. Let’s first work on the server side file. Paste into the new `[pokemon].rs` file the following code: ```rust // src/routes/pokemons/[pokemon].rs use serde::{Deserialize, Serialize}; use tuono_lib::{Props, Request, Response}; const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon"; #[derive(Debug, Serialize, Deserialize)] struct Pokemon { name: String, id: u16, weight: u16, height: u16, } #[tuono_lib::handler] async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response { // The param `pokemon` is defined in the route filename [pokemon].rs let pokemon = req.params.get("pokemon").unwrap(); return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await { Ok(res) => { let data = res.json::().await.unwrap(); Response::Props(Props::new(data)) } Err(_err) => Response::Props(Props::new("{}")) }; } ``` Then let’s work on the frontend. Paste into the `[pokemon].tsx` file the following code: ```tsx import { TuonoProps } from "tuono"; import PokemonView from "../../components/PokemonView"; export default function Pokemon({ data }: TuonoProps): JSX.Element { return ; } ``` The browser should complain that the component `PokemonView` does not exist. Let’s create it then! ```tsx // components/PokemonView.tsx import { Link } from "tuono"; import styles from "./PokemonView.module.css"; interface Pokemon { name: string id: string weight: number height: number } export default function PokemonView({ pokemon, }: { pokemon?: Pokemon; }): JSX.Element { return (
Back {pokemon?.name && (

{pokemon.name}

Weight:
{pokemon.weight}lbs
Height:
{pokemon.height}ft
)}
); } ``` ```css /* components/PokemonView.module.css */ .back-btn { background-color: white; border-radius: 10px; padding: 7px 15px; color: black; text-decoration: none; border: solid #f0f0f0 1px; font-size: 20px; } .back-btn:hover { box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; } .pokemon { display: flex; justify-content: space-between; margin-top: 20px; } .name { font-size: 50px; font-weight: 700; } .pokemon img { width: 400px; } .spec { display: flex; font-size: 18px; margin-top: 10px; } .label { font-weight: 700; } ``` ## Error handling With the current setup all the routes always return a `200 Success` http status no matter the response type. In order to return a more meaningful status code to the browser the `Props` struct can be initialized with also the `Props::new_with_status()` method. Let's see how it works! ```diff // src/routes/pokemons/[pokemon].rs ++ use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use tuono_lib::{Props, Request, Response}; const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon"; #[derive(Debug, Serialize, Deserialize)] struct Pokemon { name: String, id: u16, weight: u16, height: u16, } #[tuono_lib::handler] async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response { // The param `pokemon` is defined in the route filename [pokemon].rs let pokemon = req.params.get("pokemon").unwrap(); return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await { Ok(res) => { ++ if res.status() == StatusCode::NOT_FOUND { ++ return Response::Props(Props::new_with_status("{}", StatusCode::NOT_FOUND)); ++ } let data = res.json::().await.unwrap(); Response::Props(Props::new(data)) } -- Err(_err) => Response::Props(Props::new( ++ Err(_err) => Response::Props(Props::new_with_status( ++ "{}", ++ StatusCode::INTERNAL_SERVER_ERROR, )), }; } ``` ```diff // src/routes/index.rs ++ use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use tuono_lib::{Props, Request, Response}; const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151"; #[derive(Debug, Serialize, Deserialize)] struct Pokemons { results: Vec, } #[derive(Debug, Serialize, Deserialize)] struct Pokemon { name: String, url: String, } #[tuono_lib::handler] async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response { return match fetch.get(ALL_POKEMON).send().await { Ok(res) => { let data = res.json::().await.unwrap(); Response::Props(Props::new(data)) } -- Err(_err) => Response::Props(Props::new( ++ Err(_err) => Response::Props(Props::new_with_status( ++ "{}", // Return empty JSON ++ StatusCode::INTERNAL_SERVER_ERROR, )), }; } ``` If you now try to load a not existing pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will correctly receive a 404 status code in the console. ## Conclusion That’s it! You just created a multi thread full stack application with rust and react. The project is still under heavy development and the script for the production build (`tuono build`) is not ready yet but I hope you got the taste of what is like working with rust and react in the same stack.