# Shuttle This file contains the full content of all blog posts and pages from https://www.shuttle.dev/ --- # Claude Agent Skills: What They Are and How to Build and Use One - Complete Guide Source: https://www.shuttle.dev/blog/2025/12/02/claude-skills-complete-guide Date: 2 December 2025 Author: dcodes Tags: claude, ai, skills, shuttle, rust, deployment A practical guide to Claude's Agent Skills, reusable knowledge packages that activate automatically across all your conversations. We'll build a custom Shuttle deployment skill and put it to work. Anthropic recently released [Claude Agent Skills](https://www.claude.com/blog/skills), and there's been a lot of confusion around what they actually are. I was confused at first too. At first glance it looks like we already have a way to do what Skills do. We already have `CLAUDE.md` files, MCP servers, and custom commands that help AI agents work the way we want them to. Unlike traditional AI assistants that require constant re-prompting, Claude skills let you upload specialized expertise or knowledge once and have it activate automatically whenever it's relevant. Your prompts become portable across conversations and projects, a significant improvement for software development workflows. In this blog post, I'll explain what Claude skills are, how they differ from existing features, and then we'll build a practical one together: a Shuttle deployment skill that helps Claude scaffold and deploy Rust projects correctly. ## What Are Claude Skills? Skills are folders containing a `SKILL.md` file that can include three types of content: - **Instructions**: Guidelines, best practices, and conventions Claude should follow - **Scripts**: Executable Python or bash code Claude can run as part of workflows - **Resources**: Reference documentation, templates, and examples Claude can pull from When you start a conversation, Claude loads the headers and descriptions of your available skills into context. This doesn't take much space. The full content only gets pulled in when Claude detects something relevant to your task. This is the key insight that makes Skills different from just dumping a bunch of context into your conversation. Context bloat is a real problem in agentic coding. We talked about this in more detail in the [Claude Code Best Practices](https://www.shuttle.dev/blog/2025/10/16/claude-code-best-practices) post. Every instruction, every piece of documentation, every tool definition sits in your context window whether you use it or not. MCPs and other external tools have this problem: their tool definitions consume tokens from the moment you enable them, increasing token usage even when you don't need them. Skills solve this by being lazy. Headers only until needed. Full content on demand. This approach helps the agent's ability to maintain context over long conversations without running into limits. Anthropic describes Skills as having four core properties: - **Composable**: Skills stack together, and Claude identifies which ones are needed and coordinates their use automatically. - **Portable**: Same format everywhere. Build once, use across Claude.ai, Claude Code, and the API. - **Efficient**: Only loads what's needed, when it's needed. - **Powerful**: Can include executable code for tasks where traditional programming is more reliable than token generation. ### Composable You don't have to pick one skill per conversation. Say you're tackling complex tasks like building a full-stack app with a Shuttle backend, SQLx for database operations, and a Svelte frontend. Claude can activate multiple AI agents' worth of knowledge simultaneously. The Shuttle skill handles deployment patterns and resource annotations, the SQLx skill ensures your queries use compile-time verification correctly, and the Svelte skill keeps your components following best practices. Each skill handles its domain, and Claude coordinates between them. You build small, focused skills and let them work together. ### Portable Most of us developers work on multiple projects simultaneously, and repetitive tasks like re-configuring your agent for each project gets tedious fast. Writing a slash command for one project and then manually copying it to another is exactly the kind of routine task that should be automated. Skills solve this. Think of them like npm packages or Rust crates, but for Claude knowledge. Write once, use everywhere. The same `SKILL.md` file works in Claude.ai's web interface, Claude Code in your terminal, and the API. ### Efficient Traditional approaches to customization load everything upfront. Skills take the opposite approach: only headers and descriptions load initially, which takes minimal context. Full content loads when Claude detects relevance. Ten installed skills don't mean ten skills worth of context consumption. ### Powerful Sometimes you don't want Claude generating code. You want it running deterministic scripts that do exactly what you need. Skills can include specialized tools in the form of Python and bash scripts that Claude can execute for specific tasks, giving you a hybrid of AI flexibility and programmatic precision. ## Claude Skills vs AI Agents and AI Assistants If you've been using Claude or other AI agents for a while, you might be wondering how Skills differ from the other ways to customize agent behavior. Here's the breakdown: **Custom Instructions** (in Claude.ai web) are tied to your account settings. They apply globally but can't include resources, scripts, or structured documentation. They're also limited in length. **Projects** (in Claude.ai web) give you a dedicated space with custom instructions and uploaded files, but you have to be in that specific project to use them. If you want the same behavior across multiple projects, you're copying and pasting. **Slash Commands** (in Claude Code) let you trigger specific behaviors with `/command`, but they require explicit invocation. You have to remember to use them. **Skills** are universal and automatic. You install them once, and Claude activates them based on context. You don't need to be in a specific project. You don't need to remember a command. You just start working, and if your task matches a skill, Claude uses it. The token usage angle matters too. With Projects or custom instructions, everything loads into context at the start. With Skills, only the headers and descriptions load initially. If you have ten skills installed but only need two for a given conversation, you're not paying the context cost for the other eight. ## Building a Shuttle Skill I hope the explanation above gave you some insight into what Skills are and how they differ from existing features. Now, let's build something practical. Shuttle has its own Rust macros, configuration format, and deployment patterns. Claude doesn't always get these right out of the box - it might use outdated syntax, miss resource annotations, or structure the project incorrectly. A Shuttle skill solves this by giving Claude the exact patterns it needs. When you ask Claude to build or deploy a Shuttle project, the skill activates and provides correct context. ### Skill Folder Structure Before we dive into building, let's understand how skills are organized. A skill is simply a folder with a specific structure: ```text my-skill/ ├── SKILL.md (required) ├── reference.md (optional documentation) ├── examples.md (optional examples) ├── scripts/ │ └── helper.py (optional utility) └── templates/ └── template.txt (optional template) ``` The only required file is `SKILL.md` - it's the entry point that defines your skill's name, description, and core instructions. Everything else is optional and loaded on-demand. You can add reference documentation, code examples, Python or bash scripts, and templates as needed. Claude will have access to all files in the skill folder, but it only loads them when the conversation actually requires that information. From within `SKILL.md`, you can link to other files in your skill folder. When Claude needs more details, it follows these references automatically: ````markdown Check [examples.md](examples.md) for complete code samples. To format the output, run: ```bash python scripts/formatter.py --input data.json ``` ```` This progressive loading is where the efficiency comes in. Your skill can include detailed API documentation, complex examples, and utility scripts, but Claude only reads them when the conversation actually needs that information. Comprehensive skills don't mean bloated context. ### The Skill Creator Skill Claude.ai already has a skill for creating skills. The `skill-creator` skill comes built-in, and it contains all the knowledge about how to write effective `SKILL.md` files, structure skill folders, and follow best practices. This means you can literally tell Claude "create a skill for X" and it will load the skill-creator skill and use that knowledge to build your new skill. A skill that creates skills. To make sure it's enabled, go to Settings → Capabilities and verify that `skill-creator` is turned on: Once enabled, you can ask Claude to create any skill you need, and it'll handle the entire process - from structuring the folder to writing the `SKILL.md` content with proper headers, activation patterns, and instructions. ### Creating the Shuttle Skill Let's put the skill-creator to work. I gave Claude a simple prompt: "Create a skill for the Shuttle Rust hosting platform." Claude loads the `skill-creator` skill and starts reading its content to understand how to build a proper skill: Then it gets to work, creating the skill structure and gathering context about Shuttle's patterns: The first version was functional but verbose. I gave it more comprehensive knowledge about Shuttle's macros, resource annotations, and configuration format, then asked it to make the content more concise. The `skill-creator` helped refine it into something practical: The result is a focused skill that covers the essential patterns developers need when working with Shuttle - proper macro usage, resource provisioning, and project configuration: The skill now knows about `#[shuttle_runtime::main]`, how to use database macros like `#[shuttle_shared_db::Postgres]`, and the correct `Shuttle.toml` configuration format. When I ask Claude to scaffold or deploy a Shuttle project, it'll activate this skill and use these patterns automatically. You can press the Download button to get the skill as a zip file, then load it into Claude Code or any other Claude interface. > Skills can be created directly in Claude Code too, but I used Claude.ai for this because the built-in skill-creator makes the process more guided. In Claude Code, you'd manually create the folder structure and write the `SKILL.md` file yourself. ### Installing the Skill After downloading the skill, I created a project directory and unzipped it. The skill extracted into a clean structure: The `shuttle` skill that Claude created includes (you can add more files according to your needs): - `SKILL.md` - The main skill file with activation patterns and core instructions - `references/framework_examples.md` - Code examples for different web frameworks (Axum, Actix, Rocket) - `references/custom_resources.md` - Documentation on Shuttle's resource provisioning system This modular structure means Claude only loads the framework examples or custom resources documentation when the conversation actually needs them. The base `SKILL.md` provides the core patterns, and the reference files add depth on demand. To install it, move the unzipped folder to `.claude/skills/`. In my case, I placed it at `.claude/skills/shuttle`. Once the skill is in the right location, Claude Code detects it automatically: - `~/.claude/skills/` - Personal skills, available globally across all projects - `.claude/skills/` - Project skills, scoped to that specific project ## Using the Shuttle Skill Let's see the skill in action. I started a new Claude Code session and gave it a straightforward prompt: "Use the Shuttle Skill to build a todo list app with a postgres database and sqlx and deploy to Shuttle." In this case, I had to explicitly ask Claude to use the Shuttle skill. While skills are designed to activate automatically based on context, Claude doesn't always detect them on the first try and it's actually a bit conservative by default. Sometimes you need to be explicit about which skill to use. When you reference a skill, Claude Code asks for permission to read it: These permission prompts are part of the security measures built into Claude Code. The agent won't read files or execute scripts without your approval. After accepting, Claude has full context on how to use Shuttle: the correct macros, resource annotations, project structure, and deployment patterns. Claude breaks down the task into clear steps, from setting up dependencies and migrations to implementing CRUD endpoints and deploying. Each step follows the patterns from the skill. After the build succeeded, Claude deployed the application to Shuttle: ## Conclusion Skills are what turn Claude into one of the more sophisticated AI agents available today. Instead of dumping everything into your conversation upfront or manually triggering commands, you get knowledge that activates when relevant and stays quiet when it's not. The Shuttle skill we built is a good example of where this shines. Deployment patterns, macro syntax, and resource annotations are things you need exactly when you need them, not cluttering every conversation. Build skills for your common workflows, your team's conventions, and your favorite frameworks. They stack, they're portable, and they don't bloat your context. If you want to try Shuttle yourself, you can get started with: ```bash shuttle init --template axum ``` Download the Shuttle skill and install it in your Claude Code or Claude.ai account to get started. Happy building! --- # Building a Full-Stack Rust Web App with Claude Opus 4.5 Source: https://www.shuttle.dev/blog/2025/11/26/build-rust-app-claude-opus-4.5 Date: 26 November 2025 Author: dcodes Tags: rust, claude, ai, axum, sqlx, postgres, full-stack Testing Claude Opus 4.5's coding abilities by building a complete production-ready Rust web application with database, frontend, and deployment - all from a single prompt Last week Google released Gemini 3, and it really killed the competition. It only took a week for Anthropic to respond with Claude Opus 4.5, beating Gemini 3 on SWE-bench Verified and not letting Google take over the top spot for too long. Benchmarks are useful, but what matters more is real-world performance. In this post, I'll test Claude Opus 4.5 by building a production-ready Rust web application with database, frontend, and deployment - all from a single prompt, letting the AI agent handle the entire process. ## Claude Opus 4.5 Anthropic just released Claude Opus 4.5, and according to benchmarks, claims, and community response, it's a genuine leap forward for coding with AI. It's now the best model in the world for software engineering, scoring 80.9% on SWE-bench Verified - outperforming every other frontier model including GPT-5.1 and Gemini 3 Pro. According to Anthropic, Claude Opus 4.5 outperformed every human candidate on their notoriously difficult performance engineering take-home exam. The improvements are across the board: better reasoning under ambiguity, creative problem-solving, and state-of-the-art performance in most domains. Claude Opus 4.5 also uses dramatically fewer tokens to reach better outcomes than its predecessors. I gave Claude Opus 4.5 a task: build a full-stack Rust web application from scratch, complete with database migrations, frontend assets, and deploy it to Shuttle. One comprehensive prompt, no follow-up corrections. The application is a personal finance tracker with transaction management, budget tracking, spending insights with charts, and a modern UI. The stack uses Rust with Axum and SQLx for the backend, PostgreSQL for the database, and vanilla HTML/CSS/JS for the frontend. The requirements are using SQLx compile-time checked query macros throughout (no raw queries), proper database migrations, a clean modern UI, and everything deployed to Shuttle with the database provisioned automatically. ## The Prompt Here's the complete prompt I used: ```markdown Build a Personal Finance Tracker web application with the following requirements: **Backend (Rust + Axum + SQLx):** - Use Rust with the Axum web framework - Use SQLx for database operations with PostgreSQL - Use SQLx compile-time checked query macros (query!, query_as!, etc.) throughout - no raw queries - Database is running on localhost:5432 - Create proper database migrations using `sqlx migrate add` commands - Implement migrations to create necessary tables (transactions, categories, budgets, etc.) - Run migrations automatically or provide clear instructions - Before deployment, run `cargo sqlx prepare` to generate query metadata for offline compilation - Create RESTful API endpoints for: - Adding/editing/deleting transactions - Categorizing transactions - Getting spending summaries by category/time period - Budget management **Frontend (HTML/CSS/JS):** - Create a modern, clean, and slick UI using vanilla HTML, CSS, and JavaScript - Make it responsive and mobile-friendly - Include data visualizations (charts for spending by category, trends over time) - Use a nice color scheme and contemporary design patterns - Place all frontend assets in a `dist/` directory **Deployment:** - Deploy to Shuttle - Configure the Shuttle.toml to include frontend assets - Use the Shuttle MCP server to handle the deployment - You can also use the Shuttle MCP server to search Shuttle documentation if needed **Features to implement:** - Transaction management (add, edit, delete income/expenses) - Automatic and manual categorization - Budget setting and tracking - Spending insights with charts (pie charts, bar charts, line graphs) - Date range filtering - Summary statistics (total spent, by category, monthly trends) Build this as a complete, production-ready application with proper error handling, validation, and a polished user experience. ``` This tests a lot of things: Rust idioms, database design, API design, frontend skills, and platform-specific deployment knowledge. ## Setting Up the Test I started with a fresh Shuttle Axum project to give Claude Opus 4.5 a clean slate: ```bash shuttle init --template axum ``` For this experiment, I used Cursor with Claude Opus 4.5 through the Agent feature. I pasted the entire prompt into the Agent view and hit enter. ## Watching It Work Before writing any code, just like other frontier models, Claude Opus 4.5 started by collecting context. One of its first actions was using the Shuttle MCP server's documentation search tool to understand how Shuttle works, what features are available, and how to structure the deployment configuration. This is smart behavior, it gives it a good understanding of the platform, it verified current best practices and platform capabilities. > **My problem** with other frontier models was that even though they'd look up the documentation, they'd still make the mistake of using outdated dependencies and syntax, especially with Axum. Within seconds of finishing its research, it started generating code. It worked through the requirements - setting up the database schema, creating migrations, building out API endpoints, and crafting the frontend. In just a few minutes, it had written over 2,500 lines of code across multiple files. The agent view showed it was now attempting to build the project. What impressed me most wasn't just the speed - it was the attention to detail. I was specifically watching for common mistakes that trip up other frontier models, particularly around Axum's routing syntax. In Axum 0.8, the dynamic route syntax changed from `/:id` to `/{id}` (curly braces instead of colons). This is a subtle but breaking change that causes runtime errors. I've tested plenty of models on Axum projects, and they consistently get this wrong - even Claude Sonnet 4.5 makes this mistake. Claude Opus 4.5 got it right. For me personally, this is a huge improvement over Sonnet 4.5 because this would always cause a runtime error - it's subtle but very important. Every single route used the correct `/{id}` syntax. Claude Opus 4.5 also used the latest versions of all the crates - Axum, SQLx, tower-http, and everything else - without any prompting. This is something other frontier models consistently get wrong, often pulling outdated versions from their training data. ## The Build Process Claude Opus 4.5 organized its work into a clear todo list, systematically checking off each step: The dependencies it chose were spot-on - SQLx with the right features, serde for serialization, tower-http for serving static files, and all the other pieces needed for a production application. After writing all the migrations, API handlers, and frontend code, it ran `cargo build`. Before the successful compilation, running `cargo sqlx prepare` failed a few times. Claude Opus 4.5 caught the errors and corrected itself twice, adjusting the database queries and schema setup. It's impressive to see the best model debug its own work and iterate toward a solution without human intervention. Once it worked through those issues, it compiled successfully. The only correction I had to make was providing the local database password for running migrations. Claude Opus 4.5 generated the SQLx commands correctly, but since I hadn't specified my local PostgreSQL password in the prompt, it used a placeholder that needed updating. That's it. One prompt, one password fix, and everything else worked perfectly. Claude Opus 4.5 then moved on to deployment, using the Shuttle MCP server to deploy the application. It found the existing project ID and started the deployment process. ## The Result A few minutes later, the deployment completed successfully: The application was live at a production URL with everything I asked for: - **Backend**: Axum with RESTful API, SQLx compile-time checked queries, proper error handling - **Database**: PostgreSQL with three migrations (categories with seed data, transactions, budgets) - **Frontend**: Dark-themed modern UI with Chart.js visualizations, responsive design, modal forms The feature set was complete: It implemented a full dashboard with stats cards and charts, complete transaction management with filtering, budget tracking with progress bars, analytics views, and eight pre-seeded categories with icons and colors. The API had all the endpoints needed for CRUD operations on transactions, budgets, and categories, plus analytics endpoints for summaries and trends. ## The Application in Action The application was live. Let me show you what Claude Opus 4.5 built. The dashboard greets you with a clean, modern dark theme. Stats cards show your financial overview - total income, expenses, and balance. Below that, a pie chart breaks down spending by category and a line chart tracks monthly trends. The transactions page has all the functionality you'd expect - date range filters, category and type dropdowns, and a clean list of transactions with their icons and amounts. Each transaction can be edited or deleted. Budget management: set budgets per category, and progress bars show how much you've spent versus your limit, with color coding to indicate status. The analytics section provides deeper insights with bar charts comparing income vs expenses, pie charts for expense breakdowns, and horizontal bar charts showing top spending categories. The entire UI is responsive, the charts are interactive, and everything works as you'd expect from a production application. Adding a transaction brings up a polished modal with proper form controls - toggle buttons for income/expense, amount input, description field, category dropdown with icons, and a date picker. All of these are wired up to the Rust backend API. Every button you see is functional. Add, edit, delete - they all make proper API calls to the Axum backend, which validates the data and updates the PostgreSQL database through SQLx's compile-time checked queries. ## Final Thoughts Claude Opus 4.5 is the best coding model I've used, no doubt about it. One prompt built a complete full-stack application with a Rust backend, database migrations, a polished frontend, and deployment to production. All that without making any major mistakes or getting stuck. Every crate dependency was current. The Axum routing syntax was correct. The SQLx queries used the right macros. The UI looks good. The deployment worked on the first try. Claude Opus 4.5 is noticeably slower than Sonnet 4.5 - you'll wait longer for responses. But for complex coding tasks where accuracy matters more than speed, the wait is worth it. When you have the best model handling your code, I'd rather wait an extra minute for code that works than iterate multiple times fixing mistakes from a faster model. The Shuttle MCP integration made deployment seamless. Claude Opus 4.5 used it to search documentation when needed and handled the entire deployment process autonomously. ## When to Use Claude Opus 4.5 Here's when Claude Opus 4.5 is the right tool for the job versus when you should reach for something faster: | Use Claude Opus 4.5 For | Use Faster Models For | | ----------------------------------------------- | ----------------------------------------- | | Complex multi-file features | Small adjustments and quick fixes | | System design and architecture decisions | Simple refactoring (renaming, formatting) | | Building new applications from scratch | Documentation updates | | Decision-making with multiple valid approaches | Adding comments or docstrings | | Brainstorming and generating ideas | Tweaking CSS or UI styling | | Implementing features that require deep context | Single-line bug fixes | For the small, fast edits, I use Cursor's Composer - it's incredibly fast for those tasks. We covered why Composer excels at quick iterations in [this post](https://www.shuttle.dev/blog/2025/11/05/cursor-composer-hands-on?utm_source=shuttle_blog&utm_medium=blog&utm_campaign=opus_4_5_rust_app). Refactoring is a gray area. For simple renames or extractions, Composer wins on speed. But for architectural refactoring where you're restructuring modules or changing patterns across multiple files, Claude Opus 4.5's deeper reasoning is worth the wait. The pattern I've found works best: use Claude Opus 4.5 when accuracy and architectural decisions matter more than speed, and use Composer (or any other fast model) when you need rapid iteration on smaller changes. ## Try It Yourself Want to build your own Rust web application? Get started with Shuttle (it's free): ```bash shuttle init --template axum ``` Join the Discord to discuss Claude Opus 4.5 and share your thoughts on this model. --- # Best AI Coding Assistant Tools For Developers (November 2025) Source: https://www.shuttle.dev/blog/2025/11/20/ai-coding-tools-for-developers Date: 20 November 2025 Author: dcodes Tags: ai, coding, cursor, claude, github-copilot, development, tools, windsurf, gemini, openai A comprehensive guide to choosing the right AI coding assistant for your workflow. Compare Cursor, Claude Code, GitHub Copilot, and more to find what actually fits how you work. Remember when we had to actually remember all those API methods? Yeah, me neither - that was roughly 18 months ago in developer years, which is basically ancient history now. AI coding assistants have evolved from fancy autocomplete into something genuinely useful, but the problem is that there are way too many options and it's difficult to choose the right one. Everyone has their own use case when it comes to coding - you might be a developer who wants to build complex enterprise features, or you might be a vibe coder who wants to quickly prototype ideas. This article will help you choose the right AI coding assistant for your needs. We'll look at the most popular AI coding tools and explain each one and why they're important. We'll end the article with a quick decision matrix to help you choose the right tool specifically for your needs. Before we dive in, this isn't an either/or assessment. You can use multiple tools together based on what each does best. Personally, I use Claude Code for extensive coding sessions, Cursor for its quick and intelligent autocomplete via Cursor Tab and its Composer model for quick tasks, and GitHub Copilot to get answers from public repositories. Each tool excels at different things, and combining them creates a more powerful workflow. It's also important to keep an eye out for new tools launching - the landscape is evolving quickly. ## What Makes a Good AI Coding Assistant? Not all AI coding tools are created equal and not all of them are created for the same purpose. I evaluated these based on real developer challenges - the stuff that actually slows you down: - **Debugging and error resolution** - Can it help you fix bugs, or does it just generate more? - **Seamless integration** - Does it fit your workflow or force you to adapt? - **Scalability and maintainability** - Does it help with refactoring large codebases? - **Adapting to technology** - Can it work with new frameworks and libraries? - **Security and vulnerability mitigation** - Does it introduce security holes? - **Model access and flexibility** - Which AI models can you use? Can you switch between Claude, GPT, Gemini, and others? Different models excel at different tasks, so having choice matters. Tools that lock you into a single model limit your options when that model struggles with your specific use case. These should be kept in mind when you're choosing the right tool for your needs. Let's get into the tools. ## The Tools Breakdown ### IDE-Based Tools #### Cursor: AI-First Code Editor (Most Popular) Cursor is a fork of VS Code that treats AI as a first-class citizen rather than a plugin afterthought. The native AI chat understands your entire codebase, and the multi-file editing works really well. Cursor can execute multiple tools at the same time, making code editing much faster and more efficient. Cursor gives you access to all the major AI models: Claude Sonnet 4.5, GPT-5.1, [Gemini 3 Pro](https://www.shuttle.dev/blog/2025/11/18/gemini-3?utm_source=shuttle_blog&utm_medium=interlink&utm_campaign=ai_coding_tools), and their own Composer model. You can switch between models based on your needs, which is particularly useful when different models excel at different types of tasks. Cursor also provides a feature called **Cursor Tab** which provides context-aware suggestions that understand relationships between files and an agent mode for agentic coding. Cursor's **Composer** model is designed for speed and intelligence at the same time. The speed is impressive, but the good thing is that it doesn't compromise much on its intelligence. Personally, Composer is my favorite and most used model when it comes to easy to medium-level tasks. Two standout features set Cursor apart: **Parallel agent mode** lets you run the same task across multiple models simultaneously - Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro - then compare which one solved it better. This is particularly useful for complex tasks where you expect most models might fail on the first try. Running multiple agents in parallel increases your chances of getting at least one correct solution. **Background agents** run in the cloud and create PRs against your repository when complete. This integrates with Linear - you can mention Cursor in Linear tickets to handle specific tasks in the background. Combined with Vercel's preview deployments, you can review the changes immediately without blocking your local work. This works really well for easy to medium tasks that don't require extensive debugging. We covered what we built with Cursor Composer in [our hands-on review](https://www.shuttle.dev/blog/2025/11/05/cursor-composer-hands-on?utm_source=shuttle_blog&utm_medium=interlink&utm_campaign=ai_coding_tools). Cursor recently released version 2.0 with significant changes to the agent system and UI. Read more about [what changed in Cursor 2.0](https://www.shuttle.dev/blog/2025/10/31/cursor-2.0?utm_source=shuttle_blog&utm_medium=interlink&utm_campaign=ai_coding_tools). **Best for:** Heavy coders and developers who want AI baked into their editor from the ground up. It's also great for vibe coders who want to only use natural language to code. #### Windsurf (formerly Codeium): AI-First Code Editor Windsurf is similar to Cursor in approach - it's a VS Code fork with AI as a first-class citizen rather than an afterthought. The AI chat understands your entire codebase, and it provides context-aware suggestions with multi-file editing capabilities. After some industry drama where Anthropic cut Claude access following OpenAI's acquisition of Windsurf, the platform now primarily uses GPT-5.1 as the default model. You can bring your own API key to access Claude 4 models, and Windsurf also offers Gemini 3 Pro and their own base model built on Llama 3.1 70B. The agent system handles coordinated changes across files, making it solid for refactoring and feature development. While Cursor has more market share, Windsurf delivers comparable AI assistance. **Best for:** Heavy coders and developers who want AI baked into their editor from the ground up. Great for vibe coders who want to use natural language to code. #### GitHub Copilot: The Industry Standard GitHub Copilot is the tool that started the AI coding assistant wave. It excels at autocomplete and has deep GitHub integration - pull request summaries, code review suggestions, and repo-wide code understanding. It also gives you access to cutting-edge models like OpenAI's latest releases. It integrates seamlessly into VSCode as an extension, making it incredibly easy to get started. The suggestion quality is consistently good, and the GitHub integration provides PR summaries and code review assistance. Multi-line completions work well for boilerplate. One particularly useful feature I use constantly is the codebase chat. While Copilot works as a plugin in your IDE, you can also use it directly on the GitHub website. When I'm looking at any repository on GitHub, I click the Copilot icon and chat about the codebase without cloning it locally. I find this incredibly useful for understanding libraries, frameworks, or open source projects I'm unfamiliar with - especially ones with poor or outdated documentation. The free tier is generous enough that you can use this feature extensively without paying. I find myself using this constantly when exploring codebases that aren't mine and I have no context about them. Given the free tier and easy VSCode integration, I highly recommend trying Copilot - it's a great starting point for AI-assisted coding. **Best for:** Teams already using GitHub who want reliable autocomplete and PR integration. Also excellent for quickly understanding unfamiliar codebases directly on GitHub. #### Kiro by AWS: Spec-Driven Development Kiro flips the usual flow: instead of jumping straight to code, it generates detailed requirements documents first. You review and approve the spec, then Kiro generates implementation. The platform offers free access to Claude Sonnet 4.5 and 3.7 models during public preview. You can choose between Claude Sonnet 4.5 for advanced coding or use Auto mode, which mixes frontier models to balance quality, latency, and cost. This approach catches architectural issues before you write code. It's particularly useful for complex features where requirements need clarity before implementation. The downside is that this can be painfully slow sometimes and feels like overkill for most projects. But for enterprise and production applications where getting the architecture right matters, this is really good. **Best for:** Enterprise developers in the AWS ecosystem who want to validate requirements before implementation. Skip this if you're prototyping or building personal projects - the spec-driven approach will slow you down unnecessarily. #### Google Antigravity: Agent-First IDE Antigravity is Google's new agentic development platform. It offers a dual interface: an Editor view (traditional AI-powered IDE) and an agent-first Manager view where you can spawn and orchestrate multiple agents across workspaces in parallel. Released alongside [Gemini 3](https://www.shuttle.dev/blog/2025/11/18/gemini-3?utm_source=shuttle_blog&utm_medium=interlink&utm_campaign=ai_coding_tools) and available in public preview at no charge. It's designed for the next era of autonomous coding where agents can work asynchronously across multiple surfaces. What sets Antigravity apart is its multi-model approach. You get access to Gemini 3 Pro with generous rate limits, plus Claude Sonnet 4.5 and GPT-OSS. This cross-platform model access lets you leverage different models' strengths within a single platform. The agent system generates artifacts (task lists, implementation plans, walkthroughs, screenshots, browser recordings) to help you verify its work. It's built around four tenets: trust, autonomy, feedback, and self-improvement. The feedback system lets you comment on artifacts Google-doc-style, and the agent learns from past work through a knowledge base. Currently available as a free public preview for MacOS, Windows, and Linux. **Best for:** Developers who want truly autonomous agents that can work across multiple surfaces (editor, terminal, browser) and handle complex end-to-end tasks asynchronously. ### Browser-Based Tools #### Lovable: Full-Stack App Builder Lovable lets you build full-stack applications entirely in the browser. You describe what you want, it generates the code, and you can iterate immediately with a live preview. Lovable switched to Claude 4 platform-wide, delivering approximately 25% fewer errors and 40% faster prompt execution. The team chose Claude after evaluating commercial and open-source models because it performed best at generating production-ready code. The platform builds full-stack apps with React, Tailwind, Vite on the frontend and connects to Supabase for backend and database. The browser-native development means no local setup. The company has grown to 2.3 million monthly active users and 180,000 paying subscribers with this approach. **Best for:** Rapid prototyping and MVPs when you want to validate ideas quickly. #### Bolt: Prompt-To-App Generation Bolt (from StackBlitz) takes natural language prompts and generates full-stack applications. Like Lovable, it's browser-based, but with stronger focus on framework flexibility - it can generate Next.js, Remix, or vanilla setups. Bolt partnered with Anthropic and now provides Claude Sonnet 4 to all users. Claude 3.5 Sonnet was described by StackBlitz as "the enabling technology that made this product possible" for Bolt. Within four weeks of launching with Claude, Bolt went from zero to $4 million in ARR. The generated code is surprisingly production-ready. It handles routing, state management, and API integration. You can download the code and continue locally. **Best for:** Quick MVPs and demos when you need to ship something functional fast. #### v0: AI-Powered UI Generator v0 (previously v0.dev, now v0.app) is Vercel's AI builder that generates React code using shadcn/ui and Tailwind CSS. You describe the UI you want - like "a pricing section with three plans" - and v0 instantly generates clean, editable code. The platform evolved beyond simple UI generation into a full agentic system. It can research, debug, plan, conduct web searches, read files, inspect sites, and manage tasks. Vercel uses a composite approach: retrieval to ground the model, a frontier LLM for reasoning, and a streaming post-processor called "AutoFix" that catches errors during generation. Founders use v0 for everything from pitch decks to live MVPs, including landing pages, onboarding flows, dashboards, and data capture. The Platform API lets you build your own AI app builders on top of v0's code generation. **Best for:** UI-first development and Next.js applications when you want production-quality components. #### Convex Chef: Full-Stack App Builder Convex Chef builds complete full-stack applications with proper backend infrastructure. Unlike other AI builders that focus mainly on UI, Chef generates both frontend and backend using Convex's TypeScript APIs. You get a complete stack: database, auth, file storage, and background workflows. The platform supports multiple AI providers including GPT-4, Claude, and Gemini, plus a built-in OpenAI proxy for AI app prototyping. Chef is fully open source under Apache 2.0. Over 250,000 people have used it to create full-stack projects. Available locally or at chef.convex.dev. **Best for:** Applications that need real backend functionality - databases, authentication, file uploads, and background jobs without manual setup. #### Replit: Interactive Coding Platform Replit combines an online IDE with AI assistance and real-time collaboration. You can pair program with teammates or with the AI, and everything runs in the browser. Replit Agent v2 (in early access) is powered by Claude 3.7 Sonnet and was rebuilt from the ground up for better autonomy and debugging. It features real-time app design preview that renders interfaces as they're being built - an industry first. The agent forms hypotheses, searches for the right files, and only makes changes when it has enough context. The platform also includes Replit Assistant for code completion, debugging, refactoring, and explanations across most languages and frameworks. Real-time collaboration works like Google Docs for code - multiple people can work in the same project simultaneously. Over 500,000 businesses use Replit for collaboration and prototyping. **Best for:** Real-time collaboration, rapid prototyping, and teaching or learning to code with AI assistance. ### Terminal & CLI Tools #### Claude Code: Natural Language Terminal Assistant Claude Code brings conversational AI to your terminal. It understands your file system, can execute multi-step tasks, and maintains context across commands. Claude Code uses Claude Sonnet 4.5 as the default model, with access to Claude Opus 4.1 and Claude Haiku 4.5. All of these are Anthropic's latest models, giving you access to some of the best reasoning and coding capabilities available. You can ask it to debug issues, refactor code, set up projects, or explain what code does. It has full file system awareness and can execute bash commands, edit files, and manage git operations. Check out our [best practices guide for Claude Code](https://www.shuttle.dev/blog/2025/10/16/claude-code-best-practices?utm_source=shuttle_blog&utm_medium=interlink&utm_campaign=ai_coding_tools) to get the most out of it. **Best for:** Heavy coders who live in the terminal and want natural language control powered by Claude Sonnet 4.5, the leading coding model available today. #### Warp: AI-Powered Terminal Warp reimagines the terminal with a block-based UI where each command and output is a discrete block. The AI can suggest commands, explain errors, and help you build complex command chains. Warp supports multiple AI models including GPT-5, GPT-4o, Claude 3.5 Sonnet and Haiku, Claude 4.x models, and Gemini 2.5. Free users get limited access to advanced models like Claude 4.x, while premium Lightspeed subscribers get unlimited access to the latest models from OpenAI, Anthropic, and Google. You can switch between models by clicking the displayed model name to access a dropdown with all available options. The command palette learns your patterns and suggests relevant commands. The block-based interface makes it easier to reference previous outputs and build command sequences. **Best for:** Terminal power users who want command suggestions and better terminal UX. #### Gemini CLI: Google's Terminal Agent Google's CLI agent leverages the 1M token context window for deep codebase understanding. You can point it at large codebases and ask architectural questions. Gemini CLI is free and open-source. When you log in with a personal Google account, you get free access to Gemini 2.5 Pro. Google AI Ultra subscribers and paid API users can access Gemini 3 Pro, Google's most intelligent model with state-of-the-art reasoning capabilities. The massive context window means it can analyze entire projects without losing track. It's particularly good for navigating unfamiliar large codebases and understanding system architecture. **Best for:** Heavy coders navigating and understanding large codebases with many files. ### Open Source & Models #### OpenAI Codex: ChatGPT-Powered CLI Codex is OpenAI's answer to Claude Code - a CLI agent for software development tasks, powered by GPT-5 models. It works directly in your terminal and integrates with your IDE through extensions. The tool can edit code, run commands, and handle multi-file changes. It supports the Agent Client Protocol (ACP) for seamless IDE integration and offers an SDK for embedding into custom workflows. Available through API key, ChatGPT subscriptions, or GitHub Copilot integration. The CLI supports state-of-the-art code generation with the GPT-5.1-Codex family of models. **Best for:** Developers wanting OpenAI's latest models integrated into their command-line workflow. #### Kimi CLI: Open-Source Agent Tool Kimi CLI is an open-source command-line agent from Moonshot AI. It handles coding tasks, file management, and terminal operations with a 128K token context window suitable for large codebases. The tool operates in dual modes - you can switch between agent mode and shell mode with Ctrl-X. It supports the Agent Client Protocol for IDE integration and is built on the Model Context Protocol (MCP) for interoperability. Powered by the Kimi K2 Thinking model, which achieves competitive performance on coding benchmarks (71.3% on SWE-Bench Verified, 83.1% on LiveCodeBench V6). K2 competes with models like Claude Sonnet 4.5 at a fraction of the cost. We tested K2 extensively in [our hands-on review](https://www.shuttle.dev/blog/2025/11/17/kimi-k2-thinking-hands-on-review?utm_source=shuttle_blog&utm_medium=interlink&utm_campaign=ai_coding_tools) and found it to be a really good open-source alternative. **Best for:** Developers wanting an open-source CLI agent with strong reasoning capabilities and cost efficiency. ## Honorable Mentions **Tabnine** - Privacy-focused code completion with on-premises deployment options and team learning. Good for enterprises with strict data requirements. **Cline** - Local-first VS Code agent that runs entirely on your machine. Great for privacy-conscious developers who want AI assistance without cloud dependencies. **Aider** - Git-aware pair programming tool that understands your repository structure and commit history. Excellent for maintaining context in version-controlled projects. ## How to Choose the Right Tool for Your Needs Before diving into specific recommendations, consider these criteria when evaluating AI coding tools: **Development approach** - Some tools excel at quick prototyping, others at maintaining large codebases. Match the tool to your project phase. **Environment preference** - Terminal developers work differently from IDE users. Browser-based tools suit different workflows than local-first solutions. **Team vs solo** - Collaboration features matter for team projects. Solo developers benefit from different capabilities. **Model flexibility** - Access to multiple AI models (Claude, GPT, Gemini) gives you options when one struggles with your specific task. **Codebase size** - Tools with large context windows handle big projects better. Smaller projects work fine with any tool. **Integration requirements** - GitHub workflows, AWS services, or specific frameworks might push you toward certain tools. ### Quick Decision Matrix | Tool | For Developers | For Vibe Coders | Environment | Why | | ------------------ | -------------- | --------------- | ----------- | --------------------------------------------- | | Cursor | Yes | Yes | IDE | Strong autocomplete + natural language agents | | Windsurf | Yes | Yes | IDE | Multi-file editing with natural language | | GitHub Copilot | Yes | Yes | IDE Plugin | Great autocomplete, limited natural language | | Kiro | Yes | Yes | IDE Plugin | Spec-driven slows vibe coding | | Google Antigravity | Yes | Yes | IDE | Autonomous agents with natural language | | Lovable | Kinda | Yes | Browser | Full-stack generation, less precise control | | Bolt | Kinda | Yes | Browser | Prompt-to-app, less granular control | | Replit | Yes | Yes | Browser | Collaboration + AI assistance | | Claude Code | Yes | Yes | CLI | Terminal-native with natural language | | Warp | Yes | Kinda | CLI | Better UX, limited natural language | | Gemini CLI | Yes | Kinda | CLI | Large context, less conversational | | OpenAI Codex | Yes | Yes | API | GPT-5 with natural language workflow | | Tabnine | Yes | No | IDE Plugin | Autocomplete only, no agents | | Cline | Yes | Yes | IDE Plugin | Local agent with natural language | | Aider | Yes | Kinda | CLI | Git-focused, requires some precision | ### For Vibe Coders For developers who want to describe features in plain English and let AI handle implementation: **Cursor** leads here with **Agent mode** and parallel agents. The natural language understanding is strong, and the multi-file editing works reliably. Background agents let you describe tasks and get PRs when complete. **Claude Code** excels in the terminal for natural language workflows. You can describe complex multi-step operations, and it handles file system operations, git commands, and code changes while maintaining context. **Windsurf** offers similar capabilities to Cursor with solid natural language understanding and coordinated multi-file changes. ### UI-Focused Development When you need visual interfaces quickly: **v0** generates production-quality React components with shadcn/ui and Tailwind. The components are clean, accessible, and ready to drop into Next.js applications. The iteration speed is impressive - describe changes and see them instantly. **Bolt** creates complete UI with routing and state management. The framework flexibility means you can generate Next.js, Remix, or vanilla setups based on project requirements. **Lovable** builds full interfaces with live preview. The Claude 4 integration delivers fewer errors and faster execution. Good for when you need a complete frontend quickly. ### Full-Stack Applications For building complete applications with backend infrastructure: **Convex Chef** stands out by generating both frontend and backend. You get database, auth, file storage, and background workflows without manual setup. The TypeScript-first approach means type safety across your entire stack. Being open source lets you customize the generation or self-host if needed. ### Heavy Development For experienced developers working on complex, production codebases: Use **Cursor** and **Claude Code** together. This combination gives you the best of both worlds. **Cursor Tab** provides inline suggestions that are fast and context-aware. The autocomplete speed is impressive - it suggests completions as you type with minimal latency. Use this for day-to-day coding when you know what you're building but want intelligent assistance with syntax and boilerplate. **Composer** is Cursor's fast model designed for quick tasks. It handles simple refactors, bug fixes, and straightforward features quickly without the cost or latency of frontier models. Use Composer for tasks you could handle yourself but want to speed up. **Claude Code** excels at planning and executing complex, multi-layer features. Open a terminal alongside Cursor and run Claude Code there. Use it for architectural decisions, complex refactoring that requires understanding system interactions, debugging intricate issues, and tasks that span multiple components. Claude Code's strength is maintaining context across complex operations and coordinating file edits with terminal commands. The workflow: code in Cursor with Tab for inline suggestions and Composer for quick tasks. When you hit a complex feature that needs planning and coordination, switch to Claude Code in your terminal and toggle to **Plan Mode**. If you run out of Claude Code tokens, fall back to Cursor's agent mode to continue. This combination keeps you productive regardless of task complexity or token limits. ## The Future: Multi-Agent Systems The most interesting development isn't better autocomplete - it's coordinated multi-agent systems. Tools like Windsurf's Cascade, Google Antigravity's agent workspace, and Claude Code's sub-agent system show where things are heading. Instead of asking an AI to make specific changes, you describe the outcome you want and agents coordinate to make it happen. One agent analyzes architecture, another handles refactoring, a third updates tests, and a fourth generates documentation. Claude Code implements this with built-in specialized agents. The Explore agent uses Haiku (cheaper and faster) to search your codebase and gather context for the main orchestrator agent. You can also create custom sub-agents for specific tasks in your workflow. This agent hierarchy means the expensive frontier model only runs when you need deep reasoning, while cheaper models handle research and context gathering. This means less manual orchestration. You spend less time thinking about how to break down changes and more time reviewing results. The agent systems handle coordination, dependency ordering, and error recovery. The trend is toward specialized agents collaborating on distinct tasks rather than one monolithic AI trying to do everything. Each agent has specific expertise and responsibilities, similar to how you'd divide work among team members. ## Key Takeaways Most tools offer free tiers - experiment to find what works for you. Context-aware assistants that understand your codebase beat simple autocomplete every time. Multi-agent systems represent the next evolution in AI coding assistance. Security features matter when you're shipping production code - evaluate what data leaves your machine. There's no one-size-fits-all solution. Choose based on your actual workflow, not feature lists. Open source options exist for developers with privacy concerns or specific customization needs. The tools that fit your development environment naturally will get used. The ones that force workflow changes will gather dust. ## Conclusion AI coding assistants moved from novelty to necessity faster than most of us expected. The best tool depends on your environment, use case, and workflow - not which one has the most impressive demo. Start with free tiers to find what actually fits your development style. The future is collaborative AI agents working together to handle complex tasks autonomously. The tools exist. The question is which ones match how you actually work. ## Try Shuttle for Your Next Rust Project Speaking of better development workflows - if you're building in Rust, Shuttle removes infrastructure headaches so you can focus on code. Deploy your Rust applications with a single command: ```bash shuttle init --template axum ``` And when you're ready to deploy, just run: ```bash shuttle deploy ``` And you're done! Check out our templates and get started at [Shuttle Templates](https://console.shuttle.dev/templates) --- ## Frequently Asked Questions --- # Gemini 3 Pro: Google's Latest AI Model Hits the Scene Source: https://www.shuttle.dev/blog/2025/11/18/gemini-3 Date: 18 November 2025 Author: dcodes Tags: ai, gemini, llm, benchmarks Google drops Gemini 3 Pro with impressive benchmarks and real-world performance. We look at what the numbers say and what developers are actually experiencing. Google released Gemini 3 Pro today on _November 18, 2025_, and the AI community wasted no time putting it through its paces. The model tops the [LMArena](https://lmarena.ai/leaderboard) with a 1501 Elo score, the highest rating on the leaderboard. ## What's New Gemini 3 Pro combines reasoning, multimodal understanding, and agentic capabilities in one model. Google positions it as a significant step toward AGI, though we'll let the benchmarks and real-world usage speak for themselves. The model comes in two flavors: - **Gemini 3 Pro**: The standard model with strong baseline performance - **Gemini 3 Deep Think**: An enhanced reasoning mode that pushes performance further on complex problems ## Performance Numbers The benchmarks show strong performance across academic reasoning, math, visual understanding, and coding tasks. Gemini 3 Pro outperforms previous models on most tests, with particularly notable improvements in visual understanding (ScreenSpot-Pro jumps from 11.4% to 72.7%) and competitive math problems (MathArena Apex at 23.4% vs. 0.5% for Gemini 2.5 Pro). ## What Developers Are Saying The community is overwhelmingly positive. Users report that Gemini 3 Pro handles math, physics, and coding tasks well. Several developers mention it's passing private benchmarks where other state-of-the-art models fail. One notable aspect is visual understanding. The model's ability to recognize and understand elements in images is impressive. For coding, developers using it in Cursor IDE report positive experiences. The model appears to handle complex spatial reasoning problems better than previous models, with users mentioning it solving problems that typically trip up other AI systems. The Deep Think mode, when set to "thinking mode high," shows DeepThink-like performance. It's capable of solving complex problems that typically trip up other AI systems. ## Gemini 3 Deep Think Gemini 3 Deep Think represents a step-change in reasoning capabilities, effectively setting a new State of the Art (SOTA) for complex problem-solving. It pushes boundaries where standard models plateau. In benchmarks, it delivers impressive results: - **Humanity's Last Exam**: 41.0% (without tools) - **GPQA Diamond**: 93.8% - **ARC-AGI-2**: 45.1% (with code execution) For developers, this matters because of the focus on "novel challenges." The 45.1% score on ARC-AGI-2 is particularly telling - it measures the model's ability to adapt to new problems rather than regurgitating memorized patterns. This suggests Deep Think will be a more reliable partner for debugging obscure race conditions or architecting complex systems where there isn't a StackOverflow answer ready to copy-paste. ## Three Core Use Cases Google positions Gemini 3 around three main capabilities: **Learn Anything**: The model handles multimodal learning across text, images, video, audio, and code with a 1 million-token context window. It can translate handwritten recipes, generate interactive materials from academic papers, and analyze sports videos. AI Mode in Google Search now uses Gemini 3 for its generative UI experiences. **Build Anything**: Positioned as Google's best coding model yet, it is available in Google AI Studio, Vertex AI, the Gemini CLI, and Google Antigravity (more on that below). Third-party integrations include Cursor, GitHub, JetBrains, Manus, and Replit. **Plan Anything**: The model tops Vending-Bench 2 for long-horizon planning and can maintain consistent tool usage over extended workflows. Google is making a Gemini Agent available for Google AI Ultra subscribers in the coming weeks. ## Google Antigravity Alongside Gemini 3, Google launched Antigravity, an agentic development platform that gives AI agents direct access to the editor, terminal, and browser. The system uses Gemini 3 Pro, Gemini 2.5 Computer Use, and Nano Banana (Gemini 2.5 Image) to autonomously plan and execute software tasks. The platform aims to make AI more of an active development partner rather than just a tool you query. Agents can handle complex, end-to-end tasks with less hand-holding. ## Safety and Availability Google emphasizes that Gemini 3 includes comprehensive safety evaluations with reduced sycophancy, increased resistance to prompt injections, and enhanced protection against cyberattacks. The model has been evaluated by independent assessors and security experts. **Current Availability:** - Gemini app and AI Mode in Search (Pro/Ultra subscribers) - Google AI Studio and Antigravity - Gemini CLI and Vertex AI - Third-party integrations (Cursor, GitHub, JetBrains, etc.) The Deep Think mode will roll out to Google AI Ultra subscribers in the coming weeks. ## What This Means for Developers We are developers and we're super excited when a new model comes out that we can use to build better software. Let's dive in on what this means for us developers. Gemini 3 Pro brings some practical improvements to the table. The agentic coding capabilities stand out. The model can actually use a terminal, not just write code, but execute commands, debug issues, and handle multi-step workflows. It is available in Cursor, GitHub, JetBrains, Cline, and other IDEs you are already using. For vibe coding (turning natural language into working apps), Gemini 3 Pro tops the WebDev Arena leaderboard at 1487 Elo. The model handles complex instruction following well enough that you can describe what you want and get functional, interactive code without multiple rounds of refinement. Google AI Studio's Build mode is optimized for this, going from a single prompt to a working app. The multimodal understanding improvements matter for practical applications. Document understanding goes beyond simple OCR to handle complex layouts and reasoning. Spatial reasoning enables screen understanding for computer use agents, allowing the model to interpret UI elements, mouse movements, and screen annotations. Video understanding handles high frame rates and long-context recall, which is useful for processing hours of footage. Gemini 3 Pro integrates into production workflows through the Gemini API (available in Google AI Studio and Vertex AI). Pricing is $2/million input tokens and $12/million output tokens for prompts under 200k tokens. You get rate-limited free access in Google AI Studio for testing. The model includes a client-side bash tool for local filesystem navigation and system operations, plus a server-side bash tool for multi-language code generation. Grounding with Google Search and URL context now work with structured outputs, which helps when building agents that fetch data and need specific output formats. Google Antigravity is their new agentic development platform where you work with autonomous agents that have direct access to the editor, terminal, and browser. The agents handle planning and execution while you focus on architecture. It is available now for MacOS, Windows, and Linux. ## Final Thoughts Gemini 3 Pro is the first model on the LMArena leaderboard, and according to developer communities, it is likely the best coding model available right now. The multimodal capabilities and coding performance stand out. What makes Deep Think particularly interesting is its focus on "novel challenges." or problems that are not in the training data. The 45.1% score on ARC-AGI-2 suggests it can adapt to new problems rather than just regurgitating memorized patterns. For developers, this promises a more reliable partner for debugging obscure race conditions or architecting complex systems where you can't just rely on existing answers. If you are building with AI, Gemini 3 Pro is worth testing, especially for agentic workflows and terminal-based development. The model integrates into the tools developers actually use, and the pricing is reasonable for experimentation. ## Get Started with Rust and AI LLMs are getting more powerful for Rust development. Try our Axum template and let us know how it worked out for you: ```bash shuttle init --template axum ``` Join our Discord to discuss Gemini 3 Pro and share your thoughts on the model. --- # Kimi K2 Thinking Review: Testing the Open-Source Reasoning Model on Real Code Source: https://www.shuttle.dev/blog/2025/11/17/kimi-k2-thinking-hands-on-review Date: 17 November 2025 Author: dcodes Tags: ai, llm, coding, rust I put Moonshot AI's new K2 Thinking large language model to the test on the Shuttle codebase to see if the hype around its reasoning capabilities holds up in practice Last week Moonshot AI released an upgraded version of their Kimi K2 open source model with thinking capabilities. The benchmarks are competitive and the pricing is cheaper than other proprietary models. But competitive benchmarks don't tell the whole story. That's not what caught my attention. The real problem with AI coding assistants is tool use. They forget context, repeat themselves, or drift off track. For developers working with codebases, this is a dealbreaker. You need file searches, code reads, edits, test runs, all chained together without the large language model falling apart. K2 Thinking is built and tested specifically to handle large amounts of sequential tool calls while maintaining coherence. It performs continuous cycles of reasoning, searching, browsing, and code implementation without losing the thread. For coding tasks that involve MCP servers and extensive tool use, this changes what's possible. I tested it on a production codebase to see how it performs on real software engineering tasks. ## What Makes K2 Thinking Different Before we dive into the hands-on experience, let's take a look at the technical details of the model. Kimi K2 is open source, and unlike the closed source models, it shows you its reasoning process before responding. The model architecture uses a Mixture-of-Experts (MoE) design with **1 trillion token parameters**, but only **32 billion active parameters per inference** across **61 layers with 384 experts**, selecting **8 per token** and offering a **256K token context window**. The MoE design is what makes it remotely runnable on consumer hardware. Instead of loading all trillion parameters, you're working with 32 billion active parameters at any given time while the rest sit dormant until needed. ## The Benchmark Story Moonshot published some strong numbers, and they're competing directly with closed models like GPT-5 and Claude Sonnet 4.5: | Benchmark | K2 Thinking Score | Notes | | -------------------- | ----------------- | ----------------------------------------------------------- | | SWE-Bench Verified | 71.3% | The one I care about for coding tasks | | BrowseComp | 60.2% | vs GPT-5's 54.9% for agentic capabilities in search | | AIME25 | 99.1% | Near-perfect on math competition problems with Python tools | | LiveCodeBench v6 | 83.1% | Competitive programming | | Humanity's Last Exam | 44.9% | Beating both GPT-5 and Claude with tools | The SWE-Bench score is particularly interesting because that benchmark measures benchmark performance on actual software engineering tasks - the kind of real world testing I wanted to evaluate. ## Review: Kimi K2 Thinking on a Real Codebase Benchmarks always inflate the capabilities of the model, and sometimes they don't accurately reflect the model's reasoning capabilities when it comes to actual use cases. For that reason, I'm going to review the model on a real codebase and give it a real task to solve. The task I've chosen includes a large codebase with very large files, some of the files are over 2000 lines of code. It requires a lot of context along with reasoning to solve the problem. It also requires codebase navigation to find the relevant files and pieces of code, which makes it a good test for the model's tool use and agentic capabilities. The GitHub issue that I chose can be found [here in the Shuttle repository](https://github.com/shuttle-hq/shuttle/issues/2055). > To summarize the issue: when you create a project with the Shuttle CLI, it generates a `.shuttle/config.toml` file storing your project ID, but when you run `shuttle project delete`, the project gets removed from the platform while the _stale project ID_ stays in your local config file. This stale value causes confusion in subsequent CLI commands since the CLI tries to use a project ID that doesn't exist anymore. Similarly, if you delete a project and create a new one, the old ID persists instead of updating to the new project's ID. The fix needs config file management to delete the project ID when running `shuttle project delete` and update the config with the new project ID when running `shuttle project create`. ### The Prompt I wanted to give it a very high level prompt without delving into the details and give the model a chance to understand the problem and come up with a plan by itself. Here's the prompt: ````md # Issue #2055: Auto update project id in .shuttle/config.toml on project creation Project ID stored in `.shuttle/config.toml` will be stale if the project is deleted or a new project is created. Update `.shuttle/config.toml` with the new project ID when creating a project with `shuttle project create` and delete it when the project is deleted with `shuttle project delete`, so it doesn't keep the old ID after deletion. `.shuttle/config.toml` example: ```toml id = "proj_01K9CHACCQ3RN8D5HJ91F141AN" ``` ```` ### Kimi K2 Thinking in Action Like any other model, the first step is to search the codebase and collect context. K2 used `find` to locate project-related files, ran `Grep` searches for `project create` and `project delete`, then found and read `cargo-shuttle/src/lib.rs` and `cargo-shuttle/src/config.rs` to understand how the config management works. Worth noting: the `lib.rs` file is over 2000 lines, and most AI coding assistants struggle with files this size. K2 Thinking read it, understood the structure, and correctly identified where the code generation needed to happen. After the exploration phase, it presented a plan and started the code implementation. ## Testing the Implementation It updated the codebase and wrote some code. The code compiles without any errors, but the real question is if it works as expected. The script that I used for testing is below, it just compiles the code and creates an alias for the dev CLI: ```sh #!/bin/sh (cd /home/dev/Desktop/shuttle/ && cargo build) # Compile the code alias shuttle-dev=/home/dev/Desktop/shuttle/target/debug/shuttle # Create an alias for the dev CLI # Subsequent commands can be run with the alias: # shuttle-dev project create --name my-axum-app # shuttle-dev project delete # shuttle-dev project create --name my-axum-app ``` **Test 1: Project Creation** Running `shuttle-dev project create --name my-axum-app`: Success. The project was created with ID `proj_01KA3S8JN8TNWBGSJZFJRXM7PG`, and checking `.shuttle/config.toml`: The config file was updated correctly with the new project ID. **Test 2: Project Deletion** Running `shuttle-dev project delete`: The project was deleted from the platform. Checking the config file: Empty. The stale project ID is gone. **Test 3: Creating a New Project After Deletion** The final scenario: delete a project, create a new one, and verify the config syncs with the new ID. Running `shuttle-dev project create --name my-new-axum-app`: The config updated with the new project ID (`proj_01KA3SE04947EBMCF2RMZQT3BP`). Everything stayed in sync. All three test cases ✅ passed. The solution performed exactly as needed. --- ## Review Summary K2 Thinking handled this specific task well. It explored a large codebase systematically, understood the context across a 2000+ line file, and implemented a working solution on the first try. The thinking capabilities showed their value in the methodical reasoning process. Rather than jumping to conclusions, it mapped out the codebase structure before making changes. This was a real-world coding task, not a synthetic benchmark, and it worked well. That said, it doesn't mean it will work on all projects. Success depends on the type of complexity involved. For this particular issue (understanding config file management and modifying CLI commands), the model performed solidly. The tradeoff is speed. This isn't a fast model. The thinking phase adds latency, and for simple tasks, that overhead might not be worth it. But for complex codebases where understanding context matters more than raw speed, the deliberate approach pays off. Would I use it for quick one-off scripts or simple bug fixes? Probably not. For navigating unfamiliar production codebases and making changes that need to be right the first time? It's a solid option, especially considering it's an open source model and you can run it locally (if you have the hardware). The benchmark numbers hold up in practice. The 71.3% SWE-Bench score makes sense after seeing it work through a real engineering task. Not perfect, but capable enough to be useful. ## Working with AI Coding Tools If you're interested in getting the most out of AI coding assistants like Claude Code, check out our [comprehensive guide on best practices](https://www.shuttle.dev/blog/2025/10/16/claude-code-best-practices?utm_source=shuttle_blog&utm_medium=blog&utm_campaign=kimi_k2_thinking). It covers practical techniques for prompt engineering, codebase navigation, debugging workflows, and how to structure your projects to work effectively with AI assistants. Join our Discord server to stay updated on the latest AI coding tools and best practices. --- # Infrastructure as Code Problems: Why Developers Are Wasting Their Time Source: https://www.shuttle.dev/blog/2025/11/13/infrastructure-as-code-problems Date: 13 November 2025 Author: demola Tags: infrastructure, devops, terraform, deployment You didn't hire developers to debug Terraform state files. Yet here we are. This breakdown shows what IaC is really costing you and how to stop the bleeding. 51% of your developers are spending more than 20% of their time managing infrastructure code. At an average salary of $150,000 a year, that's $30,000 in lost productivity per engineer. For a 10-person team, you're spending $300,000 a year to babysit YAML instead of building products. Infrastructure as Code (IaC) was supposed to give you [automation](https://www.shuttle.dev/blog/2025/10/21/github-integration), version control, and stable environments. So why are modules piling up faster than your team can refactor them? Why does every [infrastructure change](https://www.shuttle.dev/blog/2022/05/09/ifc) feel like a gamble? Why do state files keep drifting out of sync, blocking work you thought was already done? Let's break down how Infrastructure as Code has become a hidden cost center and what a better path forward looks like. ## How Infrastructure as Code is Slowing Developers Down When you have a dedicated infrastructure team, even simple requests can turn into a waiting game. Spinning up a quick proof-of-concept app to show a colleague might mean going through approvals, resource requests, and tickets before anything happens. That process kills momentum. You lose the freedom to experiment, which slows shipping. Here's what makes it worse: ### Too Many Config Files IaC usually starts small. Maybe it's a 50-line Terraform config to spin up a basic web service. But as the project grows, you need to consider scalability, databases, and networking. Suddenly, your once-minimal configuration balloons to 500+ lines spread across multiple files. Every new service, subnet, or secret adds another layer of complexity to an already bloated system. ### Refactoring Pain Variables, modules, and providers make IaC flexible and reusable, but maintaining it is still tricky. Most of the time, you end up digging through multiple files to make a small change. For example, renaming a resource might sound simple, but if you miss updating every reference to it, you could accidentally destroy and recreate infrastructure in production. As [Matt Moore](https://thenewstack.io/infrastructure-as-code-in-2024-why-its-still-so-terrible/), CTO at Chainguard, put it: "Having used Terraform extensively, refactoring is extremely painful." This is not an exaggeration. Even a small refactor can break dependencies or invalidate states, forcing you to rewrite chunks of code. That's valuable time that could have gone into shipping new features instead of firefighting broken infrastructure. ### State File Problem Terraform uses a state file to stay consistent and reliable while managing infrastructure across multiple cloud providers. As important as it is, the state file can also become a major pain point. Since it serves as the single source of truth, it's also a single point of failure. It's prone to drift, corruption, or merge conflicts during team collaboration. Something as simple as your colleague running a `terraform apply` might lock the state file and block your deployment. The net effect of all the challenges above hits harder than most teams admit. Development cycles slow down, features miss deadlines, and engineers spend evenings debugging HashiCorp Configuration Language (HCL) syntax instead of solving interesting problems. A [recent report](https://stackgen.com/stackedup-infographic-2025) shows that 75% of infrastructure stakeholders feel frustrated chasing these configuration errors, which makes development drag on much longer than it should. The moment you factor in how much time your team is spending to keep things stable, the cost becomes hard to ignore. ## The Hidden Costs of Infrastructure as Code for Teams As we calculated earlier, a 10-person team spending one day a week on infrastructure loses $300,000 in time alone. But that isn't the cost that hurts the most. **Knowledge silos** form faster than you can stop them. The same "Terraform person" who helped ship early features suddenly becomes the bottleneck for every new project. When new hires join, onboarding takes weeks because that one expert is juggling infrastructure work while also trying to explain thousands of lines of configuration spread across multiple repositories. Then there's **burnout**. Engineers join to build things, not debug infrastructure that won't reconcile after a provider update. No developer dreams of that. They want to create products, solve problems, and see their work make an impact. When debugging infrastructure consumes a big part of their day, frustration builds, and some start looking for exits. Eventually, every team hits the same wall: too many configs, too much boilerplate, and not enough actual building. It's not that IaC failed; it just became heavy. The good news is that a new kind of platform is changing the story. Instead of writing long scripts or wrestling with complex IaC module logic, you describe your environment alongside your application logic, and the tooling handles the "how." ## From Infrastructure as Code to Infrastructure from Code If IaC feels like extra work, it's because it is. It forces you to separate how you build from how you deploy. You're writing configuration in one language and application logic in another, even though they describe the same system. That split creates constant friction and slows development. So, what's the fix? Instead of managing a parallel stack of configuration files, what if you could eliminate them? That is the power of Infrastructure from Code. The IfC model takes a different stance. It argues that your infrastructure needs should live inside your application code, right where you use them. Instead of maintaining a 300-line Terraform file, your infrastructure becomes a short annotation or macro on the function that requires the resource. This isn't just about convenience. It removes an entire category of work. With Infrastructure from Code, your infrastructure is: - Co-located: It lives directly in your `main.rs` file, not in a separate infra repository. - Obvious: Anyone reading your code can see exactly what resources it depends on. - Always in sync: It is reviewed, versioned, and deployed as a single unit with your application. There is no state drift because the code is the definition. Shuttle was built on this core philosophy of making your code declare its own needs. ## How Shuttle Manages Infrastructure and Development Together [Shuttle](https://www.shuttle.dev/) takes a fundamentally different approach. Instead of managing infrastructure through separate configuration files, you declare what you need directly in your application code using annotations. To see this in action, let's compare it with IaC. Say you want to deploy a web application with a database and plan to scale as you go. With IaC, your configuration might look like this: - `main.tf`: Provider configuration with backend setup (approximately 30 lines) - `vpc.tf`: VPC, subnets, internet gateway, route tables (80 lines) - `security-groups.tf`: Ingress/egress rules for app and database (40 lines) - `rds.tf`: Database instance, parameter groups, backup config (50 lines) - `ecs.tf`: Cluster, task definitions, service configuration (60 lines) - `variables.tf`: Input variables (20 lines) - `outputs.tf`: Connection strings and endpoints (15 lines) That's roughly 300 lines of IaC code across seven files, not counting logging, monitoring, or environment-specific configurations. In comparison, doing the same thing with Shuttle is far simpler. You don't need separate files for your application logic and infrastructure. You simply create your application and annotate the resources you need (a database in this example): ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> ShuttleAxum { pool.execute(include_str!("../schema.sql")) .await .map_err(CustomError::new)?; // other application code here } ``` This looks like your normal application code, with a few additions. You'll notice the use of Rust macros to annotate the code: - `#[shuttle_runtime::main]` provisions the Shuttle runtime environment where your app runs. - `#[shuttle_shared_db::Postgres]` configures a Postgres database and injects a connection pool that your app can query directly. Once you run the `shuttle deploy` command, Shuttle automatically initializes and provisions the resources your application needs, leaving you free to focus on what matters. That's all you need to do. No YAML, HCL, or state files to manage. Your development and infrastructure code remain tightly coupled, giving you greater control as you build and scale. With Shuttle, your IaC burden drops significantly: - Updates happen automatically, and provider changes, security patches, and infrastructure updates are handled behind the scenes while your annotations stay the same. - You can prototype quickly with low effort and get built-in support for popular Rust frameworks. - You ship faster since you no longer need to switch contexts between development and infrastructure code. - Fast redeployment and local iteration make it easy to test changes without long build times. Once you see how Shuttle keeps your app and infrastructure in sync, the next question is obvious: How do you start using it? The good news is you don't have to rewrite everything from scratch. Shuttle's migration flow is designed to meet you where your code is and get you running fast. ## How to Migrate to Shuttle Without Breaking Things You don't need to treat migration like a six-month project. You can start in ten minutes. Every day you delay is another day of lost developer time. Here's how to move forward without disruption: - Start with new services: Stop building new projects on the old stack. Every new service you create should begin on Shuttle. It keeps your infrastructure close to your code and prevents the sprawl that slows teams down. - Migrate one small, painful service: Pick a microservice that is always difficult to deploy. The one with fragile pipelines or constant configuration drift. Move that first. You'll have it running on Shuttle in an afternoon. - Run in parallel: Keep your existing setup alive while testing the Shuttle version. You'll see the difference immediately. Deployments that once took hours now take seconds. - Build confidence gradually: Once you've seen the results, migrate other services one by one. Each move will take less time than the last. - Yes, Rust has a learning curve. But here's what you're not accounting for: you're already paying that cost in IaC complexity. The difference is, Rust's complexity is front-loaded and one-time, while IaC complexity compounds over time. Migration is about recovery. Every week spent maintaining IaC is time and money lost. Shuttle gives that time back. ## Stop Paying Developers To Manage Infrastructure Every year, your developers spend thousands of hours maintaining infrastructure they were never hired to manage. That's $30,000 of wasted time per engineer, and $300,000 for a ten-person team that could be building instead. IaC was meant to make engineering easier. Instead, it created a new set of challenges that drain time, money, and morale. Shuttle's approach of collapsing configuration into annotations, automating provisioning, and eliminating state management isn't just incrementally better. It's a fundamental rethinking of how infrastructure should work. This means more building and less time babysitting configuration files. Ready to see how Shuttle eliminates IaC complexity? Try out our axum template and deploy in minutes. ```bash shuttle init --template axum ``` --- # Building Rust Web Apps Source: https://www.shuttle.dev/blog/2025/11/12/build-rust-web-apps Date: 12 November 2025 Author: dcodes Tags: rust, web development, guide, axum, sqlx A practical guide to building production-ready web applications in Rust, cutting through framework confusion to help you make informed stack decisions. Rust promises zero-cost abstractions and blazingly fast performance, but when you're ready to build a web application, you quickly realize it's not "batteries included" like other languages, especially for the web. Unlike Go with its standard library HTTP server or Python with Django, Rust makes you choose everything: your web framework, database library, templating engine, and more. The world of web development using Rust has evolved significantly, with frameworks like Actix Web and Axum leading the charge. This guide provides practical resources and real-world examples to help developers build web applications without getting lost in framework comparisons. Here's what I've learned from trying out many different frameworks and libraries: Rust's lack of a garbage collector and minimal memory footprint make it exceptional for web development and microservices. You can scale horizontally without the overhead of spawning a garbage collector for each service—your services can be using as little as 5MB of memory when idle. Also when it comes to processing large datasets or building performance-critical web applications, Rust's speed will put you in the fast lane. This guide cuts through the noise to help you make informed choices about your web stack without spending weeks researching. It will be a little opinionated as well when it comes to recommendations section. So, let's get started! ## Why Rust for Web Development? Rust delivers on its promise of zero-cost abstractions. You get functional programming patterns like `Option` and `Result` types, powerful pattern matching, and efficient iterators without runtime overhead. The compiler enforces memory safety at compile time, and without a garbage collector your performance is predictable. Rust does have a learning curve, especially if you're coming from garbage-collected languages. The borrow checker takes time to understand, and you'll initially spend more time satisfying the compiler than writing features. But once you internalize ownership and borrowing, you'll write safer code naturally and implement features more confidently. Whether you're building a simple rust web page or a complex microservices architecture, these fundamentals apply. There are areas in which you wouldn't want to use Rust, for example rapid prototyping or if you're still learning web development concepts. Go or JavaScript might be better choices in those cases. Rust shines when you need reliability, performance, and optimized resource consumption. For developers building production web applications, these trade-offs often make sense. ## The Framework Landscape ### Choosing a Web Framework Rust isn't _"Batteries Included"_ but the good news is that Rust for the web has a mature ecosystem of production-ready backend frameworks. Many of these frameworks are actively maintained and have a strong community behind them, some of the most popular ones are: **Axum** is what I recommend for beginners. It produces fewer _arcane_ compiler errors because it uses common ecosystem crates rather than reinventing everything. The learning curve is gentler, and you'll spend less time fighting the type system. For example, setting up a basic route handler to process HTTP requests is straightforward and intuitive. **Actix Web** is the battle-tested performance leader with excellent documentation. If you need proven reliability at scale, this is your choice. Many production projects using Rust run on Actix Web, handling millions of requests per day. **Rocket** offers solid ergonomics and a pleasant development experience. It's a dependable alternative that many teams use successfully. **Tide** focuses on productivity with minimal bloat. It's clean and straightforward. There's also **Rouille**, a synchronous framework worth mentioning. Most developers assume async is always better, but Rouille takes a different approach. It ignores async I/O complexity and provides an easy-to-use synchronous API where each HTTP request is handled in its own dedicated thread, and responses are sent back immediately. The reasoning is pragmatic: async I/O libraries in Rust are still maturing, and you'd need async database clients and async file loading to fully benefit from async frameworks. Until the ecosystem catches up, Rouille focuses on simplicity. ### Database Integration **SQLx** is the pragmatic choice and widely adopted in the Rust community. It lets you write direct SQL rather than wrestling with ORM abstractions, which means your queries are clearer and you maintain full control—it also gives you better performance than ORMs. The optional compile-time query verification catches SQL errors before runtime. SQLx supports PostgreSQL, MySQL, and SQLite out of the box, making it easy to create database connections for any project. When choosing a database, don't optimize for ease of setup. Choose based on your data modeling needs: **PostgreSQL** is a popular, widely-used relational database known for its performance, scalability, and rich feature set. **SQLite** is perfect for embedded use cases or applications where you want zero configuration. **MySQL** is another widely-adopted relational database that's known for its speed and reliability in web applications. If you prefer ORMs, the Rust ecosystem has solid options: **SeaORM** is a modern async ORM with a developer-friendly API. It generates entities from your database schema and provides a fluent query builder. The API feels natural for developers coming from other ecosystems. **Diesel** is a mature, compile-time verified ORM that's been production-tested for years, used extensively by the Crates.io repository. It's more opinionated about structure but catches query errors at compile time. Some teams find its macro-heavy approach verbose, but others appreciate the safety guarantees. You can check out our blog about Rust ORMs [here](https://www.shuttle.dev/blog/2024/01/16/best-orm-rust?utm_source=building_rust_web_apps&utm_medium=blog&utm_campaign=building_rust_web_apps). ### Server Side Rendering with Templates Templating engines let you generate HTML dynamically by combining static markup with data from your application. They handle the common patterns of web rendering: loops, conditionals, variable interpolation, and template inheritance. Instead of manually concatenating strings or building HTML in your Rust code, you write templates that separate presentation from logic. For example, `{{ user.name }}` in Jinja2/Tera, `<%= user.name %>` in ERB, or `{{ user.name }}` in Handlebars all inject data into your HTML. **[Tera](https://keats.github.io/tera/)** integrates strongly with Actix-web and has solid documentation. If you've used Jinja2 or Django templates, Tera's syntax will feel familiar. It's the safe, practical choice for server-side rendering your rust web page. Here's an example of a Tera template: ```html

Welcome, {{ user.name }}

    {% for item in items %}
  • {{ item.title }} - ${{ item.price }}
  • {% endfor %}
``` ### Frontend Considerations When building web applications, you need to decide how to handle the user interface. The frontend is what users interact with in their browsers, and there are different approaches to building it. You can render HTML on the server and send complete pages, or build a client-side application that runs JavaScript in the browser and communicates with your backend via API URLs. Not every website needs to be a [single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application). Before reaching for a frontend framework, consider server side rendering with your backend framework. SSR with templates like Tera can handle many use cases without the complexity, and lets you implement features faster. If you do need a frontend framework, here are your Rust options: **Yew** is the most mature Rust WASM framework. It uses actual HTML, which means you're learning standard web technologies rather than custom macro syntax. The ecosystem has grown considerably. **Perseus** is a modern alternative worth exploring. It provides a Next.js-like experience with server-side rendering and static generation support. The architecture is thoughtful and the documentation is improving. **Seed** takes a different approach with an Elm-like architecture. However, I'd avoid frameworks that rely heavily on macros for templating. Learning HTML plus macro syntax is double work, and you lose the benefit of standard tooling. However, in reality, Rust WASM frontend frameworks aren't quite production-ready for all use cases. Backend frameworks are mature and battle-tested, but frontend frameworks are still evolving. For production applications today, consider using React, Vue, or Svelte for your frontend and Rust for your backend. **HTMX** deserves special mention here. It's not a framework but a library that lets you access modern browser features directly from HTML. Combined with server side rendering, HTMX gives you dynamic interfaces without complex client-side state management. This is a pragmatic middle ground worth considering, allowing developers to create interactive web pages without writing JavaScript. ## Project Structure As your Rust web application grows beyond a simple example, modularity becomes crucial. Splitting your code into modules provides cleaner separation of concerns, makes testing easier, and keeps your codebase maintainable. A monolithic `main.rs` quickly becomes unwieldy in production applications. When starting a new project, choosing the right structure from the beginning saves refactoring time later. Whether you're building your first Rust project or refactoring an existing one, there are many ways to organize your code to help you create maintainable applications. Here are two common approaches: **Function-based structure** groups by technical role: `routes/`, `handlers/`, `models/`, and `services/`. This makes the technical architecture clear at a glance and helps developers understand the project organization quickly. **Model-based structure** groups by domain: `users/routes.rs`, `users/handlers.rs`, `posts/routes.rs`, `posts/handlers.rs`. If you need user code, you know exactly where to find it. Everything related to users lives in one place. The tradeoff is you'll have many files with the same name, which can make searching less convenient. Both are solid choices. My preference is the model-based approach because it makes finding code easier. When working on a feature, all the related code is grouped together rather than scattered across different directories. This structure also makes sharing data between related modules simpler, as you can keep data models and handlers close together. ## Learning Resources If you're serious about Rust web development, read these books in order: Start with **"The Rust Programming Language"** (The Book). This is your foundation. Don't skip it. Next, read **"Code like a Pro in Rust"** by Brenden Matthews. Skip directly to the HTTP REST API chapter once you've finished The Book. It bridges the gap between knowing Rust and building web services. Then dive into **"Zero to Production in Rust"** by Luca Palmieri. This book is opinionated and comprehensive, with a strong focus on test-driven development and professional practices. It covers real-world deployment considerations you won't find elsewhere. This is the deep dive into production-grade backend development. For practical examples, check out the Realworld Axum SQLx implementation on GitHub [launchbadge/realworld-axum-sqlx](https://github.com/launchbadge/realworld-axum-sqlx). It's a complete application showing how these pieces fit together. ## Deployment Options for Your Web Application **VPS Hosting** is the most common way to deploy web applications. It's a machine that you have full control over. You can add or remove any software on the VPS that you want (or don't want!) to use. However, you'll have to manage everything: load balancing, SSL certs, building, deploying, CI/CD. It can be a pain, but you'll have full control. Both vertical and horizontal scaling becomes a challenge. **Containers** are more reliable and portable than deploying directly to a VPS. It's better to deploy as a container rather than directly running on the host machine. It's more predictable and you'll avoid the "it works on my machine" issue. Docker and containerization give you reproducible deployments and easier rollbacks and better isolation and security. For production scale, **Kubernetes** or **ECS** provide the orchestration you'll eventually need. This is more enterprise scale and it can be quite expensive. It still needs a whole lot of managing and a dedicated team. Don't start there unless you already have that infrastructure and the wherewithal to manage it. **Shuttle** is the easiest choice. It's easy to build, deploy, and get SSL certs. You'll just add a macro to your main function and run `shuttle deploy`, and everything will be handled for you: SSL certs, database included (will be provisioned automatically). It offers a generous free tier and works with any Rust web framework (even your own custom one). For Rust web applications, this is the fastest path to production. Developers can create and deploy projects in minutes. The downside is it only works for Rust at the moment. You can [read the Shuttle documentation](https://docs.shuttle.dev/) for more information about how to deploy a Rust web app to Shuttle. ## Common Pitfalls & Trade-offs ### Async Complexity There's a common misconception that async is always necessary. Jim Blandy's benchmarks show context switching costs are measured in nanoseconds. For most applications, the majority of CPU time should be spent executing business logic, not managing async overhead. If your application is compute-heavy rather than I/O-bound, a synchronous framework might be simpler and just as fast. ### Choice Paralysis Unlike Go's "batteries included" philosophy, Rust requires upfront decisions about your stack. This can feel overwhelming initially. The good news is that the ecosystem has matured significantly. Community consensus exists around solid options for backend frameworks, databases, templating engines, and deployment strategies. The frameworks I've recommended here are all production-ready and well-supported, making it easier for developers to implement their projects with confidence. ### Frontend Maturity The backend story is excellent. Rust backend frameworks are production-ready today. The frontend WASM story is still maturing. Yew has "grown quite nicely" according to its maintainers, but developers should be cautious about using Rust WASM frameworks like Yew or Perseus for production applications unless you've validated they meet your specific needs. The pragmatic approach is to use what works today: HTMX with server side rendering, or a mature JavaScript framework like React for your frontend while leveraging Rust's strengths on the backend to handle API requests and create robust server-side logic. ## Putting It All Together Here's the stack I recommend for most Rust web applications: **Backend:** Axum for its balance of power and approachability. **Database:** SQLx with PostgreSQL for flexibility and reliability. **Templating:** Tera for server-side rendering. **Frontend:** Start with server side rendering and HTMX. Add WASM later only if you have a specific need. **Deployment:** Shuttle for the fastest path to production, with deployment in five minutes using `shuttle deploy`. This stack is production-ready, well-documented, and supported by active communities. ## Conclusion Rust web development requires more upfront research than alternatives like Go or Python. You're making architectural decisions that other ecosystems have made for you. But the payoff is significant: exceptional performance, memory safety guarantees, and predictable runtime behavior. Developers who invest the time to learn Rust will create more efficient and reliable web applications. The ecosystem offers mature, well-documented options for backend development. Choose tools based on your actual requirements rather than hype. Start simple and add complexity only when you need it. The performance and reliability benefits of Rust pay off for production applications, especially when you're building microservices or processing large amounts of data. The initial investment in learning and choosing your stack will pay dividends as your application scales. ## Get Started ```bash # Get started quickly with Shuttle shuttle init --template axum ``` This single command scaffolds a working Axum application with a proper project structure. From there, you're minutes away from a deployed web service. You can then implement your business logic and create the features your application needs. ## Frequently Asked Questions --- # Testing Cursor Composer: The AI Coding Model Built for Speed Source: https://www.shuttle.dev/blog/2025/11/05/cursor-composer-hands-on Date: 5 November 2025 Author: dcodes Tags: ai, composer, cursor, development I spent a day testing Cursor Composer to find out what it's actually good at. Here's what I learned about its speed, capabilities, and limitations. Cursor released their new AI coding model (Composer) last week. The main pitch is speed - turning natural language into working code faster than before. I wanted to see what it's actually good at, so I spent some time putting it through different scenarios. Cursor claims this model to be a competitive AI close to the most frontier models in intelligence, which is why I wanted to test it out, it's the first time we have a model that can compete in both speed and intelligence. I tested Composer across a few key areas: executing tasks of varying complexity, searching codebases and documentation, and planning. These are common software engineering tasks that developers face daily. I'll walk through what worked and what didn't. ## Simple Coding Tasks: Building a REST API with Cursor I started with a straightforward prompt to build a REST API in Rust with CRUD operations: It executed the entire task in 20 seconds, writing the code and running `cargo check` to verify everything compiled without any errors. First time trying this, I didn't expect it to be this fast! I mean I expected it to be fast, but not _this fast_ 🙂. Gave Composer a broad instruction and let it choose whatever framework it thinks best. It chose Axum _obviously_ as every model seems to be using it these days. The code compiles without warnings and the server app is ready to run on port 3000. For developers doing this 3 years ago, it would mean hours of work, especially if you were new to Rust. Twenty seconds from natural language instructions to working code without any compiler errors is bonkers. No runtime errors as well. I tested all the endpoints - creating tasks, listing them, getting by ID, updating, and deleting. Everything works exactly as it should. Okay, this task was very simple: It was a completely new repository with no existing code, no complexity to make difficult decisions, we'll try something a little bit harder in the last section. ## Codebase Search: Finding Relevant Files with Cursor's AI Before planning my tasks when developing features, sometimes I do a codebase search using a fast model to find the relevant file paths for a specific task that needs to be implemented. Then, I'll use the results to plan out the task using a more intelligent model, this helps in speeding things up and saving tokens. So, let's try and do exactly that with Cursor's Composer. I tested this with a real GitHub issue from the [shuttle-hq/shuttle](https://github.com/shuttle-hq/shuttle) repository. The task was to add a `--quiet` flag to suppress non-error output from the `shuttle run` command. I wanted to see how fast Cursor's Composer can find all the relevant files in the codebase. Composer scanned the entire codebase and returned results in 15 seconds: Everything was spot on, and only in 15 seconds. _Crazy fast and crazy good_. ## From Search to Strategy: Creating an Implementation Roadmap Creating plans is a common process when developing software. Let's see if Composer can take those search results and create an implementation plan? I started a fresh chat and fed it the issue description along with all the file paths and locations it had just found. Composer generated a well-structured plan: The plan included an overview, clear implementation steps, specific file locations with line numbers, and code examples showing exactly what needs to be added. It broke down the task into logical steps, pretty much like any other AI model would do. The structure looks solid, but the real test is execution and I don't really know if the approach is actually good or not. So, let's see if Composer can complete the task. ## Complex Tasks: Implementing the Solution Opened a new chat and fed Composer the implementation steps and asked it to implement the changes. It completed the task in 40 seconds, making changes across multiple files. But speed doesn't matter if the code doesn't work. Time to review what it actually did. The code review looked clean. Cursor's reviewer agent didn't flag any issues either. The implementation added the quiet field to the `RunArgs` struct, properly wrapped the print statements, and compiled without errors. I created a new Shuttle project using the official Shuttle Axum template. Then, I verified the flag works as expected by running `shuttle run` without the flag: All the build logs, runtime startup messages, and info logs appear - the default behavior. Now with `shuttle run --quiet`: The build logs from cargo-shuttle disappeared. Only runtime logs remain visible. Exactly what the issue asked for - suppress non-error output from cargo-shuttle while keeping the runtime output. Perfect. The implementation works. ## Final Thoughts: Natural Language to Code at Speed After testing Composer across different scenarios, here's what I think about it: **Codebase search:** Excellent. This is where Composer really excels. Fifteen seconds to scan an entire repository and find every relevant file with specific line numbers is incredibly useful. **Simple to medium tasks:** Really good. The in-memory REST API took 20 seconds and worked perfectly. The GitHub issue implementation worked across multiple files in 40 seconds. For well-defined tasks with clear requirements, it can generate and deliver fast, working code. **Complex architectural decisions:** I wouldn't trust it here. For tasks that require deep technical reasoning, evaluating tradeoffs, or making non-obvious choices, you want a more capable model like Sonnet 4.5. The practical takeaway for software engineering is that Composer gives us an AI tool that's both fast and smart enough for real work. That's a useful combination. I can see developers using Composer every day - searching codebases, making config changes, implementing straightforward backend or frontend features. The kind of tasks where waiting 1min+ for a heavier model feels unnecessary. It's not about replacing more intelligent models. It's about having the right tool for different jobs. For quick, well-scoped tasks, this approach gets you from natural language instructions to working code faster than anything else I've tried. ## Conclusion: The Future of AI-Assisted Development Speed matters in modern software engineering and development work. Cursor's new AI coding assistant (Composer) proved it can handle everyday tasks - searching codebases, implementing features, fixing bugs - in seconds rather than minutes. It's not perfect for everything, but it doesn't need to be. Having a fast model that's reliable for common software development tasks changes how I think about using AI tools in my workflow. Get started with our backend API template and let us know what you'll build with Composer: ```bash shuttle init --template axum ``` --- # Cursor 2.0 is Out! Here is What's New Source: https://www.shuttle.dev/blog/2025/10/31/cursor-2.0 Date: 31 October 2025 Author: dcodes Tags: cursor, v2, ai, development Cursor 2.0 brings their first coding model Composer, a reimagined agent-first interface, built-in browser testing, and parallel multi-agent workflows that change how we build software. Cursor just released version 2.0 with a major update introducing a new agent model **Composer** and a redesigned interface built around agentic workflows. Composer's speed particularly stands out especially when its intelligence is close to frontier models. Beyond the new model, the interface has been rebuilt to treat agents as the primary way you interact with your codebase rather than files. In this article, we'll go over the most important new features and how they change the way you work with AI-assisted coding. ## Composer: Built for Speed Probably the most exciting new feature is the new model **Composer** which is Cursor's first competitive coding model, and it's designed specifically for low-latency agentic coding. The claim is 4x faster than similarly intelligent models, with most tasks completing in under 30 seconds. The performance is the most interesting to me, I've lost count of how many times I've been waiting for a model to complete a relatively simple task only for me to lose patience and do it manually. Not all tasks are complex, not all tasks require _ultrathinking_ and not all tasks require _state of the art_ intelligence. When you're iterating on a feature, waiting 2 minutes versus 30 seconds compounds quickly. Fast feedback loops change how you think about using AI assistance. With that being said, **Composer** is still close to frontier models in terms of intelligence as well, so you'll get the best of both worlds (hopefully). Interestingly, Composer was actually running in Cursor for a while under the name "Cheetah" before this official release. One particularly clever detail: Composer was trained with codebase-wide semantic search as a core tool, making it significantly better at navigating and understanding large codebases. It can also edit multiple files in parallel, which is wild when you see it in action. ## Agent-First Interface The new interface is a complete departure from traditional IDE layouts. The core idea of this new interface is to focus on outcomes and let agents handle the details. You can quickly switch between the agent-focused view and the classic IDE view by clicking the "Agents" button in the top left corner. ## Running Multiple Agents in Parallel Cursor 2.0 lets you run multiple agents in parallel without them stepping on each other's toes. Under the hood, this uses git worktrees or remote machines to isolate each agent's workspace. The idea here is to run the same task across multiple models simultaneously and compare results. For harder problems, this _"bake off"_ approach can potentially produce a better final output than relying on a single model. ## Built-In Browser Testing One of the most practical additions is the native browser tool. Instead of constantly switching between your IDE and browser when working on web applications, the browser lives right inside Cursor. More importantly, agents can interact with it directly. This means an agent can make changes to your code, test them in the browser, see what broke, and iterate until it works. That feedback loop happening automatically is genuinely useful, especially for frontend work where visual verification matters. ## Review Changes As agents do more coding, reviewing their output becomes the real job. Cursor 2.0 introduces a dedicated review interface that makes it much faster to scan through what changed and dive deeper when something looks off. It's not revolutionary, but it's a thoughtful addition that shows they're thinking about the actual workflow, not just generating code. ## Voice Mode Voice mode lets you speak your intentions and have them turned into code. Essentially Speech-to-Code, it's good to see that it's finally here. ## Pricing The pricing structure remains competitive with other AI coding tools. Composer could be one of the best models for the price, especially given its speed to intelligence ratio. ## What This Means Cursor 2.0 feels like a bet on a specific future: one where you spend more time directing multiple AI agents and less time writing code manually. Whether that's the future you want is a different question entirely. For teams that are all-in on AI-assisted development, Cursor 2.0 provides some genuinely useful capabilities. The speed of Composer, the parallel agent workflows, and the built-in browser testing address real pain points that emerge when you're heavily using AI coding tools. The question is whether this agent-first approach becomes the standard way we build software, or if it ends up being a power feature that most developers occasionally use while sticking primarily to traditional file-based editing. Time will tell. Join our Discord and let us know what you think about Cursor 2.0! --- # How to Build a Streamable HTTP MCP Server in Rust Source: https://www.shuttle.dev/blog/2025/10/29/stream-http-mcp Date: 29 October 2025 Author: dcodes Tags: rust, mcp, http, sse, shuttle, ai Learn how to build and deploy a task manager MCP server using streamable HTTP transport in Rust. This guide covers the MCP protocol, real-time updates with SSE, and deployment to Shuttle for production use. [Local MCP servers](https://www.shuttle.dev/blog/2025/07/18/how-to-build-a-stdio-mcp-server-in-rust?utm_source=shuttle_blog&utm_medium=blog&utm_campaign=http_stream_mcp_server&utm_content=intro_link) are great, but they come with friction. Users need to install them on their machines, have the right runtimes (Python, Node.js, etc.) with compatible versions, and manually update them when new versions are released. Tools like `npx` can help with this, but it's still an extra step. There's also the trust factor. Local MCP servers can execute code on your machine, so you're essentially giving the authors system-level permissions. That requires a high degree of trust. HTTP MCP servers or _Remote MCP servers_ sidestep these issues. Instead of running code locally, your MCP client communicates with a live URL. No installation required. Updates happen server-side, and the trust model is simpler since the server can't touch your local machine. Streamable HTTP is the successor to the older HTTP+SSE transport from protocol version 2024-11-05. The current protocol revision (2025-03-26) offers improved flexibility for both basic and feature-rich servers with **streaming** capabilities. In this guide, we'll build a task manager MCP server using _streamable HTTP_ transport. The server demonstrates session-based communication with Server-Sent Events (SSE) for streaming updates from server to client, and HTTP POST requests for client-to-server messages. ## Understanding Streamable HTTP Transport Streamable HTTP works through session-based communication. During initialization, the server assigns a **session ID** and returns it in the `Mcp-Session-Id` header. Clients send messages through HTTP POST requests, and servers can respond in two ways: - Direct JSON response (`application/json`) for simple operations - SSE stream (`text/event-stream`) for operations requiring multiple updates or server-initiated messages Clients also maintain a dedicated SSE connection via HTTP GET to receive server-initiated messages between POST requests. In our task manager example, the flow works like this: when a client (like Cursor) sends a POST request to add a task, the server processes it and can immediately push the result back through an SSE stream. The client maintains an open connection listening for these events, receiving real-time updates as tasks are added or completed. This means the AI agent doesn't need to poll for changes-it gets notified instantly when something happens on the server. For details on other transport types and the complete specification, see the [MCP documentation](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports). ## Building a Streamable HTTP MCP Server In this guide, we'll build a task manager MCP server that keeps track of tasks and allows you to add, complete, list, and retrieve tasks with real-time updates. The task manager implemenation is relatively simple, but it demonstrates the core patterns for building streamable HTTP MCP servers. > Note: This project is designed for learning and demonstration purposes. It doesn't follow production best practices like proper error handling, authentication, or persistent storage. For production use, you'd want to add these features. ### Project Setup We'll need the official [RMCP crate](https://crates.io/crates/rmcp) made by the [modelcontextprotocol.io](https://modelcontextprotocol.io/) team. Create a new project and add the dependencies: ```toml [dependencies] tokio = { version = "1", features = ["full"] } rmcp = { version = "0.8", features = [ "server", "macros", "transport-streamable-http-server", ] } axum = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" schemars = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } ``` The `rmcp` crate with the `transport-streamable-http-server` feature provides everything we need for building streamable HTTP servers. We'll also use the `axum` framework for handling HTTP requests and responses and it integrates seamlessly with the `rmcp` crate. ### Task Manager Implementation First, we'll define the task structure and the manager that holds our state: ```rust #[derive(Debug, Clone, Serialize, Deserialize)] struct Task { id: usize, title: String, description: String, completed: bool, } #[derive(Debug, Clone)] struct TaskManager { tasks: Arc>>, next_id: Arc>, // Counter for generating unique task IDs tool_router: ToolRouter, } ``` `Arc>` ensures thread-safe access to our shared state-multiple clients might be connected simultaneously, and this pattern handles concurrent operations safely. The `next_id` counter increments each time we create a task, giving each one a unique identifier. The `tool_router` (type `ToolRouter` from the `rmcp` crate) handles routing incoming tool calls to the appropriate methods we'll define below. ### Implementing MCP Tools The `#[tool_router]` and `#[tool]` macros make defining MCP tools straightforward. RMCP handles all the protocol specifications behind the scenes, so we just need to focus on implementing our tools. You'd implement the tools just like how you would do it in any other Rust project, write your code and let the macros handle the rest. Here's how I implement the `add_task` tool: ```rust #[tool_router] impl TaskManager { fn new() -> Self { Self { tasks: Arc::new(Mutex::new(Vec::new())), next_id: Arc::new(Mutex::new(1)), tool_router: Self::tool_router(), } } #[tool(description = "Add a new task to the task manager")] async fn add_task( &self, Parameters(AddTaskRequest { title, description }): Parameters, ) -> Result { let mut tasks = self.tasks.lock().await; let mut next_id = self.next_id.lock().await; let task = Task { id: *next_id, title: title.clone(), description, completed: false, }; *next_id += 1; tasks.push(task.clone()); let response = serde_json::json!({ "success": true, "task": task, "message": format!("Task '{}' added successfully with ID {}", title, task.id) }); Ok(CallToolResult::success(vec![Content::text( serde_json::to_string_pretty(&response).unwrap(), )])) } } ``` The `Parameters` wrapper automatically validates and deserializes the input based on the request structure: ```rust #[derive(Debug, Deserialize, schemars::JsonSchema)] struct AddTaskRequest { #[schemars(description = "The title of the task")] title: String, #[schemars(description = "A detailed description of the task")] description: String, } ``` The `schemars` descriptions help AI agents understand what each parameter means, improving their ability to use the tools correctly. I've implemented similar tools for completing tasks, listing all tasks, and retrieving specific tasks by ID. Each tool follows the same pattern: validate input, perform the operation, return structured results. ### Server Handler Implementation The `ServerHandler` trait defines server metadata and capabilities. Every MCP server must implement this trait to be compliant with the MCP protocol, the AI agents will query this metadata to understand what the server is used for and which specification version it supports. > In a real life project you'll want to be more descriptive with the server metadata and capabilities, but for the sake of this guide we'll keep it simple. ```rust #[tool_handler] impl ServerHandler for TaskManager { fn get_info(&self) -> ServerInfo { ServerInfo { protocol_version: ProtocolVersion::V_2025_03_26, capabilities: ServerCapabilities::builder() .enable_tools() .build(), server_info: Implementation { name: "task-manager".to_string(), version: "0.1.0".to_string(), title: None, website_url: None, icons: None, }, instructions: Some( "A task manager MCP server that allows you to add, complete, list, and retrieve tasks with real-time updates." .to_string(), ), } } } ``` Setting up the MCP service is as easy as that. Now we need to wire everything together and serve it over HTTP, and for that we'll use the `axum` framework which integrates seamlessly with the `rmcp` crate. ## Serving the MCP Server over HTTP With our task manager service defined, the final step is creating the HTTP server that exposes it. We'll configure logging with `tracing`, initialize our `StreamableHttpService` with the `TaskManager`, and start an Axum server to handle incoming MCP requests over HTTP. ```rust {11-15, 17} #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info".to_string().into()), ) .with(tracing_subscriber::fmt::layer()) .init(); let service = StreamableHttpService::new( || Ok(TaskManager::new()), LocalSessionManager::default().into(), Default::default(), ); let router = axum::Router::new().nest_service("/mcp", service); let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await?; tracing::info!("Server ready at http://127.0.0.1:8000/mcp"); axum::serve(tcp_listener, router) .with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap(); }) .await?; Ok(()) } ``` The service factory `|| Ok(TaskManager::new())` creates a new instance for each session. This means each connected client gets their own isolated task list-tasks added by one client won't appear in another client's list. For a production task manager, you'd likely want shared state across sessions using a [database](https://docs.shuttle.dev/resources/shuttle-shared-db?utm_source=shuttle_blog&utm_medium=blog&utm_campaign=http_stream_mcp_server&utm_content=database_resource). `LocalSessionManager::default()` handles all the session lifecycle management. It creates sessions, routes messages to the correct connections, and cleans up when clients disconnect. ### How Messages Flow In our task manager, the client establishes a persistent SSE connection via GET request to listen for updates. When the client sends a POST request to add or complete a task or run any other MCP tool, the server processes it and pushes the result back through that established SSE stream in real-time. This way the client receives instant notifications without polling. ## Testing with MCP Inspector Now that everything is set up, you can run it locally: ```bash cargo run ``` Now the server is ready, add it to your MCP client - in my case I'll use Cursor. ```json { "mcpServers": { "Tasks": { "url": "http://127.0.0.1:8000/mcp" } } } ``` Let's try it out, I'll ask the AI agent to add a few tasks and run all the other MCP tools to see how it works. Perfect! 🎉 Everything works as expected. With the task manager running and integrated into Cursor, you have a fully functional MCP server handling real-time task operations through streamable HTTP. This works perfectly for local development, but in real applications you'll want your server accessible from anywhere so users can connect to it. In the next section, we'll deploy to Shuttle to get a public URL. > Make sure you read the [security warnings](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#security-warning) before deploying your MCP server to production. ## Deploying Your MCP Server to the Cloud In order to deploy your MCP server to Shuttle, you'll need to install the Shuttle crates. At the moment the latest versions are `0.57` but make sure to check the [latest Shuttle versions here](https://github.com/shuttle-hq/shuttle/releases). ```toml [dependencies] shuttle-runtime = "0.57" shuttle-axum = "0.57" ``` Update your `main.rs` file to use Shuttle: ```rust {1-2, 15} #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { // Bind Address variable no longer needed. Shuttle will handle the binding for us. tracing::info!("Starting Task Manager MCP Server"); let service = rmcp::transport::streamable_http_server::StreamableHttpService::new( || Ok(TaskManager::new()), rmcp::transport::streamable_http_server::session::local::LocalSessionManager::default() .into(), Default::default(), ); let router = axum::Router::new().nest_service("/mcp", service); Ok(router.into()) } ``` We removed `tracing_subscriber` as well, because Shuttle has it's own logging system and it's already configured for us. Migrating to Shuttle is as simple as that. Run your project locally to see it working with `shuttle run`. ```bash shuttle run ``` Perfect! 🎉 Your MCP server is now running locally using Shuttle. ### Shuttle Deploy Make sure you're logged in to Shuttle first to deploy, you'll need to [create an account](https://console.shuttle.dev?utm_source=shuttle_blog&utm_medium=blog&utm_campaign=http_stream_mcp_server&utm_content=create_account) and run `shuttle login` first. We'll need a Shuttle project first: ```bash shuttle project create --name task-manager-mcp-server ``` Then deploy with the following command: ```bash shuttle deploy --name task-manager-mcp-server ``` Perfect! 🎉 Your MCP server is now deployed to Shuttle. Update your `mcp.json` file to use the new project URL: ```json { "mcpServers": { "Tasks": { "url": "https://task-manager-mcp-server-djf4.shuttle.app/mcp" } } } ``` ## Next Steps This task manager demonstrates the core patterns for building streamable HTTP MCP servers. The same approach scales to more complex scenarios like [database integration](https://docs.shuttle.dev/resources/shuttle-shared-db?utm_source=shuttle_blog&utm_medium=blog&utm_campaign=http_stream_mcp_server&utm_content=next_steps) or external API calls. One of the best parts about HTTP MCP servers is the deployment story. Your users don't need to install anything locally or manage dependencies - they just add your server's URL to their `mcp.json` and it works. This makes distribution and updates simple since you control the server and everyone automatically gets the latest version. You can download the complete project from the following command: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder mcp/http-stream-mcp ``` And deploy to Shuttle: ```bash shuttle deploy ``` Happy Coding! --- # Launching an Army of Haiku 4.5 Agents Source: https://www.shuttle.dev/blog/2025/10/23/using-haiku-4.5-agents Date: 23 October 2025 Author: dcodes Tags: ai, claude, haiku, agents Using Sonnet 4.5 as an orchestrator to deploy multiple Haiku 4.5 agents in parallel. We build a new CLI feature for the Shuttle repository to demonstrate how parallel agent orchestration handles complex tasks faster and more cost-effectively. Last week, Anthropic launched Haiku 4.5, their newest and most capable small model yet. It matches Claude Sonnet 4's coding performance which used to be the best model just a few months ago, outperforms it in certain tasks like computer use, runs at more than twice the speed, and costs one-third as much. The numbers look good on paper, but does this actually work in practice? Anthropic recommends using Haiku 4.5 through a parallel orchestration approach - Sonnet 4.5 breaks down complex problems and does the planning while multiple Haiku 4.5 agents handle subtasks in parallel. One of the great advantages of spawning agents by the main agent is that each agent will have it's own context window and they will have a clean context to work with because they will not automatically add `CLAUDE.md` files to their context window. They operate only based on the instructions given to them by the main agent. This also means that your main agent's context window will not be cluttered with old context from the workers and it will only get the summarized details from each worker agent, this way you'll have a more efficient main agent as well. To demonstrate this in action, we're working with the official [Shuttle repository](https://github.com/shuttle-hq/shuttle). This is a real production codebase that's been actively developed and maintained over the years. It's the perfect testbed for putting Haiku 4.5 to the test because the repository has a lot of code and is a good example of a large codebase that can be used to demonstrate this approach. In this blog post, we'll use this method to build a new feature for the Shuttle CLI. We'll explain the feature to Sonnet 4.5 and let it plan the implementation, then launch multiple Haiku agents to execute the plan in parallel, and see what we learn from applying it to a real codebase. ## The Feature: AI Rules Command The feature we're building is a new CLI command that helps developers set up AI rules for Shuttle for their code editor. Different editors like Cursor, Claude Code, and Windsurf use these files to provide context to their AI assistants. The command `shuttle ai rules` will interactively ask which editor you're using, or accept arguments like `shuttle ai rules --claude` to skip interaction. This helps AI agents to understand how Shuttle works and how to use it. Giving proper context and instructions to avoid hallucinations and improve accuracy. We have a `ai-rules.md` file that contains the context required for the AI agents to understand how Shuttle works and how to use it. And when the command runs, a file will be created with the content of the `ai-rules.md` file in the corresponding editor's rules directory. ## Creating a Worker Agent First, I create a worker agent by running `/agents` in Claude Code and describing its purpose: The prompt is simple: "A worker agent that's not very smart but it can get things done if the task isn't too complex and enough context is given." This explanation sets the expectations for the orchestrator when launching agents to not offload too complex tasks to the workers. Next, select Haiku 4.5 as the model: Perfect! Our worker agent is ready to use! ## The Prompt: Building a New CLI Feature I'll enter this prompt in plan mode, which lets Sonnet think through the approach before executing. Here's the prompt I'm giving to Sonnet to build a new feature for the Shuttle CLI: The prompt includes everything Sonnet needs to know: what to build, how to research the existing codebase patterns, the interactive flow, and the argument handling. In plan mode, Sonnet spawns the built-in "Explore" agent (powered by Haiku) to search for existing interactive implementations, analyze the CLI structure, and gather context. This is another great use of the Haiku model - it's fast and efficient for codebase exploration. ## Minimizing Ambiguity After searching the codebase, Claude doesn't immediately write the plan. Instead, it finds some ambiguity and asks clarifying questions: This is a newer feature in Claude Code that sets it apart from many other coding agents. Most AI coding tools are goal-oriented - they try to complete the task no matter what, often making assumptions that lead to side effects or inaccurate implementations. Claude Code takes a different approach. When it encounters ambiguity, it stops and asks questions. After answering the questions, Claude has everything it needs: The answers clarify the remaining ambiguities - skip VSCode since it doesn't have native AI rules support, use `.windsurf/rules/shuttle.md` for Windsurf, bundle the ai-rules.md into the CLI binary, and make it work from any directory. With this context locked in, Claude is ready to create the implementation plan. This is exactly what I wanted. The plan covers both interactive and non-interactive flows, includes key implementation points like using `ColorfulTheme::default()` to match existing patterns, and even outlines a testing approach. The planner looked at the codebase, understood the existing conventions, and created a plan that fits naturally into the project. Now that the plan is ready, let's start launching the Haiku agents. ## Running Tasks in Parallel To make Claude run multiple agents in parallel, you need to tell it explicitly. After reviewing the plan, I added a simple instruction: _"Use multiple @agent-task-worker agents to handle each task in parallel."_ This tells Sonnet to spawn multiple _task-worker_ agents (the Haiku 4.5 agents we configured earlier) and distribute the implementation tasks across them, executing them concurrently instead of sequentially. ## The Army Executes You can see three Haiku-powered task-worker agents running simultaneously - one updating `args.rs` with the AI command, another creating the `ai.rs` module implementation, and a third updating `lib.rs` to wire the AI command into the CLI. Each agent is reading files, running cargo checks, and making progress on its assigned task independently. Sonnet orchestrates while multiple Haiku agents execute the implementation in parallel. The orchestrator prevents conflicts by assigning non-overlapping files and including necessary context about dependencies in each agent's instructions. When Agent A needs types that Agent B is creating, Sonnet provides those definitions upfront. If something breaks, the orchestrator detects it and adjusts. But it still remains my main concern when launching many agents in parallel. ## The Result The agents finish writing their code. Then Sonnet takes over, testing the CLI and handling all the edge cases. Everything works exactly as requested. All the success criteria pass. The new `shuttle ai rules` command works in both interactive and non-interactive modes, bundles the ai-rules.md content into the binary, handles platform-specific file paths correctly, and works from any directory. This would've taken hours if done manually - learning the codebase, finding patterns, handling the interactive flow and file operations. The _Explore_ agents gather context about the codebase structure and conventions, Sonnet plans the implementation, and _task-worker_ agents execute it. ## When to Use Which Model | Use Haiku When | Use Sonnet When | | -------------------------------------------- | ----------------------------------------------- | | Exploring codebases or gathering context | Orchestrating multiple sub-tasks | | Task is well-defined with clear requirements | Problem requires complex reasoning or planning | | Following existing code patterns | Making architectural decisions | | Implementing specific features from a plan | Breaking down ambiguous requirements | | Writing tests or documentation | Testing implementations and handling edge cases | | Task can run in parallel with others | Need sequential decision-making | | Cost and speed matter more than complexity | Accuracy matters more than speed | Haiku excels at execution when you know what needs to be done. Sonnet excels at figuring out what needs to be done. Use Sonnet to plan and orchestrate, then spawn Haiku agents for codebase exploration and implementation to execute the plan. ## Conclusion This approach changes how we think about AI-assisted development. Instead of a single model grinding through every task sequentially and bloat the context window, Sonnet 4.5 acts as an intelligent orchestrator while Haiku 4.5 agents handle the execution in parallel, the main agent's context window remains clean throughout the coding session. The result is faster, cheaper, and more accurate than either model working alone. Throughout the blog, we saw multiple use cases of using Haiku agents, they're not just great for execution, but also for exploration and gathering context. Give Sonnet one comprehensive prompt in plan mode, let it research the codebase by spawning Explore agents, clarify any ambiguities through questions, then spawn worker agents to execute in parallel. While this worked for me in this particular example, it might not always be the case and there could be specific scenarios where this approach might not be the best fit. Start building your applications and deploy to Shuttle in 5 minutes with our Rust & Axum template. ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/ai-assisted ``` --- # Shuttle Raised $6M Seed to Build the AI Platform Engineer Source: https://www.shuttle.dev/blog/2025/10/22/shuttle-raises-6-million Date: 22 October 2025 Author: shuttle Tags: funding, announcement, ai, deployment Shuttle announced $6M in seed funding to build the AI platform engineer that helps developers deploy backends as fast as they code them with AI assistance. Today, we're announcing that Shuttle has raised $6 million to accelerate our mission: make building and operating backends as simple as writing code with AI. In short, Shuttle is the AI platform engineer, helping builders go from idea to production in minutes. [TechCrunch covered our announcement](https://techcrunch.com/2025/10/22/shuttle-raises-6-million-to-fix-vibe-codings-deployment-problem/) as well - check out their story on how we're fixing the "vibe coding" deployment problem. Our goal is to be the go-to cloud infrastructure platform for developers building with tools like Cursor and GitHub Copilot. As AI accelerates coding, infrastructure has become the bottleneck. Our mission has always been to remove it. To get there, we've raised $6M from world-class partners and operators, including: - Y Combinator - Global Founders Capital - Thomas Dohmke, former CEO of GitHub - Calvin French-Owen, Founder of Segment - Senior leaders from OpenAI, Deel, Confluent, and others Our vision is simple: Backends must be as composable and understandable as code to unlock the next wave of AI-assisted development. "In the era of AI, developers are writing apps faster than ever with the help of coding agents like GitHub Copilot or Claude Code. Deploying and running these applications as fast as creating them is the next major frontier," said Thomas Dohmke, former CEO of GitHub. "And Shuttle is uniquely positioned to be a leader in this space and enable quick iteration cycles for every full-stack builder." ## Why now? AI is changing how software is built. Developers are becoming orchestrators who prompt systems to generate code, wire APIs, and scaffold products. As a result, the amount of code being generated and pushed to production has exploded, but the backend, with all the queues, storage, networking, security, and deployments, still takes days of work to make it real. The definition of "developer" is expanding to include people who may not come from traditional engineering backgrounds. This shift means more individuals are now building and deploying software than ever before. This not only means our audience is getting bigger - it also makes the need for simpler, more accessible infrastructure greater than ever. This is where Shuttle comes in. We are building an AI-native, language-agnostic platform where code and infrastructure live together and are reviewable by people and AI. ## What we've proven so far We started in Rust, where developers demanded performance and a great developer experience. Shuttle is now one of the top choices for deploying Rust backends in minutes. It is used by tens of thousands of developers, and recently crossed 130,000 deployments. Teams describe Shuttle as "Vercel for backends," and our PMF survey shows strong resonance: 60% of developers would be "very disappointed" if they could no longer use Shuttle. We have also built an active and engaged community around our mission. ## What this funding enables We're taking the zero-config experience that Rust developers love and bringing it to every language. We are also making the Shuttle experience even more integrated with the AI coding workflows. In other words, we're building the expert co-pilot that provisions and wires your entire cloud stack for you. Imagine a compact infrastructure-as-data spec that is entirely language-agnostic. With seamless integrations, you'll be able to deploy applications from within Cursor or with the help of Claude Code or GitHub Copilot. No YAML, no Terraform, no CI glue. Use this to deploy to AWS, GCP, or your own cloud with opinionated defaults and guardrails. We will provide reusable backend patterns for APIs, jobs, queues, vector stores, and more. And if you're building within a team, you will be able to spin up preview environments, and get granular access control, cost and architecture suggestions. We're launching the Beta of our new platform in the upcoming weeks. Stay tuned for more updates and get in touch via [Discord](https://discord.com/invite/shuttle) or email if you want to know more! --- # From Push to Production: Deploy from GitHub with Shuttle Source: https://www.shuttle.dev/blog/2025/10/21/github-integration Date: 21 October 2025 Author: shuttle Tags: github, deployment, ci-cd, automation, rust Deploy Rust applications directly from GitHub to Shuttle with automatic builds and deployments. No CLI, no manual redeploys - just git push and go. If you're building in Rust, chances are you spend more time crafting elegant code than thinking about deployments and that's exactly how it should be. At Shuttle, we believe deploying your Rust application should feel as natural as writing it. That's why we've built a new **GitHub integration** feature that takes you from _push_ to _production_ automatically. No manual redeploys. No juggling CLI commands. Just push your code to GitHub, and we'll handle the rest. 🚀 ## Why It Matters Every developer knows the drill, push code, switch to your deploy tool, trigger a build, wait, fix a config, deploy again. It's not exactly the smoothest workflow. With the new **Shuttle + GitHub** integration, that entire process collapses into one seamless step: **git push**. When you connect your GitHub repository to Shuttle, your deployment pipeline syncs directly with your codebase. Push updates to your selected branch, and Shuttle automatically rebuilds and redeploys your app. Your project stays in sync with your latest code - no extra clicks required. Fewer steps, fewer tools, and more time spent actually building. ## How It Works Connecting GitHub to Shuttle is simple - and it all happens inside the Shuttle console. 1. Head to your [**Integrations**](https://console.shuttle.dev/account/integrations) page 2. Click **"Connect to GitHub"** and authorize Shuttle. 3. Choose the repositories you want to deploy from. Once connected, your GitHub account appears in the Shuttle dashboard. From there, you can: - Select a repository and deploy directly from the dashboard. - Choose which branch to deploy. - Add environment secrets. - Enable automatic deployments with one toggle. Pro tip: Next time you push to that branch, your code is live - automatically. ## Key Features Here's what makes the GitHub integration more than just a connection: 1. **Automatic Deployments** - Every push triggers a fresh build and deployment. No manual redeploys. 2. **Deploy from Dashboard** - Launch new versions directly from the Shuttle console without touching the CLI. 3. **Pre-Built Templates** - Deploy example apps instantly from our GitHub templates — ideal for getting started fast. 4. **Link Existing Repos** - Bring your own repository and connect it to any Shuttle project, new or existing. 5. **Fully Hosted, Fully Rust** - All the performance and control of Rust, without worrying about infra. ## A Simpler Workflow for Rust Developers Let's say you're building a Rust API. Normally, you'd have to: - Push your code to GitHub. - Open your deployment tool or CI/CD pipeline. - Configure build steps, secrets, and environments. - Trigger a redeploy manually. With Shuttle? You push to GitHub and... that's it. Your app rebuilds and redeploys automatically — live, hosted, and ready before you've finished your coffee. It's the clean, modern workflow that lets you focus on your code, not your config files. --- ## Deploy Templates in Seconds Don't have a repository yet? [Shuttle's **template library**](https://console.shuttle.dev/templates) make it easy to start. Pick a template from our [Templates page](https://console.shuttle.dev/templates), connect your GitHub account, and Shuttle automatically: - Creates a new repository in your GitHub account - Sets up a ready-to-run Shuttle project - Deploys it instantly to production It's the fastest way to go from zero to a live Rust app — no CLI installation, no setup pain. ## Managing Your Connection You can manage all your GitHub connections right inside Shuttle: - View and change linked repositories per project. - Disconnect or reconnect at any time. - Enable or disable automatic deployments with a single switch. All without affecting your existing deployments or GitHub repos. ## Start Deploying Smarter The GitHub integration is available to all Shuttle users. Connect your repo, push your code, and let Shuttle handle the rest — from build to production. 👉 [**Connect your GitHub account and deploy now →**](https://console.shuttle.dev/) --- # Claude Code Best Practices - Use Claude to Its Full Potential Source: https://www.shuttle.dev/blog/2025/10/16/claude-code-best-practices Date: 16 October 2025 Author: dcodes Tags: claude, claude code, ai, coding, best-practices Learn how to maintain high-quality context and get consistently accurate results from Claude Code as your projects grow more complex I've been using Claude Code pretty heavily over the past few months, and I've learned that there's a clear pattern to how it goes wrong. When your codebase is small, everything works beautifully. You describe what you want, Claude builds it, everything clicks. But as your codebase grows larger and more complex, things start falling apart. Responses get less accurate, context gets messy, and you end up spending more time correcting it than you save. The issue isn't Claude Code itself. It's how we use it. I've figured out what separates productive sessions from frustrating ones, and it all comes down to context management, throughout the blog, you'll see that most practices revolve around context management but in different shapes and forms. This guide walks through the practices that keep Claude Code useful as your projects grow more complex, guaranteeing you'll get a consistently high-quality output from Claude as your project grows. ## Context is Everything Context management makes or breaks your Claude Code experience. Every decision you make should optimize for context efficiency. Garbage in, garbage out has never been more relevant than today. The quality of information in your context window directly determines the quality of Claude's responses. Feed it noise, and you get noisy output. Feed it clean, relevant information, and you get precise, accurate code. Most people think about context as a capacity problem - they worry about hitting token limits. That's missing the point entirely. The real issue is context quality, not quantity. You could be at 10% of your context window and still get terrible results if that 10% is filled with irrelevant command outputs, stale error logs, and outdated architectural decisions. Your context window has limited space. Bad information doesn't just waste tokens - it actively degrades responses. When your context fills up with old command outputs, stale error logs, or outdated notes, Claude will have a hard time distinguishing what's important. Your critical architectural patterns get treated the same as your debug output. Everything blends into noise, and responses become less accurate. This is why being conservative with your context isn't just about saving tokens. It's also about maintaining quality control. Every piece of information you include should be current, accurate, and directly relevant to the task at hand. If something doesn't meet that bar, it's actively hurting your results. The practices below all serve this goal: keeping your context window filled with high-quality, relevant information and nothing else. ### Clear Context After Every Prompt This might sound extreme and unnecessary at first, but the thing is, the more you keep your session running, the more noise and junk you're collecting in your LLM's context window. What matters is that you keep your context window clean and full of high-quality relevant information rather than obfuscated command outputs and error logs that can produce tons of noise and make your AI agent go astray and forget about your project rules and guidelines. It's a good practice to chop down your tasks into smaller chunks and clear the context as soon as Claude finishes the work, clear the context and start a completely new chat. With Claude Code, you can use the `/clear` command - this should become muscle memory that you execute every few minutes. I have seen developers keep their chats running for hours without ever clearing. This makes Claude forget about all the rules and guidelines of your project and collects a lot of noise and junk in your context window. It also costs you money and burns through your usage limit faster than necessary. Clear the context after 1-3 messages. It's fine to occasionally send more messages if Claude couldn't finish the task on the first tries, but it's important to be conservative with it. There's a catch to this approach though: if we keep clearing context, things get repetitive. You have to repeat yourself constantly and tell Claude about specific things you want to do, or keep explaining the proejct guidelines and standards. The CLAUDE.md files and custom commands covered in the next sections helps you solve this problem. They let you maintain consistency without manually repeating prompts every single time. ### Customize CLAUDE.md Files Claude Code automatically pulls in the CLAUDE.md files in your project directory and subdirectories into context whenever it starts executing a task. Your CLAUDE.md files are your project's knowledge base. You should make sure that they provide high-quality and highly relevant information about your project and keep them updated almost every day. An easy way to get started with this is running the `/init` command in Claude Code. This will tell Claude Code to search your codebase and let it understand your project and create a CLAUDE.md file for you. However, this is a very basic approach. It's only good to get started. It's extremely important that you get your hands dirty and update the CLAUDE.md file yourself, review it, and remove any wrong or unnecessary information. Keep in mind, the shorter these files are, the better and more focused your AI agent will be. Customize the top-level CLAUDE.md and the ones in subdirectories. Keep them under 100 lines and make sure they're explaining the project's structure, patterns, and standards. Every piece of information you give Claude needs to be as context-efficient as possible. The subdirectory files matter just as much as the main one. Focus on project-specific patterns, architectural decisions, and common pitfalls specific to your codebase. Include coding standards, naming conventions, and any non-obvious relationships between components. The goal is to give Claude the context it needs to make intelligent decisions without reading your entire codebase. If you find yourself repeating the same instructions across multiple sessions, that's a sign it belongs in your CLAUDE.md file. ### Custom Slash Commands Slash commands are a great way to avoid repeating yourself constantly and tell Claude about specific things you want to do. For example, you might find yourself repeating the same instructions across multiple sessions, for example you might want to tell Claude to review your code then search the codebase to ensure it didn't create a new function that already exists in the codebase. Instead of prompting it each time, you can create a slash command that does it for you. You can add slash commands as markdown files in your `./.claude/commands` directory. This will save you tons of time in the long run, and you'll be saved from the frustration of having to repeat yourself constantly. You can always tweak these slash commands to make them better and better until you're happy with them. The combination of slash commands and CLAUDE.md files, if you use them properly and carefully, can make your agentic coding experience exceptionally and significantly better. But we're not done yet. There are more techniques that you can follow to make your agentic coding experience even better. ## Always Plan Before Implementation One of the most common mistakes I see developers make is jumping straight into implementation without a clear plan. You give Claude a vague description of what you want, it starts writing code immediately, and halfway through you realize the approach is completely wrong. Now you're stuck - do you try to salvage the broken implementation, or start over? Either way, you've wasted time and tokens. The planning phase is where you catch these issues early. When Claude generates a plan, you can review the approach before any code gets written. If something's off, you can course-correct immediately. This is infinitely cheaper than fixing broken implementations. Claude Code has a built-in planning mode that you can trigger, but the key is actually reviewing those plans critically. Don't just approve plans on autopilot. Read through them carefully. Does the approach make sense for your codebase? Are there edge cases being missed? Is Claude proposing to create new utilities that already exist in your project? If a plan is fundamentally wrong, don't try to salvage it by editing within the same session. The planning process itself has already consumed significant context with exploratory file reads, architecture analysis, and multiple iterations. Copy the plan to a markdown file, clear your context with `/clear`, and start fresh. This gives Claude a clean slate to generate a better plan without the noise from the failed attempt. The separation between planning and execution is crucial for context efficiency. Planning involves broad codebase exploration - searching for existing patterns, reading multiple files, analyzing architecture. This generates a lot of exploratory noise. Execution, on the other hand, should be focused and precise. By separating these phases, you keep your implementation context clean and focused on the specific files and functions that need to change. ## Spec-Driven Development GitHub recently introduced [Spec Driven Development](https://github.blog/ai-and-ml/generative-ai/spec-driven-development-with-ai-get-started-with-a-new-open-source-toolkit/) as a technique for AI-assisted coding that prevents hallucinations and maintains your repository's standards. The core idea is solid, but this approach can be painfully slow. Creating separate spec, plan, and tasks files, then executing them in different sessions, burns through tokens and arguably is a waste of time. I've found a better implementation that works exceptionally well: combine everything into a single file. Claude Code does have a built-in planning mode, but I prefer this approach because it gives me more control over the planning process and keeps everything consolidated in one place that I can reference across different sessions. Instead of managing multiple documents, create one plan file that contains: - **Spec**: High-level overview of the finished task from an end-user perspective (1-3 paragraphs) - **Plan**: Your approach and how you'll achieve the goal (1-3 paragraphs) - **Tasks**: Checkboxes splitting the feature into smaller, executable tasks (as many as needed) - **Context**: File paths and descriptions of what they do, relevant to achieving the task. (the more the better) This single-file approach hits the sweet spot for context efficiency. The key is using **Gemini 2.5 Pro** to gather the context first and make the plan, then having Claude Code jump straight to implementation. I recommend Gemini 2.5 Pro specifically for this because planning requires understanding complex codebases and making architectural decisions - not every model is smart enough for this task. Smaller models often miss important patterns or suggest approaches that don't align with your existing architecture. Gemini 2.5 Pro consistently generates accurate, well-structured plans that respect your codebase's existing patterns. Here is a simplified example of a custom command for plan generation: ```text Search the codebase for the relevant files that might be needed for the given task below and generate a `plan.md` file in the `plans//plan.md` file. The file must include these sections: - **Spec**: A high-level overview of the finished task, which can be a description of the final result or an end-user perspective (1-3 paragraphs max). - **Plan**: What's your approach? How are you trying to solve this prompt or achieve this goal? (1-3 paragraphs) - **Tasks**: A list of checkboxes that split the feature into multiple smaller tasks that can be executed separately (as many as needed). - **Context**: A list of relevant file paths and descriptions of what they are responsible for and what they do. ``` The workflow is straightforward: run this command in **Gemini 2.5 Pro** to create your plan file, then switch to Claude Code to execute it. Gemini 2.5 Pro handles the token-intensive research and planning, Claude Code handles the implementation. ## Offload Context Gathering to Save Tokens If you're doing heavy development, you'll likely hit Claude Code's 5-hour or weekly usage limits. Unless you have the $200/month subscription, these limits become a real constraint on how much you can get done. But token limits aren't the only reason to offload context gathering. Even if you never hit your limits, using Sonnet 4.5 for codebase searches and summarization is inefficient. Context gathering is a task where even smaller, faster models excel. This connects directly back to the context quality principle from earlier. When Claude Code searches your codebase, most of what fills your context window is noise. You get raw outputs from `ls` commands listing directory contents, `grep` results showing dozens of potential matches, `find` commands enumerating file paths, and file reads that may or may not be relevant. Each search operation pollutes your context with low-quality information that drowns out what actually matters. The better approach: let a smaller, faster model deal with that noise. It can run all the messy searches, sift through the results, and distill everything down to a clean, high-quality document. Then you take that document and execute it in a completely fresh Claude Code session with zero noise. One of the best ways to extend your coding productivity is running **Gemini CLI** in a separate terminal alongside Claude Code. Use it to handle the context-gathering work-searching the codebase, analyzing relevant files, and creating plan.md files that Claude Code can consume. You can use Gemini CLI (free tier: 60 requests/min, 1,000 requests/day), or Claude Code with a Haiku-based agent, Cursor if you have a subscription, or any other AI tool. What matters is that you don't waste your Claude Code tokens and don't waste time on the slow responses of Claude Code on tasks that faster models handle better. When you prompt Claude Code to implement the plan file, it doesn't waste tokens searching through your entire codebase. The previous model has already done that work and included all the necessary context in the plan. Claude Code can jump straight to implementation with fresh context and no accumulated noise from the research phase. This division of labor keeps your Claude Code sessions focused on what it does best: writing code. The context-gathering work gets offloaded to faster, more efficient models. ### Turn Off Auto-Compact Auto-compact is a feature that quietly consumes a massive amount of your context window before you even start coding. Open a new Claude Code instance and run `/context` to see your current context usage, and you might be shocked at what you find. In this example, the autocompact buffer is consuming 45k tokens - that's 22.5% of your context window gone before writing a single line of code. This accumulated context comes from previous chat sessions, sitting there whether you need it or not. Claude Code added auto-compact as a feature to help remember context from old conversations, while it might work for some projects, what's making me to hesitate to use it is that I don't have full control over what context is being added, there is no way for me to know what is in the context and to edit or remove wrong information from it ultimately leaving us with no guarantee of having high-quality context. We already have better solutions for maintaining context across sessions: CLAUDE.md files capture your project's patterns and standards, custom commands encode repetitive workflows, and plan files consolidate everything needed for a specific task. These approaches give you explicit control over exactly what context Claude receives. Auto-compact, by contrast, pulls in old context that may no longer be relevant to your current work. Turn it off. Run `/config`, navigate to the Auto-compact option, and press space to toggle it to `false`. After turning off auto-compact and running `/context` again, you'll see the immediate impact: The autocompact buffer is completely gone. Went from 45k tokens of accumulated old context to having 176k tokens of free space (88.1% of your context window). This immediately frees up tens of thousands of tokens for context that actually matters to your current task. Combined with frequent `/clear` usage and well-maintained CLAUDE.md files, you'll have complete control over the context instead of letting old sessions pollute your current work. ## Too Many MCP Servers MCP servers are powerful, they give your AI agent tools to interact with the world beyond its normal boundaries. The problem is they consume context behind the scenes to explain how to use themselves. Some MCP servers take 4-10k tokens just sitting there, doing nothing and most people don't even know they're there. Some developers add MCP servers and completely forget about them. They install something once, never use it, and without knowing that their AI output is getting worse. The context window is being eaten up by tools that aren't even relevant to the current task. With Claude Code, you can toggle MCP servers on and off without completely removing them. Type `@` followed by the MCP server name in Claude Code, and you can quickly disable or enable servers as needed. This gives you fine-grained control over which tools are consuming context at any given moment. If you don't need an MCP server anymore, just completely remove it. Remove every MCP server you're not actively using. The fewer MCP servers you run, the more context Claude has for actual work. The better approach is project-scoped MCP servers instead of user-scoped ones. Add MCP servers to specific project configurations rather than your global settings. This way, a database MCP only loads when you're working on projects that actually need database access, not when you're writing documentation or working on frontend code. ## Quick Reference We've covered a lot of techniques throughout this guide, so here's everything consolidated into one place. These are the practices that will keep your Claude Code sessions productive as your codebase grows. Each one serves the same goal: maintaining high-quality context while minimizing noise. | Practice | What to Do | | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | | Clear Context | Use `/clear` after 1-3 messages to prevent context bloat | | CLAUDE.md Files | Keep under 100 lines with project-specific patterns and standards | | Slash Commands | Create reusable commands in `~/.claude/commands` for repetitive tasks | | Turn Off Auto-Compact | Disable in `/config` to prevent old context from consuming 40k+ tokens | | MCP Servers | Toggle with `@` or remove unused servers to free up context | | Planning Mode | Use for complex features, separate planning from execution | | Spec-Driven Development | Use Gemini 2.5 Pro for planning - combine spec, plan, tasks, and context in one file and use Claude Code to implement | ## Conclusion AI coding tools work exactly like any other software - output quality directly reflects input quality. What you feed into Claude Code determines what you get back. The difference between developers who get consistent value from AI tools and those who struggle comes down to how deliberately they manage context. Every practice in this guide exists to maintain that quality bar. These aren't one-time setup tasks. Keep your CLAUDE.md files current as your architecture evolves, refine your slash commands as you identify repetitive patterns, and revisit your planning workflows when they stop serving you. The practices compound over time. Your CLAUDE.md files become more accurate as you encounter edge cases. Your slash commands become more refined as you understand what actually saves time. Your planning process becomes more efficient as you learn which details matter and which don't. The real test isn't whether Claude Code works well on day one with a small codebase - it will. The test is whether it still delivers accurate, useful results six months later when your project has grown ten times larger. That's where these practices matter. The developers who maintain these habits are the ones still getting value from AI coding tools while others have given up out of frustration. Try these practices with Shuttle's AI-assisted template: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/ai-assisted ``` ## Frequently Asked Questions Clear context after every 1-3 messages using the `/clear` command. This prevents context bloat and keeps Claude focused on current tasks rather than being confused by accumulated noise from previous interactions. Keep it under 100 lines and focus on project-specific patterns, architectural decisions, coding standards, naming conventions, and non-obvious relationships between components. Avoid generic information - only include what Claude needs to make intelligent decisions about your specific codebase. Auto-compact can consume 40k+ tokens (over 20% of your context window) with old context from previous sessions. You have no control over what's included, and it may contain outdated or irrelevant information. CLAUDE.md files and plan files give you explicit control over context quality. Use Gemini 2.5 Pro for planning and context gathering - it handles complex codebase analysis well and is more token-efficient for research tasks. Use Claude Code (Sonnet 4.5) for implementation where its coding capabilities shine. Each MCP server consumes 4-10k tokens just to explain its capabilities, even when not being used. Toggle off unused servers with `@servername` or remove them entirely. Use project-scoped MCP servers instead of global ones so they only load when relevant. Built-in planning mode is convenient but gives you less control. Spec-driven development with a single plan file (containing spec, plan, tasks, and context) lets you use a separate model for research, maintain the plan across sessions, and start Claude Code with clean, focused context. --- # MCP Servers for Rust Developers Source: https://www.shuttle.dev/blog/2025/10/10/mcp-servers-for-rust-developers Date: 10 October 2025 Author: shuttle Tags: mcp, rust, tutorial, video Learn what MCP servers are and see them in action with GitHub, Context7, and Shuttle integrations Watch how MCP servers transform AI coding assistants from text generators into practical development tools. In this video, David and Mark (Senior Software Engineer at Shuttle) demonstrate three MCP servers that eliminate the constant context switching developers face: - **GitHub MCP** - Managing issues and pull requests without leaving your editor - **Context7 MCP** - Getting up-to-date library documentation on demand - **Shuttle MCP Server** - Deploying Rust applications with simple commands You'll see each server in action and learn how to set them up yourself. ## What You'll Learn The video shows how MCP servers give AI assistants direct access to tools and data sources. Instead of explaining what you need and watching the AI generate code, you can have it perform actual operations - deploying apps, searching documentation, or managing GitHub issues. ### Setting Up the Shuttle MCP Server In the video, you'll see the Shuttle MCP Server in action. It connects your AI assistant directly to Shuttle's deployment platform, handling everything from project creation to log monitoring. For Cursor, you can add it by clicking the "Add to Cursor" button below. Or add this to your `mcp.json`: ```json { "mcpServers": { "Shuttle": { "command": "shuttle", "args": ["mcp", "start"] } } } ``` The video walks through the complete setup process and shows real deployment examples. Learn more: [Shuttle MCP Server](https://www.shuttle.dev/blog/2025/10/08/shuttle-mcp?utm_source=shuttle_blog&utm_medium=video&utm_campaign=mcp_servers) Full documentation: [MCP Server Documentation](https://docs.shuttle.dev/integrations/mcp-server?utm_source=shuttle_blog&utm_medium=video&utm_campaign=mcp_servers) ### Try It Yourself Get started with Shuttle and deploy your first Rust app: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/hello-world ``` Then connect the Shuttle MCP Server to your AI assistant and deploy directly from your coding session. Happy coding! --- # Shuttle MCP Server Source: https://www.shuttle.dev/blog/2025/10/08/shuttle-mcp Date: 8 October 2025 Author: dcodes Tags: mcp, ai, shuttle, deployment, claude, workflow How we improved the Shuttle MCP server to make AI agents more reliable, with better error handling and context for seamless deployment workflows AI agents are changing how we build software, and the Model Context Protocol (MCP) is at the heart of this shift. MCP lets AI agents connect with external tools and APIs - kind of like giving your AI assistant hands to actually do things instead of just talking about them. We've updated our Shuttle MCP server, and it's a proper workflow upgrade. Here's what's new. ## What Does the Shuttle MCP do? The Shuttle MCP server gives AI agents direct access to Shuttle's platform and documentation. Your AI agent can now: - Search through Shuttle documentation to understand how things work, ensuring you have the latest information - Deploy projects directly to Shuttle - List all your Shuttle projects - Get detailed information about specific projects - View deployment logs in real-time for debugging Instead of copy-pasting commands and switching between your terminal, documentation, and AI chat, your agent handles the entire workflow. ## The Problem With the Previous Version The first version of our MCP server worked well for most users. The tools were functional, AI agents could call them, and they'd return the expected data for common workflows. But we noticed struggles in edge cases. When dealing with unusual deployment configurations or error states, agents would sometimes call the wrong tool or miss required parameters. When something failed in these scenarios, they'd get stuck because error messages didn't provide enough context to self-correct. We realized the problem wasn't the tools themselves as they were powerful enough - it was how we presented them to the agents. In edge cases, agents didn't have enough guidance to understand when or how to use each tool. We'd built powerful functionality but needed to be documented better for AI agents to understand. ## What's New in This Version The updated server doesn't add flashy new features - it makes the existing ones actually work the way they should. The Agents now have much higher success rates, and even when things go wrong, they know how to recover and fix issues on their own. We've enhanced how AI agents understand Shuttle. Each MCP tool includes detailed instructions that help agents understand not just what a tool does, but when and how to use it. We've also reworked error handling. When something goes wrong now, the error messages are designed for AI agents - they provide enough context and guidance for the agent to understand what happened and how to fix it. Instead of getting stuck, agents can self-correct and try again with a high chance of success. With this, your AI agent works faster and makes fewer mistakes. It understands deployment workflows and can troubleshoot issues on its own. ## What We Learned About Building MCP Servers Building MCP tools isn't just about writing the code that exposes functionality. It's about documentation and context - specifically, documentation written for AI models rather than humans. A powerful tool isn't very efficient if the agent doesn't know when and how to use it. You need to provide condensed, structured context that models can actually parse and understand. Otherwise, you're handing your agent a toolbox without labels on any of the tools leaving the agent to guess what to do with it. This means thinking differently about how you write tool descriptions, parameter explanations, and error messages. Every piece of text needs to be optimized for model comprehension, not human readability. If you're building your own MCP server, spend as much time on how you describe your tools as you do on implementing them. The quality of that documentation directly determines whether AI agents can actually use what you've built. ## Real-World Benefits Here's where this gets practical. You can now tell your AI agent "Deploy my Shuttle App" and it handles everything using the Shuttle MCP server: 1. Creates a project for you if you don't have one already 2. Deploys the project to the cloud 3. Handles edge cases 4. Catches and fixes common deployment issues automatically The entire workflow - from project creation to a live deployment - happens through your AI agent. We tested this with Claude Sonnet 4.5 to build a complete Rust API from scratch, you can see the MCP server in action. [See the full walkthrough here](https://www.shuttle.dev/blog/2025/10/01/build-rust-api-sonnet-4-5?utm_source=shuttle_blog&utm_campaign=shuttle_mcp_update_announcement). ## Getting Started ### Prerequisites First, you'll need the latest Shuttle CLI. If you don't have it installed: **Linux/macOS:** ```bash curl -sSfL https://www.shuttle.dev/install | bash ``` **Windows (PowerShell):** ```powershell iwr https://www.shuttle.dev/install-win | iex ``` If you already have Shuttle installed, upgrade to the latest version: ```bash shuttle upgrade ``` ### Configuring the MCP Server After installing the CLI, configure the MCP server in your IDE or MCP client. For Cursor, you can add it by clicking the "Add to Cursor" button below. Or add this to your `mcp.json`: ```json { "mcpServers": { "Shuttle": { "command": "shuttle", "args": ["mcp", "start"] } } } ``` For other IDEs and MCP clients, check the [configuration guide](https://docs.shuttle.dev/integrations/mcp-server?utm_source=shuttle_blog&utm_campaign=shuttle_mcp_update_announcement) to learn more. ### Quick Start The fastest way to try this is with a Shuttle template. ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/hello-world ``` Connect your AI agent with the MCP server, and tell it to deploy. Your app will be live in minutes. ## How to Use the Shuttle MCP Server Here are some practical prompts to test with your AI agent once you have the MCP server configured: > **Note**: If the AI agent tries to execute the prompt directly without using the MCP tools first, make sure to prompt it explicitly to use the Shuttle MCP server. **Deployment and Migration:** - Deploy my app to Shuttle - Migrate my Rust app to a Shuttle app - Set up a Shuttle Database for me **Debugging and Monitoring:** - Check my production logs and see if we have any issues - How many Shuttle projects do I have? **Questions and Documentation:** - How to scale my compute size on Shuttle? - How to set up a Shuttle Database? ## Conclusion This changes how you code with Shuttle. Your AI agent becomes a proper development partner that understands your entire deployment workflow - it can answer questions about your project configuration, check deployment status, review logs when something breaks, and guide you through scaling decisions. The friction between writing code and shipping it basically disappears. Try it now and see the difference for yourself. Check out our [getting started guide](https://docs.shuttle.dev/getting-started/quick-start?utm_source=shuttle_blog&utm_campaign=shuttle_mcp_update_announcement) to set everything up. Or get started with our most popular template: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/hello-world ``` --- # Building a Rust API with Claude Sonnet 4.5 Source: https://www.shuttle.dev/blog/2025/10/01/build-rust-api-sonnet-4-5 Date: 1 October 2025 Author: dcodes Tags: rust, ai, claude, mcp, axum, api Putting Anthropic's bold claim to the test by building a Rust API from scratch with Sonnet 4.5 Anthropic just dropped Sonnet 4.5 with a very **BOLD CLAIM**: it's the _"best coding model in the world."_ That's a pretty confident statement in a crowded field of AI coding models. Sonnet has proven itself to be very powerful and capable of handling complex tasks, but lately the community were claiming that Claude is slowly getting worse and it's not as good as it first was. This is especially after OpenAI released their leading coding model **Codex** model for their open source [Codex CLI](https://github.com/openai/codex). To bring back their users, Anthropic is finally back with a new model **Sonnet 4.5** and the Claude Code VSCode extension has also been updated, so you can use Sonnet 4.5 directly in VSCode or Cursor for a better experience. So let's see if Sonnet 4.5 is actually worth the hype and put it to the test, we as Rust developers like coding in Rust so this is what this blog post is about, we'll let Sonnet 4.5 build a Rust API for us that collects from 3 RSS feeds and serves them through an HTTP API. ## Building an RSS Aggregator API with Sonnet 4.5 The goal might seem simple at first, but it's not what we're interested in, building the API is easy, what matters is how well Sonnet 4.5 can handle the task and with how many prompts and iterations we can get the API to work exactly as we want. Here is what we care about: - **Up to date code**: Writing up to date code without out of date dependencies - **Accuracy**: How well Sonnet 4.5 can understand the task and deliver the correct code - **Speed**: How fast Sonnet 4.5 compared to Sonnet 4 - **MCP Capabilities**: How good is it in calling MCP tools - **Error Prediction**: How well Sonnet 4.5 can predict and handle errors Let's give it the first prompt: > Build a Rust RSS aggregator API that fetches these 3 feeds: > >
> Hacker News: https://news.ycombinator.com/rss > >
> Rust Blog: https://blog.rust-lang.org/feed.xml >
> XKCD: https://xkcd.com/rss.xml >
> Endpoints: >
> > GET /feeds - all items as JSON > >
> > GET /feeds/\{source\} - filtered by source > >
> Use Axum This is a very high level prompt, the only technical requirement is to use Axum and the endpoints should be GET /feeds and GET /feeds/\{source\}. ## First Impressions Sonnet 4.5 got to work immediately, created the project and wrote the code for implementing all requirements. A few things stood out right away: **The Good:** - It nailed the implementation. The code was straightforward, idiomatic Rust with proper async/await patterns - Noticeably faster than Sonnet 4.0. The responses came back quicker, and it seemed more decisive in its approach - It understood the task completely and delivered exactly what we asked for **The Not-So-Good:** - It used Axum 0.7 instead of the current 0.8, which has been out for almost 10 months. - The RSS feeds we provided actually have different XML formats, Sonnet was supposed to normalize them into a unified JSON structure, but it didn't. Looking at the code, the code compiles and runs, the API however isn't returning the feed for the Rust Blog due to lack of normalization. ## Iterating: Normalizing the Feeds I gave Sonnet a follow-up prompt to normalize the feeds: > The structure of each feed is different, make sure you fetch them first and then normalize them into a unified JSON structure. It performed exceptionally well, it first sent a GET request to each of the RSS feeds to fetch the data and understand their structure, then it normalized the data into a unified JSON structure, which is exactly what I wanted it to do. Testing the `/feeds` endpoint, everything was working as expected. Two prompts. That's all it took to go from initial implementation to a fully working, normalized RSS aggregator. That's really impressive, and the speed in which it did it is something I can't express enough. ## Keeping Dependencies Fresh There was still one thing bugging me: the outdated Axum version. Let's give another high level prompt and ask it to update the dependencies: > Some of the crates you've used are outdated. Make sure they're all up to date and read their documentation to make sure there aren't any breaking changes. Didn't specify which crates were outdated, let's let Sonnet figure it out itself. There's something to note here, Axum 0.8 has a breaking change in the route syntax, the old 0.7 way was `/:id` for path parameters, but 0.8 requires `/\{id\}` instead. If Sonnet doesn't catch this breaking change, it will cause the application to panic at runtime. As you might be aware by now, MCPs aren't very friendly when it comes to keeping the context clean, my first thought was "Too many MCP calls is going to blow up the context window." which is true but the important thing is that the model stays sharp and doesn't lose focus. It actually loooked up the documentation for Axum 0.8 and updated the route syntax correctly. `cargo build` works, and no runtime errors and both endpoints work perfectly. Three prompts total with zero compilation errors so far is quite impressive. ## Deploying to Shuttle With a working API in hand, there was one more test: deployment. Claude Code has MCP (Model Context Protocol) integration with Shuttle, so I wanted to see if Sonnet could handle the entire deployment workflow. I have the [Shuttle MCP](https://docs.shuttle.dev/integrations/mcp-server) installed in Claude, so it can interact with the Shuttle platform. To install it, you can run: ```bash claude mcp add Shuttle shuttle mcp start ``` For Cursor, you can add it by clicking the "Add to Cursor" button below. Or updating your `mcp.json` file to include the following: ```json { "mcpServers": { "Shuttle": { "command": "shuttle", "args": ["mcp", "start"] } } } ``` You should also have the [Shuttle CLI installed](https://docs.shuttle.dev/getting-started/installation) and logged in to Shuttle, I recommend running `shuttle upgrade` if you already have it installed, you can do this by running: ```bash shuttle login ``` I gave it this prompt: > I want you to use the Shuttle MCP to search the docs on how to convert the app to a Shuttle app and then deploy it to Shuttle. It identified the required changes and updated the dependencies to include Shuttle runtime. Then converted the app to a Shuttle app. With the conversion complete, it proceeded to deploy the API. There you go, the RSS aggregator was live and accessible. The days have changed, the entire process from prompt to production took three simple instructions and maybe five minutes of actual work. ## Results Summary Here's how Sonnet 4.5 performed on our key criteria: | Criteria | Performance | Notes | | -------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------- | | **Up to date code** | ⚠️ Mixed | Initially used Axum 0.7, but quickly updated to 0.8 when prompted and correctly handled breaking changes | | **Accuracy** | ✅ Excellent | Nailed the implementation on first try, understood complex requirements like feed normalization and no compilation errors | | **Speed** | ✅ Excellent | Noticeably faster than Sonnet 4.0, responses came back quicker and more decisively | | **MCP Capabilities** | ✅ Excellent | Seamlessly used MCP tools to fetch RSS feeds, check documentation, and deploy to Shuttle | | **Error Prediction** | ⚠️ Good | Caught Axum 0.8 breaking changes and updated route syntax correctly, but missed that RSS feeds have different structures | **Overall Score: 9/10** - Sonnet 4.5 delivered a production-ready API in just 3 prompts with zero compilation errors and successful deployment. ## Conclusion Sonnet 4.5 delivered on its promise. Three prompts, zero compilation errors, and a fully deployed Rust API. The speed improvements are noticeable, and the MCP integration makes deployment seamless. While it's not perfect (it did use outdated dependencies initially), it quickly corrected course when prompted. What matters is that you always keep the context clean and write your `CLAUDE.md` files with caution, do not overfill the context window with junk and always keep updating it and iterating on it until you get the results you want. The "best coding model" could be true. The combination of accuracy, speed, and tool integration makes it a compelling choice for Rust development. Want to try building something similar? Get started quickly with: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/ai-assisted ``` --- # Pandas vs Polars: Which Data Processor Runs Faster Source: https://www.shuttle.dev/blog/2025/09/24/pandas-vs-polars Date: 24 September 2025 Author: jeremiah Tags: rust, data-pipelines, pandas, polars, performance, etl If your workflows hit performance walls, the choice between Pandas vs Polars is critical. Learn benchmarks, code, and how Shuttle enables faster ETL pipelines. When your data science workflows start hitting performance walls, the choice between Python's Pandas and Rust's Polars becomes critical. I recently discovered this firsthand while processing millions of rows of data that pushed my Python scripts to their breaking point. The problem many data engineering teams face today isn't just about handling large datasets; it's about doing it efficiently without burning through compute resources or waiting hours for ETL processes to complete. Traditional Python approaches with Pandas, while familiar and feature-rich, often become bottlenecks as data volumes grow. This article will walk you through a comprehensive performance comparison between Pandas and Polars using real-world data processing tasks. You'll see exact code implementations, actual benchmark results, and learn how to deploy high-performance data pipelines using Shuttle. The results will change how you approach data processing in production. ## Benchmark Dataset: NYC Taxi Trip Data for Real-World ETL For this comparison, I used the [NYC Yellow Taxi dataset from January 2015—12.7 million trip records](https://www.kaggle.com/datasets/elemento/nyc-yellow-taxi-trip-data) stored in CSV files totalling about 2.1 GB. This dataset serves as an excellent proxy for real-world ETL challenges that data science teams encounter daily. The dataset characteristics make it representative of typical production scenarios: - **Scale**: 12.7 million rows with 19 columns across multiple data types - **Data quality issues**: Missing values, invalid coordinates, and outlier detection requirements - **Mixed operations**: Requires loading data, cleaning, aggregations, and complex filtering - **Real-world complexity**: Timestamps, geospatial coordinates, and categorical data sources The ETL pipeline covers five core operations that appear in most data processing workflows: 1. **Load**: Reading CSV data from storage into memory or lazy frames 2. **Clean**: Handling missing data, filtering invalid values, and data type conversions 3. **Aggregate**: Grouping operations across temporal and categorical dimensions 4. **Filter**: Complex multi-condition filtering and sorting operations 5. **Export**: Writing processed results back to storage systems This represents typical data pipeline tasks in production environments where teams process transaction logs, sensor data, or user behaviour analytics regularly. ## Performance Bottlenecks in Python ETL with Pandas Before diving into solutions, let's examine the specific performance bottlenecks that make Pandas challenging for large-scale data processing operations. These limitations become apparent when working with datasets that exceed available system memory or require complex data transformations. ### Eager Loading and Memory Bloat Pandas uses eager evaluation, meaning every operation executes immediately and creates intermediate results in memory. When you load CSV files, Pandas reads the entire dataset into RAM regardless of whether you'll use all columns or rows: ```python import pandas as pd import time # Pandas immediately loads entire file into memory start = time.time() df = pd.read_csv("yellow_tripdata_2015-01.csv") # 2.1 GB file load_time = time.time() - start print(f"Loaded {len(df):,} rows in {load_time:.2f}s") print(f"Memory usage: ~4.6 GB peak") ``` This eager approach creates memory pressure as each transformation step generates new DataFrames, leading to memory usage that can exceed 2-3x the original dataset size during processing operations. ### The Global Interpreter Lock Problem Python's GIL prevents true multi-threaded execution for CPU-intensive operations, meaning Pandas can only utilize one CPU core at a time for most data processing tasks: ```python # This aggregation uses only one CPU core despite having 8+ cores available daily_stats = df.groupby(df['tpep_pickup_datetime'].dt.date).agg({ 'trip_distance': ['count', 'mean', 'sum'], 'total_amount': ['mean', 'sum', 'std'], 'passenger_count': ['sum', 'mean'] }) ``` Modern systems with 8, 16, or more CPU cores remain underutilized, creating a significant performance bottleneck for data-intensive operations. ### Handling Missing Values and Data Types Pandas processes missing values through multiple passes over the data, with each operation requiring a full scan of all rows and columns: ```python # Each operation scans the entire dataset separately df_cleaned = df.dropna(subset=['pickup_longitude', 'pickup_latitude']) df_filled = df_cleaned.fillna({'passenger_count': 1}) df_typed = df_filled.astype({'passenger_count': 'int32'}) ``` These sequential operations become increasingly expensive as datasets grow, particularly when dealing with wide schemas containing many columns with different data types. ## How Polars Uses Rust for Fast, Multi-Threaded Data Processing Polars takes a fundamentally different approach to data processing by leveraging Rust's performance characteristics and implementing lazy evaluation throughout the system. This architecture enables significant performance improvements for ETL operations on large datasets. To understand why polars works so well, let's look at the key features of polars: ### Lazy Evaluation and Query Planning Instead of executing operations immediately, Polars builds a query plan that gets optimized before any actual data processing begins: ```rust use polars::prelude::*; // Create lazy frame - no data loading yet let df = LazyFrame::scan_csv("yellow_tripdata_2015-01.csv", ScanArgsCSV::default())? .filter(col("trip_distance").gt(0)) .select([col("pickup_datetime"), col("trip_distance"), col("total_amount")]) .group_by([col("pickup_datetime").dt().date()]) .agg([col("trip_distance").mean(), col("total_amount").sum()]); // Only execute when explicitly requested let result = df.collect()?; ``` This lazy approach allows Polars to analyze the entire pipeline and apply optimizations like predicate pushdown, column pruning, and operation fusion before touching any data. ### Query Optimization Techniques Polars automatically applies several query optimization techniques that reduce I/O operations and memory usage: **Predicate Pushdown**: Filters get moved closer to data sources, reducing the amount of data that needs to be loaded: ```rust // Polars pushes this filter down to the CSV reader level let filtered_data = LazyFrame::scan_csv("data.csv", ScanArgsCSV::default())? .filter(col("passenger_count").gt(0)) // Applied during file reading .select([col("trip_distance"), col("total_amount")]); ``` **Column Pruning**: Only required columns get loaded from storage, reducing memory usage and I/O: ```rust // Only loads pickup_datetime and trip_distance columns let df = LazyFrame::scan_csv("data.csv", ScanArgsCSV::default())? .select([col("pickup_datetime"), col("trip_distance")]) .collect()?; ``` ### Multi-Core Processing and Memory Efficiency Rust's native threading capabilities allow Polars to utilize all available CPU cores automatically. Operations like aggregations, joins, and sorting distribute work across threads without the GIL limitations that constrain Python: ```rust // Automatically uses all CPU cores for groupby operations let daily_stats = LazyFrame::scan_csv("data.csv", ScanArgsCSV::default())? .group_by([col("pickup_datetime").dt().date()]) .agg([ col("trip_distance").count().alias("trip_count"), col("trip_distance").mean().alias("avg_distance"), col("total_amount").sum().alias("total_revenue") ]) .collect()?; // Parallel execution across all cores ``` Memory efficiency comes from Rust's ownership system and Polars' streaming capabilities, which process data in chunks rather than loading entire datasets into memory. As the graph belows shows, Polars also maximizes CPU utilization, distributing work across all cores for consistently fast execution. ## Pandas vs Polars ETL Pipeline Examples Let's examine side-by-side implementations of the same ETL pipeline using both libraries. These examples show identical data processing logic implemented with each tool's best practices. ### Pandas Implementation: Traditional ETL Approach ```python import pandas as pd import numpy as np from datetime import datetime import time import json class PandasETL: def __init__(self, file_path): self.file_path = file_path self.df = None self.metrics = {} def load_and_clean_data(self): """Load CSV data and perform cleaning operations""" print("Loading and cleaning data...") start_time = time.time() # Load entire CSV into memory self.df = pd.read_csv(self.file_path) # Clean invalid coordinates and trip data self.df = self.df[ (self.df['pickup_longitude'] != 0) & (self.df['pickup_latitude'] != 0) & (self.df['trip_distance'] > 0) & (self.df['trip_distance'] < 100) & (self.df['passenger_count'] > 0) & (self.df['passenger_count'] <= 6) ] # Convert datetime columns self.df['tpep_pickup_datetime'] = pd.to_datetime( self.df['tpep_pickup_datetime'] ) self.df['tpep_dropoff_datetime'] = pd.to_datetime( self.df['tpep_dropoff_datetime'] ) # Calculate trip duration self.df['trip_duration_minutes'] = ( self.df['tpep_dropoff_datetime'] - self.df['tpep_pickup_datetime'] ).dt.total_seconds() / 60 # Remove trips with invalid duration self.df = self.df[ (self.df['trip_duration_minutes'] > 0) & (self.df['trip_duration_minutes'] < 480) ] load_clean_time = time.time() - start_time self.metrics['load_clean_time'] = load_clean_time print(f"✅ Loaded and cleaned {len(self.df):,} rows in {load_clean_time:.2f}s") return self def aggregate_data(self): """Perform aggregation operations""" print("Performing aggregations...") start_time = time.time() # Add date columns for grouping self.df['date'] = self.df['tpep_pickup_datetime'].dt.date self.df['hour'] = self.df['tpep_pickup_datetime'].dt.hour # Daily statistics daily_stats = self.df.groupby('date').agg({ 'trip_distance': ['count', 'mean', 'sum'], 'trip_duration_minutes': 'mean', 'passenger_count': 'sum', 'total_amount': ['mean', 'sum'] }) # Hourly patterns hourly_stats = self.df.groupby('hour').agg({ 'trip_distance': ['count', 'mean'], 'total_amount': 'mean' }) aggregate_time = time.time() - start_time self.metrics['aggregate_time'] = aggregate_time print(f"✅ Aggregations completed in {aggregate_time:.2f}s") return self ``` ### Polars Implementation: Lazy ETL Pipeline ```rust use polars::prelude::*; use std::collections::HashMap; use std::time::Instant; pub struct PolarsETL { metrics: HashMap, } impl PolarsETL { pub fn new() -> Self { Self { metrics: HashMap::new() } } pub fn run_etl_pipeline(&mut self, file_path: &str) -> PolarsResult { println!("🚀 Starting Polars ETL pipeline..."); let total_start = Instant::now(); // Build lazy query plan let lazy_df = LazyFrame::scan_csv(file_path, ScanArgsCSV::default())? .select([ col("pickup_longitude"), col("pickup_latitude"), col("trip_distance"), col("passenger_count"), col("tpep_pickup_datetime"), col("tpep_dropoff_datetime"), col("total_amount") ]) // Apply filters (pushed down to scan level) .filter( col("pickup_longitude").neq(lit(0.0)) .and(col("pickup_latitude").neq(lit(0.0))) .and(col("trip_distance").gt(lit(0.0))) .and(col("trip_distance").lt(lit(100.0))) .and(col("passenger_count").gt(lit(0))) .and(col("passenger_count").lt_eq(lit(6))) ) // Parse datetime columns .with_columns([ col("tpep_pickup_datetime").str().strptime( DataType::Datetime(TimeUnit::Microseconds, None), StrptimeOptions::default(), lit("coerce") ), col("tpep_dropoff_datetime").str().strptime( DataType::Datetime(TimeUnit::Microseconds, None), StrptimeOptions::default(), lit("coerce") ) ]) // Calculate trip duration .with_columns([ (col("tpep_dropoff_datetime") - col("tpep_pickup_datetime")) .dt().total_minutes() .alias("trip_duration_minutes") ]) // Filter by trip duration .filter( col("trip_duration_minutes").gt(lit(0.0)) .and(col("trip_duration_minutes").lt(lit(480.0))) ); // Execute aggregations let daily_stats = lazy_df .clone() .with_columns([col("tpep_pickup_datetime").dt().date().alias("date")]) .group_by([col("date")]) .agg([ col("trip_distance").count().alias("trip_count"), col("trip_distance").mean().alias("avg_trip_distance"), col("trip_distance").sum().alias("total_trip_distance"), col("trip_duration_minutes").mean().alias("avg_duration"), col("passenger_count").sum().alias("total_passengers"), col("total_amount").mean().alias("avg_fare"), col("total_amount").sum().alias("total_revenue") ]) .collect()?; let total_time = total_start.elapsed().as_secs_f64(); self.metrics.insert("total_time".into(), total_time); println!("✅ ETL pipeline completed in {:.2f}s", total_time); Ok(daily_stats) } } ``` ### Environment Setup for Reproducible Results To run these benchmarks consistently, I used the following setup: ```bash # Python environment python3 -m venv pandas_env source pandas_env/bin/activate pip install pandas==2.1.0 numpy==1.24.0 psutil # Rust environment curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh cargo --version rustc --version # System specifications for reproducibility # CPU: 8-core Intel i7 (16 threads) # RAM: 32GB DDR4 # Storage: NVMe SSD # OS: Ubuntu 22.04 LTS ``` These side-by-side implementations highlight the key design differences between Pandas and Polars. Pandas follows an eager, memory-intensive approach, while Polars builds an optimized lazy query plan that executes more efficiently. With both pipelines producing the same analytical outputs, the real distinction emerges in how they perform at scale. ## Polars vs Pandas Performance on ETL Tasks After running identical ETL operations on the 12.7 million row NYC taxi dataset, the performance differences are substantial. Here are the detailed benchmark results across all major operations: ### Execution Time Comparison | ETL Operation | Pandas (seconds) | Polars (seconds) | Speedup Factor | Notes | | -------------- | ---------------- | ---------------- | -------------- | ----------------------------------------------------- | | Load + Clean | 43.60 | Deferred | - | Polars defers load/clean until execution phase | | Aggregations | 9.21 | 13.80 | 0.67x \* | Polars executes load + clean + aggregation together | | Filter + Sort | 9.42 | 5.25 | 1.8x | Polars benefits from predicate pushdown & parallelism | | Export Results | 0.14 | 0.05 | 2.8x | Polars writes faster due to streaming | | Total Pipeline | 62.37s | 19.10s | 3.3x | | \*Includes deferred load/clean phase in Polars ### Memory Usage Analysis These results are environment-specific. In practice, Polars often uses 30-60% less memory on large CSV workloads due to column pruning and streaming, though actual savings depend on schema and operations. #### Pandas Memory Profile - Peak usage: 4,658 MB during processing operations - Memory pattern: Immediate spike during CSV loading - Garbage collection: Frequent pauses for cleanup - Intermediate objects: Multiple DataFrame copies in memory #### Polars Memory Profile - Peak usage: ~2,100 MB during aggregation execution - Memory pattern: Steady increase only during actual processing - No garbage collection: Rust's ownership system manages memory - Streaming operations: Data processed in manageable chunks ### CPU Utilization Patterns Polars automatically parallelizes across available cores. Pandas relies on single-threaded execution for most operations unless explicitly offloaded (e.g., via Dask, Modin). #### Pandas CPU Usage - Single-thread utilization: ~12.5% of 8-core system (1 core) - GIL limitations: Other threads blocked during computation - Load balancing: Uneven system resource usage #### Polars CPU Usage - Multi-thread utilization: ~85% of 8-core system (all cores) - Parallel operations: Concurrent processing across cores - Efficient scheduling: Even load distribution across threads ## Why Polars is Faster The dramatic performance differences stem from fundamental architectural choices. Understanding these differences helps explain when and why you might choose one approach over the other for your data processing systems. ### Eager vs Lazy Execution Models **Pandas Eager Execution:** ```python # Each operation executes immediately df = pd.read_csv("data.csv") # Load: 32.5s df_clean = df[df['distance'] > 0] # Filter: 8.2s df_agg = df_clean.groupby('date').sum() # Aggregate: 9.1s # Total: 49.8s across separate operations ``` **Polars Lazy Execution:** ```rust // Build query plan without execution let df = LazyFrame::scan_csv("data.csv", ScanArgsCSV::default())? .filter(col("distance").gt(0)) // Added to plan: ~0s .group_by([col("date")]) // Added to plan: ~0s .sum() // Added to plan: ~0s .collect()?; // Execute all: 13.8s ``` The lazy approach allows Polars to optimize the entire pipeline as a single operation, eliminating intermediate steps and reducing data movement. ### Efficient Memory Access Patterns Polars leverages several memory optimization techniques: **Columnar Data Layout**: Data stored column-wise enables better cache locality and vectorized operations. **SIMD Instructions**: Single Instruction, Multiple Data processing accelerates numerical computations. **Zero-Copy Operations**: Data transformations avoid unnecessary memory allocation when possible. **Streaming Execution**: Large datasets are processed in chunks that fit in the CPU cache. ### Query Rewriting and Optimization Polars automatically rewrites queries for better performance: ```rust // Original query let result = LazyFrame::scan_csv("data.csv", ScanArgsCSV::default())? .select([col("*")]) // Select all columns .filter(col("amount").gt(100)) // Filter expensive trips .select([col("date"), col("amount")]) // Select subset .collect()?; // Polars optimization rewrites this to: // 1. Scan only date and amount columns (column pruning) // 2. Apply filter during CSV reading (predicate pushdown) // 3. Skip unnecessary intermediate selections ``` These optimizations occur automatically, without requiring code changes, making Polars faster while maintaining simplicity. **But performance alone isn't the full story.** Once you've squeezed every ounce of speed from your ETL pipeline, the next challenge emerges: _how do you take that optimized workflow and actually run it in production at scale, reliably, and without DevOps headaches?_ Benchmarking on your laptop is one thing; managing deployments, scaling, SSL certificates, and infrastructure is another. **That's where Shuttle comes in.** With Shuttle, you can deploy your Polars ETL pipeline as a production-ready API in just a few commands, no containers, no load balancers, no endless YAML files. ## Deploying a Rust ETL Pipeline with Shuttle Of course, benchmarks are only half the story. The real challenge is turning a fast local pipeline into something production-ready. That's where Shuttle helps: it lets you deploy Polars pipelines as APIs without wrestling with infra. Traditional Rust deployment can be complex, but Shuttle abstracts away the infrastructure management. ### Building a Production ETL API Here's how to wrap our Polars ETL pipeline in a web API suitable for production use: ```rust use axum::{routing::get, Router, Json}; use serde::{Serialize, Deserialize}; use shuttle_runtime::main; use tower_http::cors::CorsLayer; #[derive(Serialize)] struct ETLResults { processing_time_seconds: f64, rows_processed: u64, daily_statistics: Vec, performance_summary: String, } #[derive(Serialize)] struct DailyStats { date: String, trip_count: u64, avg_distance: f64, total_revenue: f64, } async fn run_etl_benchmark() -> Json { let mut etl = PolarsETL::new(); // In production, you'd load from your data warehouse // For demo purposes, we return representative results let results = ETLResults { processing_time_seconds: 19.1, rows_processed: 12_748_986, daily_statistics: create_sample_stats(), performance_summary: "Processed 12.7M taxi records in 19.1 seconds using Polars".to_string(), }; Json(results) } async fn health_check() -> Json { Json(serde_json::json!({ "status": "healthy", "service": "Polars ETL Pipeline", "capabilities": ["high_throughput_processing", "multi_core_execution", "memory_efficient"] })) } #[main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new() .route("/", get(health_check)) .route("/etl/benchmark", get(run_etl_benchmark)) .route("/health", get(health_check)) .layer(CorsLayer::permissive()); Ok(router.into()) } ``` ### Simple Shuttle Deployment Process Deploying this ETL pipeline to production requires minimal configuration: **1\. Deploy with three commands:** ```bash # Install Shuttle CLI cargo install cargo-shuttle # Login to Shuttle shuttle login # Deploy to production shuttle deploy ``` Shuttle handles all the complex infrastructure concerns: - Container orchestration and scaling - Load balancing and networking - SSL certificate management - Monitoring and logging systems - Automatic deployments from Git ### Integration with Data Systems For production use, you can connect this pipeline to various data sources and destinations: ```rust // Example: Reading from cloud storage let df = LazyFrame::scan_csv("s3://data-bucket/taxi-data/*.csv", ScanArgsCSV::default())?; // Example: Writing to data warehouse let result = df.collect()?; result.write_parquet("s3://output-bucket/processed-data.parquet", ParquetWriteOptions::default())?; // Example: Streaming processing let streaming_df = LazyFrame::scan_csv("data/*.csv", ScanArgsCSV::default())? .with_streaming(true) // Process in chunks .collect()?; ``` ## Migrating from Pandas to Polars: A Practical Guide The decision to migrate from Pandas to Polars shouldn't be all-or-nothing. Here's a practical approach for teams considering the transition while minimizing risk and disruption. ### When to Switch and When to Stick with Pandas **Consider Polars when**: - Processing datasets larger than available RAM - ETL operations take more than a few minutes to complete - Memory usage becomes a limiting factor in your systems - CPU cores remain underutilized during data processing - You need predictable performance characteristics **Stick with Pandas when**: - Working with datasets under 1GB consistently - Heavy use of domain-specific libraries that integrate with Pandas - Rapid prototyping, where development speed matters more than execution speed - Team lacks Rust experience, and the timeline is tight - Complex data science workflows with many specialized functions ### Hybrid Workflows: Wrapping Heavy Steps in Polars You don't need to rewrite entire systems. Start by identifying performance bottlenecks and replacing them with Polars operations: ```python import pandas as pd import polars as pl def hybrid_etl_pipeline(data_path): # Use Polars for heavy data loading and cleaning polars_df = pl.scan_csv(data_path)\ .filter(pl.col("amount") > 0)\ .with_columns([ pl.col("timestamp").str.strptime(pl.Date), (pl.col("end_time") - pl.col("start_time")).alias("duration") ])\ .collect() # Convert to Pandas for specialized analysis pandas_df = polars_df.to_pandas() # Use existing Pandas-based analysis code result = perform_statistical_analysis(pandas_df) # Convert back to Polars for final aggregation final_result = pl.from_pandas(result)\ .group_by("category")\ .agg([pl.col("value").sum(), pl.col("count").count()])\ .collect() return final_result def perform_statistical_analysis(df): # Existing Pandas code remains unchanged return df.apply(lambda x: complex_statistical_function(x)) ``` ### Testing Polars Without Rewriting Your Pipeline Start with a proof-of-concept approach that validates performance improvements: ```python # 1. Benchmark existing Pandas operations import time def benchmark_pandas_operation(): start = time.time() df = pd.read_csv("large_dataset.csv") result = df.groupby("category").agg({ "amount": ["sum", "mean", "count"], "duration": "mean" }) pandas_time = time.time() - start return result, pandas_time # 2. Implement equivalent Polars version def benchmark_polars_operation(): start = time.time() result = pl.scan_csv("large_dataset.csv")\ .group_by("category")\ .agg([ pl.col("amount").sum().alias("amount_sum"), pl.col("amount").mean().alias("amount_mean"), pl.col("amount").count().alias("amount_count"), pl.col("duration").mean().alias("duration_mean") ])\ .collect() polars_time = time.time() - start return result, polars_time # 3. Compare results and performance pandas_result, pandas_time = benchmark_pandas_operation() polars_result, polars_time = benchmark_polars_operation() print(f"Pandas: {pandas_time:.2f}s") print(f"Polars: {polars_time:.2f}s") print(f"Speedup: {pandas_time/polars_time:.1f}x") ``` ### Gradual Migration Strategy #### **Phase 1: Identify bottlenecks** - Profile existing code to find slowest operations - Measure current memory usage and processing time - Document data types and transformations used #### **Phase 2: Proof of concept** - Implement one critical operation in Polars - Validate identical results between implementations - Measure performance improvements #### **Phase 3: Expand coverage** - Replace additional heavy operations - Build team familiarity with Polars syntax - Update deployment processes to handle Rust code #### **Phase 4: Full migration** - Convert remaining operations where beneficial - Optimize query patterns for maximum performance - Update monitoring and alerting systems ## Final Takeaways Our benchmarks showed Polars delivering a **3.3x** speedup over Pandas for this ETL workload, with significantly lower memory usage and full CPU utilization. This performance comes from its modern architecture: **lazy evaluation**, **query optimization**, and native **multi-threading** powered by Rust. However, performance isn't everything. Pandas remains the pragmatic choice for smaller datasets (\<1 GB), rapid prototyping, and tasks deeply integrated with the broader Python data science ecosystem. In practice, many teams adopt a hybrid strategy: using Polars for heavy data preparation and falling back to Pandas for specialized analysis and ML model integration. Ultimately, if your current pipelines are hitting performance walls, Polars offers a clear path to faster, more scalable processing. But turning that local speed into a production-ready system presents the next hurdle. This is where Shuttle completes the picture. It abstracts away the complexity of containers and infrastructure, allowing you to deploy a high-performance Polars pipeline as a scalable API in minutes, not days. It turns benchmarks into real-world applications without the DevOps overhead. Ready to see the difference yourself? Deploy the complete Polars ETL benchmark and run your own comparisons with real data. ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/polars-otel-shuttle ``` Then deploy it with: ```bash shuttle deploy ``` Or you can view the complete benchmark code on [GitHub](https://github.com/AdepojuJeremy/pandas-vs-polars-benchmark). Cheers! 🎉 --- # How to Monitor Data Pipelines in Rust Using OpenTelemetry and Shuttle Source: https://www.shuttle.dev/blog/2025/09/23/monitor-data-pipelines-in-rust Date: 23 September 2025 Author: jeremiah Tags: rust, data-pipelines, monitoring Silent data pipeline failures slow teams down. Learn how to monitor data pipelines in Rust using OpenTelemetry and Shuttle to ensure observable ETL workflows. **TL;DR**: _Data pipelines often fail silently, corrupting data before anyone notices. We show you how to build a high-performance, observable ETL pipeline in Rust using Polars for speed, OpenTelemetry for detailed metrics and traces, and Shuttle for zero-config deployment and telemetry export. This guide provides commented code, deployment steps, and dashboard examples to help you catch failures early, pinpoint bottlenecks, and ensure data quality from start to finish._ ETL and data engineering workflows break silently in production. A CSV file processes millions of records, but somewhere in the pipeline, wrong values slip through data validation, memory usage spikes during aggregation stages, or processing slows to a crawl without clear indicators of where the bottleneck occurs. By the time data engineers notice the issue, corrupted data has already propagated downstream. Python developers working with Pandas frequently encounter this challenge. Memory blowups happen during large dataset processing, errors cascade across transformation stages with minimal context, and debugging performance issues requires manually adding print statements throughout the code. Traditional application monitoring tools focus on web request metrics such as latency, throughput, and error rates, but data pipelines need visibility into processing stages, memory consumption patterns, and data quality indicators. Here's the alternative: Rust applications with Polars deliver predictable performance, OpenTelemetry provides standardized observability, and Shuttle eliminates infrastructure complexity. By implementing such a combination, you get clean, fast, observable data pipelines that surface issues before they corrupt your data. This article explains building a production-ready data pipeline that processes real-world datasets while maintaining complete visibility into every processing stage, memory usage pattern, and performance characteristic. ## Benchmark Pipeline: CSV Processing with Polars Let me show you a working ETL example that processes [NYC taxi CSV files](https://www.kaggle.com/datasets/elemento/nyc-yellow-taxi-trip-data). This pipeline demonstrates the observability challenges that make data engineering workflows different from typical web applications. The data pipeline follows these stages: load massive CSV files (1.9GB with 12.7 million records), parse datetime fields and geographic coordinates, filter invalid records and outliers, aggregate trip patterns by time periods, and write results for downstream analysis. Each stage presents monitoring challenges. Loading CSV files requires tracking memory allocation patterns as data enters the system. Parsing operations can fail silently when date formats don't match expectations. Filtering stages need visibility into how many records get rejected and why. Aggregation operations consume the most memory and require peak usage tracking. Output operations need validation that results match expected patterns. Polars provides the performance foundation for this pipeline. Its lazy evaluation approach minimizes memory pressure, columnar data processing delivers consistent performance across large datasets, and structured operations make precise measurement possible. Unlike Pandas operations that can consume unpredictable amounts of memory, Polars operations have measurable resource requirements that can be monitored and alerted on. The NYC taxi dataset contains all the characteristics that make observability critical: high record volume, data quality issues with missing coordinates and invalid timestamps, complex geographic and temporal transformations, and memory-intensive aggregation operations. Processing this dataset demonstrates how proper instrumentation reveals bottlenecks and data quality issues before they impact production systems. ## What to Monitor in Data Pipelines (and Why) Data pipeline monitoring differs fundamentally from web application observability. Instead of request latency and error rates, you need visibility into processing characteristics that determine pipeline reliability and data quality. - **Stage-level execution time** reveals processing bottlenecks. While web applications measure individual request duration, data pipelines need timing for load operations, parsing stages, transformation steps, aggregation phases, and output generation. A single slow stage can bottleneck the entire pipeline. - **Throughput and record counts** provide data volume insights. Monitor records processed per second, total record counts per stage, and record rejection rates during validation. These metrics help identify processing capacity limits and data quality trends. - **Peak memory consumption per stage** prevents out-of-memory failures. Filtering operations might spike memory usage when loading large intermediate results. Aggregation stages require materializing grouped data. Memory patterns vary dramatically based on dataset characteristics and processing logic. - **Error paths and panics in Rust code** need special attention. Unlike garbage-collected languages, Rust memory safety prevents many runtime failures, but data pipeline errors often relate to schema mismatches, missing files, or invalid data formats. Capturing error context enables rapid debugging. These metrics differ from typical web application monitoring because data pipelines process discrete batches rather than continuous streams, consume resources in predictable patterns based on dataset size, and fail differently than request-response applications. Traditional application observability focuses on user-facing performance and system health. Data pipeline observability focuses on data quality, processing efficiency, and resource utilization patterns that affect downstream systems and analysis accuracy. ## Adding Observability to Rust ETL Pipeline with OpenTelemetry Instrumenting Rust applications for data pipeline monitoring requires strategic placement of tracing spans and metrics collection. The observability approach needs to capture processing stage performance, memory usage patterns, and data quality indicators without significantly impacting pipeline throughput. Here's how to structure observability in your Rust data pipeline using OpenTelemetry and tracing: ```rust use polars::prelude::*; use std::collections::HashMap; use std::time::Instant; use tracing::{info, instrument, span, Level}; // Memory monitoring function for RSS tracking fn rss_mb() -> f64 { if let Ok(s) = std::fs::read_to_string("/proc/self/status") { for line in s.lines() { if let Some(val) = line.strip_prefix("VmRSS:") { let kb: f64 = val.split_whitespace().nth(0).unwrap_or("0").parse().unwrap_or(0.0); return kb / 1024.0; } } } 0.0 } // Track peak memory across processing stages fn bump_peak(metrics: &mut HashMap, label: &str) { let m = rss_mb(); metrics.insert(format!("{}_memory_mb", label), m); let peak = metrics.get("peak_memory_mb").cloned().unwrap_or(0.0); if m > peak { metrics.insert("peak_memory_mb".into(), m); } } pub struct PolarsETL { df: Option, metrics: HashMap, // Custom metrics storage for pipeline-specific data } impl PolarsETL { pub fn new() -> Self { Self { df: None, metrics: HashMap::new() } } // Automatic span creation with contextual attributes (file path) #[instrument(level = "info", skip(self))] pub fn load_data(&mut self, file_path: &str) -> PolarsResult<&mut Self> { // Manual span creation for nested tracing hierarchy let load_span = span!(Level::INFO, "etl_load", file = file_path); let _enter = load_span.enter(); // Structured logging with contextual information info!("Loading CSV file: {}", file_path); let start = Instant::now(); // Performance timing measurement let lf = LazyCsvReader::new(file_path) .with_has_header(true) .with_infer_schema_length(Some(2000)) .finish()? .select([ col("pickup_longitude"), col("pickup_latitude"), col("trip_distance"), col("passenger_count"), col("total_amount"), ]); self.df = Some(lf); let t = start.elapsed().as_secs_f64(); // Duration calculation self.metrics.insert("load_time".into(), t); // Custom metric storage bump_peak(&mut self.metrics, "after_load"); // Memory usage tracking // Structured logging with key-value metrics for observability platforms info!( etl.load.duration = t, // Performance metric etl.load.memory_mb = self.metrics.get("after_load_memory_mb").unwrap_or(&0.0), // Resource usage stage = "load", // Pipeline stage identifier "CSV load completed" // Human-readable message ); Ok(self) } // Automatic instrumentation with span naming and skipping of complex parameters #[instrument(level = "info", skip(self))] pub fn clean_data(&mut self) -> PolarsResult<&mut Self> { // Nested span for detailed tracing hierarchy let clean_span = span!(Level::INFO, "etl_clean"); let _enter = clean_span.enter(); // Stage-specific logging with contextual information info!("Starting data cleaning stage"); let start = Instant::now(); // Performance measurement start if let Some(df) = &self.df { let cleaned = df .clone() .filter( col("pickup_longitude").neq(lit(0.0)) .and(col("pickup_latitude").neq(lit(0.0))) .and(col("trip_distance").gt(lit(0.0))) .and(col("passenger_count").gt(lit(0))) ) .cache(); self.df = Some(cleaned); } let t = start.elapsed().as_secs_f64(); // Performance timing self.metrics.insert("clean_time".into(), t); // Stage-specific metrics bump_peak(&mut self.metrics, "after_clean"); // Memory tracking per stage // Multi-dimensional metrics emission with contextual attributes info!( etl.clean.duration = t, // Stage performance etl.clean.memory_mb = self.metrics.get("after_clean_memory_mb").unwrap_or(&0.0), // Resource consumption stage = "clean", // Pipeline stage tag "Data cleaning completed" // Completion status ); Ok(self) } } ``` - **Spans with contextual attributes** provide distributed tracing capability. Each processing stage gets its own span with resource attributes like stage, file, and processing metadata. This enables tracing the whole process across complex pipeline operations. - **Instrumented functions** using the `#[instrument]` attribute automatically capture function entry/exit, execution time, and error conditions. The tracing framework handles span lifecycle management and context propagation. - **Metrics emission** follows OpenTelemetry conventions with structured field names like `etl.load.duration` and `etl.clean.memory_mb`. These metrics integrate with observability platforms for dashboard creation and alerting. Memory monitoring through `rss_mb()` reads actual RSS memory consumption from `/proc/self/status`, providing accurate memory usage data during each processing stage. This approach works reliably in containerized environments and cloud deployments. ⚠️ **Platform-Specific Code**: This memory monitoring function reads from the `/proc` filesystem, which is specific to Linux. For cross-platform compatibility, consider using a crate like `sysinfo` that provides unified system information APIs across Windows, macOS, and Linux. The image below shows the memory usage monitoring for Polars, demonstrating increased memory consumption during processing. These images show the system monitoring during ETL pipeline execution, where the first image displays baseline performance with low CPU utilization (1-8%) and stable 5.1GB memory usage, the next image shows increased processing activity with CPU8 reaching 32.4% while memory remains stable, and the last one captures peak ETL operations with significantly higher CPU utilization (CPU8 at 85.9%) and elevated memory consumption at 9.9GB, effectively demonstrating how the Rust pipeline scales resource usage during different processing phases and validating the importance of memory monitoring. By establishing these observability goals upfront, tracking performance, memory usage, and data quality, you create a foundation for reliable monitoring. ## Exporting Observability Data to BetterStack via Shuttle Shuttle provides out-of-the-box OpenTelemetry integration that eliminates traditional observability infrastructure complexity. Instead of configuring collectors, managing exporters, or writing YAML configurations, you get automatic telemetry data export to your chosen observability platform. To enable this integration, include [Shuttle's setup-otel-exporter feature](https://www.shuttle.dev/blog/2025/02/19/using-shuttle-with-betterstack) in your runtime configuration: ```toml [dependencies] shuttle-runtime = { version = "0.56", features = ["setup-otel-exporter"] } shuttle-axum = "0.56" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["json"] } opentelemetry = { version = "0.30", features = ["trace"] } ``` Then configure observability in your application code: ```rust use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; pub fn init_tracing(service_name: &str) { let env_filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("info")); let fmt_layer = tracing_subscriber::fmt::layer() .json() .with_current_span(true) .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339()); tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .init(); } ``` Shuttle's approach eliminates the infrastructure complexity typically associated with observability. No Docker containers running collectors, no YAML configuration files for exporters, no manual OTLP endpoint management. The platform handles OpenTelemetry protocol export, authentication, and routing automatically. When you enable BetterStack integration in your Shuttle project settings, telemetry data flows directly from your Rust application to BetterStack's ingestion endpoints. The integration includes automatic retry logic, batching for performance, and error handling that would normally require manual configuration. This approach works because Shuttle's runtime includes a tracer provider that automatically exports spans and metrics when observability integrations are enabled. Your application code focuses on emitting telemetry data using standard OpenTelemetry patterns, while the platform handles infrastructure concerns. The images above demonstrate the BetterStack observability platform receiving telemetry data from the Rust ETL pipeline, where the first screen shows basic log streams with application startup events including tracing subscriber initialization and service startup messages, while the second reveals detailed OpenTelemetry span data displaying trace IDs, span IDs, and stage timing information, showcasing how the integration captures both high-level application logs and granular distributed tracing data for comprehensive pipeline monitoring. ## Deploying the Observable Pipeline with Shuttle Shuttle deployment transforms your observable Rust application into a production service with a single command. The platform's infrastructure-from-code approach means your deployment configuration lives in your application rather than external configuration files. The deployment process is broken down into steps: **Step 1: Set up your application for Shuttle deployment:** ```rust mod etl; mod observability; use axum::{routing::get, extract::Query, response::Json, Router}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tower_http::{cors::CorsLayer, trace::TraceLayer}; use tracing::info; #[shuttle_runtime::main] async fn shuttle_main() -> shuttle_axum::ShuttleAxum { observability::init_tracing("polars-etl-pipeline"); let app = api_router() .layer(CorsLayer::permissive()) .layer(TraceLayer::new_for_http()); info!(event = "startup", service = "polars-etl", "Application ready"); Ok(app.into()) } #[derive(Serialize)] struct BenchmarkResult { metrics: HashMap, message: String, throughput_summary: String, } async fn health() -> &'static str { info!( etl.health.check = 1, endpoint = "/health", "Health check performed" ); "OK" } async fn benchmark(_query: Query>) -> Json { let start = std::time::Instant::now(); info!( etl.benchmark.start = 1, endpoint = "/benchmark", "Benchmark endpoint called" ); // Representative pipeline metrics (avoiding heavy processing on production dyno) let mut metrics = HashMap::new(); metrics.insert("load_duration_ms".to_string(), 1200.0); metrics.insert("clean_duration_ms".to_string(), 800.0); metrics.insert("aggregate_duration_ms".to_string(), 400.0); metrics.insert("total_duration_ms".to_string(), 2800.0); metrics.insert("records_processed".to_string(), 12_748_986.0); // Emit metrics for BetterStack dashboards info!( etl.load.duration = 1.2, etl.clean.duration = 0.8, etl.aggregate.duration = 0.4, etl.rows.processed = 12_748_986_i64, stage = "complete", "Pipeline benchmark completed" ); let elapsed = start.elapsed().as_millis() as f64; info!( etl.request.duration = elapsed, endpoint = "/benchmark", "Request completed" ); let throughput = 12_748_986.0 / 2.8; Json(BenchmarkResult { metrics, message: "Benchmark completed successfully".to_string(), throughput_summary: format!("Processed 12.7M records in 2.8s → {:.0} rows/sec", throughput), }) } fn api_router() -> Router { Router::new() .route("/", get(health)) .route("/health", get(health)) .route("/benchmark", get(benchmark)) } ``` **Step 2: Deploy your observable pipeline:** ```bash # Deploy to Shuttle platform shuttle deploy # Monitor deployment and access logs shuttle status shuttle logs --follow ``` **Step 3: Testing endpoints confirms telemetry data flows correctly:** ```bash # Test health endpoint curl https://your-app.shuttle.app/health # Generate benchmark telemetry curl https://your-app.shuttle.app/benchmark ``` Shuttle automatically streams metrics to BetterStack once you configure the integration in your project settings. The platform handles authentication, retry logic, and batching that would normally require manual OTLP configuration. The above images demonstrate the complete development-to-production workflow for the observable Rust ETL pipeline. The initial screens show local CLI execution with structured JSON logging, revealing detailed performance metrics including 108-second total runtime and stage-by-stage timing breakdown, while subsequent displays present comprehensive telemetry output with dataset information, processing statistics of 12.7M records processed at 4.5M records/second, and deployment configuration details. The workflow continues with the deployed Shuttle application's API endpoints for health checks and benchmarking, followed by captures of the Shuttle deployment process and dashboard showing the running application with telemetry integration and log streaming capabilities, further showing how the same codebase provides both detailed local benchmarking and production-ready observability through feature flags and deployment automation. ## Building Dashboards for ETL Monitoring in BetterStack When running ETL pipelines, it's not enough to just know they work; you need visibility into how they perform over time. BetterStack dashboards provide that visibility at a glance by transforming your telemetry data into actionable monitoring dashboards. The platform automatically recognises the metric naming patterns from your Rust application and enables sophisticated visualization and alerting capabilities. Here are some common ETL-specific metrics and their naming conventions you should follow: - **etl.load.duration**: measures the time spent loading CSV - **etl.clean.duration**: tracks the performance of data cleaning stage - **etl.aggregate.duration**: captures how long aggregation operation takes - **etl.rows.processed**: shows record throughput and overall volume - **etl.memory.peak**: reports peak memory consumption per stage You can configure dashboard for data pipeline monitoring like: **Memory Usage Over Time Dashboard**: ```json { "metric": "etl.memory.peak", "aggregation": "max", "group_by": ["stage"], "visualization": "line_chart", "time_range": "1h" } ``` **Stage-by-Stage Duration Analysis**: ```json { "metrics": [ "etl.load.duration", "etl.clean.duration", "etl.aggregate.duration" ], "aggregation": "avg", "visualization": "stacked_area", "group_by": ["stage"] } ``` **Processing Throughput Tracking**: ```json { "metric": "etl.rows.processed", "aggregation": "rate", "time_window": "1m", "visualization": "gauge" } ``` These visualizations provide insights into processing performance trends, memory consumption patterns during different pipeline stages, and throughput characteristics that help with capacity planning and performance optimization. These images show Betterstack monitoring dashboards tracking ETL pipeline performance, where the Telemetry interface displays Clean Time and Aggregate Time charts with regular periodic spikes indicating scheduled batch operations, while a separate CPU usage chart reveals sustained high utilization around 1,200-1,500T during data processing, providing essential visibility into pipeline performance and resource monitoring. With BetterStack's alerting capabilities, you can enable proactive monitoring by setting alerts for memory usage exceeding thresholds, processing duration degradation, or throughput drops below expected levels with integration into incident management systems for automated escalation. ## Local Development and Testing Setup So, how can you test and benchmark this pipeline on your own local machine with a real dataset? Running the full ETL process within our deployed web service isn't practical; web endpoints need fast responses, not long-running data processing jobs. Instead, we'll use Rust's feature flags to create a separate CLI for local, in-depth testing. This approach lets you experiment with different datasets, measure actual performance characteristics, and validate your observability setup before deploying to production. Feature flags enable different build targets for local development versus production deployment. This approach allows for comprehensive local testing with real datasets while maintaining production builds as lightweight and cost-effective as possible. **Configure feature-based compilation in Cargo.toml:** ```toml [features] bench-cli = [] default = [] ``` **Local development implementation with full ETL processing:** ```rust #[cfg(feature = "bench-cli")] fn main() -> Result<(), Box> { observability::init_tracing("polars-etl-benchmark-cli"); println!("Starting Polars ETL benchmark with observability"); let data_file = std::env::var("DATA_PATH") .unwrap_or_else(|_| "data/yellow_tripdata_2015-01.csv".to_string()); if !std::path::Path::new(&data_file).exists() { eprintln!("CSV file not found: {}", &data_file); eprintln!("Set DATA_PATH environment variable or place file at default location"); return Ok(()); } let total_start = std::time::Instant::now(); let mut etl = PolarsETL::new(); match etl .load_data(&data_file)? .clean_data()? .aggregate_data()? .save_results("results") { Ok(_) => { let total_time = total_start.elapsed().as_secs_f64(); println!("ETL benchmark completed in {:.2} seconds", total_time); let metrics = etl.get_metrics(); println!("Performance metrics:"); for (key, value) in metrics { if key.contains("time") { println!(" {}: {:.2}s", key.replace('_', " "), value); } } } Err(e) => { eprintln!("ETL processing error: {}", e); } } Ok(()) } ``` **CLI workflow for benchmarking locally** with comprehensive logs and metrics: ```bash # Process full dataset with detailed telemetry RUST_LOG=debug cargo run --release --features bench-cli # Use custom dataset location DATA_PATH=/path/to/your/dataset.csv \ RUST_LOG=info cargo run --release --features bench-cli ``` **Dataset loading via CLI** enables testing with different data characteristics. The CLI version processes actual CSV files and provides accurate performance measurements, memory usage patterns, and processing throughput data that inform production capacity planning. Local testing reveals performance characteristics that aren't visible in production monitoring. Memory allocation patterns during aggregation stages, processing bottlenecks with different dataset sizes, and error handling behaviour with malformed data files. ## Wrapping Up: Fast Pipelines with Precise Monitoring Rust with Polars delivers consistent performance that traditional Python workflows often lack. While Python pipelines struggle with unpredictable memory spikes, Rust provides predictable resource usage that scales linearly with dataset size. Moreover, compile-time error checking prevents the runtime failures that typically corrupt downstream analysis in dynamically-typed environments. Building on this foundation, OpenTelemetry adds precise monitoring with minimal performance overhead. This enables comprehensive dashboards and proactive alerts that surface issues before they impact data quality. Meanwhile, Shuttle eliminates the deployment complexity that often delays pipeline rollouts, providing zero-config OTLP export, automatic retry handling, and managed authentication out of the box. The business impact is substantial: teams ship data products 3x faster without infrastructure bottlenecks, process larger datasets with predictable costs, and maintain higher system reliability that directly improves user experience. Together, these tools create production-ready workflows that deliver fast processing through Rust and Polars, comprehensive visibility via OpenTelemetry, and frictionless deployment through Shuttle. This proactive monitoring approach prevents costly memory failures, enables accurate capacity planning, and catches performance bottlenecks before they affect end users. **Ready to build observable data pipelines? Get started with the complete example:** ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/polars-otel-shuttle ``` Then deploy with: ```bash shuttle deploy ``` ## Troubleshooting Telemetry Issues in Rust Pipelines - FAQ **Q: My metrics aren't showing up in BetterStack. What's wrong?** **A:** First, double-check your integration settings in the Shuttle console and verify your source token hasn't expired. Second, confirm your metric names in the code (e.g., `etl.load.duration`) exactly match your dashboard queries—BetterStack requires precise field name matching. Finally, redeploy your application after making integration changes, as the OTLP exporter configuration updates during deployment. Test the flow by running `curl https://your-app.shuttle.app/benchmark` and checking if logs appear in BetterStack's log stream before expecting dashboard data. **Q: Why are my spans missing or not connecting in distributed traces?** **A:** Check that your OpenTelemetry dependencies use compatible versions; mismatches between `tracing-opentelemetry` and `opentelemetry` crates can break span context propagation. Ensure your instrumented functions include appropriate resource attributes like `stage` or `file`, as missing context reduces trace value. Also, verify your tracer provider configuration includes your service name and resource attributes needed for filtering and grouping in trace analysis. **Q: My pipeline works in production but fails during local testing. What should I check?** **A:** Start by validating CSV file locations and permissions—your application needs read access to data files and write access to results directories. Check that your dataset format is compatible with Polars CSV parsing, as schema mismatches or encoding issues can cause silent failures. Monitor memory consumption during testing since large datasets might exceed available memory during aggregation stages. Finally, debug your environment variable configuration, ensuring `DATA_PATH` points to accessible file locations. **Q: How can I get better error information when my pipeline fails?** **A:** Implement structured error logging that includes file paths, record counts, and processing stage information in error messages. This context enables rapid issue resolution by showing exactly where and why failures occur, rather than generic error messages that require extensive debugging to understand. --- # Introducing Shuttle Cobra Source: https://www.shuttle.dev/blog/2025/09/18/introducing-shuttle-cobra Date: 18 September 2025 Author: dcodes Tags: shuttle, python, aws, deployment, infrastructure Shuttle Cobra brings Infrastructure-from-Code to Python using type hints and decorators, making it easy to deploy Python applications to your own AWS infrastructure. We spent years building Shuttle for Rust, we listened to the community and the response was incredible. Developers love it because it solves a fundamental problem: the massive gap between writing code and deploying it. Shuttle's success comes from Infrastructure-from-Code (IfC). Instead of juggling multiple config languages, you write Rust macros that control your infrastructure directly. This lets developers focus on their application logic without the need for a complicated deployment setup. Over the years, we took advantage of the simplicity of the same pattern for other resources as well. Database provisioning demonstrates this well - in our current platform, developers can add one annotation and have a database ready regardless of the environment they are in and Shuttle will handle each environment specifically: If it's development, it will create a docker container with a Postgres instance and connect it to your code and if it's production, it will either create a dedicated AWS RDS instance or a shared DB based on the annotation. Everything connects with full type safety without the need to define separate configuration files. This is what makes Shuttle loved by the community. After many iterations and fixing many issues, we were finally able to provide a seamless experience providing infrastructure to our users. That got us to thinking: _could we step outside of Rust and bring the same experience to other languages?_ **This project is our shot at bringing this same experience to Python.** ## Shuttle Cobra Shuttle Cobra is our first step to explore this idea and bring IfC to Python and potentially other languages in the future. **Shuttle Cobra** is a Python framework that makes it easy to deploy Python code to your own AWS infrastructure using Python **type hints** and **decorators**. Unlike our main Shuttle platform, Cobra deploys directly to your own AWS account - you own the infrastructure and pay the AWS costs. **Shuttle Cobra** is experimental and a POC (proof of concept) at the moment, our main goal is to gather feedback from the community and monitor adoption. ## How Shuttle Cobra Works Just like Shuttle for Rust, the main goal for **Shuttle Cobra** is also **Simplicity**, we've made it so simple that you can deploy a Python job to AWS just by adding a few lines of code and if you already have a **Python job**, you can just add the same annotations and you're good to go. Here is an example of a Python job that reads from an S3 bucket and writes to a database: ```python from typing import Annotated import shuttle_runtime import shuttle_task from shuttle_common import Bucket, BucketOptions, AllowWrite, RdsPostgres, RdsPostgresOptions @shuttle_task.cron(schedule="0 * * * ? *") async def main( bucket: Annotated[ Bucket, BucketOptions( bucket_name="grafana-exporter-1234abcd", policies=[ AllowWrite(account_id="842910673255", role_name="SessionTrackerService") ] ) ], db: Annotated[RdsPostgres, RdsPostgresOptions()], ): # ... if __name__ == "__main__": shuttle_runtime.main(main) ``` Then just run `shuttle deploy` and you're done. There is **no vendor lock-in** here, you're still using the same AWS libraries (`boto3`, `aioboto3`) that you'd use without **Shuttle Cobra**. The framework just handles the infrastructure setup and you will write code the same way you'd write without it. ## What Happens Under the Hood When you use `Annotated[Bucket, BucketOptions(...)]`, you're telling **Shuttle Cobra** "I need an S3 bucket with these settings." Here's what happens when you deploy: 1. The **Shuttle Cobra** CLI reads your type hints to see what AWS resources you need 2. It generates the CloudFormation templates for you 3. Provisions everything in your AWS account 4. Packages your code into a container and deploys it to ECS 5. Sets up the CRON schedule with EventBridge Your dependencies get injected at runtime, so your code stays clean. ## Fair Warning This is experimental. Right now it only handles CRON jobs, but we think the approach could work for web services, Lambda functions and other workloads too. We built this to see if the IfC idea that works so well in Rust could work in other languages. Turns out it can. ## What's Next? We'd love to hear what you think about **Shuttle Cobra**. Since this is an experimental approach, your feedback will shape where we take it next. Try it out and let us know: - Does the IfC pattern feel natural in Python? - What other AWS resources would be useful? - What types of workloads beyond CRON jobs would you want to deploy this way? ## Links - [GitHub repo](https://github.com/shuttle-hq/shuttle-cobra) - [Docs](https://docs.cobra.shuttle.dev/?utm_source=shuttle_blog&utm_campaign=shuttle_cobra) - [Discord](https://discord.gg/shuttle) --- # A Hands-on Comparison of Best MCP Servers for Rust Developers Source: https://www.shuttle.dev/blog/2025/09/15/mcp-servers-rust-comparison Date: 15 September 2025 Author: adetokunbo Tags: shuttle, mcp, rust, docker, github, deployment, axum Compare the best MCP servers for Rust development. Learn how to deploy Rust applications directly from your IDE. Model Context Protocol (MCP) servers boost developer productivity by working with an AI coding assistants like GitHub Copilot, Cursor, and Claude. With MCP having full access to the Rust project, the AI assistants are no longer limited to the currently open file; they can now follow instructions to execute commands, analyze the entire codebase, run tests, and automate workflows, making development faster. Most developers don't rely on a single MCP; instead, they use multiple servers, depending on their task. Each MCP comes with its own strengths and limitations. In this article, we'll walk through the best MCP servers for Rust developers. We will also compare the most popular MCP servers to break down what each server does best, and show you how to deploy your Rust applications directly from your IDE. Here is a quick overview of the comparison between the best MCP Servers | MCP Server | Rust Integration | AI Compatibility | Security Setup | Deployment Support | Notes | | ----------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Context7 MCP | Provides up-to-date, version-specific documentation for all libraries including Rust crates | AI assistant can fetch real-time docs and code examples for accurate code generation | Low risk, read-only access to documentation APIs | None | Essential for preventing hallucinated APIs and outdated code examples | | Docker MCP | Good for developing containerized Rust applications. | AI assistance can issue Docker commands through MCP | Requires elevated privileges for certain operations, which can pose risks if not isolated | Can build, package, and push Rust apps to a Docker Registry | Suitable for those interested in or running Rust applications in Docker containers | | GitHub Server MCP | Integrates with Rust code repositories and supports CI/CD workflows (GitHub Actions) | The AI assistant can raise PRs, manage issues, and interact with GitHub workflows | Requires GitHub auth token access with limited or supervised privileges | Works effectively with GitHub Actions workflow | Great for managing repositories and automating workflows | | File System MCP Servers | Provide direct access to Rust source code | AI assistance has complete visibility of the Rust project | Risky because AI assistant can read and modify project files. Sandbox is recommended | Requires a custom script for deployment | Useful for testing workflows that rely on code modifications | | Memory MCP Servers | No direct Rust project support | Helpful in maintaining conversational context across tasks | Low risk since it operates only in memory | None | It is used in combination with other MCPs or systems | | Browsing MCP Servers | No direct Rust integration. This is useful for fetching Rust documentation and creating crates.io packages | AI assistant can read and retrieve information from Rust documentation | None | None | Great for referencing external Rust documentation and libraries | | Shuttle MCP | Built specifically for Rust. It integrates perfectly into the Rust ecosystem | It works well with AI assistants to manage and deploy Rust applications | It handles secrets and other sensitive details securely on the Shuttle Platform | It can deploy Rust applications directly from a developer's IDE | It is helpful for Rust deployment | Before we deploy our Rust applications directly from the IDE, let's understand what Model Context Protocol (MCP) is and what makes a good MCP for Rust developers. ### What is the Model Context Protocol (MCP)? Large Language Models (LLMs) such as GPT, Claude, and Gemini are powerful tools that significantly increase developer productivity. However, they share a fundamental limitation: their knowledge is restricted to the time of their training. For example, an LLM trained in early 2024 will not be aware of new frameworks, events, libraries, or any new information that emerges in 2025 or beyond unless it is updated. MCP was designed to close a critical gap in the AI coding assistant ecosystem. It allows AI assistants to go beyond code generation. MCP is an open standard that was introduced by Anthropic in November 2024. It enables LLMs to connect to various data sources, tools, and APIs, thereby fetching realtime information or performing tasks that exceed their built-in knowledge. ### What Makes a Good MCP for Rust Developers? When evaluating an MCP server for Rust development, it is essential to keep a few key criteria in mind: 1. **Rust-Native Workflows:** Does the MCP work smoothly with Cargo and popular frameworks like Axum, Actix-Web, or Rocket? The best servers will integrate effectively with the existing Rust ecosystem. 2. **AI Assistant Compatibility:** Can your AI assistant access the MCP server, either through LLMs or a developer IDE? Developers should consider how the MCP will fit into their current workflow. Ideally, the MCP should support both IDE and LLM integrations. 3. **Security and Access:** Security is a crucial factor when choosing the right MCP. Developers need to be confident that their secrets are kept safe and that the MCP server avoids unnecessary privilege escalation. 4. **Setup Complexity:** A good MCP server should be simple to configure and use. It shouldn't require complex installation steps, whether in an IDE or through an LLM. 5. **Deployment Capability:** Can the MCP server assist with end-to-end deployment, beyond just code generation? The best MCPs should be able to generate code, build, test, package, and even deploy Rust applications directly from an IDE. With these criteria in mind, let's look at the seven most common MCP servers Rust developers actually use. ## The 7 Most Common MCP Servers for Rust Developers Here is a list of popular MCP servers that are particularly useful for Rust development. ### 1. Context7 MCP Server Context7 MCP Server addresses one of the most critical challenges in AI-assisted Rust development: outdated and hallucinated documentation. When working with Rust's rapidly evolving ecosystem, developers often encounter AI assistants that generate code based on year-old training data, leading to deprecated APIs, incorrect function signatures, and non-existent methods. Context7 solves this by providing real-time access to up-to-date, version-specific documentation directly from the source. For Rust developers, this means accurate code examples for popular crates like Axum, Tokio, Serde, and emerging libraries that weren't in the AI's training data. The main advantage is eliminating the frustration of debugging AI-generated code that uses outdated APIs. Instead of spending time fixing deprecated methods or non-existent functions, developers can trust that the AI assistant has access to current documentation and working code examples. ### 2. Docker MCP Server Docker MCP Server is ideal for Rust developers managing complex dependencies or building applications that require isolated environments. By running a Rust project inside a Docker container, the AI assistant can conveniently perform builds, run tests, and refactor code in a secure and reproducible way. Developers can easily reproduce builds and dependencies consistently without affecting the other parts of the codebase. Developers can also run commands inside the Docker container, such as **cargo build**, with confidence that the results will be identical in both production and non-production environments. This consistency is helpful when resolving dependency-related build problems. Rust developers can leverage pairing Docker MCP with File system MCP to streamline their day-to-day coding activities and automate a smooth workflow. Docker MCP is best used for integration testing, Continuous Integration workflow, or sandbox experimentation. ### 3. GitHub MCP Server Rust developers derive the most benefits from using the GitHub MCP server when their code repository is hosted on GitHub. This MCP server automates workflows such as setting up Continuous Integration (CI) pipelines with GitHub Actions and managing releases. Developers can ask their AI assistant to handle tasks such as bumping crate versions, generating and maintaining changelog files, and other routine repository management tasks. The GitHub MCP Server is tightly coupled to GitHub, which means that its full capabilities are only available within the GitHub ecosystem. Teams hosting their code on GitLab, Bitbucket, or other platforms may not find the GitHub MCP server as useful. The GitHub MCP Server is best used when paired with Docker MCP to provide a complete workflow from development to deployment. ### 4. File System MCP Server The File System MCP Server provides the AI assistant with direct access to read, write, and modify Rust project files. This allows Rust developers to perform tasks such as refactoring code, automatically fixing errors in the codebase, creating new features, and updating Rust files accordingly. When using the MCP server, developers should have measures in place to track which files that have been modified by the assistant. Combined with GitHub MCP server, version control can be implemented, making it easier to track the changes introduced by File system MCP. For example, an AI assistant could create a new feature, test it within a Docker container, and then set up a CI workflow using GitHub's MCP; this demonstrates how Docker, GitHub, and File System MCP can all work together. ### 5. Memory MCP Memory MCP stores project-specific knowledge across sessions, making it easier for an AI assistant to remember naming conventions, coding standards, and recurring compiler errors. For Rust developers working across different teams or modules, this helps to enforce consistency and maintain project wide standards. The main limitation is that the MCP is bound to a single context, which may restrict productivity in large projects that demand switching between multiple contexts. However, when paired with File System or Docker MCP, it can guide developers or an AI assistant during editing or refactoring, ensuring modifications align with the project standard. ### 6. Browsing MCP Browsing MCP gives the AI assistant realtime access to external information such as crates documentation, Rust official documentation, or usage examples. This MCP is beneficial when working on a task and needing to look up an example implementation or consult documentation without leaving the development workflow. The main limitation is that Browsing MCP can not modify the file system directly. When combined with other MCPs, such as File System or Docker MCP, the AI assistant can utilize the information it discovers to apply it to the project by creating code, updating files, or testing changes. ### 7. Shuttle MCP Shuttle MCP is tailored for deploying Rust applications, such as Axum, Actix, or Rocket. For a development team focused on rapid iteration, this MCP allows developers to deploy the application quickly from code to a live environment. Developers can concentrate on writing code while Shuttle handles server setup, SSL certificate management and deployment. Shuttle focuses on Rust projects and it integrates smoothly with Rust ecosystem. When combined with Docker and File System MCPs, developers can build and test their Rust projects locally. Then deploy to live environment with Shuttle, providing a smooth and end to end workflow ## The Challenges of Managing Multiple MCPs Using individual MCPs can help accelerate developer productivity, but managing multiple MCPs simultaneously introduces additional complexity. Here are some of the challenges I have encountered while working with various MCPs. ### Resource Management and Performance When working with multiple MCP servers, there is always the possibility that they will compete for system resources such as CPU and memory. In large Rust projects, this competition can negatively impact IDE performance. It becomes a significant bottleneck when projects rely heavily on multiple MCP servers. To ensure you get the most out of these MCPs while still maintaining top performance in your Rust projects, here are a few best practices to follow: - **Monitor and limit resource usage:** Carefully track CPU and memory usage and limit the number of active MCPs to prevent overload. - **Allocate CPU and memory per MCP:** Assign specific resources to each MCP server to ensure no single server dominates system resources. ### Security and Authentication Complexity As a developer, when I want to use an MCP server, each server requires its own authentication and security setup. When multiple MCP servers, such as Docker, GitHub, file system, and browsing MCPs, are running simultaneously, managing all the secrets, API keys, access tokens, and security configurations can become challenging. If one MCP is compromised or becomes vulnerable, it could put the entire MCP setup at risk. To make multiple MCPs safe for use, you can do the following: - **Implement strict isolation** between servers to prevent a compromised MCP from affecting others. This can be achieved by: - Running each MCP on its own container or virtual machine. - Storing secrets or API Keys in a dedicated, secure vault with a separate namespace, so that if one secret is compromised, it does not affect others. - **Perform regular secret rotation**, for example, yearly or quarterly, to minimize the risk of leaked credentials. - **Continuously monitor the environment** for any suspicious activity. The real challenge arises when developers need to use multiple MCP servers simultaneously, such as Docker and GitHub. Each server has its own authentication, logs, and configuration, which can quickly turn into an overwhelming experience. Without a unifying layer, developers end up spending more time managing connections than actually building. ## How MCP Aggregators and API Gateways Solve These Problems As a developer, I want a solution that enables me to leverage multiple MCP servers with my AI assistant, unifying different tools and data sources into a single, seamless workflow. Platforms like [**Lunar.dev**](http://Lunar.dev), along with other MCP aggregators such as [**Solo.io**](http://Solo.io) and **MCP Bridge**, help tackle the complexity of managing multiple MCP servers. These platforms act as middleware, allowing you to configure various MCPs through a single interface. Here is how MCP aggregators help: - **Unified logging:** Developers can view a stream of logs from multiple MCP servers, which is more helpful in debugging than viewing logs from a single MCP server. - **Security monitoring and access control:** Aggregators provide fine-grained access control, making it easier to identify which permissions are granted to a specific developer. - **Performance optimization and caching:** Frequently accessed or requested results can be cached at the aggregator level, reducing latency, improving response times for developers, and ultimately saving time. Rust developers can increase their productivity by integrating AI-assisted workflows into their ecosystem. These aggregators add tremendous value by simplifying integration, reducing overhead, and maintaining a secure and performant development environment, allowing engineers to focus on building code. ### How Shuttle Solves the Deployment Problem Rust developers are currently facing a significant challenge with deployment. With the assistance of AI in accelerating the generation of code, deploying Rust projects to live environments often requires extra steps compared to other languages like Python or Node JS, which benefits from a mature Platform as a Service (PaaS) solutions, strong community support and extensive resources. On the other hand, Rust ecosystem is still evolving with fewer tutorials, deployment focused tools and best practices available. You may have generated clean Axum handlers, written comprehensive tests, and even have users waiting for your feature. Yet the moment you are ready to deploy, you are forced into manual deployment processes. The current deployment workflows for Rust applications require developers to: - Set up cloud infrastructure - Set up monitoring and logging - Handle SSL certificates and domain configuration There should be a workflow that allows developer to deploy their Rust project directly from their IDE. Shuttle MCP enables Rust developers to deploy applications directly from their IDE by integrating with Shuttle CLI, which requires a separate installation. This approach boosts productivity by removing the manual steps involved in traditional deployment. Shuttle packages, compiles, and deploys your application automatically, so that you can stay focused on building. Unlike other solutions that requires complex scripts or Infrastructure as a Code (IaC) configurations which rely on YAML files or provider specific configuration. Shuttle follows an Infrastructure from Code (IfC) approach, which means that your infrastructure is defined directly in your Rust codebase alongside with the application logic. You can ask your AI assistant to: - Deploy your Axum API to the live environment - Check the logs of the deployed service ## Hands-on Example: Build and Deploy a Snippet Sharing API with Shuttle In this hands-on section, we'll build a Rust + Axum API that powers a simple code snippet sharing service (similar to Pastebin) and demonstrate how to fetch documentation with Context7 MCP Server, create a pull request with GitHub MCP Server and deploy the Rust code using Shuttle MCP Server, with Cursor AI assistance. The API will include: - Endpoints to create and retrieve snippets - Filtering snippets by programming language - Clean, shareable URLs with 8-character IDs ### API Endpoints | Method | Endpoint | Description | | ------ | ---------------- | ------------------------- | | GET | /health | Health check endpoint | | POST | /snippets | Create a new code snippet | | GET | /snippets | List all snippets | | GET | /snippets/\{id\} | Get a single snippet | | DELETE | /snippets/\{id\} | Delete a snippet | #### Step 1: Setting Up Your Environment: Install Rust and Shuttle CLI Firstly, ensure you have the following software running: 1. **Rust and Cargo:** For local development and testing. 2. **Shuttle CLI:** For deployment into a live environment. You can install it via Cargo, or use the installation script by following this link: [Shuttle CLI Installation.](https://docs.shuttle.dev/getting-started/installation) 3. **Cursor:** An AI coding assistant. We will use it to interact with MPC Servers (Context7, GitHub and Shuttle) for documentation lookup, creating PRs and deploying the Rust application. #### Step 2: Log in to Shuttle Authenticate your Shuttle account: ```bash shuttle login ``` This connects your CLI to your Shuttle account, enabling authorized deployments. #### Step 3: Setting Up Our Project Next, we will create our Rust project. Below is an example of the Cargo.toml configuration. You can also generate the below file `Cargo.toml` by running the shuttle command `shuttle init` . Running the command will initializes a new Shuttle project in your current directory. ```toml [package] name = "code-snippet-sharing-app" version = "0.1.0" edition = "2021" [dependencies] axum = "0.8" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } shuttle-axum = "0.57.0" shuttle-runtime = "0.57.0" shuttle-shared-db = { version = "0.57.0", features = ["postgres", "sqlx"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.0", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } nanoid = "0.4" sqlx = { version = "0.8", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid", ] } ``` #### Step 3: Build the Rust Code Locally Build the Rust code by running this command below to build the Rust project to confirm that there are no errors: ```bash cargo build ``` #### Step 4: Run the Rust Code Locally Run the Rust code by running this command below: ```bash shuttle run ``` #### Step 5: Deploy your app Now, let's deploy your application using Shuttle and see how it makes deployment easier by handling all the heavy lifting for you. We will be deploying the application using Shuttle MCP Server. > The URLs used in the following steps are examples. When you deploy your own application, Shuttle will provide you with a unique URL. From your cursor IDE. Run the command on the prompt ```bash #Prompt Deploy the Rust code Use the Shuttle MCP server. ``` The AI assistant will deploy the application and a link will be generated. You can access your application with the provided link. For instance, in this case, the link is `https://code-snippet-share-ow2x.shuttle.app`. Shuttle will: - Package your code - Compile it in the cloud - Provision infrastructure - Manage SSL certificate - Deploy your app to a live environment #### Step 6: Test Your App Deployed Using Shuttle: Health Endpoint Let's test the application we deployed with Shuttle to ensure all the endpoints are working correctly. **Health Endpoint:** `https://code-snippet-share-ow2x.shuttle.app/health` You can use curl from the command line or a tool like Postman. In this example, we will use Postman. #### Step 7: Test Your App Deployed Using Shuttle: POST Snippet Endpoint Send a POST request to `https://code-snippet-share-ow2x.shuttle.app/snippets` with a JSON payload to create a new snippet record. ```json { "content": "A Hands-on Comparison of Best MCP Servers for Rust Developers", "language": "Rust", "title": "Shuttle MCP", "description": "Explore Shuttle MCP", "expires_in_hours": 24, "is_public": true } ``` #### Step 8: Test Your App Deployed Using Shuttle: GET All Snippets Endpoint Send a GET request to `https://code-snippet-share-ow2x.shuttle.app/snippets` to retrieve all snippets. #### Step 9: Test Your App Deployed Using Shuttle: GET a Single Snippet Endpoint Send a GET request to `https://code-snippet-share-ow2x.shuttle.app/snippets/qjs0BzAq` to retrieve a specific snippet by its ID. #### Step 10: Use GitHub MCP Server We will explore how to use the GitHub MCP Server to create a Pull Request (PR) and manage the Rust project on GitHub. ```bash # Prompt Raise a PR with my new changes Using GitHub MCP Server ``` #### Step 11: Use Context7 MCP Server We will explore how to use Context7 MCP Server to ask questions about the Rust code and access documentation related to the project. In the example below, we demonstrate how to use Context7 MCP Server to ask a question and reference documentation "_I need to reference a Rust documentation on how i can persistent snippet data instead of using in memory HashMap_". ```bash # Prompt I need to reference a Rust documentation on how i can persistent snippet data instead of using in memory HashMap. Using context7 MCP ``` ## Wrapping Up The MCP ecosystem is opening new possibilities for Rust developers. Whether you are managing repositories with GitHub MCP, restructuring projects through the Filesystem MCP, or streamlining research with the Browser MCP, each option reduces friction in its own way. By combining multiple MCP servers, AI assistants can support the entire application lifecycle, allowing developers to focus on writing Rust instead of wrestling with complex infrastructure tools. Shuttle simplifies deployment, letting you build features, test ideas, and ship production-ready applications in minutes. Ready to try this out for yourself? Run the following command to get started immediately: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/code-snippet-sharing-app ``` Other ways to get started: - [Sign up for Shuttle](https://console.shuttle.dev/) to start building with Shuttle. - [Clone the repository](https://github.com/shuttle-hq/shuttle-examples/tree/main/axum/code-snippet-sharing-app) used in this article to explore how to build with Shuttle. - [Learn more about Shuttle](https://docs.shuttle.dev) to get started quickly. Whichever MCP fits your workflow, **Shuttle** makes deploying Rust applications effortless. --- # How to Migrate to Shuttle Using Cursor and the Shuttle MCP Server Source: https://www.shuttle.dev/blog/2025/09/11/migrate-to-shuttle Date: 11 September 2025 Author: dcodes Tags: shuttle, cursor, mcp, migration, axum, rust Learn how to migrate your existing Axum application to Shuttle using Cursor and the Shuttle MCP server for seamless deployment. We've made migration to Shuttle very easy by leveraging our Shuttle MCP server and Cursor. The Shuttle MCP provides up to date documentation to your AI agents so that they'll be able to understand how to interact with the Shuttle platform having the latest information. In this short tutorial, we'll walk through migrating an existing Axum todo application to Shuttle and we'll deploy the application to Shuttle, we'll do all of that in just a few minutes. The final migrated project is available at [todo-app example](https://github.com/shuttle-hq/shuttle-examples/tree/main/axum/todo-app). So, let's get started. ## Prerequisites You'll need to have a Shuttle account, so if you don't have one, make sure to [create an account](https://console.shuttle.dev) it takes less than a minute. After signing up, you'll need to install the Shuttle CLI: ```bash # Install via the official installer (recommended) curl -sSfL https://www.shuttle.dev/install | bash shuttle --version ``` > If you already have shuttle installed, make sure you update it by running `shuttle upgrade`. ## Login to Shuttle Once the CLI is installed, login to your Shuttle account: ```bash shuttle login ``` This command will redirect you to the Shuttle console in your browser. Click "Authorize" to login. ## Installing the Shuttle MCP Server in Cursor Installing the Shuttle MCP Server is quite easy to do, you can add it just by clicking the "Add to Cursor" button below. If not using Cursor no problem, you can manually update your `mcp.json` file to add it to your MCP client. Example: ```json { "mcpServers": { "Shuttle": { "command": "shuttle", "args": ["mcp", "start"] } } } ``` The green status indicates that the MCP server is working and all tools ready for your AI agent to use. ## Migrating Your Project Now comes the interesting part - with this approach, we're gonna make it very easy to migrate to Shuttle. We've already put together a prompt that will guide your AI agent to migrate your existing Axum project to Shuttle. Along with the MCP server, your AI agent will have every bit of information it needs to help you migrate your project. ````md Convert this existing Rust web application to run on Shuttle using the platform's features: ## Instructions **CRITICAL: Search Shuttle MCP docs before each task. Pattern: search docs → do task → search docs → do task** **Always use the latest version of Shuttle dependencies** **Do not modify versions of existing non-Shuttle dependencies - only add/update Shuttle-specific dependencies** 1. **Analyze codebase** - Examine `main.rs`, `Cargo.toml`, configs 2. **Update dependencies** - Update Cargo.toml 3. **Convert main function** - Transform to use Shuttle runtime 4. **Database integration** - If PostgreSQL is used, search "postgres" using the MCP, then migrate to Shuttle's managed Postgres 5. **Manage secrets** - If environment variables are used, create Secrets.toml with placeholder keys 6. **Auto migrations** - If database is used, ensure migrations run on startup 7. **Create Shuttle.toml** - Create if app serves static assets: ```toml [build] assets = ["static/*", "public/*"] ``` 8. **Configure tracing** - Remove existing tracing init (keep macros/spans) 9. **Add secrets macro** - Use `#[shuttle_runtime::Secrets]` 10. **Add DB macro** - Use `#[shuttle_shared_db::Postgres]` 11. **Update .gitignore** - Add: ```gitignore /target .shuttle* Secrets*.toml ``` 12. **Test locally** - Test if compiles with `shuttle run` 13. **Create project** - Run `shuttle project create --name ` and `shuttle project link --id ` ## Requirements - Preserve all functionality and API behavior - Make minimal changes - focus on entry point and infrastructure only - Use `shuttle_runtime::main` - If PostgreSQL is used: add `shuttle_shared_db::Postgres` and `sqlx::migrate!()` - If environment variables are used: add `shuttle_runtime::Secrets` - Remove tracing init (keep macros) - Ensure works with `shuttle run` and in production ## Next Steps After Migration After completing the migration, inform about these remaining manual steps: 1. **Fill in actual secret values** in `Secrets.toml` if created (only placeholders can be created) 2. **Review all changes** made during the migration for production readiness 3. **Deploy to production** - request deployment after secrets are set and changes are reviewed ```` Paste the prompt to your AI agent and let it do the work for you. As you can see, the AI agent is using the Shuttle MCP server to search the documentation and get the latest information about the Shuttle platform. ## Key Migration Changes The main transformation involves updating your `main` function from a standard Tokio setup to Shuttle's runtime. ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] pool: sqlx::PgPool, ) -> shuttle_axum::ShuttleAxum { ... } ``` The `shuttle_runtime::main` make the project work with Shuttle's runtime and the `shuttle_shared_db::Postgres` macro is used to provision a PostgreSQL database in production. **Shuttle Dependencies:** Some Shuttle dependencies are added to the `Cargo.toml` file: ```toml [dependencies] shuttle-runtime = "0.56.0" shuttle-shared-db = { version = "0.56.0", features = ["postgres", "sqlx"] } shuttle-axum = "0.56.0" ``` - `shuttle-runtime` - Shuttle's runtime - `shuttle-shared-db` - To provision a PostgreSQL database in production and Docker for development, [read more about how Shuttle shared db works](https://docs.shuttle.dev/resources/shuttle-shared-db) - `shuttle-axum` - Shuttle Axum dependencies, we'll use this to create the API router **Shuttle.toml:** ```toml [build] assets = ["static/*"] ``` A `Shuttle.toml` is required for Shuttle to know about the static assets that need to be uploaded. ## Deploying to Shuttle Once your project is migrated, the AI agent will attempt to create and deploy a project for you. If it didn't do it for you, you can ask it again to do it. ```text Deploy the project to Shuttle ``` Cursor will call the `deploy` tool to deploy the project to Shuttle and Shuttle will start building your project. After building is done, you can see the project URL in your Shuttle console and visit the website. Opening the project URL in the browser, you can see the website is working. Perfect! 🎉 We've successfully migrated our Axum todo application to Shuttle and deployed it to production. ## Next Steps Shuttle has more features and we're always adding more and improving the platform, you can check out the [Shuttle documentation](https://docs.shuttle.dev) for more information. Clone the todo app example by running the following command and start building your own project: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/todo-app ``` --- # Best AI Coding Tools for Rust Projects: IDEs vs Terminals Source: https://www.shuttle.dev/blog/2025/09/09/ai-coding-tools-rust Date: 9 September 2025 Author: demola Tags: rust, ai, coding-tools, development We tested seven AI coding tools on the same Rust project. See how each performed on speed, accuracy, and terminal vs IDE workflows. As Rust developers, we tend to rely on familiar setups like Visual Studio Code (VS Code) or the terminal because they offer the speed and control we need. AI coding tools extend these workflows but approach them in different ways. Some plug directly into IDEs, while others work in the terminal. If you are building Rust projects, the real question is which of these tools are actually useful in practice. To find out, we tested seven popular AI coding assistants by building the same Rust HTTP server with Axum. We compared how quickly they generated code, the quality of what they produced, and how well they fit into everyday Rust workflows. Because each tool outputs code differently, we also needed a way to deploy their results without switching stacks or starting from scratch. We will walk you through how we solved that as well. The table below summarizes the key differences between IDE-based and terminal-based AI coding tools: | Aspect | IDE-Based Tools | Terminal-Based Tools | | ----------------- | -------------------------------------------------------- | ----------------------------------------------------- | | Integration | Deep editor integration with real-time suggestions | Command-line focused with file system awareness | | Workflow | Seamless coding experience within a familiar environment | Context-switching between the terminal and the editor | | Context Awareness | Full project context with syntax highlighting | File-based context with smart project understanding | | Learning Curve | Minimal - extends existing IDE workflow | Moderate - requires learning CLI commands | | Collaboration | Individual developer focused | Often better for pair programming and code review | | Customization | Limited to IDE extension capabilities | Highly customizable through configuration files | ## AI Coding Tools: IDEs vs. Terminals Choosing an AI coding tool is about features, as well as matching the tool to how you think and work. Some developers thrive with constant AI suggestions appearing in their editor. Others find this distracting and prefer tools that allow them to describe a problem and then generate complete solutions. The divide between IDE-integrated and terminal-based tools reflects these different working styles. ### What Are IDE-based AI Coding Tools? IDE-based AI coding tools integrate directly into graphical Integrated Development Environments (IDEs). They enhance the development experience with intelligent code completion, refactoring, optimization, and debugging. Many also bring real-time suggestions, multi-file editing, project-wide refactoring, and built-in chat panels for natural language queries. With IDE-based tools, you can expect: - Graphical Interface: Work inside a visual IDE with inline code suggestions, multiple views, and interactive chat panels for prompts. - Context-Based Suggestions: Get completions and refactoring ideas based on the IDE's deep understanding of your codebase. - Deep Integration: Leverage the IDE's ecosystem, including project navigation, file management, and version control, all enhanced by AI. - Higher Resource Usage: Keep in mind that graphical interfaces and IDE overhead usually demand more system resources. Some of the most popular IDE-based AI coding tools are: - Cursor - Windsurf (formerly Codeium) - VS Code + GitHub Copilot - Kiro by AWS **Cursor:** Cursor is an AI-first code editor built with AI assistance at its core. It takes the familiar foundation of VS Code and layers in advanced AI capabilities, so you can work faster and smarter without giving up your usual workflow. If you've used VS Code before, you'll instantly recognize the interface and navigation, but you'll also see new tools that make coding feel collaborative. Key features include: - Native AI Chat Interface: Talk to AI right inside your editor. - Multi-File Editing with AI: Edit across multiple files in one request. - Codebase-Wide Understanding: AI understands your whole project. - Custom AI Model Selection: Pick the AI engine that fits your needs. - Real-Time Code Generation: Code gets written as you type. - Smart Autocomplete: Smarter, context-aware autocomplete. **Windsurf (formerly Codeium):** Windsurf, rebranded from Codeium, is an AI-powered IDE forked from VS Code and designed to weave AI assistance throughout the entire code development process. Compared to Cursor, Windsurf leans toward a more polished and minimal aesthetic. You can think of it as Apple-like refinement compared to Microsoft's functionality-first approach. One of Windsurf's standout features is **Cascade**, which enables true agentic programming. With Cascade, the AI can understand your project across multiple files and take coordinated action without requiring you to micromanage every step. Key features include: - Intelligent Code Refactoring: Automatically clean up and restructure code without breaking functionality. - Architecture-Aware Suggestions: Get recommendations that fit your project's overall design. - Cross-Language Project Support: Seamlessly work across multiple programming languages. - Advanced Debugging Assistance: Understand errors and get targeted, multi-file fixes. - Code Quality Insights: Instant feedback on clarity, maintainability, and best practices. - Deep Static Analysis Integration: Catch hidden bugs, security risks, and performance issues early. **VS Code + GitHub Copilot:** GitHub Copilot is one of the most widely used AI coding assistants. It integrates directly into VS Code as well as other popular IDEs and works in real time to suggest code, generate functions from natural language prompts, and explain existing code. Whether you're writing in JavaScript, Python, Go, or working across multiple languages, Copilot blends seamlessly into your environment while tying neatly into the broader GitHub ecosystem. Key features include: - AI-Powered Code Suggestions: Get instant code completions from single lines to full functions. - Natural Language to Code: Describe it in plain English, and let AI write the code. - Chat and Inline Assistance (Copilot Chat): Ask coding questions and get inline help without leaving your editor. - Code Explanation and Documentation: Turn complex code into clear explanations. - Test and Code Refactoring Support: Auto-generate tests and improve code readability safely. - Multi-Language and Framework Support: Works across dozens of languages and frameworks. - Integration with GitHub Ecosystem: Easily connects with repos, PRs, and Actions. **Kiro by AWS:** Kiro is an AI-native IDE built by AWS and launched in 2025. It was designed to solve the challenges of "vibe coding" by introducing structured, spec-driven development. With this approach, Kiro generates a requirements document that includes user stories, mermaid diagrams, acceptance criteria, and more about what you want to build. You can review the spec, tweak it, and customize it to your needs before writing a single line of code. This way, you don't have to keep prompting over and over to get what you're looking for. Key features include: - Spec-Driven Development: Work from a single spec file that acts as your source of truth. AI agents generate, maintain, and evolve your code directly from it. - Customizable Agent Behavior: Fine-tune how agents operate using steering configs in .kiro/steering/. Set rules like avoiding blocking calls, enforcing structured logging, or running tests on every commit to keep quality consistent. - Automation Hooks: Add event-based automations to streamline your workflow. For example, regenerate scaffolding when specs change or trigger end-to-end tests when a pull request is opened. - Multimodal Chat Interface: Collaborate with AI through text, code, and other formats. Get natural help with debugging, explanations, and brainstorming. - Agentic Programming: Let AI agents handle multi-step tasks. They can plan projects, update specs, and keep changes synced across your codebase. - AWS Integration: Connect seamlessly with AWS services for deployment, monitoring, and scaling so your projects move smoothly into production. The key difference between IDE-based AI coding tools is in their approach. Cursor, Windsurf, and Copilot focus on real-time code suggestions and inline assistance for quick iteration. Kiro, on the other hand, takes a different path with agentic, spec-driven workflows that emphasize production readiness and cut down on ad-hoc "vibe coding" chaos. This makes it more agent-led and customizable, giving teams the guardrails and automations they need instead of just editor-centric features. ### What are Terminal-Based AI Coding Tools? Terminal-based AI coding tools run entirely in the command-line interface (CLI), letting you work with AI models through simple commands and prompts. If you prefer a lightweight, text-driven workflow and often rely on Git or other version control systems, these tools will feel right at home. With terminal-based tools, you can expect: - Command Line Interface: Operates fully in the terminal with commands and prompts, often with little or no GUI. - Lightweight and Fast: Uses minimal system resources, making it ideal for low-spec machines or headless environments such as servers. - Direct LLM Access: Connect directly to large language models (LLMs) like Claude, GPT, or Gemini, with support for multiple providers. - Git Integration: Streamline version control workflows by automatically committing changes to Git. - Automation-Focused: Excel at automating repetitive tasks, running tests, and executing commands without leaving the terminal. - Steeper Learning Curve: Requires comfort with command-line workflows and provides less visual feedback compared to IDE-based tools. Popular terminal-based AI coding tools include: - Claude Code - Aider - Gemini CLI - OpenAI Codex CLI **Claude Code:** Claude Code is an AI coding assistant built to run directly in your terminal, designed for developers who want a hands-on but intelligent coding partner. You can delegate complex tasks to it, and it will plan, execute, and explain its work step by step. Whether you're writing new code, testing, debugging, or exploring a large codebase, Claude Code acts as an agentic AI that can navigate files, run multi-step processes, and integrate with your existing tools through natural conversation. Key features include: - Natural Language Task Description: Describe what you want in plain English, and Claude handles it. - File System Awareness: Understands and edits your project's files in context. - Multi-Step Task Execution: Plans and completes tasks across multiple files seamlessly. - Flexible Configuration Management: Set up coding preferences and project rules using dotfile-style configuration with claude.md files. Configure global settings in your home directory, project-specific rules in your project root, or granular settings in subfolders for large projects. This hierarchical approach lets you define coding standards, architectural patterns, and team conventions that Claude will automatically follow. - Integration with Development Tools: Works smoothly with editors, build tools, tests, and version control. - Conversational Debugging: Debug interactively with step-by-step guidance. - Code Explanation and Documentation: Turn complex code into clear explanations and documentation. **Aider:** Aider is an open-source AI pair programming assistant built to work hand-in-hand with Git. Unlike other coding tools that simply suggest snippets, Aider operates inside your version control workflow, making it especially powerful for collaborative and production-grade projects. It uses large language models to understand your codebase, propose intelligent changes, and keep everything neatly tracked in commits. Key features include: - Git-Aware Editing: Understands your Git context and suggests edits easily. - Multi-file Coordinated Changes: Update multiple files at once for consistent refactoring. - Automatic Commit Messages: Generate clear, descriptive commits automatically. - Support for Multiple AI Models: Choose the AI model that fits your project and workflow. - Real-Time Collaboration: Get live, pair-programmer-style coding suggestions. **Gemini CLI:** **Gemini CLI** is Google's lightweight, open-source AI coding agent that brings Gemini models (like Gemini 2.5 Pro) directly into your terminal. With an ascended context window of 1 million tokens, it can analyze entire codebases at once and handle complex workflows with ease. Key features include: - Massive Context Window: Powered by Gemini 2.5 Pro, with support for up to 1 million tokens. This gives the AI a deep understanding of your codebase and enables full project analysis. - Real-time Web Search and Diagrams: Look up information on the web instantly and generate diagrams on the fly to speed up debugging and feature development. - Open-Source Flexibility: Fully open-source under Apache 2. You can customize it, extend it, and contribute back to the community. - Generous Free Tier: Get 60 requests per minute and 1,000 requests per day for free with just a personal Google account. - Security and Sandboxing: Runs in a secure, sandboxed environment with network restrictions, ensuring safe code execution. **OpenAI Codex CLI:** OpenAI Codex CLI is an open-source, lightweight coding agent that brings ChatGPT-level reasoning to your terminal. Powered by OpenAI's o3 and o4-mini models, it emphasizes local execution, privacy, and multimodal input, letting you handle tasks like code generation, file manipulation, and test execution directly from the terminal. Key features include: - Multimodal Input: Supports text, screenshots, and diagrams to generate or edit code, making feature implementation faster and more intuitive. - Local Privacy: Runs entirely on your machine, keeping your source code private unless you choose to share it. - Flexible Approval Modes: Choose from Suggest, Auto-Edit, and Full Auto modes for different levels of control over code changes and command execution. - Open-Source and Community-Driven: Encourages community contributions and supports multiple AI providers like Gemini and OpenRouter via the Chat Completion API. - Sandbox Security: Executes commands in secure, sandboxed environments using Docker on Linux and Apple Seatbelt on macOS to ensure safe operations. With the details of both IDE-based and terminal-based AI coding tools covered, let's now explore why they behave differently when you put them to work on a Rust project. ### What Makes These Categories Behave Differently in Rust? Rust's strict type system, borrow checker, and focus on safety make AI coding tools behave differently depending on how they integrate with your workflow. **IDE-based tools** like Cursor, Windsurf, and GitHub Copilot hook directly into the Rust Language Server Protocol (LSP) through rust-analyzer. This means you get real-time, type inference, inline diagnostics, and borrow checker suggestions. The immediate feedback helps catch ownership issues or lifetime mismatches early, reducing compile cycles and making it easier to write idiomatic Rust code. These tools shine in visual debugging and refactoring, using the IDE's deeper understanding of Rust's semantics to speed up iteration in larger projects. #### Pros and Cons of IDE-Based AI Tools in Rust Projects Below are some of the benefits you get when you use IDE-based AI tools in your Rust projects: - Strong support for multi-file refactoring and debugging, reducing time on ownership issues. - Visual feedback and multi-view interfaces make navigating complex Rust projects easier. - Deep integration with rust-analyzer provides context-aware fixes for the borrow checker and type errors. - Seamless real-time suggestions and autocomplete help maintain flow during Rust's error-prone early stages. While the visuals make it ideal for building and debugging large-scale projects, they still come with some limitations: - Reliance on a GUI makes them less ideal for headless or remote Rust environments, such as servers. - Higher resource consumption, which can slow things down on lower-spec machines during Rust compilations. In contrast to IDE-based tools, **terminal-based tools** such as Claude Code and Aider lean on command-line interactions and external commands like `cargo check` or `cargo build` for validation. Instead of inline suggestions, they generate suggestions on-demand through prompts, which takes an agentic approach of planning and applying multi-file changes or automations in one go. This makes them powerful for strategic code generation or bulk edits, though you'll likely need more manual builds to catch errors. They're lightweight and well-suited for server-side or low-resource development environments, but can feel less intuitive when dealing with Rust's more intricate ownership and lifetime rules without visual aids. #### Pros and Cons of Terminal-Based AI Tools in Rust Projects Below are some of the benefits you get when you use terminal-based AI tools in your Rust projects: - Direct access to LLMs for in-depth explanations of Rust concepts without IDE overhead. - Strong reasoning capabilities for planning Rust architectures or algorithms. - Lightweight and fast, making them great for Rust development on servers or low-power devices. - Well-suited for automation and Git-integrated workflows, including running builds and tests via commands. While they are fast and less resource-intensive, they also come with some drawbacks: - Less immediate feedback, as validation depends on manual runs of cargo tools, which can slow iteration. - Steeper learning curve, since you'll need comfort with the CLI to handle Rust's verbose error messages. ## Hands-On: Building a Rust API with Each Tool Now it's time to put these tools to the test by building a task management API in Rust and seeing how each one handles the challenge. **Project Scope:** Five REST endpoints (`create_task`, `get_task`, `list_tasks`, `update_task`, `delete_task`) **Architecture:** In-memory store (no database, state held in-memory with a `HashMap`) **Toolchain:** Rust 2024 edition, without pinned crate versions (`axum`, `tokio`, `serde`, `uuid`) **Evaluation Method:** Each tool was tested with the same starting prompt. We recorded the generated code, its completeness and accuracy, and its integration into the Rust workflow. ### Building with Cursor We opened a folder `rust-tasks-cursor`in Cursor, launched the **AI chat panel** (Cmd/Ctrl + L) and entered the baseline prompt: ``` Create a new Rust project for a task management API using Axum ``` Cursor created a new Rust project structure that includes a `Cargo.toml` file with the required dependencies: ```toml [package] name = "task_manager_api" version = "0.1.0" edition = "2024" [dependencies] axum = "0.8.4" hyper = "1.6.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" tokio = { version = "1.47.1", features = ["full"] } tower-http = "0.6.6" ``` It also generated a `main.rs` file with the API entry point. ```rust use axum::{Json, Router, routing::get, serve}; use serde_json::json; use std::net::SocketAddr; use tokio::net::TcpListener; async fn health_check() -> Json { Json(json!({ "status": "ok" })) } #[tokio::main] async fn main() { let app = Router::new().route("/health", get(health_check)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("Listening on {}", addr); let listener = TcpListener::bind(addr).await.unwrap(); serve(listener, app).await.unwrap(); } ``` Next, we refined the prompt in the chat panel to flesh out the endpoints: ``` Implement the complete task API with models, handlers, and main server setup ``` Cursor then updated the `main.rs` file with the API models, handlers, and expose the CRUD endpoints. **What worked:** - Multi-file editing kept models, routes, and main in sync. - Inline fixes suggested after compiler errors. - Excellent for scaffolding large chunks of code fast. **What didn't:** - Sometimes hallucinated crate versions. - Needed a re-prompt to get the right crates and correct some logic. **Verdict:** Great for **rapid prototyping and multi-file Rust projects**, especially for developers comfortable with VS Code-style workflows. ### Building with Windsurf We created a `rust-tasks-windsurf` folder in Windsurf and used the same prompt. Windsurf scaffolded a Rust project but took longer due to its **Cascade** checks. The upside is that it proposed project structure refinements (splitting `handlers.rs` and `routes.rs`) and enforced pinned versions in `Cargo.toml`. **What worked:** - Architecture-aware suggestions (modularized better than Cursor). - Helpful explanations for async/await and error handling. - Strong static analysis integration caught subtle mistakes. **What didn't:** - Generation was slower (20-30s per major step). - Occasionally over-engineered (extra traits not needed for in-memory scope). **Verdict:** Great for **planning long-lived Rust projects** where architecture matters. ### Building with VS Code + GitHub Copilot We created a new folder `rust-tasks-copilot` in VS Code and initialized the project by running the command below: ```bash cargo init ``` Copilot helped fill in dependencies, models, and handler boilerplate as we typed. It excelled at incremental completions but required additional guidance to assemble the full API. **What worked:** - Excellent line-by-line and function-level completions. - Copilot Chat explained compiler errors clearly. - Fastest at suggesting snippets. **What didn't:** - Weak at cross-file consistency (needed manual edits to sync model types). - Less effective at scaffolding entire APIs in one shot. **Verdict:** Great for **incremental coding and filling gaps** rather than full project generation. ### Building with Claude Code In Claude, we entered the same prompt in the terminal. Claude generated a project scaffold, listed the steps it would take, then produced `Cargo.toml` and `main.rs`. It explained async concepts and ownership trade-offs in detail while building. **What worked:** - Clear step-by-step reasoning. - Accurate async handling with axum. - Strong at explaining lifetimes and error messages. **What didn't:** - Slower to reach a runnable version (lots of intermediate narration). - Sometimes verbose in generated comments, cluttering code. **Verdict:** Great for **learning Rust while building** (doubles as tutor + generator) **and building large-scale projects**. ### Building with Aider For this, we initialized Git and prompted Aider. It scaffolded a Cargo project, generated dependencies, and committed each step with descriptive messages. Its Git-aware workflow was unique: each file edit was atomic, with matching commit messages. **What worked:** - Excellent Git integration (perfect history for each change). - Multi-file updates stayed consistent. - Easy rollback when things went wrong. **What didn't:** - Required more prompt iteration than Cursor/Windsurf. - Less context than IDE-native tools (sometimes needed manual cargo fixes). **Verdict:** Great for **collaborative, Git-driven workflows** in Rust. ### Building with Gemini CLI In Gemini CLI, we entered the same prompt in the terminal. Gemini CLI walked us through the steps it would take, then scaffolded a Rust project with a Cargo.toml and main.rs. It also highlighted the project dependencies needed to run the project and installed them. **What worked:** - Clear, step-by-step reasoning using the ReAct (reason + act) pattern. - Accurate async handling with `axum`. - Solid project structure with a clean separation of concerns. **What didn't:** - Took longer to get to a runnable version because the ReAct mechanism tended to overthink the flow. - Sometimes over-engineered things by introducing extra traits and impl blocks. **Verdict:** Great for **learning Rust** and refactoring **large-scale projects**. ### Building with OpenAI Codex CLI With Codex CLI, we gave it the same prompt. This time, it laid out the steps with a visual progress bar, updating as it went. It scaffolded the project, and even showed a change history for each file it touched. **What worked:** - Visual, step-by-step reasoning with clear highlights of changes. - Accurate async handling with `axum`. - Clear guidance on next steps for improving the application. **What didn't:** - No separation of concerns. Everything got bundled into `main.rs` (handlers, models, logic, the works). - Sometimes over-engineered with unnecessary packages, like extra tracing libs. **Verdict:** Great for **learning Rust** and **mapping out roadmaps for Rust projects**. If you're weighing up any of the AI coding tools against each other, here's a quick reference to help decide which fits best in different contexts: | Tool | Category | Code Quality | Best For | | ----------- | -------- | ------------ | ----------------------------------------------------- | | Cursor | IDE | 9 | Fast prototyping, full-feature builds | | Windsurf | IDE | 8.5 | Architecture planning, complex logic | | Copilot | IDE | 8 | Incremental completions, patterns | | Claude Code | Terminal | 9 | Learning and explaining Rust and large-scale projects | | Aider | Terminal | 8.5 | Git-aware, team-oriented dev | | Gemini | Terminal | 8 | Complex refactoring and project architecture | | Codex | Terminal | 8.5 | Fast prototyping, mapping out roadmaps | Using AI to generate Rust code can be powerful, but even the best tools have their blind spots. IDEs may overlook context rules, terminal tools can feel slower, and both risk introducing subtle bugs if not monitored. The gap between a quick prototype and a maintainable project often comes down to workflow design, rule enforcement, and consistent testing. In the next section, we'll cover best practices for getting the most out of AI coding tools so your Rust projects stay reliable, predictable, and scalable. ## Best Practices for Getting the Most from AI Coding Tools AI coding tools can feel like magic, but to get **reliable Rust projects**, you need **guardrails**. Start by treating your project like a **product**. Define the kinds of documents a PM would normally create: **project rules, coding standards, API conventions**, and so on. A practical approach is to create `claude.md` files (or equivalent) for each directory, describing the rules explicitly, and load them into the AI's context. This makes it much easier for any AI tool to generate code within your intended scope, especially in larger projects. Next, build **testing discipline** into your workflow. AI is excellent at TDD, but it will not enforce policy on its own. For example: - Run unit tests after every code step. - Run end-to-end tests after major milestones. - Avoid mocks and stubs unless absolutely necessary. These practices help reduce **subtle errors** that AI tools can introduce. Keep in mind that IDE-based tools such as **Cursor, Copilot, and Windsurf** sometimes ignore context-loaded rule files. CLI-based tools combined with dotfiles tend to respect them more consistently. That is why CLI workflows can be especially valuable for large or complex Rust projects. **Security and permissions** are another important consideration. CLI tools usually give you fine-grained control. You can grant permissions per command, or opt in to full access with flags like `--dangerously-skip-permissions` in Claude CLI. IDEs, by contrast, often require broader access and provide fewer safeguards. **Cost** is also a factor. CLI tools typically consume **tokens** directly from your AI subscription, which can add up quickly on big projects. IDEs such as **Cursor or Windsurf** let you switch between models, but the most capable ones often sit behind pay-as-you-go tiers that can exceed $1,000 for heavy usage. Anthropic Max, for example, offers a predictable monthly price with a token reset system. This makes budgeting easier for long-term Rust projects. In short, enforce guardrails, define clear rules, and think like a PM. With that mindset, both IDE and CLI AI environments will work more predictably, generate higher-quality Rust code, and integrate smoothly into your deployment pipeline. ## How We Deployed the Rust Applications Each tool got us to a working Rust API, but in different ways. Some needed a bit of re-prompting, while others were more direct. Instead of setting up separate Dockerfiles, CI pipelines, or custom cloud configs for each tool, we kept things simple with a single deployment workflow powered by Shuttle's [Model Context Protocol](https://docs.shuttle.dev/integrations/mcp-server) (MCP). Here's how you can do the same: 1. Set up Shuttle - Create a [free Shuttle account](https://console.shuttle.dev/signup). - Install the [Shuttle CLI](https://docs.shuttle.dev/getting-started/installation), which lets you deploy straight from your machine. - Configure the CLI by running:  ```bash Shuttle login ``` This will open a browser where you can log in with your credentials. 2. Connect your AI Coding Tool to Shuttle MCP Add the Shuttle MCP server to your tool of choice. For example, in Windsurf: - Go to Settings → Windsurf Settings → Manage MCPs → View raw config. - Paste the server configuration below into the config file: ```json { "mcpServers": { "Shuttle": { "command": "shuttle", "args": ["mcp", "start"] } } } ``` Check [Shuttle's documentation](https://docs.shuttle.dev/integrations/mcp-server) for the full list of MCP configurations. 3. Prompt the Tool to Deploy Now, ask your AI coding tool to deploy the API through the Shuttle MCP server. For example: ``` Deploy the API to Shuttle. Use the Shuttle MCP server. ``` > _As a rule of thumb, always include the phrase _`Use the Shuttle MCP server.`_ when deploying through the Shuttle MCP server so it can reference the documentation during deployment._ 4. Let the Tool Update your Project When you run the prompt, the tool will update your project for deployment on Shuttle. This includes updating `Cargo.toml` to add the required dependencies: ```toml shuttle-axum = "0.56.0" shuttle-runtime = "0.56.0" ``` It will also update the `main` function to use the Shuttle runtime: ```rust #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let app_state = AppState::new(); let app = Router::new() .route("/tasks", get(get_tasks).post(create_task)) .route("/tasks/{id}", get(get_task).put(update_task).delete(delete_task)) .route("/health", get(health_check)) .layer(CorsLayer::permissive()) .with_state(app_state); Ok(app.into()) } ``` 5. View your Deployment Finally, open your [Shuttle console](https://console.shuttle.dev/) to grab your API URL and other deployment artifacts. ### Beyond Deployment with Shuttle Shuttle goes further than just deployment. It provides tools and resources that make day-to-day development smoother and more productive: - **Ready-to-deploy resources** like databases and built-in secrets for managing environment variables. - **Low-effort prototyping and framework support**, so you can spin up projects quickly with Axum, Actix, Rocket, and more. - **Community-driven development and support**, with an active open-source community, Discord, and programs to support your workflow.  - **Fast redeploys and local iteration**, making it easy to test changes without waiting on long build times. - **Infrastructure as Code (IaC)** powered by Rust macros, which means your infrastructure lives directly inside your Rust code instead of separate config files. ## Wrapping Up The AI coding landscape for Rust is more powerful and flexible than ever. Whether you prefer the visual depth of IDE-based tools or the speed and focus of terminal applications, there's a tool that matches your style. One thing that never changes is the importance of Rust's core principles. The best AI tool for coding is the one that helps you write idiomatic, safe, and performant Rust while fitting naturally into your workflow. Whatever tool you choose, Shuttle makes building and deploying your applications effortless. Ready to see it in action? Run this command to get started with a simple Axum web server: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/hello-world ``` --- # The New Shuttle Console Built for Discovery, Speed, and Scale Source: https://www.shuttle.dev/blog/2025/09/01/new-console Date: 1 September 2025 Author: archie Tags: shuttle, console, ui, deployment Shuttle's redesigned console with better feature discovery, streamlined workflows, and production-ready design for managing projects at any scale. ## Introduction - The New Shuttle Console We've just shipped a brand new Shuttle Console. As our user base grew and platform matured we knew it was time for a _major_ upgrade. The goal was clear: make it easier to discover features, smoother to manage projects of any size, and reflective of Shuttle's production-ready quality. This update is about simplicity, ease, and scale. ## Why the Console Matters At Shuttle, our philosophy has always been simple: deploying applications should be straightforward. Infrastructure requirements and code don't need to be separate; your application should be defined, deployed, and scaled without unnecessary complexity. But while your infra requirements live in code, the console is where you see what's running. It's where you check deployments, understand resources, and discover what else Shuttle can do for you. The console is a window into Shuttle's capabilities. The new console is our biggest step yet in making that window clearer, faster, and more representative of what Shuttle is: the simplest place to deploy production applications. ## Why We Changed It - Listening and Learning The old console worked, but it wasn't keeping up with the platform. Features like resources, configurable instance sizes, or collaboration were often hidden away. Workflows weren't always consistent. And for some of our Growth users managing many projects on Shuttle, the console just didn't scale. You told us what you needed: - Better feature discoverability. - More consistent design and workflows. - A console that matched Shuttle's reliability under the hood. This redesign is our response. ## What We Changed - Feature Highlights Here's what you'll notice right away: - **Project Overview Page**: All essential features in one place. Quick actions, including stop and deploy, and a clean structure make it easy to find and use the features you need. - **Deployments Page**: Git commit ID and message. as well as full-screen logs - **Domain Setup**: Clear flow with copy-paste CNAME instructions and validation states. - **Secret Management**: Manage secrets directly from the console. - **Configurable Compute Size**: Copy pre-filled compute tier configuration directly into your project for easy instance size adjustments. - **Design Language**: A standardised, consistent design system across all pages, making every workflow feel predictable and smooth. ## Key Product Decisions - Staying True to Developers One of the biggest questions we asked ourselves was: _what should the console do, and what should stay in code?_ We kept our opinionated approach: the console shows you what's running and guides you toward the right workflows. That's why you'll see things like pre-filled config snippets instead of dropdown menus. The goal isn't to replace code, but to help you discover and apply Shuttle's features in the right way. By making these conscious choices, the console becomes more than a dashboard; it's a guide to using Shuttle the way it's meant to be used. ## What This Means for You - **Faster**: Features are easier to discover, workflows are clearer. - **Scalable**: Whether you're running one project or thirty, the console helps you manage at any level. - **Production-ready**: A design that reflects the maturity of the platform you're running on. - **Empowering**: A console that doesn't just show you state, but teaches you how to get more out of Shuttle. ## Check It Out The new console is live today. Log in to your account (or create one if you're new) and see what's changed. We built this console with you: your feedback, your workflows, your needs. Now we'd love to hear what you think. 👉 [Log in or sign up to explore the new console](https://console.shuttle.dev?utm_source=website&utm_medium=blog&utm_campaign=new_console_1) --- # How to Build and Deploy an SSE MCP Server with OAuth in Rust Source: https://www.shuttle.dev/blog/2025/08/13/sse-mcp-server-with-oauth-in-rust Date: 13 August 2025 Author: dcodes Tags: rust, mcp, sse, oauth, axum, shuttle, sqlx Build an SSE-based MCP server with OAuth 2 in Rust using rmcp and Axum and deploy to Shuttle with PostgreSQL/SQLx. AI agents have become integral to modern development workflows, transforming how we build and maintain software. While these tools are already powerful, they reach their full potential when enhanced with [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) servers that extend their capabilities through specialized tools and integrations. For developers running hosted applications or platforms, MCP servers offer a unique opportunity to provide users with natural language interfaces to your services. Consider a project management platform: instead of navigating through multiple screens, users could authorize an MCP server and then create tasks, update project statuses, or generate reports using simple conversational commands through their preferred AI client. Building secure MCP servers requires adherence to two critical standards: the Model Context Protocol specification and [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13). When implemented correctly, your MCP server becomes universally compatible with any MCP-enabled AI tool—whether users prefer [Cursor](https://cursor.sh/), [Claude Desktop](https://claude.ai/desktop), Windsurf, or other platforms. This tutorial provides a comprehensive guide to OAuth authentication patterns and walks through building a production-ready MCP server. We'll implement secure authentication flows that allow AI agents to safely interact with your hosted services, then deploy the complete solution using [Shuttle's](https://shuttle.dev/) streamlined cloud deployment. ## MCP Transport Types MCP servers use two transport mechanisms: [**STDIO**](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) (standard input/output) and [**SSE**](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#http-with-sse) (Server-Sent Events). The stdio transport runs as a local subprocess and communicates through standard input/output streams, which we covered in detail in our [comprehensive guide to building stdio MCP servers in Rust](https://www.shuttle.dev/blog/2025/07/18/how-to-build-a-stdio-mcp-server-in-rust). SSE transport type servers, on the other hand, use HTTP-based communication with Server-Sent Events for real-time messaging. SSE servers operate as cloud-hosted services, making them accessible from anywhere with proper network connectivity. Users don't need to install anything locally—they can access your MCP server through a simple URL. SSE transport type servers integrate directly with your existing backend infrastructure and support robust authentication mechanisms like OAuth 2, enabling secure access control and user management. This tutorial focuses on building an **SSE MCP server with OAuth**—the cloud-based approach that connects to your backend application and provides authenticated access to your services. ## Understanding OAuth 2 for MCP Servers In order for users to authorize their MCP clients and AI agents to perform actions on their behalf, MCP servers must implement OAuth 2 specifications. In this section, we'll dive deep into the OAuth 2 requirements, what MCP clients expect, and what the flow looks like. ### OAuth Flow The OAuth flow consists of five key phases: 1. **Discovery Phase**: Client discovers authorization server metadata 2. **Registration Phase**: Client registers itself with the authorization server 3. **Authorization Phase**: User consent and authorization code generation 4. **Token Exchange**: MCP client exchanges the authorization code for an access token and refresh token 5. **Authenticated Access**: Using access tokens to connect to the protected MCP SSE endpoints ### OAuth 2 Flow Deep Dive OAuth might seem intimidating at first glance, but it's actually a straightforward flow once you understand the components. Let's dive into the OAuth flow. ### Step 1: Discovery Phase (Metadata Endpoint) For MCP clients to discover your authorization server endpoints, we need to create a well-known route that MCP clients will always query before starting the authentication flow. MCP clients query the `/.well-known/oauth-authorization-server` endpoint to retrieve the **Authorization Server Metadata** (RFC 8414). This JSON document lists your OAuth 2.0 endpoints, grant types, and scopes. Implementing it is mandatory. Without it, MCP clients can't authenticate with your server. The metadata endpoint provides important information to the MCP client, such as: - **Registration endpoint**: The route where MCP clients can register themselves with your authorization server. - **Authorization endpoint**: The MCP client will redirect the user to this route so that the user can manually review the authorization request and approve or reject it. - **Token endpoint**: After the user approves the authorization request, the MCP client will exchange the authorization code (generated by the server in the previous step) for an access token, this token will be used for authenticated requests from the MCP client to the MCP server. ### Step 2: Registration Phase During the initial connection setup, the MCP client first discovers the authorization server endpoints via the metadata endpoint, then makes a request to the **registration endpoint**, providing relevant information about itself. The server saves this information in the database and responds with a **client ID** and **client secret** for the MCP client. ### Step 3: Authorization Phase (User Consent) The first time an MCP server is added to an MCP client, the server cannot be used until the user authenticates with it, here is an example of how the Notion MCP server looks in Cursor: When the user clicks on the **Login** button through their MCP client, the MCP client will redirect the user to the **authorization endpoint** that was provided using the **metadata endpoint**. The user can then authenticate using their existing account and approve the authorization request. After successful authentication, the server redirects the user back to the MCP client with an **authorization code** in the URL. ### Step 4: Token Exchange After the user approves the authorization request, the MCP client immediately uses the **authorization code** to make a request to the **token endpoint**, exchanging the code for an **access token** and **refresh token**. The access token authenticates the MCP client to the MCP server, while the refresh token renews the access token when it expires. The MCP client will now be able to make authenticated requests and ready for use. ### Step 5: Authenticated Access The MCP client with the use of the access token will now be able to connect to the MCP server and the MCP server will have the ability to identify the MCP client and the user that authorized it. ### Refreshing the Access Token The refresh token is used to renew the access token when it expires. The token endpoint must be designed to support both the refresh token grant type and the authorization code grant type, which we'll implement later in the tutorial. ## Building and Deploying an SSE MCP Server in Rust Now that we have a solid understanding of MCP servers and OAuth integration, we'll build a production-ready SSE MCP server that fully complies with both the MCP protocol and OAuth 2 specifications. After building and testing the MCP server, we'll then deploy it to the cloud using **Shuttle** with just a single command. You can find the complete code for this project in the [GitHub repository](https://github.com/shuttle-hq/shuttle-examples/tree/main/mcp/mcp-sse-oauth). ### Prerequisites To follow along, you'll need: - Intermediate Rust familiarity (async/await, Axum, traits) - Basic OAuth concepts (auth codes, tokens, etc.) - Experience with PostgreSQL and SQLx This tutorial focuses on key patterns. For the complete, unabridged code, please refer to the accompanying [repository](https://github.com/shuttle-hq/shuttle-examples/tree/main/mcp/mcp-sse-oauth). ### Using the MCP Inspector The MCP inspector is a tool provided by the MCP team to help you test and debug your MCP server. You need [Node.js](https://nodejs.org/en/download) and npm installed on your machine to run it. Execute the following command to install and run the MCP inspector in your terminal: ```bash npx @modelcontextprotocol/inspector ``` This will automatically open the inspector in your default browser: We'll use the MCP inspector to test our OAuth flow at each stage of the tutorial Click the "Open Auth Settings" button which will open the auth settings page that tests the OAuth steps. First, we'll implement the metadata endpoint. We'll build our server using the official `rmcp` crate and Axum, which are compatible out of the box. Add the `rmcp` crate to your `Cargo.toml` file: ```toml [dependencies] rmcp = { version = "0.5", features = ["server", "transport-sse-server", "auth"] } ``` The feature flags are self-explanatory: we need the `server` flag to build an MCP server (not a client), `transport-sse-server` for SSE transport functionality, and `auth` for OAuth server utilities. In our `main.rs` file, we have **Shuttle** boilerplate code that provisions a [PostgreSQL database](https://docs.shuttle.dev/resources/shuttle-shared-db) in production and implements the Shuttle [Service Trait](https://docs.rs/shuttle-service/0.56.0/shuttle_service/trait.Service.html) to get the socket address and run the MCP server, The `Service` trait ensures that the code will work in both development and production environments. We'll write the rest of the code in the `init.rs` file as the entry point for our backend server which will serve the authentication APIs and the MCP server endpoints. ```rust struct McpSseService { pool: PgPool, secrets: shuttle_runtime::SecretStore, } #[shuttle_runtime::async_trait] impl shuttle_runtime::Service for McpSseService { async fn bind(self, addr: SocketAddr) -> Result<(), shuttle_runtime::Error> { init::init(addr, self.pool, self.secrets).await } } #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres( local_uri = "postgres://postgres:password@localhost:5432/mcp-sse-auth" )] pool: PgPool, #[shuttle_runtime::Secrets] secrets: shuttle_runtime::SecretStore, ) -> Result { Ok(McpSseService { pool, secrets }) } ``` We use the `shuttle_shared_db::Postgres` macro to provision a PostgreSQL database in production and `local_uri` to connect to a local database for development purposes only. The `shuttle_runtime::Secrets` macro is used to access secrets from the `Secrets.toml` file for development as well as deployment, which we'll create in the next step. The `init()` function contains all the `rmcp` boilerplate required to spin up the SSE transport MCP server. You can view the [complete implementation here](https://github.com/shuttle-hq/shuttle-examples/blob/main/mcp/mcp-sse-oauth/src/init.rs). ### Creating the Secrets File Shuttle uses `Secrets.toml` files to store project secrets. We'll create two files: `Secrets.toml` for production and `Secrets.dev.toml` for development in the project root. We'll use the `openssl` command to generate a random JWT secret key. Run the following command to generate a secure key: > Security Note: Never commit your Secrets.toml files to version control. Add them to your .gitignore file to prevent accidental exposure of sensitive information. ```bash openssl rand -base64 32 # Output: sQGnE/aD76G2TAJA6HqJk9shkmYwsmwZ3b+sJlQWBVE= ``` Update your `Secrets.dev.toml` file, e.g. ```toml BASE_URL = "http://localhost:8000" JWT_SECRET = "sQGnE/aD76G2TAJA6HqJk9shkmYwsmwZ3b+sJlQWBVE=" # Replace with your own JWT secret key ``` You can get your production URL by navigating to the [Shuttle Console](https://console.shuttle.dev/) and bootstrap a new project. The URL will be displayed in the console. Generate another random JWT secret key and update your `Secrets.toml` file as well, e.g. ```toml BASE_URL = "https://your-project.shuttle.app" # Add your production URL here JWT_SECRET = "FgaRCPwUd86iRwQsAm9faAky59ghk0c3bhSijz9wbAM=" # Replace with your own JWT secret key ``` ### Running the Development Server After **Shuttle** and **rmcp** boilerplate is set up, we can run the development server: ```bash shuttle run --secrets Secrets.dev.toml ``` For an auto-reload development server, you can use [cargo-watch](https://crates.io/crates/cargo-watch) to run the following command: ```bash cargo watch -x "shuttle run --secrets Secrets.dev.toml" ``` This automatically restarts the server when you make code changes. You'll need to install [cargo-watch](https://crates.io/crates/cargo-watch) first by running `cargo install cargo-watch --locked`. Excellent! Our MCP server is now running on `http://127.0.0.1:8000` and we can test it using the MCP inspector. Update your MCP inspector to use the correct MCP server URL, in our case it's `http://127.0.0.1:8000/mcp/sse`: The reason our MCP server is running on `http://127.0.0.1:8000/mcp/sse` is because how we configured the the rmcp boilerplate code: ```rust let sse_config = SseServerConfig { bind: addr, sse_path: "/mcp/sse".to_string(), post_path: "/mcp/message".to_string(), ct: CancellationToken::new(), sse_keep_alive: Some(Duration::from_secs(15)), }; ``` Using this configuration, we've specified the MCP server to run on `http://127.0.0.1:8000/mcp/sse`. ### Setting Up the Metadata Endpoint Clients can connect to the server but can't authenticate yet. To fix this, we'll start with the discovery phase by implementing the **`/.well-known/oauth-authorization-server`** route. Clients use this endpoint to fetch auth metadata, and since `rmcp` is compatible with Axum, we can easily create this route to return a JSON response. According to the OAuth 2 specification, the metadata is expected to include the following fields: - `issuer`: The base URL of the authorization server. e.g. `https://my-app.shuttle.app`. - `registration_endpoint`: MCP clients can register themselves with the authorization server. - `authorization_endpoint`: MCP clients will redirect the user to this route so that the user can manually review the authorization request and approve or reject it. - `token_endpoint`: MCP clients will use this route to exchange the authorization code for an access token and refresh token, this route will be re-used for refreshing the access token when it expires. - `scopes_supported`: The scopes supported by the authorization server. e.g. `profile`, `email`, `mcp`. - `additional_fields`: Additional fields that can be used to customize the authorization server (according to the RFC 8414). - `jwks_uri`: The URL of the [JSON Web Key Set (JWKS)](https://datatracker.ietf.org/doc/html/rfc7517) endpoint, this is optional and can be omitted if not needed. Let's implement the metadata endpoint: ```rust pub async fn oauth_authorization_server(State(state): State>) -> impl IntoResponse { let base_url = state .secrets .get("BASE_URL") .expect("BASE_URL secret not found"); let mut additional_fields = HashMap::new(); additional_fields.insert( "response_types_supported".into(), Value::Array(vec![Value::String("code".into())]), ); additional_fields.insert( "code_challenge_methods_supported".into(), Value::Array(vec![Value::String("S256".into())]), ); let metadata = AuthorizationMetadata { issuer: Some(base_url.clone()), registration_endpoint: format!("{base_url}/oauth/register"), authorization_endpoint: format!("{base_url}/oauth/authorize"), token_endpoint: format!("{base_url}/oauth/token"), scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]), jwks_uri: None, additional_fields, }; (StatusCode::OK, Json(metadata)).into_response() } ``` We've added the following fields to advertise our support for the Authorization Code grant with PKCE (S256), in compliance with RFC 8414. ```rust let mut additional_fields = HashMap::new(); additional_fields.insert( "response_types_supported".into(), Value::Array(vec![Value::String("code".into())]), ); additional_fields.insert( "code_challenge_methods_supported".into(), Value::Array(vec![Value::String("S256".into())]), ); ``` Let's test it with the MCP inspector: Excellent! The metadata endpoint is working. Now let's implement the registration endpoint. ### Setting Up the Registration Route After using the metadata endpoint, MCP clients register themselves by sending a request to the Register Route. The server saves the client's information to the database and responds with a `client_id` and `client_secret`. First, we need a helper function to generate secure client secrets: ```rust fn generate_client_secret() -> String { use std::fmt::Write; let mut secret = String::new(); for _ in 0..32 { let byte: u8 = rand::random(); write!(&mut secret, "{byte:02x}").unwrap(); } secret } ``` Now the registration handler: ```rust #[derive(Debug, Deserialize)] pub struct ClientRegistrationRequest { pub client_name: Option, pub redirect_uris: Vec, pub scope: Option, } pub async fn client_registration( State(state): State>, Json(request): Json, ) -> impl IntoResponse { // Validate redirect URIs if request.redirect_uris.is_empty() { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "invalid_request", "error_description": "redirect_uris is required and must not be empty" }))).into_response(); } // Generate client credentials let client_id = Uuid::new_v4().to_string(); let client_secret = generate_client_secret(); let client_name = request .client_name .unwrap_or_else(|| "MCP Client".to_string()); let issued_at = chrono::Utc::now().timestamp(); let expires_at = chrono::Utc::now() + chrono::Duration::days(90); // Store client in database let query_result = sqlx::query!( r#" INSERT INTO mcp_clients (client_id, client_secret, client_name, redirect_uris, client_secret_expires_at) VALUES ($1, $2, $3, $4, $5) "#, client_id, client_secret, client_name, &request.redirect_uris, expires_at ) .execute(&state.pool) .await; match query_result { Ok(_) => { let response = ClientRegistrationResponse { client_id: client_id.clone(), client_secret, client_name, redirect_uris: request.redirect_uris, scope: "mcp".to_string(), client_id_issued_at: issued_at, client_secret_expires_at: expires_at.timestamp(), }; (StatusCode::CREATED, Json(response)).into_response() } Err(e) => { error!("Failed to register client: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "server_error", "error_description": "Failed to register client" })), ) .into_response() } } } ``` Let's test our registration endpoint to make sure it's working correctly. The registration endpoint is working. Now let's implement the authorization endpoint. ### Setting Up the Authorization Endpoint The authorization endpoint shows the user a consent screen with the requested scopes. When the user clicks "Allow," the server does three things: - Generates an authorization code. - Saves the code to the database. - Redirects the user back to the client with the code. Finally, the client exchanges this authorization code for an access token and a refresh token. > Note: In a real-world app, you would require users to be logged in before they see the consent screen, the `authorize_get` and `authorized_post` routes must be protected by your applications authentication mechanism i.e. a middleware. > For simplicity in this tutorial, we're skipping that login step. We'll simply use the `client_id` as the user identifier to keep things simple For the frontend, we'll use the template engine [Askama](https://github.com/askama-rs/askama) to render the consent UI. We've already created an HTML template that you can find in the [repository](https://github.com/shuttle-hq/shuttle-examples/tree/main/mcp/mcp-sse-oauth/templates). Using the `askama` crate, we can create a struct for template rendering and then render the template using the `render` method. ```rust #[derive(Template)] #[template(path = "authorize.html")] struct AuthorizeTemplate { client_id: String, client_name: String, redirect_uri: String, scope: String, scopes: Vec, code_challenge: String, code_challenge_method: String, state: String, } ``` We need to use the derive macro `#[derive(Template)]` to register the template and the `#[template(path = "authorize.html")]` attribute to specify the template file path. After that, we can send the rendered template as an HTML response using the `axum::response::Html` type. ```rust pub async fn authorize_get( Query(params): Query, State(state): State>, ) -> impl IntoResponse { // Validate required parameters if params.response_type != "code" { return ( StatusCode::BAD_REQUEST, Html("Unsupported response type".to_string()), ) .into_response(); } // Look up client in database let client_result = sqlx::query!( "SELECT client_name FROM mcp_clients WHERE client_id = $1", params.client_id ) .fetch_optional(&state.pool) .await; let client = match client_result { Ok(Some(client)) => client, Ok(None) => { return (StatusCode::BAD_REQUEST, Html("Invalid client".to_string())).into_response() } Err(e) => { error!("Database error: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Html("Internal server error".to_string()), ) .into_response(); } }; let scope = params.scope.unwrap_or_else(|| "profile email".to_string()); let scopes: Vec = scope.split_whitespace().map(|s| s.to_string()).collect(); let template = AuthorizeTemplate { client_id: params.client_id, client_name: client.client_name, redirect_uri: params.redirect_uri, scope: scope.clone(), scopes, code_challenge: params.code_challenge, code_challenge_method: params.code_challenge_method, state: params.state.unwrap_or_default(), }; match template.render() { Ok(html) => Html(html).into_response(), Err(e) => { error!("Template render error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Html("Template error".to_string()), ) .into_response() } } } ``` Next, let's handle the authorization click. When a user approves the request, our server will: - Generate an **authorization code** and save it to the database. - Redirect the user back to the client using the provided `redirect_uri`. ```rust pub async fn authorize_post( State(state): State>, Form(form): Form, ) -> impl IntoResponse { if form.action == "deny" { let mut redirect_url = format!("{}?error=access_denied", form.redirect_uri); if let Some(state) = form.state { redirect_url.push_str(&format!("&state={state}")); } return Redirect::to(&redirect_url).into_response(); } // Generate authorization code let auth_code = generate_authorization_code(); let expires_at = Utc::now() + Duration::minutes(10); // 10 minute expiration // Store authorization code in database let store_result = sqlx::query!( r#" INSERT INTO authorization_codes (code, client_id, redirect_uri, code_challenge, expires_at) VALUES ($1, $2, $3, $4, $5) "#, auth_code, form.client_id, form.redirect_uri, form.code_challenge, expires_at ) .execute(&state.pool) .await; match store_result { Ok(_) => { let mut redirect_url = format!("{}?code={}", form.redirect_uri, auth_code); if let Some(state) = form.state { redirect_url.push_str(&format!("&state={state}")); } Redirect::to(&redirect_url).into_response() } Err(e) => { error!("Failed to store authorization code: {}", e); let mut redirect_url = format!("{}?error=server_error", form.redirect_uri); if let Some(state) = form.state { redirect_url.push_str(&format!("&state={state}")); } Redirect::to(&redirect_url).into_response() } } } ``` Let's test our authorization endpoint with the MCP inspector: The inspector displays the backend authorization URL we just created, which you can open in a browser to see the consent UI. Click "Authorize" The inspector now **displays** the **authorization code** from the backend, which we'll use in the next step. All steps done for this phase, let's move on to the next step which is the token endpoint. ### Implementing Token Exchange So far, the user has authorized the client and been redirected back with an authorization code. Next, the client must exchange that authorization code for an actual access token. It does this by making a POST request to our token endpoint. To be fully compliant with OAuth 2.0, this single endpoint needs to handle two different grant types: - Exchanging the initial authorization code for tokens. - Exchanging a refresh token for a new access token later on. To handle this, we'll build two main functions: - `handle_authorization_code_grant`: This function will validate the incoming authorization code and PKCE verifier from the database. If they are valid, it creates the first access token and refresh token. - `handle_refresh_token_grant`: This function validates an existing refresh token. If it's valid, it issues a new access token and implements token rotation. This is a security best practice where a new refresh token is also issued, invalidating the old one. ```rust pub async fn token_post( State(state): State>, Form(request): Form, ) -> impl IntoResponse { match request.grant_type.as_str() { "authorization_code" => handle_authorization_code_grant(state, request) .await .into_response(), "refresh_token" => handle_refresh_token_grant(state, request) .await .into_response(), _ => { let error = ErrorResponse { error: "unsupported_grant_type".to_string(), error_description: Some( "Only authorization_code and refresh_token grant types are supported" .to_string(), ), }; (StatusCode::BAD_REQUEST, Json(error)).into_response() } } } ``` See the [full implementation here](https://github.com/shuttle-hq/shuttle-examples/blob/main/mcp/mcp-sse-oauth/src/auth/token.rs). Let's test our token endpoint with the MCP inspector: Perfect! All steps done for the OAuth 2.0 authentication, and we're ready to move on to the next step which is creating a middleware to protect the MCP endpoint. ### Implementing JWT Authentication Middleware To secure our MCP service, we need to validate the JWT on every request made by a client. We'll use a middleware to handle this, ensuring only authenticated clients can access our MCP tools: ### JWT Claims Structure The JWT we generated earlier contains the `client_id`. By decoding this token on every incoming request, we can extract the `client_id` to identify which client is making the call. First, we'll define a struct that mirrors the **JWT claims** we generated at the token endpoint: ```rust #[derive(Debug, Serialize, Deserialize)] pub struct Claims { pub sub: String, // client_id pub iat: i64, // issued at pub exp: i64, // expires at pub scope: String, // granted scopes } ``` ### Token Validation Middleware The middleware extracts and validates JWT tokens from the Authorization header: ```rust pub async fn validate_token_middleware( State(state): State>, mut request: Request, next: Next, ) -> Response { // Extract the access token from the Authorization header let auth_header = request.headers().get("Authorization"); let token = match auth_header { Some(header) => { let header_str = match header.to_str() { Ok(s) => s, Err(_) => { error!("Invalid Authorization header encoding"); return StatusCode::UNAUTHORIZED.into_response(); } }; if let Some(stripped) = header_str.strip_prefix("Bearer ") { stripped.to_string() } else { error!("Authorization header missing Bearer prefix"); return StatusCode::UNAUTHORIZED.into_response(); } } None => { error!("Missing Authorization header"); return StatusCode::UNAUTHORIZED.into_response(); } }; // Get JWT secret from configuration let jwt_secret = state .secrets .get("JWT_SECRET") .expect("JWT_SECRET secret not found"); // Validate JWT token let key = DecodingKey::from_secret(jwt_secret.as_bytes()); let validation = Validation::default(); match decode::(&token, &key, &validation) { Ok(token_data) => { // Check if token is expired (JWT validation already handles this, but being explicit) let now = chrono::Utc::now().timestamp(); if token_data.claims.exp < now { error!("Token has expired"); return StatusCode::UNAUTHORIZED.into_response(); } // Verify the client still exists in database let client_exists = sqlx::query!( "SELECT client_id FROM mcp_clients WHERE client_id = $1", token_data.claims.sub ) .fetch_optional(&state.pool) .await; match client_exists { Ok(Some(_)) => { // Add client_id to request extensions for downstream handlers request.extensions_mut().insert(token_data.claims.sub); next.run(request).await } Ok(None) => { error!("Client no longer exists: {}", token_data.claims.sub); StatusCode::UNAUTHORIZED.into_response() } Err(e) => { error!("Database error validating client: {}", e); StatusCode::INTERNAL_SERVER_ERROR.into_response() } } } Err(e) => { error!("Token validation failed: {}", e); StatusCode::UNAUTHORIZED.into_response() } } } ``` In the middleware, we extract the Bearer token and verify it's valid and not expired. Then we extract the `client_id` from it and verify the client exists in the database. ```rust request.extensions_mut().insert(token_data.claims.sub); ``` The above code snippet is a crucial part of the middleware, we add the `client_id` to the [request extensions](https://docs.rs/axum/latest/axum/middleware/index.html#passing-state-from-middleware-to-handlers) so it can be used by the next handlers and MCP tools. ### Defining the service struct We will define our entire service within a central `struct`. This approach allows us to implement the necessary `rmcp` traits and macros, which will contain all of our MCP logic like [tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools), [resources](https://modelcontextprotocol.io/specification/2025-06-18/server/resources), [prompts](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts), etc. ```rust use tokio::sync::Mutex; use std::sync::Arc; #[derive(Clone)] pub struct TodoService { db_pool: Arc, tool_router: ToolRouter, client_id: Arc>>, } ``` The `client_id` here is key because it will be used to identify the client that made the request in every tool call. ### Server Handler Implementation Next, we'll implement the `ServerHandler` trait. This trait handles core protocol logic and ensures our server is compliant without having to manage the low-level details. It has many methods, but most of them are optional. To get our server running, we only need to implement the following: - `initialize`: Handles the initial setup when a client connects. - `get_info`: Provides essential metadata about our service. We'll ignore the other optional methods like `ping`, `list_prompts`, and `list_resources` for the time being. ### Implementing `get_info` The `get_info` method is used to provide metadata about our MCP service, MCP clients will fetch this metadata and the AI model will understand what this MCP server is used for. ```rust #[tool_handler] impl ServerHandler for TodoService { fn get_info(&self) -> ServerInfo { ServerInfo { protocol_version: ProtocolVersion::V_2024_11_05, capabilities: ServerCapabilities::builder() .enable_prompts() .enable_resources() .enable_tools() .build(), server_info: Implementation::from_build_env(), instructions: Some("This server provides todo management tools. You can create, read, update, and delete todos. Each todo has an id, title, and completion status.".to_string()), } } } ``` ### Implementing `initialize` method The `initialize` method executes on initial MCP client-server connection. Running post-JWT middleware, it extracts the `client_id` from middleware extensions and caches it for subsequent operations. ```rust #[tool_handler] impl ServerHandler for TodoService { ... async fn initialize( &self, _request: InitializeRequestParam, context: RequestContext, ) -> Result { if let Some(http_request_part) = context.extensions.get::() { if let Some(client_id) = http_request_part.extensions.get::() { let mut writer = self.client_id.lock().await; *writer = Some(client_id.clone()); } else { tracing::warn!("No client_id found in HTTP request extensions"); } } Ok(self.get_info()) } } ``` Authentication for persistent SSE connections works differently than for standard HTTP requests. We validate the JWT only once when the connection is first established via the `initialize` method. All subsequent messages on that same connection are then considered authenticated The `initialize` method's signature takes `&self` (an immutable reference), not `&mut self`. This presents a challenge: we're not allowed to directly change our service's state (like setting the `client_id`) from within the method. To work around this restriction, we use a pattern called interior mutability. We wrap our `client_id` in two special types that allow for safe modification even from an immutable context: - `Arc`: Allows the data to be safely owned and shared across multiple asynchronous tasks. - `tokio::sync::Mutex`: Acts as a lock that ensures only one task can access and change the data at a time. This combination lets us safely mutate the value from within a method that only has a `&self` ### Building the MCP Todo Service Let's do a quick recap of how the connection is being handled so far: - The MCP client requests OAuth metadata. - User clicks the login button presented by the MCP client. - User is redirected to the authorization endpoint. - User **Authorizes** the MCP client. - Server redirects the user back to the MCP client with the authorization code. - MCP client exchanges the authorization code for the access token and refresh token. - MCP client establishes a persistent connection to the MCP endpoint (i.e `/mcp/sse`) using the access token as a Bearer token in the `Authorization` header. - The JWT middleware validates the access token and extract the `client_id` from it. - The `TodoService` has now access to the `client_id` and can use it to perform actions on behalf of the user. ### Implementing the MCP tools MCP tools work like regular functions: they have names, input parameters, and outputs. When AI agents call these tools, they pass the required parameters and receive results back through the JSON-RPC 2.0 protocol. The `rmcp` crate simplifies schema generation by using `schemars` to create JSON-RPC 2.0 compatible schemas automatically. With the derive macro, we can implement the `JsonSchema` trait and generate tool schemas without manual work. For our todo creation tool, we need just a `title` field and an optional `completed` field. The tool returns a success message confirming the operation. ```rust #[derive(Debug, Serialize, Deserialize, rmcp::schemars::JsonSchema)] pub struct CreateTodoInput { pub title: String, pub completed: Option, } ``` ### Implementing MCP Tools The `rmcp` library provides powerful macros to automatically generate MCP tools. The `#[tool_router]` macro creates a router for all our tools, while `#[tool]` generates individual tool handlers: ```rust #[tool_router] impl TodoService { pub fn new(db_pool: Arc) -> Self { Self { db_pool, tool_router: Self::tool_router(), client_id: Arc::new(tokio::sync::Mutex::new(None)), } } #[tool(description = "Create a new todo item")] async fn create_todo( &self, Parameters(input): Parameters, ) -> Result { // Extract client_id for user-specific data operations let client_id = { let reader = self.client_id.lock().await; reader.clone().ok_or_else(|| { McpError::internal_error("Client not authenticated".to_string(), None) })? }; let request = CreateTodoRequest { title: input.title, completed: input.completed, }; // Pass client_id to database operations for user isolation match db::create_todo(&self.db_pool, request, &client_id).await { Ok(todo) => { let todo_json = serde_json::to_string_pretty(&todo).map_err(|e| { McpError::internal_error(format!("Serialization error: {e}"), None) })?; Ok(CallToolResult::success(vec![Content::text(format!( "Todo created successfully:\\n{todo_json}" ))])) } Err(e) => Err(McpError::internal_error( format!("Failed to create todo: {e}"), None, )), } } // Other tools: get_todo, list_todos, update_todo, delete_todo... } ``` The database query for creating a todo item: ```rust pub async fn create_todo( pool: &PgPool, request: CreateTodoRequest, client_id: &str, ) -> Result { let row = sqlx::query!( r#" INSERT INTO todos (title, completed, client_id) VALUES ($1, COALESCE($2, FALSE), $3) RETURNING id, title, completed "#, request.title, request.completed, client_id ) .fetch_one(pool) .await?; Ok(Todo { id: row.id, title: row.title, completed: row.completed, }) } ``` ## Integrating the SSE Server So far, we've defined our `MCP` service `struct`, implemented the `ServerHandler` trait, and used the `#[tool_router]` macro to register our tools. Now, we need to integrate this service into an SSE server so it can be accessed from a URL. ```rust pub async fn init( addr: SocketAddr, pool: PgPool, secrets: shuttle_runtime::SecretStore, ) -> Result<(), shuttle_runtime::Error> { // ---- Other boilerplate code ---- // Create SSE server configuration for MCP let sse_config = SseServerConfig { bind: addr, sse_path: "/mcp/sse".to_string(), post_path: "/mcp/message".to_string(), ct: CancellationToken::new(), sse_keep_alive: Some(Duration::from_secs(15)), }; // Create SSE server let (sse_server, sse_router) = SseServer::new(sse_config); // Create protected SSE routes (require authorization) let protected_sse_router = sse_router.layer(middleware::from_fn_with_state( app_state.clone(), // Applying the middleware for authentication validate_token_middleware, )); // Create HTTP router with auth routes (non-protected) and protected SSE router let app = Router::new() .merge(auth_router) .with_state(app_state.clone()) .merge(protected_sse_router) .layer(cors_layer); // Add the `TodoService` we created to the SSE server sse_server.with_service(move || TodoService::new(Arc::new(app_state.pool.clone()))); // ---- Other boilerplate code to start the server ---- Ok(()) } ``` The middleware is applied to make sure only authenticated clients can access the MCP route: ```rust let protected_sse_router = sse_router.layer(middleware::from_fn_with_state( app_state.clone(), // Applying the middleware for authentication validate_token_middleware, )); ``` We also used Axum's `with_service` method to attach our `TodoService` to the route. This makes our service available to any client that connects to the SSE server. ```rust sse_server.with_service(move || TodoService::new(Arc::new(app_state.pool.clone()))); ``` ## Deployment and Testing Deploy to Shuttle with a single command: ```bash shuttle deploy ``` ### Adding to MCP Clients Follow the instructions below to connect your specific MCP client to the server: **Cursor Configuration:** ```json { "mcpServers": { "Todo List": { "url": "https://your-server.shuttle.app/mcp/sse" } } } ``` In Cursor, navigate to the Tools page within the Settings menu. Then click the **"Login"** button to authenticate with the MCP server. This will redirect you to the authorization page that we created. After authorizing the client, you'll be redirected to Cursor, which will now have access to the MCP tools on your account. Perfect! 🎉 Our MCP server is now hosted and ready to use by Cursor. The same process applies to any other MCP client, such as Claude Code or Windsurf. ## Conclusion We've successfully built a production-ready MCP server with the SSE transport type that combines the power of Server-Sent Events with robust OAuth 2 authentication. This project demonstrates how to create secure, real-time AI tool access that goes far beyond a simple local setup. Our implementation delivers a complete OAuth 2 flow to secure all client interactions. The SSE-based protocol enables instant tool communication, while the authorization layer ensures that actions are performed securely on behalf of specific users. The entire system, backed by **PostgreSQL**, deploys seamlessly to **Shuttle**, providing a solid foundation for building any real-world MCP service. Pushing future updates is as simple as running `shuttle deploy`, and you can easily integrate this command into a [CI/CD pipeline](https://docs.shuttle.dev/integrations/ci-cd) to fully automate the process. The combination of Rust's performance, Shuttle's deployment simplicity, and MCP's standardized protocol creates a compelling stack for modern AI tooling infrastructure that scales from prototype to production without compromise. ## Try it Yourself Ready to build your own SSE MCP server? Run the following command to clone the complete project and deploy with one command: ```bash # Clone the project shuttle init --from shuttle-hq/shuttle-examples --subfolder mcp/mcp-sse-oauth # Navigate to the project directory cd mcp-sse-oauth # Deploy the project shuttle deploy ``` Read the [Shuttle documentation](https://docs.shuttle.dev) for more information. Happy coding! --- # How to Build a stdio MCP Server in Rust Source: https://www.shuttle.dev/blog/2025/07/18/how-to-build-a-stdio-mcp-server-in-rust Date: 21 July 2025 Author: dcodes Tags: rust, mcp, ai, dns, stdio, server Learn how to build MCP server in Rust using the rmcp crate. This MCP server development guide covers stdio MCP server creation, DNS lookup MCP implementation, and AI agent extension with Model Context Protocol Rust SDK. ## Introduction AI agents are transforming how we work, but they're often limited by their training data. [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) servers bridge this gap by giving AI agents access to real-time data, external APIs, and custom tools. Building your own MCP server lets you extend AI capabilities with your specific tools and services. In this guide, we'll create a DNS lookup MCP server in **Rust** that demonstrates these concepts in action. Our MCP server will perform DNS lookups and provide real-time internet data to AI agents. Since AI models can't directly access external data sources, the server acts as a bridge, delivering live DNS information that extends the agent's capabilities beyond its training data. ## What is MCP? **MCP** stands for **Model Context Protocol**, it's a protocol that allows AI models to securely access external tools and data sources. This is extremely useful, because the AI agents will not be limited to the data they were trained on, in fact they'll be able to access external data and resources plus the ability to use tools and make API calls to external services. Once you build an MCP server, it can be used by any MCP client, like **Cursor** and **Claude Code**, or any other MCP client that supports the MCP protocol like chatbots and conversational agents. The protocol is a standard, so you can use the same MCP server with different clients. ### MCP Transport Types MCP servers use two transport mechanisms: **stdio** (standard input/output) and **SSE** (Server-Sent Events). The stdio transport communicates through standard input/output, similar to [LSP (Language Server Protocol)](https://microsoft.github.io/language-server-protocol/) servers, making it fast and efficient for local use. These servers are installed locally and have full access to your machine, for example they can interact with the file system, execute shell commands, access databases, or interact with any local services. SSE servers run externally in the cloud and connect via WebSockets. While they offer more scalability, they require network setup and don't have access to your local machine's resources. This tutorial focuses on building a **stdio MCP server** - the simpler approach that's perfect for local development and personal AI workflows. ## Building stdio MCP Server in Rust In this tutorial, we're going to build a simple MCP server using the stdio transport that allows AI agents to perform DNS lookups. We'll use the **HackerTarget** API (a public service that provides DNS lookup capabilities) to perform the DNS lookups and return the results back to the AI agent. The workflow is as follows: 1. AI agent asks the MCP server to perform a DNS lookup with a parameter: `domain` 2. MCP server will use the **HackerTarget** API to perform the DNS lookup 3. MCP server will return the results back to the AI agent 4. The AI agent will read the real-time results, providing important context for the next steps ### Setting Up the Project Now that we have laid out the plan and workflow, let's start building the **MCP server**. First, we need to create a new Rust project. ```bash cargo new dns-lookup-mcp-server cd dns-lookup-mcp-server ``` Let's install the required dependencies for the project, the [Model Context Protocol](https://modelcontextprotocol.io/) provides an official SDK for **Rust** called `rmcp`. You can find the Rust SDK for MCP here: [github.com/modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk). Add the following dependencies to your `Cargo.toml`: ```toml [package] name = "dns-lookup-mcp-server" version = "0.1.0" edition = "2021" [dependencies] tokio = { version = "1", features = ["full"] } rmcp = { version = "0.3", features = ["server", "transport-io"] } serde = { version = "1", features = ["derive"] } reqwest = "0.12" anyhow = "1.0" schemars = "1.0" ``` - `serde` crate to serialize and deserialize JSON-RPC (JSON Remote Procedure Call) data for MCP protocol - `tokio` for async operations - `reqwest` to make HTTP requests to the DNS lookup API (HackerTarget) - `schemars` for JSON schema generation - `anyhow` for error handling The `rmcp` crate provides the MCP server implementation. ## Building the DNS Service Let's create the DNS service module. First, create a new file `src/dns_mcp.rs` and add the following code: ```rust use rmcp::{ handler::server::{router::tool::ToolRouter, tool::Parameters}, model::{ErrorData as McpError, *}, schemars, tool, tool_handler, tool_router, ServerHandler, }; use serde::Deserialize; use std::{borrow::Cow, future::Future}; #[derive(Debug, Clone)] pub struct DnsService { tool_router: ToolRouter, } #[derive(Debug, Deserialize, schemars::JsonSchema)] pub struct DnsLookupRequest { #[schemars(description = "The domain name to lookup")] pub domain: String, } #[tool_router] impl DnsService { pub fn new() -> Self { Self { tool_router: Self::tool_router(), } } #[tool(description = "Perform DNS lookup for a domain name")] async fn dns_lookup( &self, Parameters(request): Parameters, ) -> Result { let response = reqwest::get(format!( "https://api.hackertarget.com/dnslookup/?q={}", request.domain )) .await .map_err(|e| McpError { code: ErrorCode(-32603), message: Cow::from(format!("Request failed: {}", e)), data: None, })?; let text = response.text().await.map_err(|e| McpError { code: ErrorCode(-32603), message: Cow::from(format!("Failed to read response: {}", e)), data: None, })?; Ok(CallToolResult::success(vec![Content::text(text)])) } } ``` The code uses several key attributes that handle MCP protocol integration: - `#[tool_router]` wires up the MCP protocol for any methods marked with `#[tool]`. - `#[tool(description = "...")]` exposes the function to AI models with a description for tool selection. - `Parameters` provides type-safe parameter extraction. - `#[schemars(description = "...")]` adds schema documentation for AI parameter generation. ## Server Configuration To configure your MCP server, implement `ServerHandler` for metadata and apply `#[tool_handler]` to auto-generate tool discovery. Add this to the end of `src/dns_mcp.rs`: ```rust #[tool_handler] impl ServerHandler for DnsService { fn get_info(&self) -> ServerInfo { ServerInfo { protocol_version: ProtocolVersion::V_2024_11_05, capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: Implementation::from_build_env(), instructions: Some("A DNS lookup service that queries domain information using the HackerTarget API. Use the dns_lookup tool to perform DNS lookups for any domain name.".to_string()), } } } ``` ## Running the Server Then, set up `stdio` communication in `src/main.rs`: ```rust use anyhow::Result; use dns_mcp::DnsService; use rmcp::{transport::stdio, ServiceExt}; mod dns_mcp; #[tokio::main] async fn main() -> Result<()> { // Create an instance of our DNS service let service = DnsService::new().serve(stdio()).await?; service.waiting().await?; Ok(()) } ``` ## Testing During Development To speed up your development and testing, you can run the MCP server directly from your project without building a release binary. In Cursor, enable this by creating a `.cursor/mcp.json` file in your project's root directory: ```json { "mcpServers": { "DNS Lookup (Dev)": { "command": "cargo", "args": ["run"] } } } ``` > Note that configuration may vary depending on your MCP client. Check your client's documentation for the specific configuration format. ## Building for Production To build and install the MCP server for production use: ```bash # Build and install the binary to ~/.cargo/bin cargo install --path . ``` This installs the `dns-lookup-mcp-server` binary to your Cargo bin directory (typically `~/.cargo/bin`), making it available system-wide if this directory is in your PATH. ## Using in Cursor To configure the MCP server globally for Cursor, edit the main settings file located at `~/.cursor/mcp.json`: ```json { "mcpServers": { "DNS Lookup": { "command": "dns-lookup-mcp-server" } } } ``` This uses the binary name directly, assuming you've installed it with `cargo install --path .` and `~/.cargo/bin` is in your PATH. ## MCP Server in Action Time to put the MCP server to work. Let's have it check the DNS records for three domains and create a table of the results. In the above screenshot, we asked the AI agent to query the DNS records for three domains and create a markdown table to display the results. The AI agent was able to use the `dns_lookup` tool to perform the DNS lookup and return the results back to the user. ## Conclusion While AI can significantly improve your development workflow, LLMs are naturally limited by their training data, knowledge cutoffs, and sandbox environment. MCP servers bridge this gap by giving AI agents access to real-time data and tools they can use when needed. This feature is essential for data that changes too quickly for static training sets. For instance, to configure an application's networking, your AI agent needs direct access to the very latest DNS records, not outdated information. The same logic applies to other live sources, like querying a real-time database or checking an API's current status. Rust shines as a great language for building MCP servers. Its type safety and performance characteristics make it ideal for creating reliable tools that AI models can use confidently. The `rmcp` crate's clean API combined with macros like `#[tool]` eliminates boilerplate, letting you focus on functionality rather than protocol details. In just a few dozen lines of code, we built a production-ready DNS lookup service that works with any MCP-compatible AI client. The standardized MCP protocol means your Rust server can integrate seamlessly across different AI platforms and tools. This is just the beginning - you can extend this pattern to build MCP servers for databases, APIs, file systems, or any external service your AI agents might need to interact with. ## Deploy your MCP server to the cloud Ready to extend AI capabilities beyond their training data? Our **SSE MCP server template** lets you build production-ready MCP servers with your own custom logic that seamlessly integrate with any AI client: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder mcp/mcp-sse ``` --- # Backend Challenge: Learn Rust Microservices for the Cloud Source: https://www.shuttle.dev/blog/2025/07/08/master-rust-microservices-cloud-shellcon-challenge Date: 8 July 2025 Author: shuttle Tags: rust, ai, mcp, cursor, shuttle Build, optimise, and deploy production-ready Rust microservices on Shuttle Cloud in the free ShellCon challenge. Learn async, SQL, memory tuning and more while shipping a live, React-powered dashboard. ## Master Rust and Cloud Development: Build Production-Ready Microservices with ShellCon Looking to level up your Rust skills while learning modern cloud deployment? ShellCon offers a unique hands-on learning experience that combines Rust microservices development with Shuttle Cloud deployment. This comprehensive full-stack project will take you from local development to cloud deployment while solving real-world performance optimization problems. ### What is ShellCon? ShellCon is an innovative learning approach that transforms Rust and cloud development education into a practical, project-based experience. Instead of working through abstract tutorials, you'll build and optimize a complete microservices architecture with three interconnected Rust services and a React dashboard. #### Development Workflow **Phase 1: Local Development** - Set up and run all three services locally, then verify the React dashboard connects properly to each service endpoint. **Phase 2: Performance Optimization** - Solve four distinct performance challenges across the microservices, each focusing on different aspects of Rust optimization. **Phase 3: Cloud Deployment** - Deploy your optimized services to Shuttle Cloud and validate everything works in a production environment. ### What You'll Build You'll construct a complete microservices ecosystem consisting of three specialized Rust services working together through REST APIs. Your development journey involves three critical phases: #### The Microservices Architecture **aqua-monitor**: An environmental monitoring service that handles sensor data collection, tank readings, and system status endpoints. You'll work with async I/O patterns and HTTP client optimization. **species-hub**: A database service managing species information, feeding schedules, and related data. This service teaches SQL optimization and efficient database interaction patterns. **aqua-brain**: An analytics engine that processes data from the other services to generate insights and analysis. You'll focus on memory optimization and efficient string handling. **React Dashboard**: A modern frontend that connects to all three services through REST APIs, providing real-time monitoring and control capabilities. ### What You'll Learn ShellCon offers a comprehensive learning experience that combines project-based learning with real-world architecture. You'll build a complete application with production-like microservices while getting immediate feedback on your optimizations. Through this hands-on approach, you'll learn: - **Rust Development:** Async/await patterns, non-blocking I/O, SQLx with PostgreSQL, heap allocation optimization, and Axum connection pooling - **Cloud Infrastructure:** Shuttle Cloud deployment, resilient microservices creation, Docker containers, and multi-stage deployment configuration - **Frontend Integration:** Connecting React to Rust microservices, designing efficient RESTful endpoints, and implementing cross-service health checks ### Challenge Structure Each challenge focuses on a specific aspect of Rust performance optimization: #### Four Key Optimization Challenges **1. Async I/O (aqua-monitor)**: Fix blocking operations in async code to improve tank readings performance. **2. SQL Queries (species-hub)**: Optimize database queries to enhance species database efficiency. **3. Memory Management (aqua-brain)**: Minimize heap allocations in the analytics engine using Rust's ownership system. **4. HTTP Client Pooling (aqua-monitor)**: Implement connection reuse patterns to boost network performance. ## Getting Started is Simple Ready to begin your Rust and cloud development journey? The setup process is straightforward: 1. **Clone the repository** and install prerequisites (Rust, Docker, Node.js, Shuttle CLI) 2. **Launch the three microservices** locally using the provided scripts 3. **Start the React dashboard** and verify all connections work 4. **Begin solving the four optimization challenges** at your own pace 5. **Deploy to Shuttle Cloud** and validate your solutions in production ### Start Your Rust Development Journey Today The demand for Rust developers continues to grow as more companies adopt Rust for performance-critical applications. ShellCon provides the perfect combination of hands-on learning and real-world application that will prepare you for professional Rust development. Don't spend months reading documentation without building anything meaningful. **Ready to build production-ready microservices and master Rust optimization?** [Clone the repository](https://github.com/shuttle-hq/shuttle-shellcon.git) and start building: ```bash git clone https://github.com/shuttle-hq/shuttle-shellcon.git cd shuttle-shellcon ``` Your journey from Rust beginner to confident microservices developer starts with a single git clone. Start building today and master the skills that will define your backend development career! --- _Join developers worldwide who are mastering Rust and cloud development through ShellCon's practical, project-based approach. Start building today and develop the microservices skills that will advance your backend development career._ --- # AI Assisted Rust Development Environment Setup: Build Rust APIs in Minutes Source: https://www.shuttle.dev/blog/2025/07/03/rust-ai-workflow-guide Date: 3 July 2025 Author: dcodes Tags: rust, ai, mcp, cursor, shuttle Learn how to build a complete Rust API with AI in 5 minutes using Cursor, MCP servers, and Shuttle. Shipping Rust code used to mean wrestling with the borrow-checker, poring over docs, and SSH-ing into servers at 2 a.m. In 2025 that workflow feels prehistoric. With AI-first IDEs such as Cursor, tool-calling standards like [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction), and one-command cloud hosts such as [Shuttle](https://www.shuttle.dev), you can spin up, test, and deploy a secure Rust API in 5 minutes before your coffee cools. This guide shows you how to assemble an AI-assisted development environment tailored for Rust: code completion that understands your entire repo, agents that run migrations and push containers, and MCP plugins that read the latest docs instead of hallucinated snippets. We'll finish by launching a task-management API live to the cloud: hands-off coding, testing, and shipping in under five minutes using a true 10x developer workflow. While this sounds cool, AI-assisted Rust development doesn't replace human developers. Treat it like a power tool: it boosts speed and efficiency, yet our craft still rests on understanding core programming concepts. ## What IDE Should You Use? [Cursor](http://cursor.com/) is one of the most popular AI-powered code editors. Forked from VS Code, it keeps everything you love about VS Code while layering in AI-first features. With Cursor, you tap into today's leading large-language models, and the built-in AI assistant writes code with full, context-aware knowledge of your entire codebase. Features like **Codebase Indexing** can help the AI agents to search through your codebase with ease, and **Cursor Tab** will help you write code much faster and more efficiently by recommending code completions and edits that you can accept with just a tab press. You can think of it as an upgraded successor to what GitHub Copilot once was for AI-assisted development. ## Setting Up Your AI-Enhanced Development Environment In this section, we'll explore how to build an AI-powered Rust development environment that handles everything from coding to cloud deployment. While we'll use Cursor as our main IDE, most techniques work with any AI coding tool, so don't worry if you're using different tools, be it **Windsurf**, **Claude Code**, or even a web based AI chatbot for rust api development. We'll set up [MCP (Model Context Protocol) servers](https://modelcontextprotocol.io/introduction) that let AI assistants interact with external systems, explore Cursor's advanced features like codebase indexing and tab completion, and cover universal strategies for effective AI-assisted development. By the end, you'll have an environment where AI can autonomously write, test, and deploy your Rust applications with automated deployment rust workflows. **(YES! Even deployments!)** ### MCP Servers: Extending AI Capabilities #### What Are MCP Servers? LLMs excel at generating text, but their fixed knowledge cut-off means they often lack your project's freshest context. To bridge that gap, you need agents that can search databases, run shell commands, and pull live data from APIs. That's where MCP servers come in. MCP servers are specialized tools that dramatically extend your AI assistant's capabilities beyond simple code generation. Think of MCP servers as powerful plugins that give your AI assistant the ability to interact with external systems, access real-time data, execute commands, and perform complex operations that would otherwise require manual intervention. #### What MCP Servers Can Do for You MCP servers transform your AI assistant from a simple code generator into a comprehensive development partner. Instead of just writing code, your AI can now: - **Run database queries:** Fetch real time data from databases and use them to generate code or give you answers. - **Perform searches** using search engines or searching documentations. - **Execute system commands** - **Interact with APIs** and external services. - **Monitor application logs** and debug production issues. - **Deploy applications directly** to cloud platforms like Shuttle. MCP servers aren't confined to a single flavor: there's an ecosystem of ready-made options to choose from, and you can even build your own to run fully custom logic. The screenshot below shows MCP servers in action. Notice how the AI assistant taps the specialized tool to deploy the Rust project to Shuttle. In this example, you can see the AI assistant using the `deploy` tool to deploy the application to the cloud, demonstrating how MCP servers extend the AI's capabilities beyond simple code generation. > Later in the article, we'll walk through how to use the Shuttle MCP server. #### Recommended MCP Servers for Rust Development Here are the essential MCP servers that will supercharge your Rust development workflow: - [**Context7 MCP**](https://context7.dev/) - Context7 solves one of the biggest problems in AI-assisted development: outdated documentation. Instead of relying on generic or outdated information from LLM training data, Context7 pulls up-to-date, version-specific documentation and code examples directly from the source. This means your AI assistant gets accurate, relevant information about the exact versions of libraries you're using, eliminating hallucinations and providing working code examples. - [**Shuttle MCP**](https://github.com/shuttle-hq/shuttle/tree/main/mcp) - Shuttle is a cloud platform specifically designed for Rust applications. This MCP server streamlines your entire deployment workflow, enabling seamless project creation, application deployment, environment management, and real-time monitoring directly within your development environment. - [**Puppeteer MCP**](https://www.npmjs.com/package/@modelcontextprotocol/server-puppeteer) - A Model Context Protocol server that provides browser automation capabilities using Puppeteer. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment. - [**GitHub MCP**](https://github.com/github/github-mcp-server) - Integrates version control operations directly into your AI workflow. Manages code repositories, handles pull requests, tracks issues, and coordinates collaborative development without switching contexts. - [**Playwright MCP**](https://github.com/microsoft/playwright-mcp) - Brings Microsoft's browser automation framework to your AI assistant. Enables cross-platform web testing across Chrome, Firefox, Safari, and Edge with enhanced reliability compared to traditional testing tools. ### Cursor Configuration #### Enable Memories Cursor provides a feature called **Memories**, this feature let's the AI agent to remember specific context about you and your project, using this feature for a while can make your AI agent more personalized and aware of your coding style, preferences and the projects you're working on. To enable this feature, navigate to **Settings** > **Rules** > **Memories** and tick the **Generate memories** option. #### Add Documentation Context AI coding assistants are more powerful when they have a good understanding of the libraries and frameworks you're using, the more context, the better results you'll get. However, copy/pasting documentation in every chat is a tedious task and we don't wanna do that by hand. Thankfully, Cursor provides us with a great developer friendly feature that allows us to add documentation context only once and we'll be able to use it as context in any chat we want. To add documentation context: Open the chat panel in Cursor and type `@Docs` in the chat. Click "Add new doc". Paste the URL prefix of the documentation you want to add (e.g., `https://docs.shuttle.dev`). Press **Enter**, this will bring us to the last step, which we can specify a **title** for our documentation, along with an **entrypoint URL** and **prefix URL** for the documentation we want to add. Cursor will automatically index the page and any sub-pages with that URL prefix. We can then mention it using `@Docs` and select the specific documentation we want to use. Claude 4.0 is especially good at this due to its large context window. Cursor will automatically fetch the relevant documents and feed it to the AI assistant, we'll get an answer almost immediately, this saves us a lot of time and effort to search through documentation manually. Recommended documentation to add: - Rust standard library: `https://doc.rust-lang.org/std` - Axum framework: `https://docs.rs/axum/latest/axum` - SQLx: `https://docs.rs/sqlx/latest/sqlx` - Shuttle: `https://docs.shuttle.dev` This ensures the AI assistant understands the specific APIs, patterns, and best practices for our tech stack. ### Rust-Specific Rules for AI Agents Rules are yet another AI feature provided by Cursor that allows us to define custom rules that guide how the AI writes code for our specific project. Most developers skimp on documentation and pay for it later. **Cursor's Rules** feature flips that script: You drop a markdown file at `.cursor/rules/my-rule.mdc` into your repo, and the AI treats those notes as living docs. No more "I'll document it later" guilt 🙂 Your guidelines, style tips, and architectural clues are always in context, keeping the assistant (and your teammates) perfectly informed. For that reason, we have **Cursor Rules**, Cursor allows us to define a set of custom rules that sets some rules for the AI agent to follow when writing code for a specific project. Rules can be specified to projects, and they can be specified to be read automatically or manually. Let's create a `.cursor/rules/rust.mdc` file in our project root with Rust-specific guidelines: ```bash . |-- .cursor | |-- rules | |-- rust.mdc | |-- ... |-- ... ``` ```markdown You are an expert Rust developer. Follow these rules when writing Rust code: ## Code Style - Use idiomatic Rust patterns and conventions - Prefer explicit error handling with Result - Use ? operator for error propagation - Implement proper ownership and borrowing - Use descriptive variable names and function signatures ## Error Handling - For comprehensive error handling, use libraries like `anyhow` for simple error handling, `thiserror` for custom error types, or `eyre` for enhanced error reporting - Never use unwrap() or expect() in production code - Provide meaningful error messages - Use `anyhow::Result` for functions that can return multiple error types - Use `thiserror::Error` derive macro for custom error enums ## Testing - Write unit tests for all public functions - Use descriptive test names that explain the scenario - Group related tests in modules ``` > Important Note: These are generic rules for Rust development. You should customize these rules for your specific project. For example, if you're using a validation library like validator, write specific rules about how to handle validations. If you're using specific architectural patterns or frameworks, document them in your rules so the AI can follow your project's conventions consistently. #### How Cursor Reads Your Specified Rules Cursor will read these rules based on a few conditions that we can manually specify: - **Always**: This will read the rules for every single chat we have with the AI assistant. - **Auto Attached**: This will read the rules based on a specified glob pattern, for example, if we want the rules to be read for every rust file, we can specify the glob pattern to be `*/*.rs`. Or if we want to add rules for a specific crate, we can specify `crates/my_macros/**/*.rs`. - **Agent Requested**: This gives us the option to add a description to the rules file, Cursor will automatically read these rules if it finds a description that matches the current chat. - **Manual**: This will read the rules only when we manually mention it in the chat by typing `@Cursor Rules` and selecting the rules that we defined. ### Best Practices for AI Prompting The general principle of **garbage in, garbage out** applies in prompt engineering as well, the more specific and detailed we are, the better the results we'll get. Here are some best practices for AI prompting: #### Use a Todo-Driven Approach It's very common for the AI assistant to write some good code for a feature, only to delete, or modify it later and therefore breaking your code. Instead of manually writing prompts each time, we can create a file for each task that we want to do, this file will contain all the tasks that have been done and that are pending for the specific feature that we want to build. This way, our AI assistant will have a complete context and understanding of what we're trying to build and what we have done so far. Create a `todo.md` file in your project root that explains your application requirements and features, then break them down into smaller, actionable tasks. Next time you want to work on a feature, you can just mention the `todo.md` file and the relevant file names that need to be changed and the AI assistant will work through the tasks systematically. Here's an example: ```markdown # Project: Task Management API This API will be a simple task management API, it will have a few endpoints: - GET /tasks - List all tasks with optional filtering - POST /tasks - Create a new task - GET /tasks/{id} - Get a specific task - PUT /tasks/{id} - Update a task - DELETE /tasks/{id} - Delete a task ## Requirements - CRUD operations for tasks - User authentication - Database persistence - REST API endpoints ## Tasks ### Project Setup - [ ] Initialize Rust project with dependencies - [ ] Set up database connection and migrations ### Authentication - [ ] Create User struct and auth endpoints - [ ] Implement JWT middleware and password hashing ### Task CRUD - [ ] Create Task model - [ ] Create the task CRUD endpoints - [ ] GET /tasks - List all tasks with optional filtering - [ ] POST /tasks - Create a new task - [ ] GET /tasks/{id} - Get a specific task - [ ] PUT /tasks/{id} - Update a task - [ ] DELETE /tasks/{id} - Delete a task - [ ] Add input validation and error handling for the task CRUD endpoints ### Testing - [ ] Write unit and integration tests - [ ] Set up test database ``` #### Provide Focused Context Give the AI only the necessary context for the task at hand. Too much context can cause the AI to lose focus or forget important details. Be selective about which files and information you include. #### Know When to Use MCP Servers vs CLIs MCP servers and CLIs are both great tools that your AI agent can use to interact with systems, both of them have their pros and cons and you should know when to use each of them. **When to use MCP servers:** MCP servers can be superior to CLIs because the AI understands how to use them properly. While the AI might struggle with lesser-known CLI tools, MCP servers provide structured interfaces that the AI can navigate confidently. AI agents might not have the proper context for the CLIs and how to use them, therefore it has a more potential for hallucinations and making errors. **When to use CLIs:** If you're using a popular CLI tool like `git`, `docker`, `cargo`, etc. In that case, you won't need a MCP server for those actions, most LLMs have quite good knowledge on these tools and how they work. Another reason to choose CLIs over MCP servers is that CLIs can stream their output and you don't need to wait for the processes to finish, you can see the code execution in real time, this streaming capability isn't available in MCP servers, on the contrary, you'll have to wait for the execution to finish before you can see the results. #### Build Custom MCP Servers If you already have an app or API, you can spin up a custom MCP server with the official SDKs, available in Rust and several other languages. A quick look at the docs shows just how straightforward the process is. ## Building a Complete Rust CRUD API with AI in 5 Minutes Now for the exciting part—let's build and deploy a complete CRUD API using nothing but AI assistance. We'll use all the tools and techniques we've learned so far to build a task management API with full database integration using the Axum rust framework. We'll also deploy the API to the cloud, we'll do all this without writing any code, we'll just let our AI assistant to do all the heavy lifting for us. > Important Note > > This approach is not a complete replacement for human developers, AI can make mistakes and write code that is prone to bugs and security vulnerabilities. This demonstration is just a proof of concept and a way to show the power of AI-assisted development. > > When building production applications, make sure you review all the AI generated code and make sure it's secure, performant and reliable. ### Ready to Build Along? To follow along with this tutorial, use our pre-configured template that includes all the necessary tools and configurations: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/ai-assisted ``` This template includes pre-configured MCP servers, Cursor rules, and everything you need to follow along with the tutorial. ### Prerequisites Before starting to code, there are a few tools that we need to install and configure. - **Shuttle CLI**: To interact with shuttle using the CLI. - **SQLx CLI**: To interact with the database using the CLI. - **Shuttle MCP Server**: This is a MCP server that allows AI agents to look up the latest Shuttle documentation and gives proper context on how to interact with the Shuttle platform. - **Docker**: To run a local database in a container. #### Install the Shuttle CLI Install the shuttle CLI, for Linux/macOS: ```bash curl -sSfL https://www.shuttle.dev/install | bash ``` For Windows: ```powershell iwr https://www.shuttle.dev/install-win | iex ``` #### Install the SQLx CLI Install the SQLx CLI, for Postgres only: ```bash # For postgres Only cargo install sqlx-cli --no-default-features --features native-tls,postgres ``` ### Setting Up MCP Servers Before we start, let's set up a few MCP servers that are going to help us build and deploy our API much easier. #### Shuttle MCP Server The Shuttle MCP server enables AI agents to understand and execute Shuttle commands directly, including **deployments**, **fetching logs**, **searching through the latest Shuttle documentation** and **more**. #### Configure Cursor for Shuttle MCP Server Add the Shuttle MCP server to your Cursor configuration. On Linux/macOS, edit `~/.cursor/mcp.json`. On Windows, edit `%APPDATA%\\Cursor\\mcp.json`. Add the following to your MCP servers configuration: ```json { "mcpServers": { "Shuttle": { "command": "shuttle", "args": ["mcp", "start"] } } } ``` ### Implementation Steps #### Step 1: Project Initialization First, we'll need to be logged in to the Shuttle CLI, you can do this by running: ```bash shuttle login ``` This will redirect you to the Shuttle login page, and redirect you back to the terminal after you've logged in. Next, let's create a new project for our task management API. ```bash shuttle init ``` We just created a new project with the following options selected: - **Project name**: `task-management-api` - **Directory**: `~/Desktop/task-management-api` - **Template and Framework**: A hello world template for **Axum** - **Create a project on Shuttle**: Yes (this creates a new project on the Shuttle platform) #### Step 2: Writing Tasks for AI Agents For our AI assistant to understand our intention on what we're trying to build, we'll need to write a `todo.md` file that contains all the necessary tasks and requirements that we want to build. Now, let's write down the tasks that we want to build: ```markdown # Task Management API This API will be a simple task management API, it will have a few endpoints, we'll use **Axum**, **Shuttle** and **SQLx** to build the API. - GET /tasks - List all tasks with optional filtering - POST /tasks - Create a new task - GET /tasks/{id} - Get a specific task - PUT /tasks/{id} - Update a task - DELETE /tasks/{id} - Delete a task ## Tasks - [ ] Install the dependencies - [ ] Update the code to use a shared database using shuttle - [ ] Create a migrations file for the tasks table - [ ] Run the migrations - [ ] Create the routes for the tasks endpoints - [ ] Commit the changes - [ ] Deploy the application to shuttle - [ ] Write some tests for the production API - [ ] Run the tests and fix the errors if any ``` #### Step 3: AI-Assisted Development Let's tell Cursor to read the `todo.md` file and work through the tasks sequentially. The todo list system is a great approach to give the AI agent a clear understanding of what we're trying to build, giving the AI agent a clear context of the details about the project. The AI agent immediately ran the tool `search_docs` to search through the latest Shuttle documentation and find the relevant information needed for the the tasks. The required migrations for the tasks have been created. Once the tasks are complete, the AI agent will commit the changes listed in `todo.md` and deploy the application to Shuttle. The AI agent then proceeds to the next step which is writing the tests to make sure the API is working as expected. When running the tests, we can see that all the tests are failing: The AI agent begins by debugging, gathering all the details it needs to fix the failing tests. It can also call the `deployment_status` and `logs` tools to inspect the latest deployment, scan the logs, and pinpoint the root cause. These two tool calls provide the AI agent with everything it needs. After reviewing the results, it understands the issue and fixes the failing tests. The issue was that `axum@0.8` changed its path parameter syntax from `/:id` to `/{id}` format. The AI agent then proceeds to fix the tests and run them again: Running the tests again: Perfect, the tests are now passing, and the API is working as expected. ## The Power of AI Agents What we just accomplished is remarkable - we built, deployed, and tested a complete API with a single prompt. The AI agent: - Created database migrations - Implemented CRUD endpoints - Deployed to production - Wrote comprehensive tests - Debugged and fixed issues - All autonomously This represents a fundamental shift in how we approach software development. What traditionally took hours or days of manual work was completed in minutes through intelligent automation. Before we celebrate, remember: AI-generated code is far from infallible. The automation is impressive, but it introduces real risks that demand our attention. ## Precautions and Security Risks AI-assisted development can supercharge productivity, yet it also opens new security fronts developers can't afford to ignore. ### Critical Security Awareness AI-generated code can contain security vulnerabilities and implementation flaws that may not be immediately obvious. **AI can be confidently wrong** about security implementations, error handling, business logic, etc. **Never blindly trust AI-generated code.** What appears correct at first glance may hide subtle, critical security flaws, or overlook key scalability and performance implications. ### Dependency Security Risks - AI may suggest outdated or unmaintained packages - Dependencies with known vulnerabilities - Packages from untrusted sources - Excessive permissions or unnecessary dependencies ## Best Practices for AI-Assisted Development The key to successful AI-assisted development is reviewing every bit of code that the AI agent writes. ### Mandatory Code Review Process **ALWAYS review AI-generated code before accepting it.** This is non-negotiable for production applications. ### Testing AI-Generated Code - **Write additional tests** beyond what AI generates - **Focus on edge cases** and error conditions - **Test security boundaries** with invalid inputs - **Perform integration testing** with real data - **Load test** critical endpoints > Remember: AI may speed up development, but it doesn't guarantee secure or reliable code. It's up to you to review each change and ensure everything is secure, performant, and dependable. ## Conclusion Just a few years ago, Rust was notorious for its steep learning curve. The borrow-checker barked at nearly every line. Today, AI-powered editors like Cursor, paired with deploy-anywhere platforms such as Shuttle, make writing and shipping a Rust API almost effortless. This is what a ridiculously productive workflow looks like. We spun up a complete Rust CRUD API from scratch, shipped it, debugged it, and even hammered the production deployment with tests. And it only took us a few minutes! This isn't a dream, it's just the reality of building with AI agents on Shuttle. It's also important to note that while AI can be extremely helpful in accelerating development time, it's still subject to making mistakes and leaving security vulnerabilities in your code. It is always best practice to review the code and ensure you are only committing high-quality, secure, and performant code. Develop the mentality of treating AI as a tool, not a replacement for human developers, this way you'll get work done much faster and much more efficiently without exposing your code to security vulnerabilities. **Don't just build. Create at the speed of thought** Ready to experience a true 10x developer workflow? Our AI-assisted Rust template is your launchpad into the future of software development. See what you can create when AI is your copilot: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/ai-assisted ``` --- # The Hidden Costs of Deploying Rust Microservices Source: https://www.shuttle.dev/blog/2025/06/18/rust-microservices-deployment-costs Date: 18 June 2025 Author: dcodes Tags: rust microservices, rust async, rust sqlx, axum framework, devops best practices Discover the hidden costs of microservices deployment at scale. Learn practical solutions to infrastructure complexity with Rust and Shuttle examples. Microservices promise speed, scale, and team autonomy. But once you deploy more than a handful, the complexity creeps in. Teams suddenly face config sprawl, CI duplication, secret management, and fractured environments. In this article, we'll explore the hidden operational costs of microservices, especially for Rust developers. You'll see how Shuttle's dev-first approach simplifies the provisioning and deployment journey, and we'll even give you a real microservices challenge to try yourself. We'll end the blog post with a special challenge for Rust developers for building Rust microservices at scale with ease. The challenge is designed to be fun and at the same time demonstrate microservice provisioning and deploying multiple microservices while using best practices. ## Monolith vs Microservices ### What is a monolith? Modern applications often evolve from monoliths to microservices, and for good reason. A **monolith** is a single, tightly coupled codebase running as one process. It's simple to start but becomes harder to scale as teams and features grow. Every small change risks affecting the entire system. **Microservices**, by contrast, are small, independently deployable services. They offer: - **Faster development:** Teams can ship features without waiting on others. - **Scalable infrastructure**: Only the services that need more resources get scaled. - **Tech flexibility**: Use Rust for performance-critical services, and Python or Go where it fits. - **Resilience**: One service can fail without taking the whole app down. - **Team autonomy**: Squads own, deploy, and monitor their own services. - **Cost-effective scaling**: Scale only the services that need resources, not the entire application stack. _Example: A Netflix clone might break into services like Recommendations, Streaming, Billing, and Users. With a monolith, they share the same release cycle. With microservices, each evolves and scales independently._ Microservices give you the ability to scale only the services that need the additional resources. But while microservices sound ideal, they introduce operational complexity, especially as your system grows. ## Hidden Costs of Deploying Rust Microservices While these benefits are compelling, microservices aren't without challenges. Behind these benefits are unexpected problems that can slow you down, add extra work, and confuse your team. Managing and deploying many small services often gets messy, takes more time, and brings surprises you did not plan for. Infrastructure management quickly becomes complex with configuration files, CI/CD pipelines, and service dependencies. The theoretical advantages of independent deployments and technology flexibility become overshadowed by infrastructure demands. ### Common pitfalls of microservices Let's dive into some of the common pitfalls of microservices. By the end of this blog post, we'll have a good understanding of the challenges and we'll talk about **tools that can help us overcome these challenges**. ### Infrastructure as Code Complexity Managing infrastructure with tools like **Terraform** becomes challenging when scaling to dozens of configuration files across multiple environments. Each service requires its own configuration files for networking, security, compute resources, and storage. ### CI/CD Pipeline Proliferation Each microservice typically requires its own CI/CD pipeline, leading to increased maintenance overhead. Teams must dedicate resources to maintaining build configurations across numerous services. ### Observability Challenges Implementing a well set up monitoring system requires integrating **logging**, **metrics**, and **tracing** across all services. Organizations frequently end up with disconnected monitoring tools, making incident investigation difficult when problems occur. ### Domains and Certificate Management Managing domains and SSL certificates across multiple microservices creates significant operational overhead. Each service and environment typically requires its own subdomain (**api.company.com**, **auth.company.com**, **payments.company.com**), and each subdomain needs SSL certificates for secure communication. They also need to be renewed periodically. Without proper tooling, this becomes extremely difficult to manage, with surprises of expired certificates. ### Environment Management Complexity Microservices multiply environment management challenges. Where a monolith might have three environments (**development**, **staging**, **production**), microservices often require environment parity across dozens of services. Each service needs its own environment-specific configurations, database connections, and service discovery settings. Maintaining consistency across environments becomes extremely difficult when services have different deployment schedules and dependency requirements. ### Database Proliferation and Management Each microservice typically requires its own database to maintain data isolation and independence. This database-per-service pattern creates operational complexity that teams often underestimate. Database provisioning, backup strategies, monitoring, and maintenance must be replicated across dozens of database instances. Each database needs its own connection pooling, and performance tuning. ### Secrets and Configuration The more services you have, the more secrets and configurations you need to manage. Without proper tooling for managing secrets and configurations, this becomes extremely difficult to manage. ## How Microservices Are Typically Deployed In conventional microservices architectures, each service typically requires its own microservice deployment pipeline and infrastructure configuration. Let's examine what this traditionally looks like and understand the complexity involved. ### Docker and Container Management Most microservice deployments start with Docker. Each service needs its own `Dockerfile` and typically a `docker-compose.yml` file for local development. Here's an example of what a traditional setup might look like: ```yaml # docker-compose.yml version: "3.8" services: blog-api: build: ./blog-service ports: - "3001:3000" environment: - DATABASE_URL=postgresql://user:password@postgres:5432/blog_db - ANALYTICS_SERVICE_URL=http://analytics-service:3000 - ANALYTICS_API_KEY=your-super-secret-api-key-here depends_on: - postgres - analytics-service analytics-service: build: ./analytics-service ports: - "3002:3000" environment: - DATABASE_URL=postgresql://user:password@postgres:5432/analytics_db - ANALYTICS_API_KEY=your-super-secret-api-key-here depends_on: - postgres postgres: image: postgres:15 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=password - POSTGRES_DB=blog_db volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" volumes: postgres_data: ``` Each service will have its own `Dockerfile` as well. Managing these needs careful attention - you'll need to make sure that the services are compatible with each other, and that the services are compatible with the infrastructure you're using. This is for a simple setup, however, when it comes to production environments with high traffic, you'll need multi node deployments and things get complex quickly, and you'll need to manage a lot of additional complexity: - Kubernetes manifests - Load balancer configurations with SSL termination - Service meshes like Istio - Auto-scaling groups - Network security - Database clusters The list goes on, and the complexity increases. Secret management also becomes extremely complex with rotation strategies, encrypted storage systems like Vault, access control policies, and certificate management across all environments. You'll also need comprehensive monitoring and observability, once you have multiple containers across multiple nodes, this becomes extremely difficult to manage. This traditional approach, while powerful and battle-tested, creates significant operational overhead where teams often spend most of their time managing infrastructure, Kubernetes configurations, and deployment pipelines instead of building features. ## Simplify Rust Microservice Deployment with Shuttle Luckily, we're not going to have to do all of that. With **Shuttle**, we can focus on building our application logic instead of wrestling with infrastructure complexity. Shuttle handles microservice provisioning, secret management, and deployment orchestration automatically, letting us deploy production-ready Rust microservices with just a few commands. ### Introducing a Developer-First Approach to Infrastructure Instead of managing infrastructure separately, we can manage it directly in our application code, using the same tools and languages we already know. **Shuttle** embodies this approach by removing the complexity of managing infrastructure and deployment. Rather than dealing with Terraform or cloud consoles, you define resources like **databases and secrets** using straightforward Rust attributes. In this tutorial, we'll build multiple interconnected Rust microservices with **Axum** and then deploy them to the cloud with **Shuttle**. We'll create a **Blog API service** and an **Analytics service** that communicate with each other, demonstrating true microservices architecture with service-to-service communication. ### Building Multiple Microservices with Shuttle For this example, we'll build two interconnected services: 1. **Analytics Service** - Tracks and processes blog post views and interactions 2. **Blog API Service** - Manages blog posts and sends analytics events Let's start by installing the **Shuttle CLI**: ### Linux and macOS ```bash curl -sSfL https://www.shuttle.dev/install | bash ``` ### Windows (PowerShell) ```bash iwr https://www.shuttle.dev/install-win | iex ``` Login to the **Shuttle CLI**: ```bash shuttle login ``` ### Install the sqlx-cli To be able to use `sqlx` and interact with the database, we'll need to install the `sqlx-cli` tool. This particular command sets it up for postgres only. ```bash # Install sqlx-cli (if not already installed) cargo install sqlx-cli --no-default-features --features native-tls,postgres ``` ### Source Code The complete source code for the microservices we'll build is available on GitHub: [Shuttle Microservices Demo](https://github.com/dcodesdev/shuttle-microservice-demo) You can clone the repository to follow along or reference the final implementation. ### Creating the Analytics Service Create the first project for our analytics service: ```bash shuttle init --template axum ``` Add dependencies for the analytics service: ```bash cargo add shuttle-shared-db serde sqlx serde_json \ --features shuttle-shared-db/postgres,shuttle-shared-db/sqlx \ --features serde/derive ``` To use `sqlx` properly to interact with the database, we'll need to set up a local database and set the `DATABASE_URL` environment variable in the `.env` file. To create a database, the easiest way to do it is by using Docker, you'll need to have docker installed on your machine, if you haven't already, make sure you [install it here](https://docs.docker.com/get-docker/) for your specific operating system. Once you have docker installed, you can run the following command to create a database: ```bash docker run -d --name postgres --restart=always -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -p 5432:5432 postgres ``` Once your database is ready to accept connections, you can set up the analytics database: Make sure you add `.env` to your `.gitignore` file. ```bash echo ".env" >> .gitignore ``` Update the migration file: ```sql -- migrations/_create_events.sql CREATE TABLE events ( id SERIAL PRIMARY KEY, event_type VARCHAR NOT NULL, post_id INTEGER, data JSONB, created_at TIMESTAMP DEFAULT NOW() ); ``` Run the migration: ```bash cargo sqlx migrate run ``` ### Securing service communication In order to secure the communication between the analytics service and the blog service, we'll need a way to verify the identity of the requester. To do that, we'll have a **secret key** that only the two services know about. If the requester has the correct secret key, the request will be allowed to proceed, otherwise it will be rejected. ### Secret management with Shuttle The **secret key** must be kept secret, therefore it **must not** be committed to version control and must be handled securely using proper tooling. For that, Shuttle provides us a way to define secrets in a secure manner, by giving us a macro that we can use directly in our application code. To define the secrets, **Shuttle** expects a `Secrets.toml` file in the root of the project. The `Secrets.toml` file is a simple configuration file that contains the secrets for the project. It's a **TOML** file, which is a simple configuration file format that is easy to read and write. Create the `Secrets.toml` file: ```toml # Secrets.toml ANALYTICS_API_KEY = "your-super-secret-api-key-here" ``` We can use the `ANALYTICS_API_KEY` secret in the code with the `#[shuttle_runtime::Secrets]` macro: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore) -> shuttle_axum::ShuttleAxum { let api_key = secrets.get("ANALYTICS_API_KEY").expect("ANALYTICS_API_KEY must be set"); // Use the api_key anywhere in your code... Ok(router.into()) } ``` ### Database provisioning with Shuttle Shuttle provides an easy way to provision databases, just by adding a macro to your `main.rs` file, you'll have a database set up and connected to after you deploy to Shuttle. ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_axum::ShuttleAxum { ... } ``` To make development easier, you can set your own `local_uri` to connect to your local database. ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres( local_uri = "postgres://postgres:password@localhost:5432/analytics_db" )] pool: PgPool, ) -> shuttle_axum::ShuttleAxum { ... } ``` Now, let's implement the routes for the service, we'll have two routes, one for receiving events and one for getting statistics. For the full version of the code, see the [GitHub repository here](https://github.com/dcodesdev/shuttle-microservice-demo). ```rust async fn receive_event( State(state): State, Json(event): Json, ) -> StatusCode { let result = sqlx::query!( "INSERT INTO events (event_type, post_id, data) VALUES ($1, $2, $3)", event.event_type, event.post_id, event.data ) .execute(&state.db) .await; match result { Ok(_) => StatusCode::CREATED, Err(_) => StatusCode::INTERNAL_SERVER_ERROR, } } async fn get_stats(State(state): State) -> Json> { let stats = sqlx::query_as!( EventStats, "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type" ) .fetch_all(&state.db) .await .unwrap_or_default(); Json(stats) } ``` Having a look at this snippet in the code: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres( local_uri = "postgres://postgres:password@localhost:5432/analytics_db" )] pool: PgPool, #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { ... } ``` Connecting to a database using Shuttle is as simple as adding a macro to your `main.rs` file. This macro will automatically provision a database for you. The `local_uri` is used to connect to your local database for development purposes. For managing secrets, we have used the `#[shuttle_runtime::Secrets]` attribute provided by Shuttle that will automatically load the secrets from the `Secrets.toml`. Shuttle will automatically push the secrets to the cloud in a secure manner when running `shuttle deploy`. Let's build a middleware function to authenticate requests using the `ANALYTICS_API_KEY` from the `Secrets.toml` file. This way, only our blog service can send analytics events to the analytics service. ```rust async fn auth_middleware( headers: HeaderMap, State(state): State, request: axum::extract::Request, next: axum::middleware::Next, ) -> Result { let auth_header = headers .get("Authorization") .and_then(|header| header.to_str().ok()) .and_then(|header| header.strip_prefix("Bearer ")); match auth_header { Some(token) if token == state.api_key => Ok(next.run(request).await), _ => Err(StatusCode::UNAUTHORIZED), } } ``` ### Deploying the Analytics Service Before we deploy the service, we'll need to follow another step that is specific to [sqlx](https://github.com/launchbadge/sqlx/), we'll need to prepare the SQL queries for production so that the code can compile without the need for an active database connection, you can read more about [how sqlx works here](https://github.com/launchbadge/sqlx/). ### Prepare the SQL queries for production ```bash cargo sqlx prepare ``` Make sure you commit the generated metadata by SQLx. ```bash # Commit the generated metadata by SQLx git add . git commit -m "Prepare SQL queries for production" ``` ### Deploy the service Deploying services with **Shuttle** is as simple as running `shuttle deploy` in the root of the project. ```bash shuttle deploy ``` This will deploy the service to the cloud, you'll see the deployed URL in the console, we'll use this URL in our blog service to send analytics events to the analytics service. ### Creating the Blog API Service Now let's create our blog service that will depend on the analytics service: ```bash shuttle init --template axum ``` Add the required dependencies for our blog service: ```bash cargo add shuttle-shared-db serde reqwest tokio sqlx serde_json \ --features shuttle-shared-db/postgres,shuttle-shared-db/sqlx \ --features serde/derive \ --features reqwest/json \ --features tokio/full ``` Set up the database environment: ```bash # Create .env file for local development echo "DATABASE_URL=postgres://postgres:password@localhost:5432/blog_db" > .env # Create the database cargo sqlx database create # Create migration cargo sqlx migrate add create_posts ``` Add `.env` to your `.gitignore` file. ```bash echo ".env" >> .gitignore ``` Update the migration file: ```sql -- migrations/_create_posts.sql CREATE TABLE posts ( id SERIAL PRIMARY KEY, title VARCHAR NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); ``` Run the migration: ```bash cargo sqlx migrate run ``` Create a `Secrets.toml` file for configuration: ```toml # Secrets.toml ANALYTICS_SERVICE_URL = "https://analytics-service-m7iz.shuttle.app" # Replace with your analytics service URL ANALYTICS_API_KEY = "your-super-secret-api-key-here" ``` Now let's implement our blog service with analytics tracking. We're going to implement a few routes: Create a post, get a post and get a list of all posts. See the [source code here](https://github.com/dcodesdev/shuttle-microservice-demo). ```rust async fn create_post( State(state): State, Json(payload): Json, ) -> Result, StatusCode> { ... } async fn get_posts(State(state): State) -> Json> { ... } async fn get_post( Path(id): Path, State(state): State, ) -> Result, StatusCode> { ... } ``` ### Service to service secure communication To send events to the analytics services, we'll need to create a new function to send analytics events to the analytics service, this function is used to communicate with the analytics service using secure `HTTP` requests. ```rust async fn send_analytics_event(state: &AppState, event: AnalyticsEvent) -> Result<(), reqwest::Error> { let client = reqwest::Client::new(); let _response = client .post(&format!("{}/events", state.analytics_url)) .header("Authorization", format!("Bearer {}", state.analytics_api_key)) .json(&event) .send() .await?; Ok(()) } ``` We have used the `ANALYTICS_SERVICE_URL` and `ANALYTICS_API_KEY` from the `Secrets.toml` file to send the analytics event to the analytics service. This way both services are independent of each other, they have their own databases and can be deployed independently. To make this communication secure, we have used the `ANALYTICS_API_KEY` to authenticate the request to the analytics service. ### Deploy the blog service The process is similar to the analytics service, we'll need to prepare the SQL queries for production and deploy the service. ```bash cargo sqlx prepare ``` Commit the generated metadata by SQLx. ```bash git add . git commit -m "Prepare SQL queries for production" ``` Deploy the service using **Shuttle**: ```bash shuttle deploy ``` 🎉 Great! Now both of our services are deployed and ready to use. ### Testing the services Let's test the services and make sure they're working as expected: ```bash # Create a blog post (this will trigger an analytics event) curl -X POST https://blog-service-3hhs.shuttle.app/posts \ -H "Content-Type: application/json" \ -d '{"title": "My First Post", "content": "Hello microservices!"}' # Response {"id":1,"title":"My First Post","content":"Hello microservices!"} # View the post (this will trigger another analytics event) curl https://blog-service-3hhs.shuttle.app/posts/1 # Response {"id":1,"title":"My First Post","content":"Hello microservices!"} # Check analytics stats curl -s https://analytics-service-m7iz.shuttle.app/stats | jq [ { "event_type": "post_viewed", "count": 2 }, { "event_type": "post_created", "count": 1 } ] ``` Great! Our services are working as expected, we have a blog service that can create and view posts, and an analytics service that can track analytics events. ### Traditional vs Shuttle: Microservice Deployment Complexity Comparison Comparing this approach with the traditional approach, we can see that we no longer need to manage multiple configuration files and orchestration tools - we can focus on building our application logic instead. With this approach, teams can focus on building smaller Rust microservices without worrying about the infrastructure, secrets management, and observability tools. This makes microservice deployment much faster and easier. | Aspect | Traditional Setup (Docker/Kubernetes/Terraform) | Shuttle Setup | | ----------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | | **Infrastructure Management** | Manual configuration of Dockerfiles, docker-compose.yml, Kubernetes manifests, and Terraform files | Automated infrastructure provisioning through Shuttle | | **Database Provisioning** | Manual setup of PostgreSQL containers, volumes, and Terraform database resources | Automatic database provisioning with Shuttle's shared database feature | | **Secret Management** | Environment variables in docker-compose.yml, Kubernetes secrets, or external tools like Vault | Integrated secret management using Shuttle's Secrets.toml | | **Deployment Complexity** | Requires manual deployment steps, CI/CD pipeline configuration, and Kubernetes/Terraform orchestration | Simple deployment with `shuttle deploy` command | | **Scalability** | Manual configuration of Kubernetes HPA, scaling policies, and load balancers | Built-in scalability features with Shuttle | | **Monitoring** | Requires additional monitoring tools, Prometheus setup, and configuration | Integrated monitoring capabilities with Shuttle | | **Development Experience** | Complex setup for local development with Docker, minikube, or kind clusters | Simplified development experience with Shuttle's local development features | ## Conclusion Microservices provide a variety of benefits - isolating different parts of your application makes deployment easier, enhances security and scalability. This way you can scale only the services that need the additional resources. However, this doesn't come without a cost. Adding more microservices means more complexity and infrastructure management, which makes development slower and more difficult. Using **Shuttle** can make microservice deployment easier. Without having to deal with the complexity of managing infrastructure, you can focus on building your application. You no longer have to worry about microservice provisioning, managing secrets, or managing your CI/CD pipeline. Learn more about [building Rust web applications with Shuttle](https://docs.shuttle.dev/getting-started/quick-start). This makes development easier and much faster, putting you ahead of the curve. You can focus on **shipping** rather than **managing infrastructure**. ## Frequently Asked Questions --- # Process Beats Perfection: Observability Best Practices Source: https://www.shuttle.dev/blog/2025/06/10/observability-best-practices Date: 10 June 2025 Author: shaaz Tags: observability, best-practices, devops, resilience Observability best practices prove process beats perfection. Learn how disciplined iteration builds resilient systems that scale with confidence. # Process Beats Perfection: Why Observability Enables Building Resilient & Scalable Systems If you've ever been in a system design whiteboarding interview where you're asked to design a scalable and highly available web service, you'll know the rabbit hole it can become. You draw a box, label it "web server," and suddenly you're cascading into a never-ending list of concerns—load balancing, database replication, regional failovers, backpressure mechanisms, retries, caching, circuit breakers. As a fresh grad, I remember thinking, _is this just an infinite loop of optimization? When do I know I've done enough?_ ## The Power of Process Over Perfection But of course, life is simpler than that. We don't have to build perfect systems upfront. In fact, trying to do so usually leads to unnecessary complexity and wasted effort. The secret to building highly available, scalable systems isn't prescience—it's process. The most resilient systems out there didn't start perfect; they became robust through consistent iteration, guided by excellent observability and a disciplined feedback loop. **That's where good service observability comes in**. It's the foundation that lets us see where things are cracking before they shatter. With tools like dashboards, traces, metrics, and logs, we can identify real-world issues as they emerge in production, prioritize based on impact, and fix them incrementally. Add a strong operational cadence—like a weekly review of key dashboards—and suddenly you're not chasing stability reactively, you're engineering it proactively. [Shuttle's new Monitoring & Observability integration with BetterStack](https://docs.shuttle.dev/docs/telemetry/betterstack) solves the long-standing pain of setting up observability. With just a few clicks, it's built into your deploys from the start. BetterStack was chosen as one of our first integrations for its simplicity and generous free tier. Want resource usage metrics? Just create a BetterStack source, update the config in the Shuttle console, redeploy your project and you should be able to graph CPU, Memory and Network metrics on a dashboard quickly. ## Observability Foundations: The Three Pillars of Metrics We build robust systems not by guessing where the next problem will strike, but by instrumenting our systems well and following signals. The [AWS Builder's Library offers a compelling model for observability](https://aws.amazon.com/builders-library/building-dashboards-for-operational-visibility/) by categorizing metrics into three main types: 1. **Customer or client metrics** 2. **Resource metrics** 3. **Diagnostic metrics** Each of these plays a unique role in guiding operational excellence and scaling systems over time. ### Customer Metrics - Alert from the Spout Customer metrics are the heartbeat of any service. They tell you how real users are experiencing the system. Hyperscalers and market-leading cloud platforms practice what [Rob Ewaschuk at Google called "alerting from the spout"](https://docs.google.com/document/d/199PqyG3UsyXlwieHaqbGiWVa8eMWi8zzAn0YfcApr8Q/edit?tab=t.0#heading=h.fs3knmjt7fjy)—that is, monitoring what the client sees, not just what the server thinks it's doing. **Why?** Because symptoms surface where the user sits. Alerting from the _outside in_—using external uptime monitoring with a probe, like the uptime feature BetterStack offers—means you catch broad classes of issues early, from timeouts to bad dependencies. Guessing root causes from low-level server metrics is like diagnosing illness by checking your pulse—you might catch something, but you're probably missing the bigger picture. To stay focused on what matters, Google's Site Reliability Engineering (SRE) book recommends measuring **the Four Golden Signals**: - **Latency**: How long does it take to process a request? - **Traffic**: How much demand is hitting your service? - **Errors**: What fraction of requests are failing? - **Saturation**: How "full" is your system—are you nearing limits? So build dashboards that clearly display latencies, error rates, and success ratios measured at the edge of your system closest to your clients, and review them weekly. That review loop gives you structured opportunities to detect regressions, catch drift, and hold yourself accountable to real user experience. The good news? Tracking these signals in a Shuttle service is straightforward—you can use the Rust **tracing** framework to emit metrics directly from your API handlers. With macros like **#[instrument]** and libraries such as **axum-tracing-opentelemetry**, which integrate OpenTelemetry with the Axum web framework, you can automatically capture spans and metrics tied closely to your code. And soon, Shuttle will offer built-in abstractions to expose an opinionated set of standard metrics out of the box, with no manual instrumentation needed. ### Resource Metrics - Know What You're Burning Resource metrics give us a peek under the hood. In classic systems terms, computers are fundamentally about compute, memory, and I/O. Your service is no different. CPU starvation, memory pressure, network congestion, or slow disks—all of these are common culprits behind degraded performance. You don't need to over-optimize upfront. But if your system is slow and client metrics say "something's wrong," resource metrics can often tell you _what_. When troubleshooting, check these next: is the CPU utilization excessively high? Is network bandwidth being throttled? Are out of memory errors causing your project to restart? [BetterStack dashboards via Shuttle's integration](https://www.shuttle.dev/blog/2025/02/19/using-shuttle-with-betterstack) surface these metrics at the instance or container level with a few clicks and a redeployment—because you can't scale if you don't know what you're exhausting. ### Diagnostic Metrics - Follow the Dependencies Diagnostic metrics complete the picture by connecting symptoms to potential causes. These often live in the messy web of service-to-service dependencies: databases, caches, auth services, third-party APIs. Anything your project talks to over a network can—and eventually will—fail. So, track these dependencies explicitly. [AWS emphasizes building **dependency dashboards** that show how your downstream services are behaving _from your service's perspective_](https://aws.amazon.com/builders-library/building-dashboards-for-operational-visibility/). If your success rates are dropping, and client metrics look red, check these dashboards: did your database start timing out? Is a rate limit being hit upstream? The more clearly you track these relationships, the faster you can isolate and fix failures. ## A Real-World Debugging Walkthrough ### Step 1 - Start with the Client Metrics Let's bring this to life with a classic troubleshooting exercise. Imagine you've committed to being oncall for a friend's side project, and your friend texts you from vacation: _"Hey, my little home project is down, can you check on it?"_ It's a simple cloud infrastructure setup—a public user-facing API service talking to another backend service, which connects to a database. You start where you always should: the **client metrics dashboard**. This is your spout. You want to know what users are seeing. The graph tells you: there's a sudden spike in HTTP 500 responses. Latency is up, and request success rates have dipped sharply. OK—there's definitely a real problem. Now you start tracing the path of a request through the system, one hop at a time. ### Step 2 - Trace Through Services First, you move to the **user-facing API service** **metrics**. Are these 500s being generated by the public API endpoints of your service itself, or is it just passing along errors from a downstream service? You look at the status code breakdown from the frontend logs. Yup—these 500s are coming from upstream. Your user-facing API is mostly healthy; it's a dependency that's the issue. That rules out the public API logic or static asset serving as the issue. Next stop: the **backend service's API metrics**. You check its logs and diagnostic dashboards. It's returning 500s internally, and now you dig deeper—what's triggering those errors? You see timeout exceptions when the backend tries to query the database. That's your first clue pointing to a failing dependency. ### Step 3 - Isolate the Root Cause Now you move to **diagnostic and resource metrics** for the database. This is where good dependency dashboarding pays off. The database connection errors started right when the client errors did. You check the metrics for the RDS instance: CPU and memory look fine, but the DB is reporting elevated I/O wait times. Maybe it hit some capacity threshold. ### Step 4 - Deploy, Rollback, Verify At this point, you've traced the issue from symptom to source: 1. Clients are seeing 500s 2. Public API service is relaying 500s from backend 3. Backend service is failing on DB timeouts 4. Database is overloaded You notice the timestamp at which the DB load started spiking, and try to correlate it to a previous deployment. Sure, you could try to root cause the real issue, but now that you've triaged the issue, you want to attempt a quick mitigation before a true root cause. So, you check if subsequent changes after the problematic deployment are safe to rollback, and redeploy your project to last known good commit via the Shuttle Console. Then, you watch the dashboards. Backend errors stop. Public API metrics normalize. Client success rate recovers on your uptime probe. This is observability at work—**a structured, top-down root cause workflow**. You didn't guess. You followed the request. And you ruled out each dependency one at a time, which is exactly what great operational visibility enables. ## Make It a Habit, Not a Hero Move This isn't magic. It's a process. And the best teams make it a habit: review dashboards weekly, tune alerts thoughtfully, and build visibility from the top down. There's another core process that's critical for iterative scaling and reliability: learning from incidents using postmortems that are made rigorous and scientific by enhanced observability. We'll discuss that in a later article. With Shuttle, you get the ability to deploy fast _and_ observe well, which means you can keep building, scaling, and debugging incrementally—without needing a crystal ball to foresee every failure. That's how high availability becomes a living system, not a paper exercise. ## So... Now What? Sure, you could keep hand-rolling dashboards in your staging-only observability graveyard—or you could try something less painful. With [Shuttle's BetterStack + OpenTelemetry integration](https://docs.shuttle.dev/docs/telemetry/getting-started), your service ships **with real metrics out of the box**—no bash scripts, no dashboards lost in tabs, no "I'll do it later" promises to your future self. Just: - [Enable your OTEL exporter in your shuttle runtime](https://docs.shuttle.dev/docs/telemetry/getting-started) - [Add a source in BetterStack](https://docs.shuttle.dev/docs/telemetry/betterstack) - Update your Shuttle project config in console - Redeploy from CLI - Watch those golden signals light up 📈 Observability: now as easy as shipping code. 🎯 [Get started with real metrics →](https://docs.shuttle.dev/docs/telemetry/getting-started) --- --- # Troubleshooting Rust Web Applications Source: https://www.shuttle.dev/blog/2025/04/29/troubleshooting-rust-web-applications Date: 29 April 2025 Author: dcodes Tags: rust, web, debugging, monitoring, tracing, logging A guide to troubleshooting Rust web applications ## Introduction Rust can save you from a lot of programming pitfalls that other languages can't, but it's not immune to bugs and issues, there are situations that you'll need to follow best practices and use the right tools to troubleshoot your application. Bugs are inevitable, but we can make them easier to find and fix. In this guide, we'll explore how to troubleshoot Rust applications using a variety of approaches and tools. ## Structured Logging For backend services, logging is crucial for getting and idea how your application is performing and what's happening behind the scenes. However, there are better ways to log than just using a simple `println!`. Without visibility into your application's behavior, debugging becomes a frustrating guessing game. Let's explore how to implement a robust logging system. Rust offers excellent crates for logging that go beyond stdout and stderr. The [tracing](https://crates.io/crates/tracing) crate has become the gold standard for modern Rust applications: ```rust fn process_request(user_id: u64) { // Bad: just text println!("Processing request for user {}", user_id); // Good: structured event with context tracing::info!(user_id, request_type = "login", "Processing request"); } ``` Output: ```bash 2025-03-21T07:55:26.540+00:00 [app] Processing request for user 999 2025-03-21T07:55:26.540+00:00 [app] INFO shuttle_telemetry: Processing request user_id=999 request_type="login" ``` While the older `log` crate remains popular, `tracing` offers richer context through its span system and structured event data. For existing applications using `log`, the `tracing_log` adapter provides compatibility, you can [read more about it here](https://crates.io/crates/tracing-log). ### Log Levels and When to Use Them For better clarity of your application's behavior, it's important that you use the right log level for the right situation. Choose appropriate log levels to balance information density with signal-to-noise ratio: - **ERROR**: Unexpected failures requiring immediate attention - **WARN**: Concerning events that don't prevent operation but might indicate problems - **INFO**: Normal operational events useful for tracking application flow - **DEBUG**: Detailed information primarily useful during development - **TRACE**: Ultra-verbose information about internal state In production, **ERROR** and **WARN** logs should be rare and actionable. **INFO** logs should provide a clear picture of normal operation without overwhelming storage. For development purposes, you can use **DEBUG** and **TRACE** to get more detailed logs, you can turn these off in production environment to avoid overwhelming the logs. ### Using Spans to Track Request Context A route handler might go through a few steps before ultimately returning a response to the client, during that process, multiple logs might be emitted to the console, it would be great to see which logs belong to which request. Without spans, if two different clients hit the same endpoint, you'd have no way to know which logs belong to which request, making it impossible identify the request you are looking for. ```rust async fn handle_request(req: Request) -> Response { // Create a span for the entire request lifetime let request_span = tracing::span!( Level::INFO, "http_request", method = %req.method(), path = %req.uri().path(), request_id = %Uuid::new_v4(), ); // Enter the span for this async task let _guard = request_span.enter(); // logs in this function now have the request context tracing::info!("Starting request processing"); let result = process_data(&req).await; if let Err(ref e) = result { // Error logs automatically include request context tracing::error!(error.msg = %e, "Request processing failed"); } tracing::info!("Completed request processing"); generate_response(result) } ``` Spans create a hierarchy of contexts, making it easy to correlate logs from the same request even across thread or task boundaries. When troubleshooting in production, this context can come in handy for following the execution path that led to a failure. ## Effective Error Handling Rust offers a great way to handle errors, error propagation is one of the best developer experience features that Rust offers, but there's a catch, if overused it can be a mess, you'll find yourself propagating everything to the bottom of the stack, without any context about what went wrong. In this section, we'll explore a few libraries that can help you handle errors in a more effective way, giving you proper context to understand what went wrong. ### Error handling with `anyhow` `anyhow` is an error handling library that provides a single error type (`anyhow::Error`) to represent all possible errors, making it easy to propagate errors without worrying about custom enums or boilerplate. It also provides the `Context` trait that is pre-implemented for any type that implements the `Error` trait, this allows you to add extra context to errors, making them easier to debug. Here's a quick example: ```rust use anyhow::Context; fn read_file(path: &str) -> anyhow::Result { let content = std::fs::read_to_string(path) .with_context(|| format!("Failed to read file at path: {}", path))?; Ok(content) } fn main() -> anyhow::Result<()> { let data = read_file("example.txt")?; println!("File content: {}", data); Ok(()) } ``` - `Result` is a type alias for `std::result::Result`. - The `with_context` method is used to provide more context to the error. ```log Error: Failed to read file at path: example.txt Caused by: No such file or directory (os error 2) ``` The error output shows both the error message and the source error, it's a great way to understand the root cause of the error. `anyhow` is a great library for error handling in Rust applications, used with the `Context` trait, it's a great way to add extra context to errors, making them easier to debug. ### Error handling with `eyre` `eyre` is a fork of `anyhow` that gives you the same great error handling features but with additional features including custom error types and better error messages. Let's re-write the previous example using `eyre`: ```rust use eyre::{Result, WrapErr}; fn read_file(path: &str) -> Result { let content = std::fs::read_to_string(path) .wrap_err_with(|| format!("Failed to read file at path: {}", path))?; Ok(content) } fn main() -> Result<()> { let data = read_file("example.txt")?; println!("File content: {}", data); Ok(()) } ``` Output: ```log Error: Failed to read file at path: example.txt Caused by: No such file or directory (os error 2) Location: examples/eyre-ex/main.rs:5:10 ``` The error output shows both the error message and the source error including the line of code where the error occurred, this can be extremely helpful when you're debugging giving you an exact location of the error that you can quickly jump to. [Read more about `eyre` here](https://github.com/eyre-rs/eyre). ### Custom Error Types Internal libraries require custom error types, if your application uses some other internal libraries (common in workspaces), you'll need to create your own custom error types to handle the errors. This can be a pain, but luckily there are libraries that can help you with this. For a better understanding of your application's behavior, you'll need more than just the dynamic error types provided by the Rust standard library. You can create your own custom error types per operation, you can implement the `std::error::Error` trait in your custom error types to get a better error message. Here's a quick implementation of the `Error` trait: ```rust use std::error::Error; #[derive(Debug)] pub enum AppError { ParseError(String), SystemError(String), Unknown, } impl Error for AppError {} impl Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppError::ParseError(e) => write!(f, "Parse error: {}", e), AppError::SystemError(e) => write!(f, "System error: {}", e), AppError::Unknown => write!(f, "Unknown error"), } } } ``` You'll also need to implement the `Display` trait for your custom error types, while this is useful, it can be too verbose and repetitive to implement, let's have a look at a library that can give use some macros to make this easier. #### Using `thiserror` to implement the `Error` trait The `thiserror` crate helps you implement the `Error` trait for your error types in a less verbose way, it provides a derive macro that can implement the `Error` trait with minimal code. Here's how the code above will look like if implemented using `thiserror`: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { #[error("Parse error: {0}")] ParseError(String), #[error("System error: {0}")] SystemError(String), #[error("Unknown error")] Unknown, } ``` The `error` attribute takes an argument that will be used as the error message, placeholders can be used to include the error details. This will automatically implement the `Error` and `Display` traits for your custom error types, so you'll no longer need to implement them manually. Making your code much more concise and at the same time gives you precise control over the error messages making it easier to debug your application. `thiserror` also defines the `source()` method automatically for your error types, the `source()` method's job is to return the source error if any, it's return type is `Option<&dyn Error>`, which can help you track down the root cause of the error. [Read more about the `source()` method here](https://doc.rust-lang.org/std/error/trait.Error.html#method.source). By combining these techniques, you'll have a better error handling system that can give you the context you need to debug your application, use `eyre` and `anyhow` for application code and `thiserror` for internal libraries, while `anyhow` gives you a simple way to handle errors, `eyre` gives you even more detailed error messages. ## Debugging with Rust-GDB Sometimes you'll need more granular control over your application's execution, for that, you can use **Rust-GDB** to debug your application. **Rust-GDB** comes in with the **Rust** toolchain, so you don't need to install it separately. **Rust-GDB** stops the execution of the application at the breakpoint and allows you to inspect the variables and the state of the application. Let's have a simple example to demonstrate how to use **Rust-GDB** to debug a Rust application. Here's a simple Rust code that calculates the area of a rectangle: ```rust fn main() { let width = 10.0; let height = 20.0; let area = calculate_area(width, height); println!("The area of the rectangle is {}", area); } fn calculate_area(width: f64, height: f64) -> f64 { width * height } ``` Before starting **Rust-GDB**, we need to build the application in **Debug mode** with the following command: ```bash cargo build ``` Now, we can start **Rust-GDB** with the following command: ```bash rust-gdb target/debug/ ``` You can create a breakpoint by using the following command: ```bash b test.rs:4 ``` `test.rs` is the name of the file and `4` is the line number where we want to set the breakpoint. ![Rust-GDB Breakpoint](/images/blog/troubleshooting-guide/rust-gdb-breakpoint.png) This will set a breakpoint at the line 4 of the `test.rs` file. Now, we can start the application by running `run` or `r`: ```bash run ``` This will start the application and pause execution at the breakpoint. You can inspect the variables and the state of the application by using the `print` command: ```bash print width ``` ![Rust-GDB Print](/images/blog/troubleshooting-guide/rust-gdb-print-var.png) You can also inspect the state of the application by using the `info` command: ```bash info locals ``` ![Rust-GDB Info](/images/blog/troubleshooting-guide/rust-gdb-info-locals.png) If you want to continue execution, you can type `continue` or `c` and if you want to step over the next line, you can type `next` or `n`. See the [GDB Documentation](https://www.sourceware.org/gdb/documentation/) for more information about the commands. Rust-GDB can come in handy when you're working with complex systems, and you need to have a better look at the execution flow of your application. ## Telemetry Another invaluable tool for troubleshooting Rust applications is telemetry, it's the process of collecting **metrics**, **logs**, and **traces** that help developers understand what's really going on with their applications. It answers the big questions: Is the app running like it should? Are requests being handled quickly? Where's the slowdown? Telemetry is a great predictor of issues, you can catch problems days before they happen, having proper telemetry setup is key to building reliable and performant applications so that you can build with confidence, a proactive approach rather than a reactive one and solve problems before they arise. Telemetry and [Structured Logging](#structured-logging) can be used together to build a great monitoring system for your Rust applications. You'll have a dashboard to monitor all your logs, metrics and resources usage all in one place. This birds-eye view can save you from problems before they arise, in this section, we'll explore what telemetry is, and we'll set up a simple Rust application with telemetry enabled along with a dashboard to visualize the data in only a few steps. By the end, you'll have a basic overview of how to use telemetry to monitor your Rust applications and improve their performance and a dashboard like the one below, by then you'll have the knowledge you need to build your own custom telemetry system for your specific needs. Let's dive in! ![BetterStack finished dashboard](/images/blog/troubleshooting-guide/better-stack-finished-dashboard.png) ### Examples of Telemetry Data Telemetry can be collected from a variety of sources, here are some examples: - **Tracing**: Tracks the flow of requests across services, helping identify bottlenecks or failures in distributed systems. - **System Metrics**: CPU usage, RAM usage, disk I/O, network traffic. - **App Metrics**: Request latency, error rates, throughput, database query times. - **User Behavior**: Session duration, feature usage, crash reports. - **Infrastructure**: Container health, service uptime, load balancer stats. - **Security**: Failed logins, unusual traffic, access logs. Collecting and processing telemetry data is a complex process, but luckily there are open source tools like **OpenTelemetry** that make the whole process easier. However, self-hosting and maintaining **OpenTelemetry** can also be complex and resource-intensive, especially for smaller teams. When using **Shuttle**, we don't need to do any of that as everything is already set up for us. ### Building a Simple Web Application with Shuttle To demonstrate telemetry in action, we'll first need to deploy a simple Rust web application to **Shuttle**. To do that, we'll need to follow a few steps. #### Setting up a new Shuttle project To set up a new shuttle project, you'll have to have a Shuttle account, if you don't have one already, make sure to create one at [shuttle.dev](https://www.shuttle.dev). The easiest way to create a new Shuttle project is to use the Shuttle CLI. Run the following command to install the CLI: ```bash curl -sSfL https://www.shuttle.dev/install | bash ``` This will detect your operating system and install the appropriate binary. Once installed, you'll need to log in to your Shuttle account by running: ```bash shuttle login ``` This will redirect you to a browser where you can log in to your Shuttle account. Once you're logged in, you can create a new project by running: ```bash shuttle init ``` This will prompt you to select a template for your project. For this guide, we'll use the `axum` template. ![Shuttle Init](/images/blog/troubleshooting-guide/shuttle-init-frameworks.png) This will also ask you to create a new shuttle project (In the Shuttle console), it's important to create the shuttle project so that we can deploy the application to Shuttle Cloud later. ![Shuttle Init Project](/images/blog/troubleshooting-guide/shuttle-init-project.png) After the template selection, you can move into the project directory with: ```bash cd my-project ``` For now, we'll keep the current _code_ as is, (which is a simple Hello World application using Axum), but we'll need to add `tracing` as a dependency (so we can emit logs, metrics, and traces from our application), and activate the `shuttle-runtime` crate's `setup-otel-exporter` feature (so that the emitted logs, metrics, and traces will be exported to BetterStack). We can do both easily with: ```bash cargo add -F shuttle-runtime/setup-otel-exporter shuttle-runtime tracing ``` For reference, the application code should look like this: ```rust use axum::{routing::get, Router}; async fn hello_world() -> &'static str { "Hello, world!" } #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/", get(hello_world)); Ok(router.into()) } ``` #### Deploying the application to Shuttle To deploy the application to **Shuttle**, you can use the following command: ```bash shuttle deploy ``` ![Shuttle Deploy](/images/blog/troubleshooting-guide/shuttle-deploy.png) This will deploy the application to **Shuttle** and make it available at a URL. ![Shuttle Deploy Success](/images/blog/troubleshooting-guide/hello-world-deployment.png) On the Shuttle dashboard, you should see the application deployed successfully. ![Shuttle Dashboard](/images/blog/troubleshooting-guide/shuttle-dashboard-deploy.png) That's it! Our application is now deployed to **Shuttle**, and we can now enable telemetry. #### Setting up Telemetry for Shuttle We'll need to export the telemetry data of the application to **BetterStack** which will collect the data and will let you build dashboards and visualize the data. **Shuttle** uses **Open Telemetry** behind the scenes to collect telemetry and **BetterStack** has support for it, so the integration is a seamless process. You'll need to create a **BetterStack** account, if you don't have one already, make sure to create one at [betterstack.com](https://betterstack.com/). Once you have a **BetterStack** account, you can add a new source for telemetry in **BetterStack**. In the **Sources** tab, click the **Connect Source** button. ![BetterStack Sources](/images/blog/troubleshooting-guide/better-stack-connect-source.png) This will give you a list of options to choose from, choose the **OpenTelemetry** option. Give your source a name and select the **OpenTelemetry** option. ![BetterStack OpenTelemetry](/images/blog/troubleshooting-guide/better-stack-otel-select.png) After that, you can scroll down and press the **Connect Source** button, or you can press enter when focusing on the source name input. This will create your source and give you the necessary credentials to connect BetterStack to Shuttle. ![BetterStack Source Created](/images/blog/troubleshooting-guide/better-stack-source-create-success.png) Now that your BetterStack source is created, you can go back to the Shuttle project page and enable telemetry for your application. This will prompt you to enter your BetterStack credentials, source token and the ingesting host that was provided in the previous step. ![Shuttle Telemetry Enable](/images/blog/troubleshooting-guide/shuttle-telemetry-enable.png) You should now see the telemetry status as **Enabled** on the Shuttle project page. ![Shuttle Telemetry Enabled](/images/blog/troubleshooting-guide/shuttle-telemetry-enabled.png) > Note: If you have already deployed your application before enabling telemetry, you'll need to redeploy your application to start > collecting telemetry data. Simply navigate to the **Deployments** tab and select the latest deployment and click **Redeploy**. Telemetry collection happens automatically (as long as the `shuttle-runtime` crate's `setup-otel-exporter` feature is enabled), so you don't need to do anything else. To see which telemetry data is being collected, [see the Shuttle telemetry docs](https://docs.shuttle.dev/docs/telemetry/overview). The telemetry data available to you depends on your Shuttle tier. The Community tier provides essential system metrics including CPU usage, memory usage, network I/O, and disk I/O. For full access to all telemetry data including application metrics, logs, and traces without export limits, you'll need a Pro or Growth tier subscription. The good news is that Shuttle offers a free trial of both the Pro and Growth tier, giving you an opportunity to explore all the telemetry features before committing to a subscription. You can set up your trial by visiting the [billing page](https://console.shuttle.dev/account/billing) in your Shuttle account. In the next step, we'll explore BetterStack to build dashboards and visualize our telemetry data. #### Building Dashboards with BetterStack Telemetry data is only useful when visualized, **BetterStack** allows you to build custom dashboards and graphs based on the data you collect. When we first created the source, a default dashboard was automatically created for us. We can navigate to the **Dashboards** page and configure it to our liking. ![BetterStack navigate to dashboards](/images/blog/troubleshooting-guide/better-stack-navigate-otel-dashboard.png) This will take you to the default dashboard that was created for us. ![BetterStack Default Dashboard](/images/blog/troubleshooting-guide/better-stack-default-dashboard.png) As you can see, the default dashboard is created with a default layout, but there is no data to be displayed yet, we need to do a few adjustments to get the data to be displayed the way we want. Let's start by adding a **CPU Usage** widget, Shuttle exports the vCPU usage for each project, let's see how much is being used for our project. Click on **Configure** to the right of the widget. ![BetterStack Configure CPU Usage](/images/blog/troubleshooting-guide/better-stack-cpu-widget-configure.png) On the configuration page, select the **Drag and Drop** option, this will allow us to customize the widget with a more user-friendly interface. Search for **vCPU** and click on the `cpu.usage.vcpu` option. ![BetterStack CPU Usage](/images/blog/troubleshooting-guide/better-stack-configure-vcpu.png) The default configuration will measure in percentage, we need to update the Y-axis to measure in virtual CPUs (vCPUs). Change the Y-axis unit to **vCPUs** and click on the **Save** button. ![BetterStack CPU Usage Configured](/images/blog/troubleshooting-guide/better-stack-y-axis-unit.png) Now we can see the vCPU usage for our project. ![BetterStack CPU Usage](/images/blog/troubleshooting-guide/better-stack-vcpu-configured.png) Going back to the dashboard, you should see the widget updated with the correct data. ![BetterStack dashboard updated 1](/images/blog/troubleshooting-guide/better-stack-dashboard-updated-1.png) You can add more widgets with other metrics to get a better understanding of your data. See [this page](https://docs.shuttle.dev/docs/telemetry/overview) to see all the exported metrics by Shuttle. ![BetterStack finished dashboard](/images/blog/troubleshooting-guide/better-stack-finished-dashboard.png) That's it! We have a basic dashboard showing vCPU usage for our project, which is key for spotting issues like resource exhaustion or performance bottlenecks. Telemetry tools help you catch problems early—whether it's high CPU usage, memory leaks, or slow response times. #### Exporting Tracing Data to BetterStack In the previous section [Structured Logging](#structured-logging), we went over how to use the `tracing` crate effectively to structure logs and track request context using spans. When using Shuttle with the `shuttle-runtime/setup-otel-exporter` feature enabled, these structured logs and spans are automatically collected and exported via OpenTelemetry. This means your `tracing::info!`, `tracing::warn!`, etc., events, along with their associated spans and key-value pairs, will appear in **BetterStack** without extra configuration. You can [read more](https://docs.shuttle.dev/docs/telemetry/custom-metrics) about the custom events you can emit using the `tracing` crate. Let's dive in with some real life examples. We need to have the feature `setup-otel-exporter` enabled for the `shuttle-runtime` crate in our `Cargo.toml` file. ```toml [dependencies] shuttle-runtime = { version = "0.53", features = ["setup-otel-exporter"] } ``` This is important for exporting the traces to **BetterStack**. ##### 1. Tracking User Signups (Monotonic Counter) It's common for most businesses to want to track the number of users who have signed up over time, this is a great use case for a monotonic counter. To track the total number of users who have signed up over time, you can use a monotonic counter. This counter only ever increases. Emit a tracing event with a field prefixed by `monotonic_counter.` whenever a signup occurs. ```rust use rand::Rng; async fn user_signup() -> &'static str { let mut rng = rand::rng(); let user_email = format!("user{}@example.com", rng.random_range(1000..9999)); tracing::info!(monotonic_counter.user_signups = 1, %user_email, "New user signed up"); "User signup recorded" } ``` In BetterStack, you can then create a graph for the metric `user_signups` (or similar, depending on collector naming) to visualize the signup trend. The `email` field will be attached as an attribute to the log event itself. ##### 2. Tracking Active Users (Up/Down Counter) To monitor the number of currently logged-in users, you can use an up/down counter (gauge). Increment the counter on login and decrement it on logout. Emit tracing events with fields prefixed by `counter.` ```rust use uuid::Uuid; async fn user_login() -> &'static str { let user_id = Uuid::new_v4(); tracing::info!(counter.active_users = 1, %user_id, "User logged in"); "User login recorded" } async fn user_logout() -> &'static str { let user_id = Uuid::new_v4(); // In reality, you'd get this from session/token tracing::info!(counter.active_users = -1, %user_id, "User logged out"); "User logout recorded" } ``` This allows you to visualize the `active_users` metric in BetterStack, showing the real-time count of users currently using the application. These simple metric conventions within `tracing` provide a convenient way to gain insights into specific application events directly alongside your logs and traces in your observability platform. ##### Visualizing Custom Metrics in BetterStack Let's visualize the data that we've been tracking in the previous sections. We need to add a **Custom Metric** in **BetterStack** to be able to visualize the `user_signups` and `active_users` metrics. Let's create both metrics. Navigate to the create chart page and click on the **Create Metric** button. ![BetterStack Create Metric](/images/blog/troubleshooting-guide/better-stack-create-metric-button.png) On the next page, click on the button **+ Metric** to add a new metric. ![BetterStack Add Metric](/images/blog/troubleshooting-guide/better-stack-add-metric-button.png) Give your metric a name, for example `user_signups` and for the **JSON dot notation** field, enter `monotonic_counter.user_signups` this is the name of the metric that we used using the `tracing::info!` macro. ![BetterStack User Signups Metric](/images/blog/troubleshooting-guide/better-stack-fill-metric-form.png) You can press the **Preview** button to see if **BetterStack** hsa received those events from **Shuttle**, if you have triggered those events, you should see the data in the preview. ![BetterStack Preview](/images/blog/troubleshooting-guide/better-stack-preview-button.png) If you see the correct data, you can then press **Create Metric** to save the metric. We'll do the same for the `counter.active_users` metric as well. ##### Creating charts to visualize the metrics Now that we've introduced the details to BetterStack about our metrics, we can create charts to visualize the data. The process is the same as we did earlier for the vCPU usage widget, we'll create a new chart and add a new widget. ![BetterStack Sum Active Users Chart](/images/blog/troubleshooting-guide/better-stack-metrics-charts.png) We can now see how many active users we have at any given time and also the number of signups over time. This data is always updated in real-time based on the events that are being emitted from our application. ## Conclusion Troubleshooting Rust web applications can be challenging, but with the right tools and techniques, it becomes a manageable and even insightful process. From structured logging with `tracing` to robust error handling using `anyhow`, `eyre` and `thiserror`, `rust-gdb` for debugging, and leveraging telemetry for real-time insights, Rust provides a rich ecosystem to help you identify and resolve issues effectively. By combining these approaches, you can build applications that are not only performant and reliable but also easier to debug and maintain, you'll catch errors before they happen, have a birds-eye view of your application's performance and resources usage, and you'll be able to troubleshoot issues faster and more effectively. --- # Introducing New Shuttle Pricing: Simple and Production-Ready Source: https://www.shuttle.dev/blog/2025/03/19/pricing-update Date: 19 March 2025 Author: shuttle Tags: shuttle Following the launch of our new platform late last year, we're updating our pricing structure Following the launch of our new platform late last year, we're updating our pricing structure to better reflect our evolution into a production-ready platform that developers can rely on for their most critical services. We're particularly excited to introduce our **new Growth Tier**, designed specifically for teams with more complex production workloads that need to handle significantly more traffic and team collaboration features. We want to be transparent about what's changing and why these changes will better serve you, whether you are an individual developer or a growing team. ## Why We're Updating Our Pricing 1. Provide production-ready infrastructure with enhanced isolation, reliability, and monitoring across all tiers 2. Balancing generosity of the free tier with long-term platform sustainability 3. Making it even clearer what you get and what you pay for, responding to some of your feedback 4. Support users with rapid scaling needs 5. Enable team collaboration features We've carefully balanced these adjustments by significantly increasing computing power and adding capabilities that matter most for real-world applications. ## Our Updated Pricing Tiers ### Community Tier: Focused and More Powerful **Free Forever** We've enhanced our free tier to better serve developers learning Rust and get meaningful projects off the ground: - **5x more computing power**: 0.25 vCPU (from 0.05) - **2.5x more memory**: 0.5GB RAM (up from 0.2GB) - **Custom domain support** included - **Running Projects**: 1 project (from 3) perfect for showcasing what you can do in Rust - **Spot Instance**: Instances may occasionally restart to maintain optimal platform performance - **7x longer log retention**: 7 days (up from 1 day) - **Monitoring & Observability**: Basic resource usage metrics through third-party integrations - **Simplified builds**: 100 build minutes per month - **Database storage**: 0.5GB (plenty to start small or experiment) While we've reduced the number of projects, each project now gets significantly more computing resources, making it possible to build and run more demanding applications. ### Pro Tier: Production-Ready Infrastructure **$20/month + usage** with a 14 day free trial! (excluding usage) If you're running production apps or you've outgrown the Community tier, the Pro plan is here to keep you covered: - **Reserved instances**: Dedicated resources with full VM isolation - **Scalable vCPU & Memory** to support larger need**s** - **3 projects included:** Down from 15 - **Add more projects: Add** up to 10 projects - **Minimum size for additional projects:** 0.5 vCPU and 1GB of Memory - **Monitoring & Observability**: Custom application metrics, logs & traces - **14-day log retention** (up from 7) - **Network egress included adjusted to 1GB** (from 10GB). - **Shared database storage included is now 0.5GB** (from 10GB). - **Usage-based pricing** for more flexibility - **Enhanced support**: 1-business day response time and migration assistance We've moved to a more flexible usage-based model where you only pay for what you need beyond the basic allocation, focusing on a stable, production-ready environment - plus our new monitoring integration helps you keep everything running smoothly. ### Growth Tier: Scale with Confidence (New!) **Starting at $250/month + usage** with a 14 day free trial! (excluding usage) For teams with more complex production workloads or the need to handle more traffic. - **10 projects included** (scalable up to 50) - **Minimum size for additional projects:** 0.5 vCPU and 1GB of Memory - **Horizontal scaling** with configurable replicas (up to 10 instances per project) - **Zero downtime deploys** with rolling deployments - **Load balancing** up to 1,000 requests per second - **Dedicated database** for consistent performance - **Team access** for up to 10 users - **30-day log retention** - **Priority business support** in either Discord or Slack with a dedicated channel If your organization needs collaboration features or you want to scale seamlessly, Growth Tier offers robust infrastructure without the complexity of managing it yourself. ### Enterprise Tier: Built for Big Ambitions (Coming Soon) **Custom pricing + usage** For large organizations with advanced security, compliance, or custom infrastructure needs. - **Bring your own cloud or single tenant** options - **Custom hardware configurations** - **Multi-region flexibility** - **Advanced Security & Compliance** - **Custom CI Pipelines** - **Priority support** with custom SLA - **Architecture design reviews** If you have strict operational, security, or compliance needs - or want to integrate Shuttle deeply into your existing ecosystem - stay tuned for Enterprise. ## Supporting Your Transition We understand that pricing changes can impact your projects. To ensure a smooth transition: - **Existing projects**: Will continue running with current resources for 30 days - **Migration support**: Our team is available in Discord to help with questions ## Frequently Asked Questions **Q: Why reduce the number of projects in** the **Community and Pro tiers?** A: With our new VM isolation and increased computing power per project, we're focusing on giving developers more resources for meaningful applications rather than many smaller projects. **Q: How does usage-based pricing work?** A: Your subscription includes base allocations, with transparent per-use pricing for additional resources like compute, storage, and bandwidth. **Q: What happens to my existing projects?** A: All existing projects will continue running without interruption during the 30-day transition period, giving you time to adjust to the new resource allocations. Check out our [complete FAQ](/) for more details on billing, upgrading/downgrading, and resource management. ## Get Started Ready to explore the new Shuttle? - View detailed [pricing and resource information](https://www.shuttle.dev/pricing) - Read our [documentation](https://docs.shuttle.dev/) - Join our [Discord community](https://discord.gg/shuttle) for support We're excited about these changes and how they'll help you build better applications. Our team is here to support you through this transition and beyond. Let's build something amazing together! 🚀 --- # Exporting your metrics to Better Stack with Shuttle Source: https://www.shuttle.dev/blog/2025/02/19/using-shuttle-with-betterstack Date: 19 February 2025 Author: josh Tags: shuttle, observability How to use Shuttle's new M&O feature to send your metrics to Better Stack ## Introduction We're excited to announce the official integration of Better Stack with Shuttle! This new integration makes it easier than ever to export your metrics from Shuttle to Better Stack, allowing you to gain real-time insights into the performance of your applications. Whether you're monitoring system health, analyzing traffic patterns, or troubleshooting issues, Better Stack's powerful visualization tools combined with Shuttle's seamless metrics export will empower you to stay on top of your project's observability like never before. In this article, we'll guide you through the simple setup process to start leveraging this integration today. ## Getting set up To get started, you'll need two things: 1. A BetterStack account. If you don't have one, you can sign up [here.](https://betterstack.com/) 2. A Shuttle project you'd like to add telemetry to (or a new project). Note that once you add the BetterStack integration, you'll need to re-deploy your project. If you haven't deployed your project before, you can just deploy it and it'll have BetterStack telemetry (if you've enabled the BetterStack integration _prior_ to deploying for the first time). ## Adding a new telemetry source The first thing you'll need to do once you've logged into BetterStack is to click onto the Sources section, then go to Connect Source which will allow you to add a new telemetry source for BetterStack. ![image.png](/images/blog/betterstack-integration/1.png) Once you've chosen a name for your new telemetry source (we _generally_ use the project name, but you're free to use any name you'd like), click the OpenTelemetry option in the "platform" section: ![image.png](/images/blog/betterstack-integration/2.png) Once done, scroll _all the way_ to the bottom of the page and hit the "connect source" button: ![image.png](/images/blog/betterstack-integration/3.png) Next, grab the source token from the new menu (note that if you just click on the field it should automatically copy the value for you). ![image.png](/images/blog/betterstack-integration/4.png) Next, you'll need to navigate to the project you want to enable telemetry for in your Shuttle console by clicking on it on the left hand side (or from the projects menu), clicking on the Settings tab, then clicking to enable BetterStack. Paste your source token in, then press Apply. ![image.png](/images/blog/betterstack-integration/5.png) > **Reminder**: if your project was already running when you clicked "Apply", you'll need to redeploy it before telemetry will start to flow. If you haven't deployed your project for the first time yet, telemetry will start flowing immediately when you do. Done! Now let's talk about how to use the telemetry export. ## How to use the telemetry export Shuttle by default sets up a `tracing_subscriber` that will automatically log information to stdout. Now that you've set up BetterStack, we'll need to tell it to also send telemetry data to BetterStack. To get started, you'll need to add the `setup-otel-exporter` feature to `shuttle-runtime` . You can do this by running the following command: ```bash cargo add shuttle-runtime -F setup-otel-exporter ``` Optionally, you can manually adjust the dependency in your `Cargo.toml` if you prefer: ```toml shuttle-runtime = { version = "0.52.0", features = ["setup-otel-exporter"] } ``` Next, you'll need to ensure your project has `tracing` as a dependency as well. As before, you can add it with `cargo`: ```markdown cargo add tracing ``` Optionally, you can manually add it as well, just be sure to check [crates.io](https://crates.io/crates/tracing) for the most recent version. Once you _have_ `tracing` available in your project, all you need to do is add some tracing to the functions you're using. Like so: ```rust async fn hello_world() -> &'static str { tracing::info!(counter.hello = 1, "Hello world from OTel!"); "Hello, world!" } ``` Doing this will send an `info` level log to stdout, as well as BetterStack, saying `Hello world from OTel!`. The log object in BetterStack will look something like this (note that log exports are a Pro feature): ```json { "attributes": { "code.filepath": "src/main.rs", "code.lineno": 4, "code.module_path": "otel_me", "counter.hello": 1 }, "dropped_attributes_count": 0, "dt": "2025-02-04T15:56:27.068644985Z", "message": "Hello world from OTel!", "observed_timestamp": "2025-02-04T15:56:27.068649239Z", "resources": { "service.name": "otel-me", "service.version": "0.1.0", "shuttle.deployment.env": "production", "shuttle.project.crate.name": "otel_me", "shuttle.project.id": "proj_01JK8SHBZQ0XF0TKW0EDWBJ8NH", "shuttle.project.name": "otel-me", "telemetry.sdk.language": "rust", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "0.27.1" }, "severity_number": 9, "severity_text": "INFO", "source_type": "opentelemetry" } ``` You can additionally see here that we have added a counter - because we use `tracing-opentelemetry` under the hood, Shuttle supports three types of metrics you can use: monotonic counters, counters and histograms. You can find more about this [on the `tracing-opentelemetry` docs.](https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/struct.MetricsLayer.html) ## Creating Betterstack dashboards from OTel data Now that we've done the hard part, we can create some dashboards from the data. If you go back to the telemetry section of the BetterStack dashboard, you can check out Dashboards where a new dashboard will have been created for you automatically. You can check out the dashboards like so: ![image.png](/images/blog/betterstack-integration/6.png) Next, you'll see some dashboard widgets that have been automatically created but will likely show "empty" (they're generic and assume the metrics being visualized are actually the _internal_ metrics of an OpenTelemetry collector instance, which your project is obviously not 😅). The default view for configuring widgets is [SQL](https://betterstack.com/docs/logs/dashboards/sql-queries/), but BetterStack also offers: - [metrics-from-filtered-logs](https://betterstack.com/docs/logs/dashboards/logs-to-metrics/) - [promql (beta)](https://betterstack.com/docs/logs/using-logtail/simplified-promql/) - a visual drag-and-drop interface > **Note:** Naturally, you're free to use the method that suits you best, but we generally recommend the drag-and-drop interface. Check out how we created our own graph to measure CPU usage by adding in `cpu_usage_vcpu` against our project name. We've also set the Y axis to use vCPU units. ![image.png](/images/blog/betterstack-integration/7.png) You can also additionally find out more about what other values you can use by checking out our API reference. --- # What even is observability, anyway? Source: https://www.shuttle.dev/blog/2025/02/17/what-is-observability-rust Date: 17 February 2025 Author: josh Tags: rust, observability Looking at what observability is, how it can help you and how to use it with Rust & Shuttle ## Introduction You've probably heard the term "observability" thrown around, but what does it actually mean? At its core, it's all about making your systems easier to understand and troubleshoot by seeing what's going on inside them. It's more than just logs and metrics—it's about having a clear view of your system's health in real-time. In this article, we'll break down what observability really is and why it's crucial for keeping your apps running smoothly. ## What makes observability useful? Observability can be as simple or as powerful as you want. It can be something like this: ```rust println!("I just did a thing!") ``` Or it can be all the way up to complete distributed tracing systems that outputs every span down to the trace in Datadog and also archives your logs in S3. Both have their use cases and have varying levels of complexity for a technical implementation. Either way, observability is an important part of being able to debug issues within your application. It's even moreso important in production because when things break, there's consequences - typically in the form of your customers voting elsewhere with their wallet. Nobody wants that (except your competitors). ## A quick look at the observability ecosystem The ecosystem for observability is quite sizeable. There are complete observability solutions like [Grafana](https://grafana.com/) (which is self-hostable) and [Datadog](https://www.datadoghq.com/), there are also specialized tools focused on specific aspects like [Prometheus](https://prometheus.io/) for metrics collection, [Elasticsearch](https://www.elastic.co/elasticsearch) for log aggregation, and [Jaeger](https://www.jaegertracing.io/) for distributed tracing. These tools often integrate with each other, providing a more holistic view of system performance and health. Other solutions like [Honeycomb](https://www.honeycomb.io/) and [New Relic](https://newrelic.com/) offer deep insights into application behavior, with an emphasis on high-resolution data for debugging and optimizing complex systems. In the Rust ecosystem, while observability isn't as mature as in other languages, there are promising tools emerging. Crates like `tracing` and `tokio-console` are gaining traction for their ability to provide structured logging and performance insights, making it easier for Rust developers to monitor and debug their applications. These tools, combined with integrations into cloud observability platforms, make it easier to build performant and reliable systems. ## Powerful tracing with OpenTelemetry Many of the aforementioned platforms also support OpenTelemetry (often referred to as OTel) - which is an open standard that allows telemetry tools, platforms, and services from different vendors to operate together. It provides reference implementations of SDKs in all popular programming languages, as well as a data collector process that can receive the telemetry data from a variety of sources, transform it, and send to a variety of consumers. They even have native Rust support with their `opentelemetry` crate as well as their [tracing-opentelemetry](https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/) crate. This is huge, as Rust is typically a popular choice for distributed systems. ### How does it work? Unlike regular logging systems, OpenTelemetry expands upon the idea of "spans" as unit of work within your program. These spans can represent either a unit of time, or some function execution (for example). Those spans are often used in traces and can consist of either more spans or lower-level operations which can correlate to what is happening at the time of a recorded span - for example, a database record insertion operation. Spans can also additionally have attributes which act as labels which allows the receiving observability platform to facilitate navigation between different spans/traces. While this is initially much more confusing than "just logs", it adds much more context which can additionally be shared over multiple machines or systems. This allows you to create a much more complete picture of what may have caused an incident. For example, let's say you want to know what happened in a service in a VM that sent a request to another service in a different VM which has then crashed. By aligning the IDs of the traces, you can create a full picture of the workflow up until that point. Interested in trying it out? Check out [our guide on using OTel.](https://www.shuttle.dev/blog/2024/04/10/using-opentelemetry-rust) ## How do I use observability on Shuttle? We're excited to announce that **starting this Wednesday**, all users will have access to our new telemetry integration for BetterStack. Our Shuttle runtime takes care of the whole process for you end-to-end, allowing you to simply use your API key with a relevant supported provider and it'll **just** work. Beyond our initial launch, we also have plans to expand to other provide other telemetry export destinations. What observability platform would you like to see supported next? [Send a message in our Github discussion now.](https://github.com/shuttle-hq/shuttle/discussions/1980) --- # Provisioning TLS Certificates in Rust With ACME Source: https://www.shuttle.dev/blog/2025/02/06/provisioning-tls-certificates-with-acme-in-rust Date: 6 February 2025 Author: oddgrd Tags: rust, acme, tls How we provision TLS certificates for custom domains in Rust using the `instant_acme` crate. ## Introduction At Shuttle, we enable users to easily deploy their Rust backend applications to our platform. Each deployed application can receive HTTPS traffic at its default domain: `-.shuttle.app`. We use a wildcard certificate that automatically covers all subdomains of `shuttle.app`. However, users can use any custom domain they want, and we need an SSL/TLS certificate for each. To facilitate this, users can request a certificate for any domain they own through the [Shuttle CLI](https://docs.shuttle.dev/docs/domain-names#set-up-ssl-certificate), after setting a DNS record that points their domain to our public IP. To streamline this process, we needed a simple, automated way to provision multiple certificates from a trusted certificate authority. This is where the ACME protocol comes into play. ![High level diagram showing the certificate provisioning process](/images/blog/provisioning-tls-certificates-with-acme/introduction-diagram.svg) ## The ACME Protocol The ACME (Automated Certificate Management Environment, [RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555)) protocol was developed by the [Internet Security Research Group](https://en.wikipedia.org/wiki/Internet_Security_Research_Group) for their Let's Encrypt service. It is an open standard that allows for the automated provisioning and renewal of certificates from a certificate authority. [Let's Encrypt](https://letsencrypt.org/) is the most popular certificate authority that implements ACME, and in fact the only way to get a certificate from them is with an ACME client. They're a non-profit, with the express goal of making the internet more secure by allowing anyone to easily obtain certificates, free-of-charge. We use Let's Encrypt at Shuttle, but there are other certificate authorities that implement the ACME protocol, for example [ZeroSSL](https://zerossl.com/) and [Google Trust Services](https://pki.goog/). Now we know a little bit at a high level about ACME, but how does it work in practice? ## ACME Challenges The ACME protocol allows us to provision certificates from an ACME server programmatically. However, for us to be allowed to provision a certificate for a given domain, we must first prove that we control that domain. In the ACME paradigm that is done by completing a challenge, and in the current version of the ACME standard, there are three [challenge types](https://letsencrypt.org/docs/challenge-types/). ### HTTP-01 This is the simplest and most commonly used challenge, and the one we currently use to support custom domains on Shuttle. For this challenge, you need to start a server under the domain you are requesting a certificate for, that is reachable by the ACME server (e.g. Let's Encrypt) on port 80. The ACME server will send an HTTP request to the endpoint `http:///.well-known/acme-challenge/`, and it will expect a specific value in the response, which we'll go in depth on later. The main drawbacks to using this challenge type, is that it does not support provisioning certificates for wildcard domains. Furthermore, it requires you to serve the challenge response on port 80, which depending on your infrastructure setup can be challenging. For the practical part of this article, we'll focus on this challenge type. ### DNS-01 This challenge requires you to prove that you control the DNS for a domain by putting a specific value under a TXT record in the DNS zone for that domain. The ACME server will then do an authoritative lookup for the record, and if it has the right value, you'll be allowed to provision a certificate. This can be challenging to automate, since you're reliant on your DNS provider to have an API you can call to set the TXT record. At Shuttle, we use this challenge internally when we provision a certificate for our default wildcard domain, `*.shuttle.app`. In the future, we will use it to allow users to request certificates for wildcard custom domains, as this is the only challenge type that can be used for wildcard certificates. We will then initiate the challenge on our end, before returning the value to the user, along with instructions on how to set it in a TXT record in their DNS. ### TLS-ALPN-01 Like the HTTP-01 challenge, this challenge, which is developed as a [separate standard](https://datatracker.ietf.org/doc/html/rfc8737), also requires you to run a server on your domain that is reachable by the ACME server. But unlike HTTP-01, you don't need to serve it on port 80. You are required to start a TLS server on port 443, that responds to specific connection attempts using the ALPN extension with identifying information. To prove that you control the domain, when the ACME server makes a connection to an address that is resolved for your domain, you need to present a self-signed certificate with some identifying information, as well as a signed challenge token. ## Using an ACME client in Rust with `instant_acme` It's time to see how this all works in practice, in Rust! Thanks to the Rust open-source community, we don't have to implement our ACME client from scratch. There are a couple of crates to choose from, but the most up-to-date and actively maintained is the [`instant_acme`](https://github.com/djc/instant-acme) crate, built on top of `tokio` and `rustls`. It supports all the ACME challenge types mentioned above. ### Creating an ACME Account The first step in the ACME certificate provisioning flow is creating an [ACME account](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2), which `instant_acme` makes very simple. When we call `Account::create`, the library sets up a Hyper client under the hood and creates an [ECDSA key pair](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm), and there we have it, our ACME client! We'll re-use this client and key pair throughout the provisioning process. The public key from the key pair will be included in the account creation request, and the request, as well as future requests using this client, will be signed using the private key. We'll use the Let's Encrypt staging environment for this example, since it's [recommended for development and testing](https://letsencrypt.org/docs/staging-environment/). ```rust use instant_acme::{LetsEncrypt, NewAccount, Account}; use anyhow::Context; #[tokio::main] async fn main() -> anyhow::Result<()> { let account = NewAccount { // Optionally add a list of contact URIs (like mailto:info@your-domain.com). contact: &[], terms_of_service_agreed: true, only_return_existing: false, }; // We'll use methods on the returned account struct for future calls to the ACME server. let (account, _credentials) = Account::create(&account, &LetsEncrypt::Staging.url(), None) .await .context("failed to create acme account")?; } ``` ### Creating an ACME Order With our account set up, we're ready to create an [order](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3) for a certificate. The order struct is a simple state machine, in accordance with the ACME spec. It has a status, a list of authorizations and an optional certificate. We'll go through the various steps of the order to drive the state to `Ready`, at which point we'll be allowed to request a certificate. ![Diagram showing the order state machine](/images/blog/provisioning-tls-certificates-with-acme/order-state-machine.svg) The order will have one authorization per domain we request a certificate for, that the ACME server requires the client to complete. For each authorization, we need to chose which challenge type we want to complete, e.g. HTTP-01 or DNS-01. For this example, we'll use an HTTP-01 challenge. > The instant_acme repository provides an > [example using a DNS-01 challenge](https://github.com/djc/instant-acme/blob/main/examples/provision.rs), > which the example below using HTTP-01 is largely based on. ```rust use instant_acme::{ AuthorizationStatus, ChallengeType, Identifier, NewOrder }; use anyhow::{bail, Context}; #[tokio::main] async fn main() -> anyhow::Result<()> { [...] let domain = "my-domain.com"; // Using the account we created earlier, create an order for our domain. let mut order = account .new_order(&NewOrder { identifiers: &[Identifier::Dns(domain.to_string())], }) .await .context("failed to order certificate")?; // Request authorizations for our order from the ACME server. let authorizations = order .authorizations() .await .context("failed to retrieve order authorizations")?; // There should only be 1 authorization as we only provided 1 domain above. let authorization = authorizations .first() .context("there should be one authorization")?; if !matches!(authorization.status, AuthorizationStatus::Pending) { bail!("order should be pending"); } // We want to complete an HTTP-01 challenge for this example, so we // extract it from the authorization. It holds the token we need to // complete the challenge. let challenge = authorization .challenges .iter() .find(|c| c.r#type == ChallengeType::Http01) .ok_or_else(|| anyhow::anyhow!("no http01 challenge found"))?; } ``` ### Setting Up An HTTP-01 Challenge Server Now, as I mentioned earlier, to complete an HTTP-01 challenge we need to serve a specific value at the `http://my-domain.com/.well-known/acme-challenge/{*token}` endpoint. The token will be in the challenge we received from the ACME server with our order. For any request to this endpoint, we will return the same token, signed with the private key from our ACME account credentials, in the response body. This signed token is known as a [key authorization](https://datatracker.ietf.org/doc/html/rfc8555#section-8.1). Before we set the challenge to ready, we need to persist the challenge token and key authorization somewhere, so we can serve it in response to ACME server requests. At Shuttle, we use an external database for this, since there are many instances of our ACME client running at a given time, behind a load balancer, and they all need to be able to complete any challenge. But in the interest of keeping this example simple, we'll just store them in a `HashMap`. ![Diagram showing the flow of an ACME HTTP-01 challenge](/images/blog/provisioning-tls-certificates-with-acme/http-01-challenge.svg) To serve the challenge endpoint, we'll set up a simple Axum server, but you could use any framework of your choosing, or even just a simple Hyper server if you want something bare-bones. We'll start by creating some utility functions to set up our Axum server. ```rust use axum::{ extract::{Path, State}, http::StatusCode, routing::any, Router, }; use instant_acme::ChallengeType; /// Set up a simple acme server to respond to http01 challenges. pub fn acme_router(challenges: HashMap) -> Router { Router::new() .route( "/.well-known/acme-challenge/{*token}", any(http01_challenge), ) .with_state(challenges) } /// Respond to HTTP-01 challenges by extracting the token from the path of the request, and then /// using the token to look up the matching key authorization in our internal state. pub async fn http01_challenge( State(challenges): State>, Path(token): Path, ) -> Result { tracing::info!(%token, "received HTTP-01 ACME challenge"); if let Some(key_auth) = challenges.get(&token) { Ok({ tracing::info!(%key_auth, "responding to ACME challenge"); key_auth.clone() }) } else { tracing::warn!(%token, "didn't find acme challenge"); Err(StatusCode::NOT_FOUND) } } #[tokio::main] async fn main() -> anyhow::Result<()> { [...] let challenge = authorization .challenges .iter() .find(|c| c.r#type == ChallengeType::Http01) .ok_or_else(|| anyhow::anyhow!("no http01 challenge found"))?; let challenges = HashMap::from([( challenge.token.clone(), order.key_authorization(challenge).as_str().to_string(), )]); tracing::info!("challenges: {:?}", challenges); // We use the utility function we created below to configure our Axum router. let acme_router = acme_router(challenges); // NOTE: when the ACME server sends the challenge request to your domain, it will always // connect on port 80, which is specified in the standard. // At Shuttle we have a load balancer in front of the ACME client, which listens for ACME // requests on port 80, and forwards them to the challenge server running on a different port. let address = "0.0.0.0:5002"; let listener = tokio::net::TcpListener::bind("0.0.0.0:5002").await.unwrap(); // Start the Axum server as a background task, so it's running while we complete the challenge // in the next steps. tokio::task::spawn(async move { axum::serve(listener, acme_router).await.unwrap() }); tracing::info!("serving HTTP-01 challenge server at: 0.0.0.0:5002"); } ``` ### Initiate The HTTP-01 Challenge Now that our HTTP-01 challenge server is up and running in the background, we can proceed with our order. The next step is to communicate to the ACME server that we are ready to complete the challenge. ```rust use anyhow::{bail, Context}; use instant_acme::OrderStatus; #[tokio::main] async fn main() -> anyhow::Result<()> { [...] // Notify the ACME server that we are ready to complete the challenge. order .set_challenge_ready(&challenge.url) .await .context("failed to notify ACME server that challenge is ready")?; // We now need to wait until the order reaches an end-state. We refresh the order in a loop, // with exponential backoff, until the order is either ready or invalid (for example if our // challenge server responded with the wrong key authorization). let mut tries = 1u8; let mut delay = Duration::from_millis(250); loop { tokio::time::sleep(delay).await; let state = order.refresh().await.unwrap(); if let OrderStatus::Ready | OrderStatus::Invalid = state.status { tracing::info!("order state: {:#?}", state); break; } delay *= 2; tries += 1; if tries < 15 { tracing::info!(?state, tries, "order is not ready, waiting {delay:?}"); } else { tracing::error!( tries, "timed out before order reached ready state: {state:#?}" ); bail!("timed out before order reached ready state"); } } let state = order.state(); if state.status != OrderStatus::Ready { bail!("unexpected order status: {:?}", state.status); } tracing::info!(?state, "challenge completed"); ``` ### Requesting a certificate with a CSR Now that the challenge is completed, we are ready to [finalize the order](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.4) and request the certificate. First, we'll need to create a [certificate signing request](https://en.wikipedia.org/wiki/Certificate_signing_request) (CSR). Creating the CSR is outside the scope of the `instant_acme` crate, so we'll need to use another library for that, [`rcgen`](https://github.com/rustls/rcgen). While requesting the certificate should be fast, it won't be ready immediately, so here again we will poll until it's ready. ```rust use anyhow::{bail, Context}; use rcgen::{CertificateParams, DistinguishedName, KeyPair}; #[tokio::main] async fn main() -> anyhow::Result<()> { [...] // Create a CSR for our domain. let mut params = CertificateParams::new(vec![domain.to_owned()])?; params.distinguished_name = DistinguishedName::new(); let private_key = KeyPair::generate()?; let signing_request = params.serialize_request(&private_key)?; // DER encode the CSR and use it to request our certificate from the ACME server. order .finalize(signing_request.der()) .await .context("failed to finalize order")?; // Poll for certificate, do this for a few rounds. let mut cert_chain_pem: Option = None; let mut retries = 5; while cert_chain_pem.is_none() && retries > 0 { cert_chain_pem = order .certificate() .await .context("failed to get the certificate for order")?; retries -= 1; tokio::time::sleep(Duration::from_secs(1)).await; } let Some(chain) = cert_chain_pem else { bail!("failed to get certificate for order before timeout"); }; tracing::info!("certificate chain:\n\n{}", chain); tracing::info!("private key:\n\n{}", private_key.serialize_pem()); Ok(()) } ``` And there we have it! A certificate we can serve for my-domain.com. At Shuttle, we store these certificates in a database, then serve them in the TLS handler of our Pingora based proxy. Note that the example in this article will fail if ran locally, since the HTTP-01 challenge server has to be served on port 80 on the IP that the domain DNS record points to. If you want to test it locally, you can use Pebble, a small and simple ACME test server. You can see an example of that, and the full source code for this article, in the [repository](https://github.com/oddgrd/acme-article). --- # Building a Discord Summarizer bot with DeepSeek, Rig & Rust Source: https://www.shuttle.dev/blog/2025/01/29/discord-summarizer-deepseek-rig-rust Date: 29 January 2025 Author: josh Tags: rust, guide, ai, discord Exploring real life use cases with AI, using DeepSeek and the Rig AI framework. ## Introduction Whether you're a community manager or part of a collaborative team, staying on top of conversations can be challenging. What if you could automate the process and generate concise, Markdown-based summaries of your server's activity every day? That's exactly what we're going to build in this tutorial! In this guide, we'll create a Discord bot that listens to your server, collects messages from the previous day, and generates a neatly formatted Markdown summary. The summary will highlight key points and group messages by topics and authors. This bot will save time, improve organisation, and make it easier to follow conversations. We'll also be using the new DeepSeek R1 model to generate our summaries with [Hyperbolic,](https://hyperbolic.xyz/) an AI cloud provider that provides GPU renting services as well as an AI inference API. Interested in checking out the full example? Find it [here.](https://github.com/joshua-mo-143/discord-message-summarizer-bot) ## Pre-Requisites Before we get started, you'll need to make sure you have the following installed: - the Rust programming language - The `cargo-shuttle` CLI tool (for deploying to Shuttle & project initialisation) - You will also additionally need a Hyperbolic API key. To get one, follow the instructions below: - You'll need to [make a Hyperbolic account](https://app.hyperbolic.xyz/) from the application. - Once registered, navigate to the [Settings](https://app.hyperbolic.xyz/settings) page on the dashboard. - You'll be able to view and copy your Hyperbolic API Key from there. Keep ahold of your API key as we'll be using it later. - A Discord API key is also required. If you don't already have one, follow the instructions to get one (it's totally free!): - Click the New Application button, name your application and click Create. - Navigate to the Bot tab in the lefthand menu, and add a new bot. - On the bot page click the Reset Token button to reveal your token. Keep ahold of this token as we'll be using it later. - For the sake of this example, you also need to scroll down on the bot page to the Message Content Intent section and enable that option. - You will also need `sqlx-cli` installed (the SQLx CLI tool) which will allow you to easily manage SQL migrations versions. ## Getting Started To get started, we will spin up a framework boilerplate that deploys a bot using the `serenity` Discord bot framework: ```bash shuttle init --template serenity ``` This will create a template with the following: - A `Bot` unit struct that has a basic `EventHandler` implementation - A `main` function that sets up a `serenity` client for automatic deployment to Shuttle You'll also notice that a `Secrets.toml` file has been created. We'll extend it to include the Hyperbolic API key & Discord we obtained before. Note that you will also need a Discord channel ID where you want your bot to send the reports to - you can select a channel by simply right clicking it and getting the channel ID. ```toml # Secrets.toml DISCORD_TOKEN = 'the contents of my discord token' CHANNEL_ID = 'the ID of the Discord channel to send your reports to' HYPERBOLIC_API_KEY = 'your hyperbolic API key' ``` ### Adding crate dependencies Before we continue, let's add our crate dependencies. You can add all the required dependencies by copying the one-liner below: ```bash cargo add chrono rig-core serde-json shuttle-shared-db sqlx -F \ chrono/serde,shuttle-shared-db/sqlx,shuttle-shared-db/postgres,\ sqlx/runtime-tokio-rustls,sqlx/postgres,sqlx/chrono,sqlx/macros ``` Let's closely examine what our new dependencies are for: - **rig-core:** The `rig` framework. - **sqlx:** A library for working with SQL. We add the `runtime-tokio-rustls` and `postgres` features (both are mandatory), as well as the `macros` and `chrono` features for enabling usage with the `chrono` crate. - **shuttle-shared-db:** A crate that allows provisioning of a Postgres database from Shuttle servers (and locally, Docker). We can allow it to output a connection pool from the Shuttle resource annotation - **chrono:** A crate for dealing with time. - **serde-json:** A crate for (de)serializing to and from JSON. ## Let's Build! ### Migrations Before we do anything, we need to create our migration table. Let's create our first migration - we'll make it reversible: ```bash sqlx migrate add -r init ``` This creates a folder called `migrations` in your project root and additionally creates an `up` and `down` file for creating and reversing migrations, respectively. We'll want to store both received messages, as well as summaries: ```sql -- Add up migration script here create table if not exists messages ( id int generated always as identity primary key, data jsonb not null, created_at timestamptz default current_timestamp not null ); create table if not exists summaries ( id int generated always as identity primary key, summary varchar not null, date date not null, created_at timestamptz default current_timestamp not null ); ``` Next, we'll set up our `down` file which will reverse the migration. You should not need this in most cases, but in case you want to drop the table for whatever reason (e.g. during development or testing), you can do so: ```sql -- Add down migration script here drop table if exists messages; drop table if exists summaries; ``` ### Storing Messages To store messages, we will upgrade our `Bot` struct (which acts as the event handler struct) to additionally include our `PgPool`. When we implement `EventHandler` for our struct, we will then be able to access the database pool to make insertion queries. ```rust // main.rs use sqlx::PgPool; struct Bot { pool: PgPool } // add convenience init method impl Bot { fn new(pool: PgPool) -> Self { Self { pool } } } ``` Next, we will make it so that any and all messages created will be stored in our Postgres instance as a JSON object - we will adjust our `impl EventHandler for Bot` block to simply convert the message into a raw JSON string then store it. ```rust // main.rs use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::prelude::*; #[async_trait] impl EventHandler for Bot { async fn message(&self, _: Context, msg: Message) { // note we can basically garuantee this will be a JSON compatible // object, so we can unwrap here while developing let message = serde_json::to_string_pretty(&msg).unwrap(); sqlx::query("INSERT INTO messages (data) values ($1)") .bind(message) .execute(&self.pool) .await .unwrap(); } async fn ready(&self, _: Context, ready: Ready) { info!("{} is connected!", ready.user.name); } } ``` That's pretty much it for the Discord bot interactions at a basic level. Nothing else required! Note that this is a relatively naive implementation. If you wanted to improve this, a good way to do so would be to have some kind of durable message queueing to ensure that there is no information loss. ### Creating a Summarization Agent This part is fortunately quite simple. For our summaries, we only need to implement a single AI agent that summarizes all the messages. We then return the result. ```rust // llm.rs use std::env; use rig::completion::Prompt; pub async fn summarize_messages(messages_json: String) -> Result> { // Create OpenAI client let client = rig::providers::hyperbolic::Client::new( &env::var("HYPERBOLIC_API_KEY").expect("HYPERBOLIC_API_KEY not set"), ); // Create agent with a single context prompt let summarizer_agent = client .agent("deepseek-ai/DeepSeek-R1") .preamble("Your job is to summarize a list of Discord messages from a single day in JSON format. The output should be in Markdown and is intended to provide a summary of important events and conversation topics from the day given. If there are no messages, simply respond 'Nothing was discussed.'") .build(); let result = summarizer_agent.prompt(&messages_json).await?; Ok(result) } ``` ### Creating and sending summaries Now for the fun part - creating and sending summaries (to a Discord channel of our choosing!). We'll split this into a couple of separate functions: - One for generating the report itself (so that we can extend it to be used elsewhere other than the scheduled task that sends generated reports to a Discord channel) - One for running a scheduled task (that carries out report generation & sending) ```rust // main.rs pub mod llm; pub async fn generate_report(pool: &PgPool) -> Result> { let date_yesterday = chrono::Utc::now().date_naive() - chrono::Days::new(1); let res: Option = sqlx::query_scalar("SELECT jsonb_agg(data) FROM messages WHERE created::date = $1") .bind(date_yesterday) .fetch_optional(pool) .await?; let Some(res) = res else { return Err("There were no messages in the database :(".into()); }; let raw_json = serde_json::to_string_pretty(&res).unwrap(); let prompt_result = match llm::summarize_messages(raw_json).await { Ok(res) => res, Err(e) => { return Err( format!("Something went wrong while trying to summarize messages: {e}").into(), ) } }; if let Err(e) = sqlx::query("INSERT INTO summaries (summary, date) VALUES ($1, $2)") .bind(&prompt_result) .bind(date_yesterday) .execute(pool) .await { return Err(format!("Error ocurred while storing summary: {e}").into()); }; Ok(prompt_result) } ``` The other half of this is creating our loop for automatically sending summaries. To ensure that the loop properly executes the task on time, we use `tokio::time::Interval` which is more accurate compared to simply just using the `tokio::time::sleep()` method. ```rust // main.rs use serenity::all::{ChannelId, Http}; pub async fn automated_summarized_messages( channel_id: ChannelId, token: String, pool: PgPool, ) { let http_client = Http::new(&token); // here we wait 24 hours let mut interval = tokio::time::interval(Duration::from_secs(86400)); loop { // wait until the next tick // we wait 24 hours here as there may be no messages in Discord interval.tick().await; // instead of returning an error here, we simply continue // due to the error potentially not being related to the bot runtime let report = match generate_report(&pool).await { Ok(res) => res, Err(e) => { println!("{e}"); continue; } }; if let Err(e) = http_client.send_message(channel_id, Vec::new(), &report).await { println!("Something went wrong while sending summary message: {e}"); }; } } ``` ### Hooking it all back up The first time we need to do is to add our `Postgres` annotation from `shuttle-shared-db` - to do so, we simply add it as a function argument to our main function (shown as annotated by the runtime macro): ```rust // main.rs use shuttle_runtime::SecretStore; #[shuttle_runtime::main] async fn serenity( #[shuttle_runtime::Secrets] secrets: SecretStore, #[shuttle_shared_db::Postgres] pool: PgPool, // add annotation here ) -> shuttle_serenity::ShuttleSerenity { sqlx::migrate!().run(&pool).await.expect("Couldn't run database migrations"); // code goes here } ``` Running your program locally will now use Docker to provision a Postgres database. Additionally, when deployed the Shuttle servers will automatically provision a Postgres database for you using the shared cluster. Note that there's a return type here - `ShuttleSerenity`. We don't need to run the Discord bot manually because the runtime does this automatically for us - instead, we return the Discord bot struct using `.into()` - this will be illustrated later on. Next, we need to get our secrets from the `SecretStore` (i.e., our `Secrets.toml` file that we created earlier). We also need to parse our channel ID into a `u64` as this will then allow us to automatically convert it into a `serenity::all::ChannelId`, which we need to then use for sending messages to with a Discord HTTP client. ```rust use anyhow::Context as _; secrets.into_iter().for_each(|(key, val)| { std::env::set_var(key, val); }); // Get the discord token set in `Secrets.toml` let token = std::env::var("DISCORD_TOKEN").context("'DISCORD_TOKEN' was not found")?; let channel_id: ChannelId = std::env::var("CHANNEL_ID") .context("'CHANNEL_ID' was not found")? .parse::() .context("Tried to convert CHANNEL_ID env var but the value is not a valid u64")? .into(); ``` Finally we will set up Discord bot and scheduled task, then return the Discord bot client, then return the bot (note that `ShuttleSerenity` implements `From` which is why we can use `.into()` here): ```rust let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let client = serenity::Client::builder(&token, intents) .event_handler(Bot::new(pool.clone())) .await .expect("Err creating client"); tokio::spawn(async move { automated_summarized_messages(channel_id, token, pool).await; }); Ok(client.into()) ``` ## Deploying Now that we've written all of the code, we can just use `shuttle deploy` and watch the magic happen! Note that we're not using a web service framework - trying to reach the deployment URL will simply return with a 404. ## Finishing up Thanks for reading! Hopefully you have found this useful. While AI assisted applications aren't quite ready to do the dishes yet, they can certainly be quite helpful in a number of ways. --- # Setting up effective CI/CD for Rust projects - a short primer Source: https://www.shuttle.dev/blog/2025/01/23/setup-rust-ci-cd Date: 23 January 2025 Author: josh Tags: rust, guide Implement Continuous Integration & Continuous Development effectively in your Rust project. ## Introduction The importance of a good CI/CD pipeline cannot go understated. The more you can automate in your deployment pipeline, the less work you have to do overall. That being said, it can be difficult to set up an effective CI/CD pipeline if it's your first time doing it. YAML files can be quite tricky to debug, and you can also additionally incur high costs from inefficient pipelines if you aren't careful. That being said, let's explore how to create effective CI/CD through Github Actions, an easy to use CI runner. ## Fundamentals of a Rust CI/CD Workflow The average Rust project might have the following things carried out in CI: - Automatic usage of `clippy`, exiting the workflow if there are **any** warnings or errors - Automatic usage of `fmt`, exiting the workflow if there is **any** diff - Automatic testing - Automatic website deployment - Dependabot Below is an example of a CI/CD workflow using YAML that you might find for a Rust project. For this file to be usable by Github Actions, it needs to be in the `.github/workflows` folder (relative to your project root). We'll call our file `workflow.yml` for the purpose of simplicity. Let's go through the steps: - Our workflow will only run on a pull request to `main`. Before we merge to main we need to ensure that the code compiles on a pull request - once the code's been pushed to main, it's a bit too late to make any changes by then and we'll have to push another PR to fix it! - We check out the code and install our required dependencies (meaning the Rust toolchain and `cargo-nextest`). Note that for external dependencies, using pure binary downloads is often far faster than trying to use `cargo install`. - We then run all the required commands (`clippy`, `fmt` and `cargo nextest run`) and exit the workflow automatically if any of the 3 commands fail. ```yaml # .github/workflows/workflow.yml name: CI on: pull_request: branches: - main jobs: build-and-test: runs-on: ubuntu-latest steps: # Checkout the repository - name: Checkout code uses: actions/checkout@v3 # Install Rust toolchain - name: Install Rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true # - name: Install cargo-nextest uses: taiki-e/install-action@cargo-nextest # Run Clippy (linting) - name: Run Clippy run: cargo clippy --all-targets -- -D warnings # Check code formatting - name: Check formatting run: cargo fmt --all --check # Run tests with cargo-nextest - name: Run Tests run: cargo nextest run ``` ## Speed up Rust CI/CD with sccache In addition to the above tools, you can use `sccache` to speed up your builds. `sccache` is a tool designed to speed up compilations (like `cacche`) by utilising caching. It supports quite a few different backends like S3 which means you're able to use it in many locations - but it also means you can use it in Github Actions. ```yaml name: CI on: pull_request: branches: - main jobs: build-and-test: runs-on: ubuntu-latest env: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: # .. initialisation steps go up here # run sccache - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.7 # run your cargo commands here ``` And now you're done! Interested in finding out more? Check out [the sccache Github readme](https://github.com/Mozilla-Actions/sccache-action?tab=readme-ov-file), which will have everything you need to know. ## Dependabot Dependabot is a tool provided by Github to help you manage dependencies effectively. While Dependabot _itself_ is not a CI tool, it is a great complement for any CI/CD pipeline on Github. Without it, you will often otherwise having to spend time manually checking dependency versions. You can set up Dependabot quickly and easily by adding it in your Github workflows like so (file should be in `# /.github/dependabot.yml`): ```yaml # Please see the documentation for all configuration options: # version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" ignore: # These are peer deps of Cargo and should not be automatically bumped - dependency-name: "semver" - dependency-name: "crates-io" rebase-strategy: "disabled" ``` Once you've added it to version control, nothing else is required. When there are new dependencies, Dependabot will automatically create issues/PRs as required. You can also additonally customise your Dependabot config much further - which you can [find out more in the Github documentation.](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates) ## Releasing new library versions with CI Let's face it, releasing new libraries is a lot of work. You need to manually add release notes and create a release on Github, you need to publish a new crate version on Github, you need to check for any breaking changes and ensure they're in the list... the list goes on. Fortunately, there's a way to easily automate all of this. You can use [release-plz](https://github.com/release-plz/release-plz) to do all of it for you - when you're ready to go through with the update, simply merge the Release PR provided by `release-plz`. ![An image showing how release-plz works](/images/blog/setup-rust-ci-cd/release-plz.png) ## Using CI/CD with Shuttle Of course, you can also use CI/CD with Shuttle. Check out [the deploy-action repo](https://github.com/shuttle-hq/deploy-action/tree/v2) where there is an easy-to-follow example on how to deploy Shuttle from your CI/CD pipeline rather than having to do it from the CLI. ```yaml name: Shuttle Deploy on: push: branches: - main workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - uses: shuttle-hq/deploy-action@v2 with: shuttle-api-key: ${{ secrets.SHUTTLE_API_KEY }} project-id: proj_0123456789 working-directory: "backend" cargo-shuttle-version: "0.48.1" extra-args: --allow-dirty --debug secrets: | MY_AWESOME_SECRET_1 = '${{ secrets.SECRET_1 }}' MY_AWESOME_SECRET_2 = '${{ secrets.SECRET_2 }}' ``` ## Finishing Up Thanks for reading! Hopefully you have gotten a good idea of how you can improve the effectiveness of your workflow using Github Actions with Rust codebases. --- # Supporting Web3: How WeaveVM Ships Rust Microservices with Shuttle Source: https://www.shuttle.dev/blog/2025/01/17/shuttle-web3-weavevm Date: 17 January 2025 Tags: rust, case-study Learn about how Shuttle can support Web3 related Rust web services off-chain. ## Introduction We're really excited by Web3. There's great work being done by really smart people. The mainstream adoption of decentralized infrastructure is moving the world in a direction of transparency and fairness and censorship resistance. Our Rustacean cousins in Web3 are spending time building robust protocols and reference implementations of validators which work and scale beautifully. At the same time, the _auxilliary_ infrastructure such as indexers, DA layers, etc. which are deployed off chain are a different story. Speaking to multiple Web3 teams, we've discovered that running these services reliably is an orthogonal skillset. While important, this also distracts them from their core mission. This is the story of how [WeaveVM](https://www.wvm.dev/) solved this painpoint with Shuttle and why a Rust-focused PaaS fits like a glove. ## About WeaveVM WeaveVM is building permanent data storage and high-throughput data availability for blockchain networks, settling 100k daily transactions and securing over $1.5B in total value across networks like Metis, Humanode, Avalanche and Dymension. Integrated with a wide network of chains and data protocols, they needed auxilliary services deployed off-chain quickly and reliably - this is where Shuttle comes in. ## Before Shuttle The WeaveVM team started with Heroku. Over time this grew into more of a headache than they asked for. Unnecessarily complex deployment flows, dependency mismatches, SSL certs were absorbing their limited and valuable time from doing what actually mattered. "We were spending precious dev time on infrastructure instead of building features that actually mattered," the team shared. "Then we found Shuttle, and it won on every single DevOps point." ## Enter Shuttle Moving to Shuttle was really quick for the WeaveVM team. Using Shuttle's annotation system on an existing service gave a migration time of less than 30 minutes (although your mileage may vary). ```rust #[shuttle_runtime::main] async fn rocket( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_rocket::ShuttleRocket { let state = MyState { pool }; let rocket = rocket::build() .mount("/", routes![hello]) .manage(state); Ok(rocket.into()) } ``` WeaveVM's CTO Rani and the team are Rust pros, so translating that expertise to infrastructure was natural and liberating. Databases, secrets management, SSL certificates - everything just worked out of the box. "Shuttle's nice DevX reduces the development and deployment time and lets us focus on the thing that matters: the code" Rani said. ## Looking at Today Today, WeaveVM's backend team ships Rust microservices - APIs, ETL pipelines, cron jobs without breaking a sweat. Their backend services, which have been migrated to Rust, scale seamlessly, while the team can quickly and easily spin up new services and experiment freely. When they need support, they get it directly through Josh and our team on Discord, a nice contrast to layers of impersonal support bots. ## Help us help you We're here to help! If what you've read sounds familiar sign up to Shuttle or reach out directly on [Discord](https://discord.gg/shuttle) or by email. Our team is excited to help you just deal with Rust and save you the headache of maintaining infrastructure. Help us help you deliver Web3. --- # The Emotional Appeal of Rust Source: https://www.shuttle.dev/blog/2025/01/14/the-appeal-of-rust Date: 14 January 2025 Author: antithesis Tags: rust, opinion What makes Rust emotionally appealing to its users? My first encounter with Rust was seeing my new CTO literally nail a rusty gear onto the office wall. This was in 2021, the gear was the size of a plate, and in its center was a capital R. So the first thing I learned about Rust was that it's not like other programming languages. Those logos appear on t-shirts, and anonymous black backpacks. Nobody nails C++ to the wall (except maybe figuratively, on the internet). The next thing I learned about Rust was that it's safe, in all these ways that earlier programming languages weren't. This caught my attention too, because the language was so raw, and so reflective of the roller coaster we get on with our first `hello world!`. I think most of us have this deep-seated emotional arc in our relationship with programming. We started in software because writing code made things happen - because code is power[^1]. Then at some point between your first line of code and your reading of this essay you learn that power is actually terrifyingly complicated. You break a build. You introduce a bug that takes your team weeks to find. And then you have to deal with how that makes you feel, and by and large you're dealing with your feelings alone because coding is a solitary activity, and at moments like this it can easily feel like even the computer isn't on your side.[^2] So when Rust comes along and offers you a helping hand, it's remarkable, because so few other languages do. Even if it sometimes feels like that helping hand is dragging you up a wall that's covered in broken glass, it's still better than plunging into the shark-filled moat below. I'm talking, of course, about the compiler, and memory safety, probably the two most distinctive features of the language. Memory safety issues mean you can't trust what you're seeing in your source code anymore. There's no program analysis you can perform to reason about your code anymore because you're breaking assumptions that your compiler has about the properties of your program, so there's a fundamental disagreement between what you see in the code and how the compiler will interpret it. But these don't (generally) happen in Rust[^3], because the compiler checks all your assumptions about memory for you, by doing a static analysis of the code when it compiles. Working with the compiler has been described as "siege warfare," and not everyone loves having to speak the language of the machine, which sometimes makes you do things like itemize the variables in code you damn well know is safe. Rust programmers call this "convincing the compiler,"[^4] and here Rust's architecture works in our favor. The compiler demands that our code is explicit, and the language provides opportunities to make it so. You can write unsafe code as long as you declare that you're doing so, and then wrap unsafe code in a safe wrapper. The type system both encourages you to modularize concerns and to declare exactly what functionality you want to expose. Make all the nullables you want, then declare how you're handling each and every case. One senior engineer at Antithesis told me that he likes to treat the Rust compiler "as a friend" - one who won't let him do anything too bad, and from this perspective, you actually don't have a lot of friends when you're coding. C++ makes you check your own safety invariants, which is fine when you wrote the entire codebase, and less fine when you're Microsoft. Javascript will happily run code with type mismatches and nulls, with a well-defined runtime behavior of crashing. That Rust provides such guardrails at all is kind of a novelty. So I'm inviting you to consider your relationship with this activity on which (I assume) you spend a great deal of your time. Programming might be a mostly rational exercise, but it's also a thing humans do, and humans are emotional creatures. I think Rust works for people not just because it works (on a technical level), but because it - and I know this phrase will be controversial - makes us feel safe as we build our worlds at the keyboard. In that light, it's unsurprising that so much of what's written about Rust has this sense of "oh my god we finally fixed programming." We've all been cut by the rough edges of our programming language, which are old and jagged and vicious. And like everything else about computing, they're our responsibility. They're not physical phenomena. We put them there. And we put them there because safety, flexibility, and performance are an iron triangle, and giving up some safety meant we could build bigger or better. Remember, the roller coaster has ups as well as downs. There are days when coding is actually fun - and writing code is fun because it makes things happen. If you're writing Rust, you're probably not just doing so because you're a fan of the language, but because you actually need it for some reason. You're writing something that needs to be fast, distributed, reliable. Something that could be very very big. Rust can do all that, because in addition to being safe, it's very, very fast. We started working with distributed systems for much the same reason - they let us build bigger and better than single systems did.[^5] But with multiple processors came multiple layers of complexity. Our environments and architectures became much more complex - we started having to worry about network faults, timing faults, and data races. And here's the thing about distributed environments. They're almost inimical to the philosophy that makes Rust safe. Explicitness is a key part of how Rust builds safety in your code, but there's no way to give your program an explicit understanding of the production environment. The environment, with all its rough edges, implicitly shapes how your code will run. The Rust compiler in a sense functions as a test for your program, and most tests, the compiler included, imply perfection in the runtime environment. What else could it do? The machine only knows what it knows. The Rust compiler may be one of the best of its kind, but it isn't going to check your business logic or the resilience of your program in the face of the kind of faults it will encounter in production. The most important thing to be aware of with any piece of safety equipment is that safety has its limits. You need to know what the compiler can't catch. But what if you could know that your code is actually safe in all those ways too? Not just memory-safe but actually reliable? What would you build? You've probably worked on something that you wouldn't have tackled except in Rust, because the compiler gave you confidence. What would coding feel like if that confidence extended to every dimension of your code? At Antithesis, we hear from customers about all kinds of projects that they wouldn't have tackled without the sense of safety we provide - complex, massively multi-threaded systems, rewrites that pay off years of tech debt, building new database systems from scratch - the truth is, safety unleashes ambition. You dream bigger if you know there's something catching your mistakes. If you'd like to know more about what it's like to combine Antithesis with the Shuttle framework, you can [try this out today.](https://antithesis.com/contact/) Or, to see what we just built, using the combination of Rust and Antithesis testing, [come see us at the Monsterscale Summit online,](https://www.scylladb.com/monster-scale-summit/#richard-hart) this March. [^1]: If you're very lucky, maybe you've held on to some of that sensation, through the interviews and the meetings and the code reviews and Jira and churn and managers who want to know what you do by the hour. I hope you have. [^2]: I mean, it isn't. It's a machine. But you probably know what I mean. [^3]: Safe Rust, at least. I know it's technically possible for this to happen. But it's rare to see this happen in everyday use, rare enough that Rust's reputation for memory safety is well deserved. [^4]: One interesting thing about this is that you're usually compiling soon after you originally wrote the code, so you're likely to remember exactly what you were trying to do, even if you don't know exactly what you did wrong. Context is a wonderful thing. [^5]: I know this isn't strictly true, but "the project won't fit on a single box" is probably the most common reason. --- # Building an arXiv Agent with Rig & Rust Source: https://www.shuttle.dev/blog/2025/01/08/arxiv-rig-rust Date: 8 January 2025 Author: josh Tags: tutorial, ai, rust Learn about using the Rig LLM framework to be able to create AI agents for assisted research via arXiv. Hi there! In this tutorial we're going to be using the Rig AI framework to create an AI agent for helping you learn by suggesting research papers based on a given subject. We're also going to use Shuttle to deploy it on the web so that other people can try it out! Interested in just looking at the code? [Check out the GitHub repository.](https://github.com/0xPlaygrounds/rig-arxiv-agent-example/tree/main/shuttle) ## Why Rig? Rig (by [Playgrounds](https://playgrounds.network/), who also additionally maintains [arc.fun](https://arc.fun)) is a new up-and-coming Rust framework designed to make AI agent creation as easy as possible. It can currently create agentic pipelines (ie. pipelines that can execute a number of prompts LLM-assisted steps), integrate RAG with AI agents as well as exposing an API that you can use to create your own tools. The framework itself is growing quite rapidly with the maintainer team using it in production, so it will be receiving updates for quite some time! They're also holding an event called [the ARC Handshake](https://www.arc.fun/handshake), which will be a showcase of AI agents built using Rig. ## Pre-requisites Before we start, you'll want to ensure you have the Rust programming language installed and additionally an OpenAI API token. If you don't have one already, you will need to sign in and [obtain an API key from the dashboard](https://platform.openai.com/settings/organization/general) as this will be required to make it work. You'll also want the `cargo-shuttle` crate installed (our CLI) - [check out our installation instructions for more info.](https://docs.shuttle.dev/getting-started/installation) ## Let's get building! If you haven't already, don't forget to create a new Shuttle project via [cargo-shuttle](https://docs.shuttle.dev/getting-started/installation): ```bash shuttle init --template axum ``` Once you've followed the prompt and it's set up, we'll need to add our required dependencies to build the project: ```bash cargo add reqwest serde serde-json anyhow thiserror quick-xml rig-core \ urlencoding tracing -F serde/derive,quick-xml/serialize ``` ## Creating our Rig AI agent The first step will be to create our AI agent. We can do this by creating a tool that will be able to send a prompt to a model (or in this case OpenAI), as well as do some functionality when called. ### Setup The arXiv export endpoint (`http://export.arxiv.org/api/query`) takes a few different query parameters that are relevant to us: - `search_query` (The actual search query we want to search for) - `max` (maximum number of papers we want in the result) Before we write the implementation block, let's declare the relevant structs we want. We want one for holding paper results, one for search arguments and one for the tool itself. ```rust // Struct to hold paper metadata #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct Paper { pub title: String, pub authors: Vec, pub abstract_text: String, pub url: String, pub categories: Vec, } impl Paper { fn new() -> Self { Self { title: String::new(), authors: Vec::new(), abstract_text: String::new(), url: String::new(), categories: Vec::new(), } } } #[derive(serde::Deserialize)] pub struct SearchArgs { query: String, max_results: Option, } // Tool to search for papers #[derive(serde::Deserialize, serde::Serialize)] pub struct ArxivSearchTool; ``` ### Error Handling While the AI agent is carrying out work, there are many different types of errors it can get. We should aim to represent these by using an enum. We additionally enhance our error type by implementing `thiserror::Error` (requiring `Debug`), which allows us to easily derive `From` for our new error type. ```rust #[derive(Debug, thiserror::Error)] pub enum ArxivError { #[error("Network error: {0}")] Network(#[from] reqwest::Error), #[error("XML parsing error: {0}")] XmlParsing(#[from] quick_xml::Error), #[error("No results found")] NoResults, #[error("UTF-8 decoding error: {0}")] Utf8Error(#[from] std::str::Utf8Error), } ``` By doing this, a couple of cool things happen: - It enables use of the `?` operator which increases readability and allows error propagation up the call stack - Makes it obvious why and/or how something has failed (we can check the enum variant) ### Rig Tool Definitions Next, we want to write the implementation block. It requires us to provide functionality for two methods - `definition()` which provides the tool definition and prompting for the model, as well as `call()` which is the actual functionality for the tool. ```rust const ARXIV_URL: &str = "http://export.arxiv.org/api/query"; impl Tool for ArxivSearchTool { const NAME: &'static str = "search_arxiv"; type Error = ArxivError; type Args = SearchArgs; type Output = Vec; async fn definition(&self, _prompt: String) -> ToolDefinition { ToolDefinition { name: "search_arxiv".to_string(), description: "Search for academic papers on arXiv".to_string(), parameters: json!({ "type": "object", "properties": { "query": { "type": "string", "description": "Search query for papers" }, "max_results": { "type": "integer", "description": "Maximum number of results to return (default: 5)" } }, "required": ["query"] }), } } async fn call(&self, args: Self::Args) -> Result { let max_results = args.max_results.unwrap_or(5); let client = reqwest::Client::new(); let response = client .get(ARXIV_URL) .query(&[ ("search_query", format!("all:{}", args.query)), ("start", 0.to_string()), ("max_results", max_results.to_string()), ]) .send() .await? .text() .await?; parse_arxiv_response(&response) // not implemented yet! } } ``` Note that the tool is simply a tool that can be called by the model - it can either be added to a given Rig prompt by itself, or it can be added to a toolset with other tools to provide a comprehensive user experience. ### Parsing the arXiv response Next, we need parse the response from arXiv. The actual response format is in XML - this is what a typical entry looks like: ```xml http://arxiv.org/abs/2407.11861v1 2024-07-16T15:48:36Z 2024-07-16T15:48:36Z What Makes a Meme a Meme? Identifying Memes for Memetics-Aware Dataset Creation Muzhaffar Hazman Susan McKeever Josephine Griffith Accepted for Publication at AAAI-ICWSM 2025 ``` In order to parse it, we will use the `quickxml` crate which offers good performance while still being relatively easy to use. To ensure codebase readability, we will create a struct that will act as our parser and hold the parser state in it: ```rust #[derive(Default)] struct ArxivParser<'a> { papers: Vec, current_paper: Option, current_authors: Vec, current_categories: Vec, in_entry: bool, current_field: Option<&'a str>, } ``` We will implement several methods on this struct: - Methods for handling different types of XML events - A public method for parsing the whole text To start with, we'll create methods for handling the start of an XML tag as well as any text that is contained within the tag: ```rust impl<'a> ArxivParser<'a> { fn parse_start_event(&mut self, event: &BytesStart) { match event.name().as_ref() { // if the tag is "entry", this means we're at the start of a new xml block // so we can clear related variables and start anew b"entry" => { self.in_entry = true; self.current_paper = Some(Paper::new()); self.current_authors.clear(); self.current_categories.clear(); } // otherwise, change the parsing state b"title" if self.in_entry => self.current_field = Some("title"), b"author" if self.in_entry => self.current_field = Some("author"), b"summary" if self.in_entry => self.current_field = Some("abstract"), b"link" if self.in_entry => self.current_field = Some("link"), b"category" if self.in_entry => self.current_field = Some("category"), _ => (), }; } fn parse_text_event(&mut self, event: &BytesText) -> Result<(), ArxivError> // if there's no current paper, just don't return anything let Some(paper) = self.current_paper.as_mut() else { return Ok(()); }; // otherwise, attempt to get the text and fill in the relevant field let text = str::from_utf8(event.as_ref())?.to_owned(); match self.current_field { Some("title") => paper.title = text, Some("author") => self.current_authors.push(text), Some("abstract") => paper.abstract_text = text, _ => (), } Ok(()) } } ``` Before we continue, we need to create a small function to be able to convert the links we get from parsing an `arXiv` XML entry to be able to return the PDF response. See below where we replace `arxiv.org/abs` with `arxiv.org/pdf`: ```rust fn convert_pdf_url(url: &str) -> String { if url.contains("arxiv.org/abs/") { // Convert abstract URL to PDF URL url.replace("arxiv.org/abs/", "arxiv.org/pdf/") .replace("http://", "https://") + ".pdf" } else if url.contains("arxiv.org/pdf/") { // Ensure PDF URL uses HTTPS url.replace("http://", "https://") } else { // Fallback for other URLs url.replace("http://", "https://") } } ``` Next, we want to parse empty XML elements. If the XML element is a Link or Category, we attempt to parse them like below and add the relevant parts to the parser state: ```rust impl<'a> ArxivParser<'a> { // .. other methods here fn parse_empty_event(&mut self, event: &BytesStart) -> Result<(), ArxivError> { // if we're not in an entry, just don't do anything if !self.in_entry { return Ok(()); } // if the element is a link, convert the URL to the relevant format // and add the URL to the paper if event.name().as_ref() == b"link" { if let Some(paper) = self.current_paper.as_mut() { for attr in event.attributes().flatten() { if attr.key.as_ref() == b"href" { let url = str::from_utf8(&attr.value)?; // Convert to HTTPS and ensure PDF URL let secure_url = convert_pdf_url(url); secure_url.clone_into(&mut paper.url); } } } } // if the element is a Category, push the category terms // into the parser's list of current categories if event.name().as_ref() == b"category" { for attr in event.attributes().flatten() { if attr.key.as_ref() == b"term" { self.current_categories .push(str::from_utf8(&attr.value)?.to_owned()); } } } Ok(()) } ``` We also need to ensure that we are correctly resetting the parser state when we reach the end of an element: - If we reach the end of an entry, we need to push our current `Paper` to the list of papers being generated by the parser - If we reach the end of another element, we just reset the `current_field` field as there is nothing left to parse within the given element. ```rust impl<'a> ArxivParser<'a> { // .. other methods fn parse_end_event(&mut self, event: &BytesEnd) -> Result<(), ArxivError> { // this is an end event - if the end tag is for an entry // add the current paper to the list of papers match event.name().as_ref() { b"entry" => { if let Some(mut paper) = self.current_paper.take() { paper.authors.clone_from(&self.current_authors); paper.categories.clone_from(&self.current_categories); self.papers.push(paper); } self.in_entry = false; } // else, just change the currently parsed field to None // as there is now nothing to parse b"title" | b"author" | b"summary" | b"link" | b"category" => { self.current_field = None; } _ => (), } Ok(()) } } ``` Finally, we need to write our method for parsing the whole response. This function simply loops until the end of the XML file has been reached (an `Event::Eof` has been reached). If there's no papers that have been parsed (no results were returned), we will return an error. ```rust impl<'a> ArxivParser<'a> { // .. other methods fn parse_response(&mut self, input: &str) -> Result, ArxivError> { let mut reader = Reader::from_str(input); reader.trim_text(true); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) => self.parse_start_event(e), Ok(Event::Text(ref e)) => self.parse_text_event(e)?, Ok(Event::Empty(ref e)) => self.parse_empty_event(e)?, Ok(Event::End(ref e)) => self.parse_end_event(e)?, // EoF means end of file - we can stop trying to parse here Ok(Event::Eof) => break, Err(e) => return Err(ArxivError::XmlParsing(e)), _ => (), } } if self.papers.is_empty() { return Err(ArxivError::NoResults); } Ok(self.papers.clone()) } } ``` Now we can move onto writing our web service! ## Writing our Rust web service In terms of application code for the web service itself, there are a few things: - Setting up application state - Setting up endpoints - Serving a frontend (of our choice) ### Setting up application state We can define the application state as a struct that holds our OpenAI client. Note that it's required to derive Clone as application state is shared over many requests, hence the Clone trait requirement (as it guarantees this behaviour). ```rust #[derive(Clone)] struct AppState { openai_client: openai::Client, } ``` ### Error handling Before we implement our endpoint, let's again take a minute to consider error handling. We want error handling to be as idiomatic as possible - and we also use `anyhow::Error` for a large majority of our errors as it's easy to use. Therefore, we should implement `From>` for our application error. We also additionally want to implement `axum::response::IntoResponse` for our error type as this allows the type to be used as part of a return type signature in an Axum function handler. ```rust use axum::response::IntoResponse; struct AppError(anyhow::Error); impl IntoResponse for AppError { fn into_response(self) -> Response { ( axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", self.0), ) .into_response() } } impl From for AppError where E: Into, { fn from(err: E) -> Self { Self(err.into()) } } ``` ### Endpoints There is only one single endpoint we need to write - the handler for receiving requests about papers to search and then carrying out the required work. The code snippet below is a relatively simple - we grab the query, we create an AI agent that uses GPT-4 and add some additional pre-amble (extra context) as well as the original prompt as part of the query. We then deserialize the resulting string and return a HTML response. ```rust use serde::Deserialize; // here we create a struct that we can use as the required request body shape #[derive(Deserialize)] struct SearchRequest { query: String, } async fn search_papers( State(state): State, Json(request): Json, ) -> Result { let paper_agent = state.openai_client .agent(GPT_4) .preamble( "You are a helpful research assistant that can search and analyze academic papers from arXiv. \ When asked about a research topic, use the search_arxiv tool to find relevant papers and \ return only the raw JSON response from the tool." ) .tool(ArxivSearchTool) .build(); let response = paper_agent .prompt(&request.query) .await?; // return the response as HTML // note that if you want to return just a JSON response // you can return `Ok(axum::Json(papers))` let papers: Vec = serde_json::from_str(&response)?; let html = tools::format_papers_as_html(&papers)?; // see below! Ok(Html(html)) } ``` ### Frontend While we won't dive into writing the frontend in this article, you can [have a look at the HTML files yourself](https://github.com/0xPlaygrounds/rig-arxiv-agent-example/tree/main/shuttle/static) and copy them in (we use a folder called `static`, which is in the project root). You'll need to create a handler to serve it: ```rust use axum::response::Html; // Handler for serving the static index.html async fn serve_index() -> impl IntoResponse { Html(include_str!("../static/index.html")) } ``` We also additionally return the table of papers (from our API endpoint) as HTML. The following function is used to combine HTML templating with the papers we've sent through our AI agent to generate a result: ```rust // HTML formatting function for papers pub fn format_papers_as_html(papers: &[Paper]) -> Result> { let tpl = std::fs::read_to_string("static/table.html")?; let mut context = tera::Context::new(); context.insert("papers", papers); let result = tera::Tera::one_off(&tpl, &context, true)?; Ok(result) } ``` ### Putting it all together Next, we'll set up our main function. We'll need to add the Secrets annotation (see below) to our function parameters, as well as adding a `CorsLayer` and our additional routes. ```rust #[shuttle_runtime::main] async fn axum( // this annotation provides our secrets from Secrets.toml #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { // Initialize OpenAI client from secrets let openai_key = secrets .get("OPENAI_API_KEY") .context("OPENAI_API_KEY secret not found")?; let openai_client = openai::Client::new(&openai_key); // Create shared state let state = AppState { openai_client }; // Set up CORS let cors = CorsLayer::new() .allow_origin(Any) .allow_methods([axum::http::Method::GET, axum::http::Method::POST]) .allow_headers(Any); // Create router let router = Router::new() .route("/", get(serve_index)) .route("/api/search", post(search_papers)) .layer(cors) .with_state(state); Ok(router.into()) } ``` ## Deploying If you've added any frontend assets, make sure you add them to a `Shuttle.toml` file in your root folder (this will allow the frontend templates folder to be included in deployment): ```toml [build] assets = ["/*"] ``` If you've followed this tutorial from start to finish, the folder name should be `static`. We add `/*` at the end to signify that we want to include the whole directory and all files inside it. To deploy, all you need to do is write `shuttle deploy` into the terminal and watch the magic happen! If you'd like to run this locally, simply run `shuttle run` then visit `localhost:8000`. ## Finishing up Thanks for reading! Hopefully this article has increased your understanding of how to implement your own AI agent using the Rig AI framework, as well as writing a web service with a frontend. Further reading: - [Building agentic RAG with Qdrant & Rust](https://www.shuttle.dev/blog/2024/05/23/building-agentic-rag-rust-qdrant) - [Get Started with Logging in Rust](https://www.shuttle.dev/blog/2023/09/20/logging-in-rust) - [Get Started with Axum](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) --- # Rethinking Cloud Pricing Source: https://www.shuttle.dev/blog/2024/12/21/rethinking-cloud-pricing Date: 21 December 2024 Author: nodar Tags: shuttle, announcement Learn about Shuttle's upcoming price changes and our thoughts on the current state of cloud pricing Have you ever noticed that cloud pricing is like booking a budget flight—the ticket looks cheap at first, but by the time you've paid for luggage, a seat, and breathing room, it's a first-class bill? Pricing shouldn't come with hidden fees or surprise add-ons, yet we've accepted that as a norm in our cloud bills. Let's talk about this—not just the numbers but the philosophy behind all this and how we do things differently at Shuttle. We're not just simplifying cloud development—we're making it joyful. That starts with pricing which you can actually understand. ## The Cloud Pricing Problem Have you ever tried deciphering a cloud service bill? What exactly are you paying for? Compute? Storage? Egress? "Mystery charges"? And just when you think you've cracked the code, surprise! Another line item shows up for a service you didn't even know existed. Let's take a familiar scenario: You're spinning up a project. You pick a cloud provider, and after 40 minutes of decision fatigue, you've chosen a virtual server. But what size? What region? Reserved instances? And wait, what's an Availability Zone? Before you've written a single line of code, you're knee-deep in decision paralysis. Now, imagine you're a startup. Infrastructure decisions multiply, DevOps becomes a bottleneck, and your bills? They look more like a Jackson Pollock painting than an itemized list. ![An actual startup cloud bill](/images/blog/rethinking-cloud-pricing/jackson-pollock-painting.webp) > Pictured: an actual cloud startup bill The complexity in cloud pricing exists because it reflects the vast array of services, configurations, and use cases cloud providers cater to. They aim to be everything to everyone—offering granular control over resources like compute, storage, and networking—while charging for every option and optimization along the way. This à la carte approach maximizes flexibility but also makes pricing opaque, as the true costs of running an application depend on a web of interdependent factors most developers don't have the time—or expertise—to untangle. We get it. Cloud pricing is too complex, too opaque, and frankly, it's sucking the joy out of development. That's where we come in. ## Our Pricing Philosophy: Value Over Complexity At Shuttle, we believe pricing shouldn't feel like calculus. It should be: 1. **Simple**: You know what you're paying for. 2. **Transparent**: No hidden fees, no cryptic bills. 3. **Focused on Developer Experience**: You're not just paying for infrastructure; you're investing in a smoother, faster, more joyful path to production. Here's the deal: Cloud infrastructure should be a commodity. The magic happens in the moments between provisioning a server and pushing a bug-free feature to production. That's what Shuttle is all about—turning "I need to deploy" into "It's live!" ## The Future: Smarter, Predictable Pricing We're dreaming big. Imagine a world where every deployment comes with a cost estimate. Thinking of scaling your app? You'll know the cost _before_ you confirm. Deploying a new feature? Get cost predictions down to the commit level. ![Understand your pricing diff from the comfort of your terminal](/images/blog/rethinking-cloud-pricing/cloud-bill.webp) In the future, cloud pricing won't just be simple—it'll be smart. We're exploring tools that integrate into your workflow, like combining commit based pricing and predictive pricing models to help you make informed decisions and avoid surprises. ## What We're Building Today In the last few months at Shuttle, we completely overhauled the platform. It's now scalable, secure, robust, and ready for _production_ use cases. The production readiness comes with new tiers, which are going live in 2025, which in turn results in a pricing model that is simpler and redesigned from the ground up. We've worked a ton on digesting the complexity, so you only get simplicity, and we're very excited to share our work with you soon. That said, this is just another step, and as Shuttle keeps evolving, so will our pricing - to better serve you. ### The Community Tier Still free. Still awesome. Perfect place to start and experiment with Shuttle, exploring Rust, or building that side project you've been dreaming about. Some minor tweaks coming and even clearer picture of what's included. ### The _New_ Pro Tier This is for those looking for a "production" level experience. A pricing model that separates raw infrastructure costs from the developer experience we provide. Yes, we still add a small margin to the basic infra cost—it helps us build the tools, maintain the platform, and provide the support that turns "ugh, DevOps" into "ah, this is nice." Here's what to expect: - Simple and transparent infrastructure costs. - Features that enhance production workflows—monitoring, observability, and priority support. - A pricing structure that grows with you, whether you're a solo developer or a scaling team. ### The Team and Enterprise Tiers For teams that need tailored support, we're working on options that scale with your ambitions. Dedicated onboarding, BYOC (Bring Your Own Cloud), features that enhance collaboration on more complex projects and flexible pricing for organizations that want to leverage Shuttle's simplicity without overhauling their existing setup. ## What's Next? We're listening. Every feature, every tier, every decision is shaped by your feedback. So, tell us—what do you need? What's working? What's not? Let's build a cloud platform (and pricing model) that developers actually love. Click [here](https://shuttlerust.typeform.com/to/awqLE9sL) to share your thoughts or sign up for early access to our Pro Tier. --- # Setting up a Status Page with BetterStack Source: https://www.shuttle.dev/blog/2024/12/20/set-up-status-page-betterstack Date: 20 December 2024 Author: josh Tags: betterstack, tutorial, guide, observability Learn about best practices and improving your observability by creating a status page with BetterStack ## Introduction Observability forms a huge part of being able to debug and resolve production incidents quickly. One way to improve observability is application monitoring and using telemetry to send your logs to an observability backend that also allows you view and analyse your logs. BetterStack (formerly known as Logtail) is a great way to do this, as they offer both a package for simple application monitoring and telemetry. In this tutorial, we'll get into how you can add a status page for your web service using BetterStack, which by default supports status history, incident reporting and maintenance notices: ![image.png](/images/blog/betterstack-status-page/result.png) ## Why use status pages at all? While status pages are a commonly used solution to improve user experience, it's also important to talk about _why_ you should set up a status page. Status pages are a commonly used convention by a lot of different types of companies and although the reasons may seem relatively obvious, there are benefits for any company with a website to set up a status page for your users. Although companies and professional teams often additionally opt for an internal status page (for internal services), we will primarily be discussing external-facing status pages below. ### Transparency builds trust Not everyone is building a critical service with high availability requirements. That being said however, giving your users the ability to check themselves whether your service is either online or offline is a great way to build trust with them. By not hiding any of the metrics, it allows the user to trust that the company is handling the situation properly rather than doing everything behind closed doors and totally controlling the narrative. ### Spend less time answering user questions Similarly, once user trust has been built they may more inclined to simply wait until the outage is over rather than creating a new help ticket. This means that overall, it should result in less questions needing to be answered regarding service availability. ## Setting up a status page with BetterStack ### Set up Status Monitoring For the most part, application monitoring with BetterStack is quite easy. Once you've signed into your account (and finished the onboarding), you'll need to make sure you're on the "Uptime" section and then click on "Create monitor" (see below): ![image.png](/images/blog/betterstack-status-page/connect-first-monitor.png) Next, you'll be brought to this menu. There's a drop-down list you can use to select different kinds of events for what should trigger a notification (non-exhaustive list below): - URL becomes unavailable - URL doesn't contain keyword - URL contains keyword - URL returns a status other than a given HTTP status - Host doesn't respond to ping You can also see that we've used [`https://shuttle.dev`](https://shuttle.dev) as the URL below - you can use whatever URL you want here, but if you have a Shuttle project in mind that you need to monitor, add the URL in here! You may want to dedicate a specific health check route for this - it's common convention to use `/healthz` or `/health` for this. ![image.png](/images/blog/betterstack-status-page/create-monitor-menu.png) Once done, you should get to a menu that looks like image below. Now whenever your web service goes down, BetterStack should automatically let you know through email (or another method, if you're on one of the paid tiers) when the alert gets triggered. ![image.png](/images/blog/betterstack-status-page/view-monitor-menu.png) ### Setting up a status page Now for the easy part! Head over to the Status Page tab then select "Create status page" like below. ![2024-12-16_10-40.png](/images/blog/betterstack-status-page/create-status-page-directions.png) Next, you should be greeted by a series of tabs with an initial Settings menu. Fill it out, then proceed to the Structure tab where we'll be adding our monitor that we created from earlier. If you click "Search to add resources", you'll be able to search for your monitor from there and add it in. Note that you can add an explanation as well as the widget type if you want to experiment. ![image.png](/images/blog/betterstack-status-page/create-status-page-menu.png) Once done, you should now be able to head over to your status page and see it in action from the Monitors menu! ## Best practices for status pages ### Use a subdomain for the status page This is an easy one to get right, but an important one nonetheless. If you're running a web service in production and have users, your users being able to check service uptime themselves can improve user experience significantly during downtime. Typically, the subdomain used would be `status`. So in our example we would use `status.shuttle.dev` as the subdomain. ### Ensure coverage of your whole service Status pages are great because they ensure that at least _one_ part (typically the health check route) is available form your status page. However, this does not guarantee a high degree of availability over your whole service - availability here meaning whether or not your service returns `2xx` or a `5xx` code). In this case, you likely want to use BetterStack's Playwright monitors to be able to use much more custom scenarios. By setting the What to Monitor option to `Playwright scenario fails`, we can now use the `playwright` JavaScript library to be able to set up tests! An example can be found below: ```rust const { test, expect } = require('@playwright/test'); test('mobile page contains Sign up', async ({ page }) => { await page.setViewportSize({ width: 360, height: 640 }); await page.goto('https://betterstack.com/'); const pageContent = await page.textContent('body'); expect(pageContent).toContain('Sign up'); }); ``` While this might be something you might not want to put on a user-facing status page, it is a great way to protect yourself from regressions and breaking code changes. The [Playwright codegen tool](https://playwright.dev/docs/codegen) is a great way to spin some tests up quickly. ### Status pages & health checks aren't a full view of observability Status pages based on external monitoring probes or health checks do not provide a full view of observability as they only represent a small sample of the total traffic to your service. Monitoring probes are similar to network pings, answering the question "is the service reachable?" Different types of metrics and application monitoring, such as total traffic, errors, response latency and resource usage, are required to get a complete picture of your service health. However, monitoring probes are an excellent and essential first line of defence, especially when hosted externally because the uptime of the monitoring tool is not correlated to the uptime of the service. ### Status pages cannot replace customer service While status pages are helpful, they should not be the final answer to user-facing observability improvements. Particularly if your users are building a business on top of your platform, they should also receive notifications through proper channels for severe outages. This ensures a clear, professional line of communication with your customers which in most cases should improve customer service. As a first step for this, a good communication channel is typically wherever your users are - be it Discord, Slack or Telegram (or even on X!). Email is also additionally another good channel to use as even when your most critical users are not on the same social media platforms you're on, there is an extremely high chance they have an email address you can send to. ### Use honest, concise language for incident reports It might seem a little bit daunting to be completely honest about what went wrong in an incident. That being said, it is important to be honest and use concise language in incident reports as it is a good way to enforce the transparency aspect of a status page and potentially reap reputational benefits. It's also extremely important that the post mortems are also blameless: while a single person may have triggered it, the root cause is typically always systemic and it's important to keep this in mind. It also looks somewhat unprofessional to place the blame on a single person and can cause a witch hunt. Understanding what exactly went wrong and communicating it publicly as well as how to fix it for the future is often much more important than hiding it, causing reputational risk. Often while incidents can seem huge at the time, eventually enough time passes that they become learning experiences and can be good educational material for the future members of your team. ## Finishing up Thanks for reading! As you can see, BetterStack can significantly improve multiple facets of observability within your application, and is extremely helpful to have. Read more: - [Sending your logs to Datadog with Rust](https://www.shuttle.dev/blog/2024/03/27/datadog-rust) - [Using OpenTelemetry with Rust](https://www.shuttle.dev/blog/2024/04/10/using-opentelemetry-rust) - [Getting started with the tracing libraries](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) --- # The Essence of Templating with Tera Source: https://www.shuttle.dev/blog/2024/11/29/the-essence-of-templating-with-tera Date: 29 November 2024 Author: jeff Tags: rust, html-templating, frontend How to get started with Shuttle and the Tera Crate ### Introduction For better or worse, we are in an era of client side rendered, JavaScript heavy web applications. Template rendered web sites are not in vogue as they once were. However, they still have their place and it's useful to know how to make them. Can we do a template driven web site in Rust? Why yes, yes we can. There are many crates in the Rust ecosystem, but today I'd like to take a look at Tera. [Tera](https://keats.github.io/tera/) is a powerful and flexible template engine for the Rust language. What is a template engine? Well, it's a way for you the developer to build a server side rendered web application and use templates to describe the structure and content of the app. Rather than build each page individually, using Tera you describe the overall structure and format, and the data can be dynamically pulled in on the fly. This is an introductory article, as such I want to focus on: - Initialize a new Rust project with a binary - Set up Tera - Basic usage - Optimization with `OnceLock` from the standard library I'm not going to build a full application in this piece, but instead want to cover the basics of getting off the ground. Let's go! ### Initialize a New project The starter we're going to create will be a simple Rust app which outputs raw HTML to the console. It won't have a web server and won't be tailored for deployment on Shuttle. The goal here is to give you the tools to explore on your own. Let's begin with Step #1, create a new Rust binary project: ```bash cargo new --bin hello-world-tera ``` This will create a new project called "hello-world-tera" and will set you up with a binary crate so that you can run the project and observe the output. ### Setting Up Tera After you've changed into the `hello-world-tera` directory, Step #2 is to add the Tera crate as a dependency to your project. ```bash cargo add tera ``` This will make the crate available in your project. Now, in the root of your project, create a `templates/` directory. This will serve as the central repository of all the templates you build for your project. As we'll see in a moment, at compile time they will all be pulled from this directory for rendering. Now we need an actual template. For the moment, create one called `base.html`. This will be the base template and serves as the baseline for the whole project. ```html {{ title }}

Welcome to {{ site_name }}

{{ message }}

``` Here you can see a very rudimentary HTML file. It has some variables though, surrounded by `{{ }}`. This is the way we denote that Tera should inject something into the particular location. How exactly? ### Rendering Templates Alright, we've got a template, what next? Let's write some actual Rust code as Step #3. In the `src` directory, go to the `main.rs` file and type in the following code: ```rust // src/main.rs // dependences use tera::{Tera, Context}; // main function fn main() { let tera = Tera::new("templates/**/*").unwrap(); // constructor is fallible, so we unwrap() to get the good value let mut context = Context::new(); context.insert("title", "First Steps with Tera"); context.insert("site_name", "example.com"); context.insert("message", "Hello, world!"); let rendered = tera.render("base.html", &context).unwrap(); // render() method is fallible, so we unwrap() println!("{}", rendered); } ``` Let's walk through it: - We need two things to render a template, the dynamic information, and a place to put it; we bring two things into scope from the `Tera` crate, namely the [Tera](https://docs.rs/tera/1.20.0/tera/struct.Tera.html) type, which is a struct, and [Context](https://docs.rs/tera/1.20.0/tera/struct.Context.html), which is also a struct - The `Tera` type and `Context` type are our two main tools for working with Tera templates - We create a new Tera instance (using the contents of the directory you created in the previous step) and bind that to a variable - Create an empty instance of `Context` which is effectively a holding container for the content to render into the template, and bind it to a variable - Fill in the empty `context` variable with our data. Remember those `{{ }}` in the template you made? Yes, we're filling all those in with this info. - There isn't any error handling happening here, `.unwrap()` will give us back good values from any fallible functions, but result in a panic otherwise That's it! If you compile and run this code, you'll get your filled template rendered back to the console. ```html First Steps with Tera

Welcome to example.com

Hello, world!

``` So, pretty easy eh? It is. There is one pitfall to watch out for though. ### Optimizing Template Rendering Templates are very expensive to render. The above example is trivial, but in something real, you don't want to be rendering the templates from scratch every time they are requested. Instead, you want to compile them once, then put them somewhere to be re-used later. This way, they're ready and the server isn't wasting time and resources re-building every template each time they are requested. How can we achieve this? We lean on the standard library and pull in [OnceLock](https://doc.rust-lang.org/std/sync/struct.OnceLock.html), which allows us to create a thing and tuck it off in the corner to use later. `OnceLock` is thread-safe as it implements the `Sync` marker trait which denotes it is safe for the type to be referenced from multiple threads. Modify your `src/main.rs` to look like this: ```rust // src/main.rs // dependences use std::sync::OnceLock; use tera::{Context, Error, Tera}; // declare a static variable to hold the initialized templates static COMPILED_TEMPLATE: OnceLock = OnceLock::new(); // function to create the tera template, handling any errors and returning them to the caller fn create_template() -> Result { let base_template = Tera::new("templates/**/*")?; Ok(base_template) } // function to build the Tera template, returns a Result type, where // the Ok variant is the rendered template and the error is the Error type provided by Tera fn get_template() -> Result<&'static Tera, Error> { let template = create_template()?; Ok(COMPILED_TEMPLATE.get_or_init(|| template)) } // function which renders the Tera templates, returns a Result type, where // the Ok variant is a string of HTML and the error is the Error type provided by Tera fn render_template() -> Result { let mut context = Context::new(); context.insert("title", "First Steps with Tera"); context.insert("site_name", "example.com"); context.insert("message", "Hello, world!"); get_template()?.render("base.html", &context) } // main function fn main() -> Result<(), Error> { let rendered = render_template(); match rendered { Ok(template) => println!("{}", template), Err(e) => eprintln!("Error: {}", e), } Ok(()) } ``` There's a lot here, and I've handled errors. What changed and how is this better? - The `OnceLock` type is brought into scope from `std::sync` in the standard library - Fun fact: `OnceLock` used to be part of the `once_cell` crate, which was brought into the Rust standard library with version 1.70 - The `Context`, `Error`, and `Tera` types are brought into scope from the `Tera` crate - We declare a static variable to hold our compiled template, it's initialized with an empty value by using the `OnceLock::new()` constructor - We need a separate function which gets our Tera instance started and adds in the base template we created in the `templates/` directory - Errors could happen in this process, so to handle them, we use the `?` operator to propagate any errors back to the caller - note that, if you use the "glob" import approach, as done here, this brings all the templates in one go - if you have templates in a different location that you want to use, you'll need to use the `.add_template_file()` for singles or the `.add_template_files()` method for multiples (see the Tera docs for more information) - We have a function which `get_template()` which leverages the function we just created, any errors are propagated back to the caller - You'll see that the `get_or_init()` method accepts a closure, we pass in our template that we just created - The closure here wants to work with an actual good value, meaning we can't handle errors effectively inside the closure without an `.unwrap()`. I like to try to show how to handle errors, rather than not. - Now that our template is compiled and placed on a shelf, so to speak, we can render it with the desired content. Two things are needed, a template, and context. Remember those variables you put into the `base.html` above? That's the context information which gets inserted into the template. - Finally, we have a main function which calls our `render_template()` function and outputs the result, or outputs any errors This approach is a better solution because we compile the templates once and they are saved in static memory and accessible in a thread-safe way. That's it! You now know how to do things with Tera templating. ### Common Mistakes There is one aspect of Tera that you have to be careful with and that's pathing. In the initialization step above (the `create_template()` function), it's really important to get your paths correct. The `Tera::new()` constructor needs a path that's absolute from the root of your project. You should also be careful to use the glob format noted above to capture everything below the template root, assuming you want that. When you use `add_template_file()` to add in a template, you have to again use an absolute path relative to your root directory. When deploying, take care to actually include your `templates` folder with the deployment files. Been there, done that, trust me. ### Conclusion and Next Steps To extend out this very basic starter, you can learn to work with multiple template files, by using the `add_template_files()` function. This function takes multiple files, by building their path and name info into a Vector. This example doesn't show you how to leverage templating in the context of a web server. You could do that very easily with Rocket or Axum. Rocket does a lot of the work for you, but in Axum you'll need to pull in `tower-http` as a dependency, to get access to `ServeDir` and it's methods for serving static files, which can be all the assets needed for your project, including the Tera templates. Here are some resources to aid in your adventures with Tera: - [Tera Documentation](https://keats.github.io/tera/) - [Tera on crates.io](https://docs.rs/tera/1.20.0/tera/) Good luck! And have fun! --- # Migrating to Shuttle Source: https://www.shuttle.dev/blog/2024/11/20/migrating-to-shuttle Date: 20 November 2024 Author: shuttle Tags: rust, shuttle How to migrate an application to be able to use Shuttle Hey! Today we're going to have a quick look at migrating a project that uses the Tokio runtime with Axum, to Shuttle. ## Setup ### Pre-requisites Before we start, make sure you have the latest version of [cargo-shuttle](https://docs.shuttle.dev/getting-started/installation) installed. For this example, we'll assume you are migrating an Axum project that has a database. ## Migrating your project ### Add your dependencies The first step is to add `shuttle-runtime` and `shuttle-axum` to your dependencies to be able to use Shuttle's runtime with the Axum framework. ```bash cargo add shuttle-runtime shuttle-axum ``` Any [secrets](https://docs.shuttle.dev/resources/shuttle-secrets) you need to use will be kept in a `Secrets.toml` file (dev secrets in `Secrets.dev.toml`) which will be placed at the `Cargo.toml` level. You can also easily get a provisioned database like so (this example will be for a provisioned PostgreSQL instance specifically): ```bash cargo add shuttle-shared-db --features postgres ``` If you have any database records you'd like to keep, it would be a good idea to export them so that they can be [migrated to the new database.](https://docs.shuttle.dev/guides/migrate-shared-postgres) **You will not need a secrets file if you only need a provisioned Postgres database - this will be automatically be provisioned and given to you in the form of a connection string or an sqlx pool.** ### Migrating your code To be able to run your project on Shuttle, you need to make a few changes to your code. Instead of the `tokio::main` macro, you will use the `shuttle_runtime::main` macro and swap out `dotenvy` for Shuttle's Postgres annotation: This is what your `main.rs` file might look like before: ```rust main.rs #[tokio::main] async fn main() { dotenvy::dotenv().ok(); let url = dotenvy::var("DATABASE_URL").expect("No database URL was set!"); let pool = sqlx::Pool::connect(&url).await.unwrap(); sqlx::migrate!() .run(&pool) .await .expect("Migrations failed :("); let router = create_api_router(pool); let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); Server::bind(&addr) .serve(router.into_make_service()) .await .unwrap() } ``` And this is what it looks like after: ```rust main.rs #[shuttle_runtime::main] pub async fn axum ( #[shuttle_shared_db::Postgres] pool: PgPool, #[shuttle_runtime::Secrets] secrets: shuttle_runtime::SecretStore, ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!() .run(&pool) .await .expect("Migrations failed :("); // Use secrets for anything that needs them let router = create_api_router(pool); Ok(router.into()) } ``` If you need more than a simple router, you'll want to create a custom struct that holds all of your required app state information inside and then create an `impl` for the struct - you can find more about that [here.](https://docs.shuttle.dev/tutorials/custom-service) Anything outside of your entry point function (the function that uses the `shuttle_runtime::main` macro) doesn't need to be changed. If you are using secrets as well as a database connection, you may wish to create a struct that holds both of these values and then pass it into the function that generates the router. Interested? We also have some [additional docs on how to do this]() ### Deploying To ensure that you get a unique project name, create a `Shuttle.toml` file at the `Cargo.toml` level to name your project to whatever you like. ```toml name = "my-unique-app-name-here" ``` Now all you need to do is to run the following commands: ```bash shuttle project start shuttle project deploy ``` Your project should now be deployed! ## Finishing up Thanks for reading! Hopefully this article has helped you learn what it takes to migrate to Shuttle. Further reading: - [How to migrate to Shuttle](https://docs.shuttle.dev/migrations/introduction) - [Get Started with Axum](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - [Building with AWS S3 using Rust](https://www.shuttle.dev/blog/2024/04/17/using-aws-s3-rust) --- # Supercharged Web Scraping with Rust & Firecrawl Source: https://www.shuttle.dev/blog/2024/11/07/web-scraping-rust-firecrawl Date: 7 November 2024 Author: josh Tags: rust, tutorial, firecrawl Simplify your Rust data pipeline with LLM-assisted web scraping Building data aggregation pipelines can be tricky work. Having built some LLM-assisted services recently as well as a data scraping service, ironing out all of the edge cases can take a lot of time and effort (as well as selecting specifically what you want from the page!). In this article, we'll check out Firecrawl - an API for scraping the web and getting back LLM-ready ingestion material - and how you can deploy it with Shuttle. Firecrawl can be immensely helpful for anyone looking to simplify their data pipeline. It is a self-hostable API that aims to make LLM-assisted website scraping data pipelines much simpler by allowing you to scrape websites then automatically convert it in a format that is LLM-ready. Website scraping, while not necessarily difficult, can often be a time-consuming and tricky task to get right. This is particularly relevant if the used data is part of an ingestion pipeline: for example, data aggregation services (or users who need to collect aggregate data) may use scraping for websites that do not have their own API. Interested in checking out the code so you can deploy it or want to try running locally? [Check it out.](https://github.com/joshua-mo-143/shuttle-firecrawl-ex) ## Why use Shuttle with Firecrawl? Shuttle allows you to deploy Rust web services seamlessly and hassle-free with one-line deploys and being able to provision your infrastructure directly from annotations (main function parameter). Whether you need a provisioned database to store your scraping results, or a frontend for your web service so you can display your scraping results, Shuttle can do it for you. The less time you spend context switching, the more time you can spend writing code and fixing problems. ## Pre-requisites To get started, you will need the following: - A [Firecrawl](https://www.firecrawl.dev/) account - A Shuttle account (with `cargo-shuttle` installed) - The Rust programming language installed Once you've created your Firecrawl account, don't forget to grab your API key! You will need it in a little bit. ## Getting Started To get started, create a new Shuttle web service using `shuttle init --template axum`. This does the following: - Creates a new Hello World template with Axum and Tokio already added, as well as Shuttle-related dependencies (to enable deploying to Shuttle) - Initialises a new Shuttle project (if you've chosen to do so) Next, we'll add our required dependencies with a one-line command: ```bash cargo add firecrawl serde -F serde/derive ``` When using Shuttle, secrets are stored in a `Secrets.toml` (`Secrets.dev.toml` for local dev) file in the project root - it should look like so: ```toml FIRECRAWL_API_KEY = "my-api-key" ``` This is then used by an annotation macro in your main function to provide an immutable key-value store for your secrets: ```rust use shuttle_runtime::SecretStore; #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore // secrets get outputted as a variable here ) // ... rest of your code ``` Now to get started on writing code. Before we do anything else, we'll create a new `AppState` struct that will hold our Firecrawl client. This helps avoid overhead for creating the client each time we want to crawl or scrape a URL. ```rust use firecrawl::FirecrawlApp; #[derive(Clone)] struct AppState { ctx: FirecrawlApp, } impl AppState { fn new(firecrawl_key: String) -> Self { let ctx = FirecrawlApp::new(firecrawl_key) .expect("FirecrawlApp to be created"); Self { ctx } } } ``` ## Scraping In this part, we'll add a simple endpoint for scraping - which will take a POST request and a URL to be scraped (as part of the JSON body). ```rust use serde::Deserialize; #[derive(Deserialize)] struct Request { url: String, } ``` When a HTTP request is received, we should then grab our application state as well as the JSON body as function parameters. Note that the `axum::extract::State` and `axum::Json` types have been destructured to automatically provide access to the inner types (`AppState` and `Request`, respectively). We then create our `ScrapeOptions` struct and use it with our Firecrawl client to scrape a given URL, which will return two things: - The scraped document, as a markdown file - The original document HTML ```rust use axum::{Json, extract::State, http::StatusCode}; use firecrawl::scrape::{ScrapeFormats, ScrapeOptions}; async fn scrape_url( State(state): State, Json(json): Json, ) -> Result { let formats = vec![ScrapeFormats::Markdown, ScrapeFormats::HTML]; let scrape_opts = ScrapeOptions { formats: Some(formats), ..Default::default() }; let result = match state.ctx.scrape_url(&json.url, scrape_opts).await { Ok(res) => res, Err(e) => return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), }; Ok(Json(result)) } ``` ## Hooking it all back up Before we deploy, we need to hook everything back up to our main function! We'll hook up the async function handler to our `axum::Router` as well as our application state, then return the router. ```rust #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore ) -> shuttle_axum::ShuttleAxum { let firecrawl_api_key = secrets .get("FIRECRAWL_API_KEY") .expect("FIRECRAWL_API_KEY secret to exist"); let state = AppState::new(firecrawl_api_key); let rtr = Router::new().route("/", post(scrape_url)).with_state(state); Ok(rtr.into()) } ``` The `secrets` variable is provided as an immutable key-value store - so we can simply use the `get()` function to try to grab our API key, erroring out if it doesn't exist. ## Deploying To deploy, all you need to do is use `shuttle deploy` and watch the magic happen! Note that you will need to add the `--allow-dirty` flag at the end if working on a dirty Git branch. ## Finishing up Thanks for reading! Hopefully this article has made it easier for you to understand how using Firecrawl can help to accelerate your workflow and simplify your data ingestion. Ideas for extending if you want to take this tutorial further: - Try adding a job queue to make website scraping requests more resilient - allow users to view the results of their scrape through adding CRUD endpoints - Try adding a database to store your results! Further reading: - [Make a RAG web service with Qdrant & Rust](https://www.shuttle.dev/blog/2024/02/28/rag-llm-rust) - [Prompting AWS Bedrock with Rust](https://www.shuttle.dev/blog/2024/05/10/prompting-aws-bedrock-rust) - [Implement an API rate limiter for your web service](https://www.shuttle.dev/blog/2024/02/22/api-rate-limiting-rust) --- # Using Rust in sprints and marathons Source: https://www.shuttle.dev/blog/2024/10/24/using-rust-in-sprints-and-marathons Date: 24 October 2024 Author: josh Tags: rust, opinion Tips for using Rust in sprints and the benefits of Rust in long term projects Rust is growing more than ever. Though not as commonplace as a language like Java or C#, it is slowly becoming a mainstream language. A lot of large companies have placed their bets on Rust: Microsoft's inclusion of Rust in the windows kernel, Android, Huawei and many of Amazon's services and more have solidified its place in the ecosystem. Through my time using Rust, I've used it in both short and longer term projects professionally and have been able to learn a lot about what Rust projects look like both in the short and long term, as well as potential pitfalls. As mentioned in our previous article [that talks about using Rust on the backend](https://www.shuttle.dev/blog/2024/07/31/rust-on-the-backend), Rust offers a lot of advantages. Of course, it does not go without saying that what advantages Rust can offer you will depend on your use case, but for many, many developers, Rust can absolutely be the right choice. ## What does Rust look like in a sprint? Unless you are working in a company that uses Rust across the stack, using it in the short term can be a contentious decision for many. When you are in an environment with a "ship fast and break things" mentality, it can be challenging to keep velocity in a new Rust project especially if you are not a domain expert. ### Speeding up time-to-ship with Shuttle Shuttle makes Rust easy to ship with by providing resource annotations from code. For example, we support Postgres database resources as a code annotation through providing it to your main function as a function parameter. As well as this, we support one-line deploys that require zero infra configuration. This makes it much easier to focus on code without context switches. On projects that require a high cognitive load this can significantly reduce complexity, as well as reduce time to first deployment and iteration. With Shuttle, you can deploy quickly and securely, which is perfect for companies like tech startups and larger companies who may be considering greenfield Rust projects. ### Not everything has to be perfect While Rust can absolutely be the right choice for a lot of projects, it can occasionally be difficult to not get caught up in over-engineering your project or making sure your project is perfectly efficient. The beauty of Rust isn't just that you can fearlessly refactor. If you don't need a perfect solution out the gate, it can oftentimes be easier to (for example) just `.clone()` variables if you are running into complicated lifetime issues. Additionally, using `.unwrap()` or `.expect()` and then refactoring later on can also be another way to speed up development. Shortcuts like these can help speed up development in Rust significantly. ### What about after the first iteration? Once you've managed to complete the first iteration, you can then focus on refactoring through adding crates like `eyre` or `anyhow` for better error handling or converting some functions to use more efficient parameters (for example, taking `&str` instead of `String`, or even using `AsRef` for more flexibility). You may also want to create traits for types where you want to add a generic implementation. ## What does Rust look like in a marathon? Of course, Rust is known for being a language that shines in large-scale projects and marathons. After shipping the MVP plus a few features, it is relatively easy to add code without breaking production (not including logic bugs!) due to compile-time assistance. Being able to easily being able to set up tests in the same files as your code also helps - or if you want more in-depth tests, you can create your own test harness. ### Fearless refactoring Fearless refactoring is not only the ability to refactor comfortably. It is also knowing that once you're done refactoring, it's much easier to ensure your program still works (potentially with the help of added tests for any additional functionality). It is no secret that Rust being a compiled, statically typed language helps significantly with this. However, traits also allow additional compile-time guarantees that are typically not possible in many languages where a programming sprint may turn into a marathon (Python without `basedpyright`, JavaScript, etc...). While it's possible to still cause deadlocks and memory leaks in Rust, typically you won't have to think about runtime errors as much due to compile-time checks. ### Onboarding new developers Typically, in a large project, you'll already have one or two senior developers who can onboard less experienced members. Rust plays extremely well into this, as coding in Rust can often lead to better dev practices. This is absolutely what you want for a long term project, especially if the team members are going to stick around for a while. ## Finishing up For both short and long term projects, Rust can be a great language, and its recent growth has showcased this. Fast, secure programming is key to sprints that lead into marathons. If you're looking to try and use Rust, whether in a new greenfield project or as an addition to a current project, feel free to reach out and let us know what you are working on! More reading: - [Everything you need to know about testing in Rust](https://www.shuttle.dev/blog/2024/03/21/testing-in-rust) - [Event driven services with Kafka and Rust](https://www.shuttle.dev/blog/2024/04/25/event-driven-services-using-kafka-rust) - [Getting started with Actix Web](https://www.shuttle.dev/blog/2023/12/15/using-actix-rust) --- # Using Kubernetes with Rust Source: https://www.shuttle.dev/blog/2024/10/22/using-kubernetes-with-rust Date: 22 October 2024 Author: jubril Tags: rust, kubernetes, tutorial Interacting with a Kubernetes cluster from Rust with `kube-rs` If you have ever tried to deploy a service in the last three to four years, there's a good chance someone has suggested using Kubernetes; however, if this is your first time hearing of Kubernetes, welcome. Kubernetes is an orchestrator that makes it easy to run large-scale containerized applications. What makes Kubernetes really great is its extensibility, by using the API users are able to create custom applications that don't ship with Kubernetes out of the box, It's how we have great tools like [ArgoCD](https://argo-cd.readthedocs.io/en/stable/). If you clicked on this post, you are likely familiar with Kubernetes and looking to take things a step further, regardless of why you are here. In this article, we will look at a bunch of useful operations for interacting with Kubernetes using the planet's most loved crab language, Rust. ## Getting Started To create a local Kubernetes cluster, we will use [KinD](https://kind.sigs.k8s.io/), a tool that allows you to run Kubernetes in Docker, hence the name KinD. Don't forget to have Docker installed! If you're on Mac, you can install kind using brew: ```bash brew install kind ``` For Linux users, if you have Go installed and [path set correctly](https://www.google.com/search?client=firefox-b-d&q=setting+gopath), you can install KinD using: ```bash go install sigs.k8s.io/kind@v0.24.0 ``` With that installed, you can create a new cluster by running: ```bash kind create cluster --name krust-playground ``` ### Pre-requisites Along with KinD, you will also need to install Kubectl. If you are feeling adventurous, you can install that right away using: ```bash curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" ``` And for ARM machines: ```bash curl -LO https://dl.k8s.io/release/v1.31.0/bin/linux/arm64/kubectl ``` Otherwise, take a look at [this section](https://kubernetes.io/docs/tasks/tools/#kubectl) of the Kubernetes docs for more specific installation instructions. ## Kube-rs? To interact with the Kubernetes API we will need a client library. As described in the project's readme, `kube-rs` is: | A [Rust](https://rust-lang.org/) client for [Kubernetes](http://kubernetes.io/) in the style of a more generic [client-go](https://github.com/kubernetes/client-go), a runtime abstraction inspired by [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime), and a derive macro for [CRDs](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) inspired by K[ubebuilder](https://book.kubebuilder.io/reference/generating-crd.html) ## Project Setup Create a new cargo project: ```bash cargo init k8s-rs ``` Add the following crates to your `Cargo.toml` ```toml [dependencies] tokio = { version = "1", features = ["full"] } kube = { version = "0.95.0", features = ["runtime", "derive" , "ws"] } k8s-openapi = { version = "0.23.0", features = ["latest"] } serde_json = "1.0" tracing = "0.1.37" tokio-util = { version = "0.7.8", features = ["io"] } tokio-stream = { version = "0.1.9", features = ["net"] } tracing-subscriber = "0.3.17" futures = "0.3.28" anyhow = "1.0.71" schemars = "0.8.12" serde = { version = "1.0", features = ["derive"] } yaml-rust2 = "0.8" ``` ## Connecting to Your Cluster We'll begin by looking at how to connect to the cluster we created earlier. By default, your connection information is stored in `~/.kube/config`. To use this in Rust, we can use the following code: We'll begin by looking at how to connect to the cluster we created earlier: ```rust use kube::{Config}; #[tokio::main] async fn main() -> Result<(), Box> { let config = Config::infer().await?; println!( "Connected to cluster at {:?}", config.cluster_url.host().unwrap() ); Ok(()) } ``` Using `Config::infer()` `kube-rs` will attempt to connect to the cluster using your default [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/). ## Creating a Client Using the config is great for printing out cluster information, but we can't use it to interact with it. To create a new cluster, we can use `Client::try_default()`, which calls `Client::infer()` under the hood and returns a new client: ```rust use kube::{Client}; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; Ok(()) } ``` ## Working with Pods Pods are a fundamental resource in Kubernetes. To create one using `kube-rs` we can use the following code: ### Create a pod ```rust use k8s_openapi::api::core::v1::Pod; use kube::{Api, Client, Config}; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; let pods: Api = Api::default_namespaced(client); // Define the Pod using a JSON spec let pod_json = json!({ "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "pong-pod" }, "spec": { "containers": [{ "name": "pong-container", "image": "ghcr.io/s1ntaxe770r/pong" }] } }); let pod = serde_json::from_value(pod_json)?; let pod = pods.create(&kube::api::PostParams::default(), &pod).await?; println!("Pod created: {}", pod.metadata.name.unwrap()); Ok(()) } ``` This works, but as we start to work with larger objects, it can quickly get confusing, and who really likes writing json? Alternatively, you can create a pod by using a struct; we will be using this method going forward. ```rust use k8s_openapi::api::core::v1::{Container, Pod, PodSpec}; use kube::api::{ObjectMeta, PostParams}; use kube::{Api, Client}; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; let pods: Api = Api::default_namespaced(client); let pod = Pod { metadata: ObjectMeta { name: Some("pong-pod".to_string()), ..Default::default() }, spec: Some(PodSpec { containers: vec![Container { name: "pong-container".to_string(), image: Some("ghcr.io/s1ntaxe770r/pong".to_string()), ..Default::default() }], ..Default::default() }), ..Default::default() }; let pod = pods.create(&PostParams::default(), &pod).await; match pod { Ok(pod) => { println!("created pod {}", pod.metadata.name.unwrap()); } Err(e) => { println!("unable to create pod {}", e) } } Ok(()) } ``` ### Listing pods Another common operation is listing pods. With the code snippet below, we can list the pods in the `kube-system` namespace. ```rust use k8s_openapi::api::core::v1::Pod; use kube::{api::ListParams, Api, Client}; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; let pods: Api = Api::namespaced(client, "kube-system"); let list_params = ListParams::default(); for pod in pods.list(&list_params).await? { println!("Found Pod {:?}", pod.metadata.name.unwrap()) } Ok(()) } ``` We can also filter what pods by using labels. Labels are customizable key/value tags attached to objects, used for organization. ```rust use k8s_openapi::api::core::v1::Pod; use kube::{api::ListParams, Api, Client}; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; let pods: Api = Api::namespaced(client, "kube-system"); let list_params = ListParams::default().labels("component=kube-apiserver"); for pod in pods.list(&list_params).await? { println!("Found Pod {:?}", pod.metadata.name.unwrap()) } Ok(()) } ``` In this example, only pods with the label `component=kube-apiserver` will be returned. ### Updating Pods While creating and listing pods are everyday tasks, you'll often need to update existing pods. Let's look at an example of adding labels to a pod. First, let's add a label to our existing `pong-pod`: ```rust use k8s_openapi::api::core::v1::Pod; use kube::{Api, Client, Config}; use kube::api::{Patch, PatchParams}; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize,Debug)] struct PodPatch { metadata: PodMetadataPatch, } #[derive(Serialize, Deserialize,Debug)] struct PodMetadataPatch { labels: std::collections::BTreeMap, } #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; let pods: Api = Api::default_namespaced(client); let mut new_labels = std::collections::BTreeMap::new(); new_labels.insert("environment".to_string(), "production".to_string()); let patch = PodPatch { metadata: PodMetadataPatch { labels: new_labels, }, }; let params = PatchParams::default(); let patched_pod = pods.patch("pong-pod", ¶ms, &Patch::Merge(&patch)).await?; println!("Pod updated with new label: {:?}", patched_pod.metadata.labels); Ok(()) } ``` ### Deleting Pods Finally, we can delete pods using the following code: ```rust use k8s_openapi::api::core::v1::Pod; use kube::api::{DeleteParams, Patch, PatchParams}; use kube::{Api, Client}; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; let pods: Api = Api::default_namespaced(client); let delete_params = DeleteParams::default(); pods.delete("pong-pod", &delete_params).await?; println!("deleted pod"); Ok(()) } ``` ## Interacting with running Pods Switching gears a little, let's explore how to interact with running pods. **Fetching logs from a Pod** Often, you'll want to see what's happening inside a pod to debug issues or monitor application behavior. We can mimic the functionality of `kubectl logs` using the kube-rs. Here's a snippet showing how to fetch logs from the API server: ```rust use k8s_openapi::api::core::v1::{Container, Pod, PodSpec, ContainerPort}; use kube::{Api, Client, ResourceExt}; use kube::api::{PostParams, WatchEvent}; use futures::{StreamExt, TryStreamExt}; use tokio::time::{Duration, sleep}; #[tokio::main] async fn main() -> Result<(), Box> { // Initialize the Kubernetes client let client = Client::try_default().await?; let pods: Api = Api::default_namespaced(client); // Define the Pod with port 80 exposed let pod = Pod { metadata: kube::api::ObjectMeta { name: Some("nginx-example".to_string()), ..Default::default() }, spec: Some(PodSpec { containers: vec![Container { name: "nginx".to_string(), image: Some("nginx:latest".to_string()), ports: Some(vec![ContainerPort { container_port: 80, ..Default::default() }]), ..Default::default() }], ..Default::default() }), ..Default::default() }; // Create the Pod let pod = pods.create(&PostParams::default(), &pod).await?; println!("Created pod: {}", pod.name_any()); // Wait for the Pod to be ready let timeout = Duration::from_secs(60); let start = std::time::Instant::now(); loop { let pod = pods.get("nginx-example").await?; let status = pod.status.as_ref().expect("Pod status should be available"); let Some(phase) = &status.phase else { if start.elapsed() > timeout { return Err("Timed out waiting for pod to be ready".into()); } sleep(Duration::from_secs(1)).await; continue; }; if phase == "Running" { println!("Pod is running"); break; } if start.elapsed() > timeout { return Err("Timed out waiting for pod to be ready".into()); } sleep(Duration::from_secs(1)).await; } // Fetch logs let logs = pods.logs("nginx-example", &Default::default()).await?; println!("Pod logs:\n{}", logs); Ok(()) } ``` This example demonstrates a slightly more realistic scenario of working with Pods in a Kubernetes cluster. We create a Pod running Nginx, wait for it to be ready, and then fetch its logs. After creating the Pod, we use the `status` field to determine if the Pod is running: ```rust let Some(phase) = &status.phase else { if start.elapsed() > timeout { return Err("Timed out waiting for pod to be ready".into()); } sleep(Duration::from_secs(1)).await; continue; }; ``` ### Port forwarding to a Pod Sometimes, you need to interact directly with a service running in a pod. We can replicate the functionality of `kubectl port-forward`. Here's a stripped-down version of port forwarding, adapted from the kube-rs repository: First, let's set up the Kubernetes client and wait for our pod to be running: ```rust use anyhow::Context; use std::net::SocketAddr; use k8s_openapi::api::core::v1::Pod; use kube::{ api::Api, runtime::wait::{await_condition, conditions::is_pod_running}, Client, ResourceExt, }; use tracing::*; use futures::StreamExt; use futures::TryStreamExt; use tokio::net::{TcpListener, TcpStream}; use tokio_stream::wrappers::TcpListenerStream; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); let client = Client::try_default().await?; let pods: Api = Api::default_namespaced(client); // Get the Pod let p = pods.get("nginx-example").await?; info!("Found pod: {}", p.name_any()); // Wait until the pod is running let running = await_condition(pods.clone(), "nginx-example", is_pod_running()); tokio::time::timeout(Duration::from_secs(60), running).await??; info!("Pod is running"); } ``` Next, we'll set up the local address to forward traffic to: ```rust // ... (in main function) let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); let pod_port = 80; info!(local_addr = %addr, pod_port, "forwarding traffic to the pod"); info!( "try opening http://{0} in a browser, or `curl http://{0}`", addr ); info!("use Ctrl-C to stop the server and delete the pod"); ``` Using a `TcpListenerStream`, we set up a local TCP server that listens for incoming connections: ```rust let server = TcpListenerStream::new(TcpListener::bind(addr).await?) .take_until(tokio::signal::ctrl_c()) .try_for_each(|client_conn| async { if let Ok(peer_addr) = client_conn.peer_addr() { info!(%peer_addr, "new connection"); } let pods = pods.clone(); tokio::spawn(async move { if let Err(e) = forward_connection(&pods, "nginx-example", 80, client_conn).await { error!( error = e.as_ref() as &dyn std::error::Error, "failed to forward connection" ); } }); Ok(()) }); if let Err(e) = server.await { error!(error = &e as &dyn std::error::Error, "server error"); } info!("Shutting down"); ``` Finally, let's look at the function that forwards the connection: ```rust async fn forward_connection( pods: &Api, pod_name: &str, port: u16, mut client_conn: TcpStream, ) -> anyhow::Result<()> { let mut forwarder = pods.portforward(pod_name, &[port]).await?; let mut upstream_conn = forwarder .take_stream(port) .context("port not found in forwarder")?; tokio::io::copy_bidirectional(&mut client_conn, &mut upstream_conn).await?; drop(upstream_conn); forwarder.join().await?; info!("connection closed"); Ok(()) } ``` ## Working with Custom Resources Kubernetes has an interesting concept called custom resources. These allow you to extend the Kubernetes API and add new resources. This by itself isn't very descriptive, so let's take things a step further with an example. Say you wanted to inform Kubernetes about a new resource called `crustacean`. By default if you run `kubectl get crustacean`, you will get an error saying `error: the server doesn't have a resource type "crustacean."`. To create the resource, start out by defining a new custom resource: ```rust use kube::CustomResource; use kube::ResourceExt; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] #[kube( group = "experiments.gopherlabs.io", version = "v1", kind = "Crustacean", printcolumn = r#" {"name": "Habitat", "type": "string", "jsonPath": ".spec.habitat"} "# )] pub struct CrustaceanSpec { pub name: String, pub species: String, pub habitat: Option, } #[tokio::main] async fn main() -> Result<(), Box> { Ok(()) } ``` We can generate a manifest using the following code: ```rust use kube::CustomResource; use kube::CustomResourceExt; use schemars::schema_for; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::Write; use yaml_rust2::{YamlEmitter, YamlLoader}; #[tokio::main] async fn main() -> Result<(), Box> { let larry = Crustacean::new( "larry", CrustaceanSpec { species: "lobster".to_string(), habitat: Some("Atlantic Ocean".to_string()), }, ); // Convert the CRD to JSON first let crd_json = serde_json::to_string(&larry)?; // Parse the JSON to yaml_rust2::Yaml let docs = YamlLoader::load_from_str(&crd_json)?; let doc = &docs[0]; // Emit YAML let mut out_str = String::new(); { let mut emitter = YamlEmitter::new(&mut out_str); emitter.dump(doc)?; } // Write the CRD to a YAML file let mut file = File::create("crustacean_crd.yaml")?; file.write_all(out_str.as_bytes())?; println!("\nCRD written to crustacean_crd.yaml"); Ok(()) } ``` Running the above code will produce the following manifest: ```yaml apiVersion: experiments.gopherlabs.io/v1 kind: Crustacean metadata: name: larry spec: species: lobster habitat: Atlantic Ocean ``` Great, but Kubernetes still has no idea how to handle this CRD. To do this, we need to apply the custom resource definition to the cluster: ```rust use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; use kube::{api::PostParams, Api, Client, CustomResource}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fs; use yaml_rust2::YamlLoader; impl Crustacean { pub fn new_crustacean(species: String, habitat: Option, name: String) -> Self { Crustacean { metadata: ObjectMeta { name: Some(name), ..Default::default() }, spec: CrustaceanSpec { species, habitat }, } } } #[tokio::main] async fn main() -> Result<(), Box> { // Create a client to interact with the Kubernetes API let client = Client::try_default().await?; // Read the Crustacean instance from the YAML file let crustacean_yaml = fs::read_to_string("crustacean_crd.yaml")?; let yaml_docs = YamlLoader::load_from_str(&crustacean_yaml)?; let yaml_doc = &yaml_docs[0]; // Extract values from YAML let name = yaml_doc["metadata"]["name"].as_str().unwrap_or("").to_string(); let species = yaml_doc["spec"]["species"] .as_str() .unwrap_or("") .to_string(); let habitat = yaml_doc["spec"]["habitat"].as_str().map(String::from); // Create Crustacean instance let crustacean = Crustacean::new_crustacean(species, habitat, name); // Apply the Crustacean instance let crustaceans: Api = Api::all(client.clone()); crustaceans .create(&PostParams::default(), &crustacean) .await?; println!("Crustacean instance applied successfully"); Ok(()) } ``` There are a couple of things going on in the snippet above, the important parts: - Using the `fs` crate, we load the yaml file from the current directory. - We introduce a helper method called `new_crustacean` to help with some of the setup for the CRD definition. After extracting the values from the yaml file, we pass the parameters to the `new_crustacean` function and apply the manifest. - The `ensure_crd` function checks if the CRD exists in the cluster before applying the manifest, this is essential because kubernetes will try and validated the manifest we apply against the CRD schema. ```rust async fn ensure_crd(client: &Client) -> Result<(), Box> { let crds: Api = Api::all(client.clone()); let lp = ListParams::default().fields("metadata.name=crustaceans.experiments.gopherlabs.io"); let existing_crds = crds.list(&lp).await?; if existing_crds.items.is_empty() { println!("CRD not found. Creating new CRD."); let crd = Crustacean::crd(); crds.create(&PostParams::default(), &crd).await?; println!("CRD created successfully") } else { println!("CRD already exists. Skipping creation."); } Ok(()) } ``` At this point, you should be able to run: ```rust kubectl get crustaceans ``` The output is similar to: ```rust NAME HABITAT larry Atlantic Ocean ``` ## Watching for Resource Changes So we've got our `Crustacean` CRD up and running. But creating resources is only half the battle. More often you want to react to events in the cluster this is where [watchers](https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes) come in, as the name suggests, watchers are used to monitor specific events. In our case, we'll monitor for new `Crustacean` resources and create a pod for each one. ```rust use futures::{stream, StreamExt, TryStreamExt}; use k8s_openapi::{api::core::v1::Pod, apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition}; use kube::{api::{PostParams, ListParams}, runtime::{watcher, WatchStreamExt}, Api, Client, CustomResource, CustomResourceExt, runtime::wait::{await_condition, conditions::is_pod_running},}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::*; use tokio::time::Duration; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::try_default().await?; tracing_subscriber::fmt::init(); // Watch for Crustacean resources let crustaceans: Api = Api::all(client.clone()); let lp = ListParams::default(); let mut crustacean_watcher = watcher(crustaceans, watcher::Config::default()).applied_objects().boxed(); // Watch loop: create a pod when a Crustacean resource is applied while let Some(crustacean) = crustacean_watcher.try_next().await? { let crustacean_name = crustacean.metadata.name.unwrap(); let pod_name = crustacean_name.clone(); info!("got crustacean {} , creating pod....",crustacean_name.clone()); // Create a pod with the same name as the Crustacean resource let pods: Api = Api::namespaced(client.clone(), "default"); let pod = Pod { metadata: kube::api::ObjectMeta { name: Some(crustacean_name.clone()), ..Default::default() }, spec: Some(k8s_openapi::api::core::v1::PodSpec { containers: vec![k8s_openapi::api::core::v1::Container { name: "crustacean-container".to_string(), image: Some("nginx".to_string()), ..Default::default() }], ..Default::default() }), ..Default::default() }; pods.create(&PostParams::default(), &pod).await?; let running = await_condition(pods.clone(), &pod_name, is_pod_running()); tokio::time::timeout(Duration::from_secs(60), running).await??; println!("Pod '{}' created for Crustacean resource.", crustacean_name); } Ok(()) } ``` Using the following code (as from the above code snippet): ```rust let mut crustacean_watcher = watcher(crustaceans, watcher::Config::default()) .applied_objects() .boxed(); ``` We set up a **watcher** to monitor the `Crustacean` custom resource, using the `applied_objects()`: method ensures that we only care about the resources that have been applied (created or updated). It filters out other events like deletions. In the **while loop**, we watch for events in the stream provided by the `crustacean_watcher`. For each `Crustacean` resource that the watcher detects, we create a Pod. Using `await_condition(pods.clone(), &pod_name, is_pod_running())`creates a future that waits for the condition where the Pod is running. `is_pod_running()` is a predefined condition function that checks the Pod's status to see if it's in the "Running" state. Running the example should result in the following output: ```rust warning: `k8s-rs` (bin "k8s-rs") generated 4 warnings (run `cargo fix --bin "k8s-rs"` to apply 3 suggestions) Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.42s Running `target/debug/k8s-rs` 2024-09-23T20:59:43.549885Z INFO k8s_rs: got crustacean larry , creating pod.... Pod 'larry' created for Crustacean resource. ``` And if we run `kubectl get pods`: ```rust NAME READY STATUS RESTARTS AGE app-deployment-6c8d544675-rn56b 1/1 Running 0 30h larry 1/1 Running 0 9s nginx-example 1/1 Running 0 19h patch-add-ephemeral-container 1/1 Running 64 (8m51s ago) 45h pong-deployment-84d87cb454-tqct9 2/2 Running 0 30h ``` Sure enough, Larry should be there. ## Summary If you made it to this part, you probably are really interested in `kube-rs`. While the library is pretty neat, Kubernetes has a LOT of API's and can quickly get confusing. When working with `kube-rs` or Kubernetes in general, here are some things to keep in mind. ### Know when to use a controller or an operator. These concepts have some overlap and can be very confusing, which is why I avoided mentioning so as not to blow this out of scope. Luckily, people far smarter than me have written on this topic. Check out Ivan Velichko's guide on [Kubernetes operators.](https://iximiuz.com/en/posts/kubernetes-operator-pattern/) Looking for something with more Rust? The folks at Metalbear have an entire guide on [creating an operator using kube-rs](https://metalbear.co/blog/writing-a-kubernetes-operator/). ### Examples don't bite The maintainers of `kube-rs` did an amazing job of creating several examples of using the crate. This guide is not an exhaustive list of all its features; some of the [examples used here](https://github.com/kube-rs/kube/tree/main/examples) were adapted from the repo. Using Rust to interact with Kubernetes can be quite refreshing, especially if you come from the [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) world. Creating a CRD by adding a [macro](https://www.shuttle.dev/blog/2022/12/23/procedural-macros) is extremely powerful, and in my experience thus far, I have written less code to achieve common functionality. If you're curious about admission controllers, check out the guide I wrote [here](https://www.civo.com/learn/kubernetes-admission-controllers-for-beginners?ref=shuttle.dev). ## Conclusion Of course, you don't need Kubernetes for everything. If you're interested in something more simple, Shuttle is your one-stop shop for easy Rust deployments. Create (or migrate) your project, use `shuttle deploy`, and watch the magic happen! --- # Shuttle's New Platform — Redefining Backend Development Source: https://www.shuttle.dev/blog/2024/10/10/shuttle-redefining-backend-development Date: 10 October 2024 Author: ivan Tags: rust, announcement, shuttle We've supercharged what developers love about Shuttle, combining our powerful developer experience with enterprise-grade infrastructure. ## Introduction Today we're introducing the new Shuttle platform! We've supercharged what developers love about Shuttle, combining our powerful developer experience with enterprise-grade infrastructure. For developers, we've kept it simple and intuitive - no complex configs, just focus on your Rust code. On the production side, we've implemented VM-level isolation, increased reliability and scalability to meet real-world demands. From solo developers to enterprise teams, Shuttle now offers the perfect blend of ease and production-ready robustness. ## Our Vision Shuttle is a new way of building and deploying cloud applications. We make dealing with cloud infrastructure simple and enjoyable so developers can focus on creating great products. Today, developers face a choice between the flexibility of large cloud providers and the simplicity of PaaS solutions, each with limitations. At Shuttle, we're forging a middle path—prioritising simplicity while preserving flexibility. Our open-source framework, [**_Infrastructure from Code_ (IfC)**](https://www.shuttle.dev/blog/2022/05/09/ifc), combined with our state-of-the-art platform, lets developers annotate their code to configure infrastructure and deploy instantly. IfC enables developers using GenAI to optimise cloud infrastructure setup and automate complex tasks, saving vast amounts of time. ### Backstory Over the past year we've seen Rust adoption increase by 64%, and our user base has tripled to 15,000+ developers. The number of projects deployed on Shuttle? That's grown five times over. It's been an exciting journey, and to embrace this growth and support more production use-cases, we've built something extraordinary. We're excited to introduce our new platform that's more scalable, secure, and powerful than ever! ## Introducing the New Platform Public Preview The new Shuttle platform takes what developers already love and cranks it up a notch. We've kept the great developer experience you're used to and combined it with production-ready infrastructure, ready to tackle your most ambitious projects. - **For Developers**: The seamless, intuitive experience you love — now smoother than ever. Our unique approach eliminates the need for complex configuration files or deep cloud expertise. With Shuttle you focus on writing your Rust code, while we handle the complexities of the cloud - deployment, scaling, and infrastructure management. Features like automatic database provisioning and effortless API integration remain at your fingertips from within your IDE, but are now enhanced with even greater reliability and performance. - **For Production**: With a Pro-tier experience tailored for putting things in production, building and operating distributed systems at scale is now a joy. We've upgraded our observability stack for even better uptime and faster incident response, letting you rest easy while we handle service restoration. Shuttle's enhanced capabilities for horizontal scaling, tenant isolation, security patching, and fault tolerance ensure your app scales seamlessly as demand grows. Watch out for exciting new features in this space! Whether you're a solo developer working on a passion project or an enterprise team deploying mission-critical applications, the new Shuttle platform has you covered. The perfect balance of development ease and production readiness. ## What's new? ### Scalability Our new system can handle 10 times more concurrent requests, 10 times more active projects with high uptime, and 20 times more concurrent builds. In simple terms, we've gone from a single-stage rocket to a fully reusable spacecraft. More payload, faster turnaround, and ready to explore new frontiers. ### Reliability We've put in place workload isolation and beefed up our monitoring. This means one resource-hungry project can't slow down others, and we can spot and fix issues faster than ever. ### Security We implemented VM-level isolation. Now, each project runs in its own little "world", completely separate from others. ### Builder Service We've completely revamped how we build projects. Our new system is more scalable, flexible and is built to handle a wider variety of build requirements. ### Laying the Groundwork for the Future By adopting container builds and a modular architecture, we've opened doors to exciting possibilities. While Rust remains our focus, we're now positioned to potentially support multiple languages in the future. Imagine bringing Shuttle's simple, joyful backend experience to your favourite language. It's ambitious, and we're one step closer to creating the best cloud experience for all developers. ### New Domain We have changed our domain name to shuttle.dev! The new platform is available to use via the [shuttle.dev](https://www.shuttle.dev) console. However, if you're an existing user looking to access the old platform and navigate between the two, you can find instructions [here](https://docs.shuttle.dev/introduction/platform-update). ## What does this mean for you? Whether you're a new user or have been with us from the start - you're in for a treat. Here's what you can expect from our new platform: - Better security with VM-level isolation between projects. - Ability to scale bigger and faster, handling sudden traffic reliably. - More stable performance across the board, unaffected by other busy projects. - Support for more complex builds, pushing the boundaries of what's possible. - Automatic restarts for improved reliability and uptime. ### Looking to migrate your project? Check out the [docs](https://docs.shuttle.dev/introduction/platform-migration) to find out more and get in touch via [Discord](https://discord.gg/shuttle), [Email](mailto:support@shuttle.rs) or Intercom if you need any support! ## Pricing for every use-case Building in the cloud shouldn't be harder to use as your demands grow. Don't sacrifice developer experience for production requirements. For us, developer experience isn't just a feature, it's at the core of our company and we're building our pricing with that in mind: **Free Tier — Perfect Starting Point** - Ideal for hobbyists, students, and new ideas - Offers a fantastic developer experience for learning and experimentation—the perfect place to start with Shuttle without incurring costs - Growth engine for our vibrant community **Pro Tier — Streamlined Production Experience** - Ideal for small to medium-sized production use cases - Offers a reliable and scalable off-the-shelf production experience - Access to advanced features (including AI-powered ones) to supercharge your development **Enterprise Tier — Tailored Solutions for Complex Needs** - Ideal for larger enterprises or more complex use-cases - Focused on providing tailor-made production value and solving organizational challenges - Offers flexible deployment options to meet your specific requirements and dedicated support **No matter your scale or needs, Shuttle has the perfect solution** to elevate your development experience and streamline your cloud deployments. From individual developers to enterprise teams, we're here to support your journey and help you build amazing applications with ease. If you have any questions for your specific use-case, [click here](https://cal.com/team/shuttle/shuttleproductdiscoverycall) book a call! ## Join Us We built Shuttle to break down cloud development barriers and provide the best backend experience possible making cloud deployment as intuitive as writing code. Whether you're a long-time user or new to Shuttle, now's the time to try the new platform. Get started now and head to [shuttle.dev](https://www.shuttle.dev) to join us redefining backend development. --- # Why I Learned Rust - as a Python dev Source: https://www.shuttle.dev/blog/2024/09/18/why-i-learned-rust-mark Date: 18 September 2024 Author: mark Tags: rust, opinion The story of how one developer, who currently works at Shuttle, came to Rust from Python. ## How It Started Way back in the ancient year of 2016 I founded a SaaS startup in the FinTech space. In addition to my role as founder, I also served as the head (read: _sole_) maintainer of the in-house utility-wrapper-client-library-*thing*¹ that actually enabled customers to _use_ the platform. It was written in Python, and included support for more than 6 different (rather niche) industry-specific softwares, enabling near-real-time synchronization between customer's brick-and-mortar stores and our backend. I say near-real-time for two reasons - first, even a minute or two of lag between a record being changed in a customer's system and that change being reflected in our backend was absolutely head and shoulders above customer ask or expectation, so I never aimed to improve that aspect of things. Second though, and more germane to the topic at hand, some of the softwares we supported hadn't been updated since (and this is not a joke or exaggeration) 1992. They did not use anything that might be considered a "modern" data storage format, nor did they even include any real concept of networked _anything_ newer than perhaps FTP. The plugin to the utility-wrapper-client-library-*thing*¹ that supported that specific software was the absolute unquestioned bane of my existence. By necessity, it had to be written initially as a hand-coded parser for the specific format the customer-side software stored data in which meant it was effectively read-only. Moreover, while writing in Python did lower the bar pretty dramatically, it also meant that it was doing disk and network I/O _in Python_. To make matters that much worse, due to a quirk of the storage format along with the requirements of what the support wrapper was actually doing meant that the plugin couldn't chunk or lazy load the file(s) in question - it _had_ to read each one into memory in its entirety every single time a sync needed to be performed. Oh and naturally - let's not forget lock contention baby. > Pictured: a biblically-accurate representation of myself working on the Python code described above ![https://images-cdn.9gag.com/photo/a04jWqQ_700b.jpg](https://images-cdn.9gag.com/photo/a04jWqQ_700b.jpg) ## Cue The Mysterious Stranger† On A Dark Night‡ Offering A Questionable Bargain§ † - StackOverflow ‡ - just a regular Tuesday § - [**_RIIR_**](https://ghost.fission.codes/content/images/2023/04/Rewrite-It-In-Rust---Postcard---Front.jpeg) Frustrated with the frankly abysmal performance of this specific plugin, I found myself diving into ever darker waters in pursuit of the dream that has haunted Python devs since time immemorial: how to make this _one_ stupid thing bottlenecking my code performant!? 😡 Naturally I ran into all the usual answers first - threading, multiprocessing, and async. Of the seemingly available options at the time multiprocessing was out as there was just no way I was going to voluntarily double down on the lock contention issues I was already dealing with. Async seemed like an attractive option, but I'd need to learn a whole new paradigm and it didn't feel like I had the breathing room to do that at the time. That just left threading which... underwhelmed for reasons that escape my memory as I'm writing this, so we're just gonna sweep that under the rug and thank you for graciously playing along for the sake of the story. What kept popping up though, were posts and articles about how libraries like NumPy, pandas, and SciPy had addressed performance issues. Specifically, things like [cython](https://github.com/cython/cython), [numba](https://numba.pydata.org/), and [nuitka](https://nuitka.net/). Heck, if I remember correctly, I think I may have even _tried_ nuitka. Ultimately, none of them really seemed like options worth pursuing. Reflecting on it now, my reasons seem shakier than I'd like to admit, but again - rug sweep; we're not here for self-reflection, we're here for a story! My thinking at the time went like this: - Cython seems like the best option, but to really get the most out of it you kinda _have_ to use the expanded syntax (which is a superset of Python, but in this case we're specifically talking about the super parts, not the Python-identical parts) - If I'm effectively going to have to learn a new programming language, maybe I should consider options other than Cython; after all, FFI is a thing and maybe there's something better? - Blow dust off C/C++ books from college- _Immediate no, hard pass_ - Hmmm... [go](https://go.dev/) has been getting a lot of press lately, can you write Python extensions in go? - Answer: surprisingly, [_yes_](https://github.com/asottile/setuptools-golang) - note: there are likely better tools for this now, but I distinctly recall `setuptools-golang` existing at the time. In the interest of full disclosure, "it exists" is about as far as I got into it though. - StackOverflow _really_ seems to love rust too though. Can you write Python extensions in rust? - Answer: [absolutely](https://github.com/PyO3/pyo3), 110% you can, and it's like... _easy_. Like almost laughably, comically easy. Though for story purposes, I didn't know that at _this_ precise moment in the narrative. - What's the real difference between rust and go? - So far outside the scope of this story I don't even have a postal code for it - _At the time though:_ - go was billed as having a less steep learning curve, and as primarily offering type-safety, speed, and _really, really_ good concurrency - rust was billed as having a near-mythical learning curve, but offering a feature-set that almost boiled down to "if it _compiles at all_, it'll run without issue" along with an almost draconian stance on compatibility and the meaning of semver on the part of the maintainers that meant that the "if it compiles..." could be extended with "without any real maintenance, basically forever" What clinched it for me was an admittedly niche experience with a hard-to-replicate bug that _somehow_ was caused by _something_ inherent to the way Python does garbage collection, throwing (perhaps unearned) shade on the fact that Go is _also_ a garbage collected language. Ultimately, the actual literal thought I had was "if I'm gonna learn a compiled language, I may as well learn one that _doesn't_ include a whole runtime and garbage collector in the final binary". > Disclaimer: I am not today the software engineer I was the better part of a decade ago ![](/images/blog/why-i-learned-rust-mark-lisa.jpg) ## Act 3: Doin' It To 'Em So, I did it. I took the plunge and learned rust. I was apprehensive, because (at the time) rust had a reputation for being frustrating while you're still on the leftward side of the learning curve. The thing that really wiped all of that away for me though was one of the very first things [the rust book](https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#:~:text=Compiler%20errors%20can%20be%20frustrating) has to say about error messages from the compiler: > Compiler errors can be frustrating, but really they only mean your program isn't safely doing what you want it to do yet; they do not mean that you're not a good programmer! > > Experienced Rustaceans still get compiler errors. That more or less did away with any apprehension I felt about the compiler or the language itself. It almost felt like I'd made a deal with the compiler - "if you'll do what I say, make the changes I suggest, I promise I'll keep your feet safe from landmines and [footguns](https://matt-rickard.com/avoiding-footguns)". Coming from Python, it was just plain reassuring that the compiler wouldn't _let_ me put code out into the world that was just going to throw an error at runtime. I mean, it's still gonna yell, but it's gonna yell _at me_ and it's always gonna do it _before_ I ship something, not halfway through a customer's once-a-night, business-critical reconciliation with our backend. That was **huge**. Even huger though might be the amount of just plain... _paranoia_ or second-guessing code I could do away with. If you're a long-time, battle-seasoned Python programmer and you've ever had to deal with code that wants to deal with disparate remote systems (or even _one_ remote system that grew -cough- _organically_ vs. intentionally), I'll bet this looks familiar: ```python def convert(record): # ... if isinstance(record.amount, str): record.amount = convert_strings_to_amounts(record.amount) elif isinstance(record.amount, float): record.amount = cast_floats_to_valid_amounts(record.amount) elif isinstance(record.amount, dict): record.amount = assemble_amount_from_dict(record.amount) elif record.amount is None: raise TypeError(type(record.amount), record.amount, "expected an integer amount in pennies") if record.amount < 1: raise ValueError("transactions must have a positive, non-zero total") # ... ``` A few things here. First, here's the equivalent rust code: ```rust pub(crate) fn convert(record: RecordFromRemoteSystem) -> Result { // ... if record.amount < 1 { return Err(RecordConversionError("transactions must have a positive, non-zero total")); } // ... } ``` That's right - no guessing, `record.amount` **will** be an integer or the code just won't compile. That's all there is to it. To the Python dev I was 10 years ago, that's 🤯. Next, taking a look at the Python code again, it's... just not right. The _correct_ version of that (in my opinion) would be something more like: ```python from dataclasses import dataclass, field @dataclass class RecordFromRemoteSystem: """A transaction record from {remote system}.""" # ... amount: int = field( metadata=dict( description="The transaction total, in whole pennies", ), ) # ... def convert_to_local_record(record: RecordFromRemoteSystem) -> LocalRecord: """Convert the supplied record from {remote system} to a record compatible with {local system}.""" # ... ``` > Note: if we want to be even more nitpicky then I'm about to be: yes, you're right, `convert_to_local_record` should be an associated method of the `RecordFromRemoteSystem` class, and it probably should be renamed `to_local_record` or somesuch, but... Rug. Swept. Hush 🤫 See the proper type annotations and docstring? In Python, you can _ask_ devs to do that, you can even do things like configure [pre-commit hooks](https://pre-commit.com/) to try and enforce it, but ultimately there's nothing in the language itself or its "standard" tooling that's going to make it anything other that the equivalent of a "come on guys, I'm super for real, you gotta do this, please". Here it is in rust: ```rust #![deny(missing_docs)] /// A transaction record from {remote system} pub(crate) struct RecordFromRemoteSystem { // ... /// The transaction total, in whole pennies amount: u32, // ... } impl RecordFromRemoteSystem { /// Convert a record from {remote system} into a /// compatible record for {local system} pub(crate) fn to_local_record(self) -> LocalRecord { // ... } } ``` Not only are the type annotations simply a required part of rust's syntax, all it takes to ensure documentation gets written is that little `#![deny(missing_docs)]` at the top there. Add that, and it suddenly becomes just as much of a compile time violation to not write documentation as it would be to call a function expecting an integer and pass it a string value. I could probably go on all day with examples, but... ## Happily Ever After\* > - - terms and conditions apply, only for certain definitions of "happy", not legally binding in the State Of California or geographic areas under the sovereign authority of Victor von Doom My journey to and through rust is (perhaps obviously) much more than these simple examples, and I wouldn't dream of trying to keep you captive for all of them. For the purposes of the narrative, I will say that the experience of learning rust unquestionably made me a better Python developer. Aside from that though, coming to rust from Python, having experienced a lot of the most common and most _frustrating_ pitfalls in Python means that a large portion of my appreciation for rust is born from the things that just _aren't_ problems in rust the same way they are in Python. By that I mean things that either outright _aren't_ problems in rust, (like "what happens if some ~~idiot~~ _downstream user_ tries to call this function on a string and not an int like it's expecting), or things that aren't the same _caliber_ of problem in rust that they are in Python (like "start documenting your code or I'll break your fingers"). There a thousand and one other little things (like how nice rust's enums are, or how _fluid_ error and option handling are, or just the `match` statement) that rust brings together that help to heal the (admittedly _absolutely self-inflicted_) battle wounds Python development left me with. I'd be lying if I didn't say that working in rust doesn't have its _own_ frustrations, but sitting here now, writing this, I can't think of a single one that I'd hold up as my "if you could tell your younger self one thing" or even "what would you tell someone looking to learn rust". I hate, hate, **hate** to repeat this specific piece of advice simply because I remember how frustratingly hand-wavy it felt when _I_ first read it, but just read the rust book and remember that the compiler is _your friend_ and that when it says "no, you can't do that", it really means "that's not safe, you'll hurt yourself somewhere down the road if I let you do it, so I'm not gonna" and temper any frustration you might feel by remembering that even when it _does_ tell you no, it almost always follows it up with a suggestion of what to do so it can tell you yes instead. ## Shameless Self-Promotion Full disclosure, I _am_ one of the ~~ancient and unfathomably powerful wizards~~ _humble and dedicated_ engineers on Shuttle's dev team. Even when I wasn't though, I'd still tell anyone who would listen - you should do the CCH challenges. For anyone interested in rust _at all_, if you're just curious, just started, or even if you've been working in rust for years - take an afternoon and work through Shuttle's [Christmas Code Hunt](https://www.shuttle.dev/cch) challenge, even if it's the middle of July when you're reading this. The challenges start off easy, all have multiple ways to solve them _and_ multiple ways to earn points, and cover a truly ridiculous cross section of rust, backend, and web technology in general. They are undeniably an incredibly fun way to learn rust fundamentals, broaden your knowledge of building backend services in general and rust specifically, _and_ you end up with a super cool project you can show off. What's not to love? 😁 --- # Rust as My First Language Source: https://www.shuttle.dev/blog/2024/08/23/rust-as-my-first-language Date: 23 August 2024 Author: jeff Tags: rust, opinion One Rust developer's story about how they got into Rust. ## Who am I? I'm an engineer who works in a field that is not related to software development. Computers are in my blood, I've been around them since I was a child. I can code and have danced with many tools over the years, but I'm not even remotely a professional. In the dark days of early COVID, I decided I wanted to learn how to code...really learn. I feel every engineer should know how to, as a means of expressing a solution to a problem. You might not do it every day, but it's a great tool to have in the belt. Also, the notion of creating something from nothing has always fascinated me. This is what you do when you code, take a less than vague idea and turn it into a purposeful idea. I set off on a self-taught journey. ## Goals I didn't start out immediately with this goal, but it's grown over time. I want to be able to make simple services for not only for myself but also the company I work for. I want to do this with an ecosystem that supports me and makes it relatively painless to get things done. I say relatively because there so no such thing as truly painless. Very often, you're picking your pain. ## Why Rust? As I started writing this piece, I started to feel really foolish. The feeling got worse the more I wrote. I have no right to use Rust. Quite frankly it's not necessary. I could work in anything that's "easier". Rust is not easy, the learning curve is legendary. The things it brings to the table are maximum overkill for literally anything I might want to create. I could use JavaScript, Go, C#, literally any garbage collected language. Why Rust indeed. I began the journey with the intent to sit down and learn C++ once and for all. Much of the world's most flexible software is written with it. I dipped a toe in, completing a very basic course on Codecademy. One thing that I kept thinking as I went along, I'm not smart enough, when the chips are down, to do anything meaningful in C++. So, if not C++, then...what? I spent time with vanilla web tech, including pure JavaScript, with a bit of dabbling into React, React Native, and NextJS. The trouble is, I dislike JavaScript...and I don't want to be the billionth person learning the traditional ways. I want to learn, use and advocate for something different. There are too many voices for the traditional tech stacks. I don't want to be part of that. I wanted something different. Rust came onto my radar in early 2021. I installed it and began investigating. There are several aspects that drew, and continue to draw, me in. _Rules of the Road_ Rust can go low and it can also go high. What I mean by that is you can get down into the dirt if you want, or you can stay up high with a web framework where someone else has dug in the dirt. The clear rules around memory use were really appealing to me. If I want to roll everything myself, I have a higher chance to create something safe out of the box, leaving me to worry mainly about logic errors. You get the power of C++ without the sleepless nights fretting about memory leaks, race conditions, and other nastiness. _Tooling_ A struggle with Javascript is layer upon layer of tooling, that you have to cobble together from different places. Newer JavaScript runtimes, such as Bun and Deno, are making strides to solve this, but there's still a level of mental overhead. In Rust land, you have `cargo` as your package manager. Once you start using it, nothing else is good enough. When you leave it, you immediately miss it its coherence. _Pair Programming_ The Rust compiler is very tough, but in a good way. Out of the box you have an immediate ally and pair programmer sitting beside you. I've learned a lot from it. I miss it when it's gone. `Clippy`, Rust's linter, is ready and waiting too, offering rich, helpful suggestions. _Testing_ This may be a cliche, but testing is a first-class concept in Rust land. You don't have to do anything special or pull in anything external to write tests. As a new, sole "developer" I need to be diligent and write tests. The fact it's so easy is really appealing. _The Challenge_ Rust is technically challenging. It presents a terrific puzzle to solve. Yes, it can be frustrating and at times can impede your progress, but the feeling when you get the pieces together is second to none. The feeling when you can just sit down and get some Rust code down to accomplish something is really wonderful. ## My Approach to Learning _Early Days_ I've always been someone who learns by example. Reading theory and trying to build straight from that has always been a challenge. I get bored quickly just exploring the individual building blocks. I need to see an end goal, then I go about putting together the pieces, breaking them down and gaining insight along the way. When I started in early 2021, there weren't a lot of learning materials from which to gain context and best practices. I used Medium articles quite a bit, and made my first attempt to get through the Rust Book, which of course, is the consensus starting point. In these early days, the power and point of the type system wasn't quite visible. I found the basics of Rust similar to C, which I knew a little from the past, so this was an early hook. I did drift away from time to time, as I do suffer from "grass is greener, maybe "X" is easier syndrome", trying out other corners and other languages. However, Rust had made a mark on me and I always kept an eye on it. The promises it brought to the table we too great to overlook. _Zero to Mastery_ I came to realize I wasn't achieving anything by dancing around. I wanted to get back to Rust. I decided I needed a course, enter [Zero to Mastery](https://zerotomastery.io) and the Rust course by Jayson Lennon. I enrolled in March 2022 and spent the next few months working through his amazing course. It's taught like a beginner programming course, building up from the very basics. I highly recommend it. Jayson is a fantastic teacher. He's very active and supportive on the Zero to Mastery Discord. _Zero to Production in Rust_ I attempted to work through [Zero to Production](https://www.zero2prod.com) in Rust, by [Luca Palmieri](https://www.lpalmieri.com) before fully completing the Zero to Mastery Rust course. It was a mistake, for two reasons, a) I wasn't ready and b) I rushed too much and didn't focus on the journey. In early 2023, I returned to it again, this time pushing myself to use a different web framework, Axum instead of Actix Web. This turned out to be an excellent move. It was challenging for sure, and admittedly, the unfortunate tendency to rush to the finished "thing" was still there, but it made me think and engage more. I had to start getting acquainted with crate documentation in order to figure out and implement the steps of the project. I highly recommend "ZerotoProd", it's an essential step for new Rustaceans. _Contributing to Open Source_ Somewhere early in 2023, I discovered [Shuttle](https://www.shuttle.dev). Re-discovered is maybe better, as I as aware of the project since late 2022. Again, I dove in with both feet and started helping with their documentation. In the summer of 2023, I tentatively put my name down to actually work in the Shuttle codebase. I helped implement a few new methods for Shuttle Persist. I felt so stupid, banging my head for hours sometimes, slinging code hoping something would stick. The Shuttle Team was so gracious, quietly coaching and pointing the way. The work I did really helped with some things I was struggling with, chief of which was testing. I had to learn how to write unit tests to support the code I added. I'm sure the Team have re-factored and thrown away what I originally wrote by now, but seeing it committed and in the codebase was a wonderful feeling. Contributing to open source...to something real, was an essential step in the journey. I continue to help with simple things and nurse the desire to, someday, really help with a big new feature. _Shuttle Christmas Code Hunt_ In December 2023, the Shuttle folks started the first ever "Christmas Code Hunt", in the vein of Advent of Code. In contrast to that, the Shuttle challenges were far more approachable. At first, I thought, I'll add to the challenge by using `Tower`, rather than a higher level web framework. By this time, crate documentation was less mystical to me, and I figured out enough theory that I felt I could get somewhere. I did...mostly. With a bit of kind community help, I completed the first few challenges before dropping off. I intend to return to these challenges in the future. _Iteration, Diffusion and Consistency_ Overall, iteration and repetition are key to me learning anything, especially Rust. I repeat things, perhaps with small variations, that build muscle memory. I can now organize a project into a binary and library, with modules, effortlessly. Project organization was difficult at first, understanding file and folder boundaries, and how Rust treats them, was a challenge. The material in the Rust Book was abstract for my liking, lacking in specific, practical examples. Once it clicked, it suddenly became very logical. What I think of as diffusion, or diffuse learning, was important too. You really do have to let your brain absorb and process things over time. It's nice to have little moments where something you struggled with for months suddenly clicks and becomes second nature. This can only happen if you give the brain time to sit back and process. Consistency is the last piece. I've done something with coding literally almost every single day since I started in late 2020. Don't underestimate how small steps over a long period of time add up to something great. ## Path Forward I will continue to use Rust wherever I can. My journey with it has been a pleasant challenge. There have been moments of frustration and drift, but I really am enjoying the journey. I haven't made much for myself personally, but am working on using Rust at work. The project I'm most proud of is a sort of "availability tracker" for staff to use that indicates their availability to take on more work. It also lets them show upcoming vacation time, so that others can plan accordingly. I built this app with Actix Web on the backend, which serves up a Tera template for the UI. A Postgres database saves all the information which is displayed by Tera template. I've also embarked on a redo of our office intranet site, which I initially did in React, but going forward it will be done with Yew, which is a very pleasant Rust frontend framework. The project will solidfy my knowledge of form handling and fetching in Yew. I'm also figuring out how to leverage the `wasm-bindgen` crate to step back and forth between Rust and JavaScript. ## Closing Thoughts I'm no computer scientist or full fledged software developer, I realize the thoughts in this piece are probably a little thin and my rationale for choosing Rust may come across as a little contrived. There are a lot of tools out there and I could have easily picked anything. If Rust didn't exist, I'd probably just be in JavaScript like everyone else, blissfully unaware. However, I'm happy there's a choice. I love that there exists a tool that takes whole classes of bugs off the table and supports me in the desire to create quality, efficient, and safe software. Although I'm not the most qualified person in the world at Rust programming, I will continue to advocate for its use and adoption. I would encourage anyone interested to dive in head first and carve a unique path. --- # Why you should use Rust on the backend Source: https://www.shuttle.dev/blog/2024/07/31/rust-on-the-backend Date: 31 July 2024 Author: josh Tags: rust, opinion What makes Rust worth using for backend web services? With Rust named most admired language in the Stack Overflow 2024 survey, more and more developers are looking to get into Rust. Though Rust is a versatile language, many newer Rust developers are using it for backend web development in particular. In this article, we'll talk about why Rust makes a good fit for backend web development compared to other languages. ## What does a Rust web service look like? Before we dive into the benefits of Rust on the backend, let's have a look at what a Rust web service looks like - using Axum as our web framework, for reference. Below is an example of adding a database pool from `sqlx` to shared application state. We'll use it within a router, with an endpoint for grabbing database records and returning a JSON response: ```rust use sqlx::PgPool; use axum::{Router, routing::get}; use tokio::net::TcpListener; // we auto-implement the Clone trait here so that this struct can be cloned // by the framework when required for requests #[derive(Clone)] struct AppState { db: PgPool } #[tokio::main] async fn main -> Result<(), Box { // this assumes we already have our database connection function set up // using the `?` operator allows us to automatically propagate errors // without needing to manually handle them or use unwraps let db = connect_to_db().await?; let state = AppState { db }; // set up router here let router = Router::new() .route("/users", get(get_users)) .with_state(state); let tcp_listener = TcpListener::bind("localhost:8000").await?; axum::serve(tcp_listener, router).await?; Ok(()) } ``` We'll also add our endpoint code below: ```rust use serde::Serialize; use axum::{extract::State, response::IntoResponse}; // here we auto-derive the FromRow trait from `sqlx` // and Serialize from `serde` to serialize our struct into JSON // as well as automatically converting retrieved Postgres rows into our struct #[derive(sqlx::FromRow, Serialize)] struct User { id: i32, name: String, email: String, } // here, we've set the function to return a non-concrete type that implements IntoResponse async fn get_users(State(state): State) -> impl IntoResponse { let query = sqlx::query_as::<_, User>("SELECT * FROM users") .fetch_all(&state.db) .await .unwrap(); Ok(Json(query)) } ``` While the syntax can be somewhat unfamiliar, idiomatic Rust code is relatively easy to follow. Although we've set up the core of our application logic in the main function, it would be easy to extract it to another function - or indeed another local crate package, for the sake of testing. ## Rust benefits ### Low Memory Footprint Rust's borrow checker system allows for much lower footprint during runtime. Optimisations made during compilation time allow for a small memory footprint, as well as providing memory safety garuantees. This makes Rust a great language to use for companies (and individuals!) who want to simultaneously up their green credentials and save on running overhead costs. Faster execution speed can also save costs on serverless functions! This generally matters more for web services than other types of applications. If you're not running your web service on a VPS, chances are you're being billed for compute and CPU usage. Being able to use a much more efficient language for writing backends can significantly reduce costs in the long run, especially for larger companies who may be switching over from something like Java, Python or Ruby. In terms of using a VPS, it would mean you may be able to downsize the VM you're using, constraints notwithstanding. The other primary consequence of this advantage is that it's far easier to run Rust web servers on IoT devices. For example, running an Actix Web server on a Raspberry Pi. You could technically do that with something like Django, Flask, or even Ruby on Rails. However, chances are with a sufficiently large application that there might not be much room for running anything else. At the very least, you'll be able to run an extra few programs on your small machine which depending on your use case, can be quite useful. Frontloading so much to the compiler does, of course, increase compilation times. Even so, your application will typically be running for much longer in production. Using [crane](https://github.com/ipetkov/crane) and [mold linker](https://github.com/rui314/mold) can also be used to reduce compilation time significantly. Although first-time compiles can take a few minutes, typically with subsequent compilations it takes much less thereafter. ### Concurrency Concurrency has always been a difficult problem to solve. Language design choices designed to abstract away complexity can often contribute to this. For example, Python's Global Interpreter Lock (GIL) has undeniably had benefits in terms of development and making it easier to run C extensions in Python. However, it can also significantly increase the complexity of multithreaded applications. This is primarily through lock acquisition and thread contention. As a consequence, things like parallelism can be much more difficult to express. In contrast, Rust doesn't hide the complexity of writing concurrent programs. This can make it somewhat intimidating to get started. However, the current ecosystem extends the stdlib primitives significantly to provide much more usable abstractions that make handling concurrency much easier at scale. For example: [dashmap](https://docs.rs/dashmap/latest/dashmap/), a concurrent `HashMap` implementation, as well as [parking-lot](https://github.com/Amanieu/parking_lot) which primarily provides the parking and unparking of threads but provides deadlock detection for mutexes and other synchronization primitives. If you're using the Tokio runtime for async Rust, it's also easy to use Tokio's synchronisation primitives which can be used across threads. If you want basic concurrency in a Rust backend service without going the whole way, making use of the primitives can be a great way to implement this: ```rust use std::sync::Arc; use tokio::sync::RwLock; use std::collections::HashMap; #[tokio::main] async fn main() { let locked_map: Arc>> = Arc::new(RwLock::new(HashMap::new())); let mut writer = locked_map.write().await.unwrap(); writer.insert("Hello".to_string(), "world!".to_string()); // manually drop the Writer lock here so we can get access back drop(writer); let reader = locked_map.read().await.unwrap(); println!("{}", reader.get("Hello".to_string())); } ``` **The difference between Rust and other languages is in the implementation detail.** Because of Rust's type system (RwLocks and Mutexes wrap types, for example), synchronization primitives don't lock threads - they lock resources. This is an important detail because it still allows other things to run. Locks are also scoped: once the function runs and it's out of scope, the lock automatically drops and gets destroyed allowing you to lock the resource again. In other languages like C++, the mutex is itself a resource that locks the whole thread, which itself can cause issues. One easy example of such an issue is forgetting to unlock a mutex, or an improper locking order. Of course, this doesn't mean Rust is immune to race conditions - resources can still get locked from attempted simultaneous access as well as lock poisoning and similar issues. Entire programming languages have been dedicated to dealing with concurrency: Erlang with BEAM (and any other languages that can run on the BEAM VM) being one example. Although Rust doesn't quite have concurrency baked in every single language design choice, if you wanted to make a fully concurrent section of your backend web service in Rust, you're given the tools to be able to do it safely and efficiently for backend Rust web services. ### Memory Safety Earlier this year, [the White House advocated for memory safe languages](https://www.whitehouse.gov/wp-content/uploads/2024/02/Final-ONCD-Technical-Report.pdf) which is a huge win for Rust. Although Rust was one of several languages in the discussion, the message is largely the same: move away from languages that don't have sufficient safeguards around introducing memory errors, whether they're memory access violations or leaks (or a simple use-after-free!). Rust is not entirely immune to memory issues. However, it does leverage sufficient safeguards such that you need to deliberately use the `unsafe` marker to bypass the forced memory safety garuantees of the compiler. For example, this code which is a method in the vector type (`Vec`) for removing an element at a given index: ```rust pub fn remove(&mut self, index: usize) -> T { // Note: `<` because it's *not* valid to remove after everything assert!(index < self.len, "index out of bounds"); unsafe { self.len -= 1; let result = ptr::read(self.ptr.as_ptr().add(index)); ptr::copy( self.ptr.as_ptr().add(index + 1), self.ptr.as_ptr().add(index), self.len - index, ); result } } ``` Note that the function is "safe" to use because it's not marked unsafe, but there is an unsafe block. This means that it's up to the library to ensure the code soundness rather than the user! While this can be intimidating for many, many libraries solve this issue by creating safe abstractions for unsafe code by using the above methodology. A specific example of this would also be the many libraries that are Rust bindings to C libraries. They have safe abstractions on top (like `image-rs` and `rust-rdkafka`), allowing users to safely use the underlying tech without memory issues. Many parts of the standard library also use unsafe code, with the synchronization examples and low-level data structures like `Vec` (vectors) and hashmaps. [The Rustonomicon](https://doc.rust-lang.org/nomicon/intro.html) actually has a guide for [re-implementing the `Vec` type from scratch](https://doc.rust-lang.org/nomicon/vec/vec.html), which is a great read for anyone who wants to dive deeper into using unsafe Rust safely. Although memory safety is not explicitly a hard requirement for most industries and use cases, memory issues are often time-consuming to resolve and can be quite costly if it's related to infrastructure. As it stands, for most developers making a memory error is practically inevitable - which is why adding compile-time checks for memory safety is an important advantage that Rust holds. Of course, there are things that get past the compiler, but if you're in a team with more junior engineers, you definitely don't want them to be shipping memory errors into production. Rust makes it much easier to ensure that it doesn't happen. ## Who's using Rust? Of course, Rust is currently being used by quite a few large companies: Amazon, Google and Microsoft (who is [in the process of migrating their Office 365 backends to Rust](https://securityboulevard.com/2024/02/microsoft-365-rust-richixbw/)) to name a few. Many companies who are also security-oriented also use Rust: [1Password](https://1password.com/) who have [made their own collection of Rust libraries for using passkeys](https://github.com/1Password/passkey-rs), [cryptee](https://crypt.ee/) who focus on encrypted document storage and photo management services, as well as the growing number of Rust companies and consultancies that are popping up to help larger companies with creating quality Rust code. [DARPA](https://www.darpa.mil/) also recently announced that they are [converting all of their C code to Rust](https://sam.gov/opp/1e45d648886b4e9ca91890285af77eb7/view) with the goal of the codebase eventually as high quality as would be from skilled Rust developers. While no small undertaking, if done successfully this will be a huge win for Rust going forward. Though it should come as no surprise, at Shuttle we also use Rust! Our platform services as well as our CLI are written in 100% Rust, and you can see this by going to [our main repository.](https://www.github.com/shuttle-hq/shuttle) ## Finishing up Thanks for reading! Although there's a lot of reasons to use Rust on the backend, hopefully this article has helped you understand some of the underlying details behind the advantages. Read more: - [Getting started with Actix Web](https://www.shuttle.dev/blog/2023/12/15/using-actix-rust) - [An intro to advanced Rust traits and generics](https://www.shuttle.dev/blog/2024/04/18/using-traits-generics-rust) - [Why type safety is important](https://www.shuttle.dev/blog/2023/11/29/type-safety) --- # ShuttleAI: Build & Deploy AI-Powered Web Services from a Single Prompt Source: https://www.shuttle.dev/blog/2024/07/18/ai-apps-from-a-single-prompt Date: 18 July 2024 Author: ivan Tags: shuttle, shuttle-ai At Shuttle, we've been working on a new tool that we think could change how developers approach AI integration. We're calling it ShuttleAI, and it allows you to build and deploy AI-powered web services from a single prompt. At Shuttle, we've been working on a new tool that we think could change how developers approach AI integration. We're calling it ShuttleAI, and it allows you to build and deploy AI-powered web services from a single prompt. Here's the TL;DR: - Describe your AI service in plain language - ShuttleAI generates a project spec for you to review - Approve or modify the spec - ShuttleAI creates the project files - You can prompt for changes or deploy It's that simple. But let's dig into the details. ## The Problem: AI Integration is Hard If you've ever tried to integrate AI into a web service, you know it's not trivial. Here are some common challenges: 1. **Complexity**: AI frameworks often require specialized knowledge. 2. **Time**: Setting up AI services can take weeks or months. 3. **Infrastructure**: Managing AI models needs robust, scalable infrastructure. 4. **Ongoing maintenance**: AI services require continuous monitoring and updates. These barriers can be significant, especially for smaller teams or developers new to noisy AI space. ## How ShuttleAI Works ShuttleAI aims to simplify this process dramatically. Here's a step-by-step breakdown: 1. **Describe Your Service**: You provide a prompt describing the AI service you want to build. For example: ``` "Build a web service that takes weather forecast data and user profiles as input, then returns personalized weather recommendations." ``` 2. **Review the Spec**: ShuttleAI generates a project specification document in markdown. This includes: - API endpoints - Data models - AI model selection - Infrastructure requirements You can review and modify this spec as needed. 3. **Generate Project Files**: Once you approve the spec, ShuttleAI creates all necessary project files. This includes: - Backend code (eg. Python with Flask) - AI model integration code - Infrastructure in the form of [Infrastructure from Code](https://docs.shuttle.dev/introduction/how-shuttle-works) 4. **Iterative Refinement**: You can prompt ShuttleAI to make changes at this stage. For example: ``` "Add rate limiting to the API endpoints" ``` ShuttleAI will update the project files accordingly. 5. **Deploy**: Once you're satisfied, ShuttleAI compiles and deploys your project on the Shuttle platform. ## Use Cases We're excited to see what developers will build with ShuttleAI. Here are a few ideas we've been thinking about: 1. **Personalized Content Engines**: Analyze user behavior and content metadata to provide tailored recommendations. 2. **Intelligent Data Processing**: Create services that clean, normalize, and enrich data using AI. 3. **Natural Language Interfaces**: Build APIs that can understand and respond to natural language queries. 4. **Predictive Analytics Services**: Develop APIs that forecast trends based on historical data. ## Beta Testing and Early Access ShuttleAI is still in development, and we're looking for beta testers. If you're interested in being one of the first to try it out, we're offering early access to the first 100 developers who sign up for our waitlist. As a beta tester, you'll get: - Early access to ShuttleAI - Direct support from our development team - The opportunity to shape the future of the tool [Click here to sign up for early access!](https://www.shuttle.dev/ai) ## What's Next? We're continuously working on improving ShuttleAI. Some features we're exploring for future releases: - Support for more AI models and APIs - Advanced customization options for generated services - A marketplace for sharing and deploying AI service templates ## We Want Your Feedback ShuttleAI is still evolving, and we want to build it in a way that truly serves developers' needs. If you have ideas, questions, or concerns, we want to hear them. Drop us a line at [hello@shuttle.rs](mailto:hello@shuttle.rs) or open an issue in our [GitHub repo](https://github.com/shuttle-hq/shuttle). Remember, the first 100 signups get early access to the beta. Don't miss out on the chance to shape the future of AI service development! [Click here to sign up for early access!](https://www.shuttle.dev/ai) --- # A Comprehensive Guide to the llm-chain Rust crate Source: https://www.shuttle.dev/blog/2024/06/06/llm-chain-langchain-rust Date: 6 June 2024 Author: josh Tags: rust, ai, llm-orchestration, guide Deep diving into the llm chain crate and leveraging Rust's version of Langchain LLM orchestration is an important part of creating AI powered applications. Particularly in business use cases, AI agents and RAG pipelines are commonly utilised for refined LLM responses. Although Langchain is currently the most popular LLM orchestration suite, there are also similar crates in Rust that we can use. In this article, we'll be diving into one of them - `llm-chain`. ## What is llm-chain? `llm-chain` is a collection of crates that describes itself as "the ultimate toolbox" for working with Large Language Models (LLMs) to create AI-powered applications and other tooling. You can find the crate's GitHub repository [here.](https://github.com/sobelio/llm-chain) ### Comparison to other LLM orchestration crates If you've looked around for different crates, you probably noticed that there are a few crates for LLM orchestration in Rust: - `llm-chain` (this one!) - `langchain-rust` - `anchor-chain` In comparison to the others, `llm-chain` is somewhat macro heavy. However, it is also the most developed in terms of having extra data processing utilities. If you're looking for an all-in-one package, `llm-chain` is the crate that's most likely to help you get over the line. They also have [their own docs.](https://docs.llm-chain.xyz/docs/introduction) ## Getting Started ### Pre-requisites Before you add `llm-chain` to your project, make sure you have access to the prompting model you want to use. For example, if you want to use OpenAI, make sure you have an OpenAI API key (set as `OPENAI_API_KEY` in environment variables). ### Setup To get started, all you need to do is to add the crate to your project (as well as Tokio for async): ```bash cargo add llm-chain cargo add tokio -F full ``` Next, we'll want to add a provider for whatever method of model prompting you want to use. Here we'll add the OpenAI integration by adding the crate to our application: ```bash cargo add llm-chain-openai ``` Note that a full list of integrations can be found [here](https://github.com/sobelio/llm-chain/tree/main/crates), split by package. ## Basic usage ### Prompting To get started with `llm-chain`, we can use their basic example as a way to quickly get something working. In this code snippet, we will: - Initialise an executor using `executor!()` - Use `prompt!()` with the system message and prompt to store both in a struct that will get used when the prompt (or chain) gets ran. - Runs the prompt and returns the results, using a reference to the executor. - Prints the results. ```rust use std::error::Error; use llm_chain::{executor, parameters, prompt}; #[tokio::main] async fn main() -> Result<(), Box> { let exec = executor!()?; let res = prompt!( "You are a robot assistant for making personalized greetings", "Make a personalized greeting for Joe" ) .run(¶meters!(), &exec) .await?; println!("{}", res); Ok(()) } ``` Running this prompt should yield a result that looks like this: ```bash Assistant: Hello Joe! I hope you're having a fantastic day filled with joy and success. Remember to keep shining bright and making a positive impact wherever you go. Have a great day! ``` The default model for the `llm_chain_openai` executor is `gpt-3.5-turbo`. The executor parameters can be defined in the macro - you can also find more about this [here](https://docs.rs/llm-chain/latest/llm_chain/macro.executor.html). ### Using Templates However, if we want to move onto more advanced pipelines the easiest way for us to do this would be to use a prompt template with parameters. You can see below that much like in the previous code snippet, we generate an executor and return the results. However, instead of using `prompt!()` by itself we use it in `Step::for_prompt_template` - which you can find more about [here](https://docs.rs/llm-chain/latest/llm_chain/step/struct.Step.html). ```rust use llm_chain::step::Step; #[tokio::main] async fn main() -> Result<(), Box> { let exec = executor!()?; let step = Step::for_prompt_template(prompt!( "You are a bot for making personalised greetings", "Make a personalized greeting tweet for {{text}}" )); let step_results = step.run(¶meters!("Emil"), &exec).await?; println!("step_results: {step_results}"); let immediate_results = step_results.to_immediate().await?.as_content(); println!("immediate results: {immediate_results}"); Ok(()) } ``` The results of the output should look like this: ```bash step_results: Assistant: "Hey @Emil! Wishing you a fantastic day filled with joy, success, and lots of smiles! Keep shining bright and making a positive impact in the world. Cheers to you! 🌟 #YouGotThis" immediate results: Assistant: "Hey @Emil! Wishing you a fantastic day filled with joy, success, and lots of smiles! Keep shining bright and making a positive impact in the world. Cheers to you! 🌟 #YouGotThis" ``` ## Chaining prompts Of course, one of the main reasons why we're using Langchain (or Langchain-like libraries) in the first place is to be able to orchestrate our LLM usage. The `llm-chain` Rust crate assists us with this by letting us create chains of LLM prompts using the `Chain` struct. There are three types of chains that we can use with `llm-chain`: - Sequential chains, which apply steps sequentially - Map-reduce chains, which use a "map" step to apply to each chunk from a loaded file and then reduce the text. This is quite useful for text summarization. - Conversational chains, which keep track of the conversation history and manage context. Conversational chains are great for chatbot applications, multi-step interactions and other places where context is essential. ### Sequential chaining The easiest to use type of chaining is sequential chaining, which simply pipes the output from each step into the next step. When creating our steps, we will use the `Chain` struct instead of creating each step individually: ```rust use llm_chain::step::Step; use llm_chain::chains::sequential::Chain; use llm_chain::prompt; // Create a chain of steps with two prompts let first_step = Step::for_prompt_template( prompt!("You are a bot for making personalized greetings", "Make personalized birthday e-mail to the whole company for {{name}} who has their birthday on {{date}}. Include their name") ); // Second step: summarize the email into a tweet. Importantly, the text parameter is the result of the previous prompt. let second_step = Step::for_prompt_template( prompt!( "You are an assistant for managing social media accounts for a company", "Summarize this email into a tweet to be sent by the company, use emojis if you can. \\n--\\n{{text}}") ); let chain: Chain = Chain::new( vec![first_step, second_step] ); ``` Next, we'll then use the `parameters!` macro to inject parameters into the prompt pipeline: ```rust use llm_chain::parameters; use llm_chain::traits::Executor as ExecutorTrait; use llm_chain_openai::chatgpt::Executor; // Create a new ChatGPT executor with the default settings let exec = Executor::new()?; // Run the chain with the provided parameters let res = chain .run( // Create a Parameters object with key-value pairs for the placeholders parameters!("name" => "Emil", "date" => "February 30th 2023"), &exec, ) .await?; // Print the result to the console println!("{}", res.to_immediate().await?.as_content()); Ok(()) } ``` Running the code should yield a result that looks like this: ```bash Assistant: 🎉🎂 Join us in celebrating Emil's birthday on February 30th! 🎈🎁 Emil, your dedication and hard work are truly commendable. Wishing you happiness and success on your special day! 🥳🎉 #HappyBirthdayEmil #TeamAppreciation 🎂 ``` ### Map-reduce chains Map-reduce chains typically consist of two steps: - A "Map" step that takes a document and applies an LLM chain to it, treating the output as a new document - The new documents are then passed to a new chain that combines the separate documents to get a single output. At the end of a Map-Reduce chain, the output can be taken for further processing by sending it to another prompting model (for instance) or as part of a sequential pipeline. To use this pattern, we need to create a prompt template: ```rust // note that we import Chain from a different module here! use llm_chain::chains::map_reduce::Chain; // Create the "map" step to summarize an article into bullet points let map_prompt = Step::for_prompt_template(prompt!( "You are a bot for summarizing wikipedia articles, you are terse and focus on accuracy", "Summarize this article into bullet points:\\n{{text}}" )); // Create the "reduce" step to combine multiple summaries into one let reduce_prompt = Step::for_prompt_template(prompt!( "You are a diligent bot that summarizes text", "Please combine the articles below into one summary as bullet points:\\n{{text}}" )); // Create a map-reduce chain with the map and reduce steps let chain = Chain::new(map_prompt, reduce_prompt); ``` Next, we need to take some text from a file and add it as a parameter - the `{{text}}` parameter in the Map prompt will automatically take in the file content: ```rust // Load the content of the article to be summarized let article = include_str!("article_to_summarize.md"); // Create a vector with the Parameters object containing the text of the article let docs = vec![parameters!(article)]; let exec = executor!()?; // Run the chain with the provided documents and an empty Parameters object for the "reduce" step // Note that there are multiple modules with a Chain struct // This one takes two different modules for let res = chain.run(docs, Parameters::new(), &exec).await?; // Print the result to the console println!("{}", res.to_immediate().await?.as_content()); ``` Note here that because we have **two** steps, the `chain.run()` function will take two different vectors - one for each step, in order. This means that we are passing the article content to the first prompt, but no parameters to the second document. ### Conversational Chains Of course, the last chain we need to talk about is conversational chains. In a nutshell, conversational chains allow you to load context from memory by using saved chat history. In situations where the platform or model cannot access saved chat history, you might store the response and then use it as extra context in the next message. To use conversational chains, like before, we need to create a `Chain` (now imported from the conversation module) and define the steps for it: ```rust use llm_chain::{ chains::conversation::Chain, executor, output::Output, parameters, prompt, step::Step, }; let exec = executor!()?; let mut chain = Chain::new( prompt!(system: "You are a robot assistant for making personalized greetings."), )?; // Define the conversation steps. let step1 = Step::for_prompt_template(prompt!(user: "Make a personalized greeting for Joe.")); let step2 = Step::for_prompt_template(prompt!(user: "Now, create a personalized greeting for Jane.")); let step3 = Step::for_prompt_template( prompt!(user: "Finally, create a personalized greeting for Alice."), ); let step4 = Step::for_prompt_template(prompt!(user: "Remind me who did we just greet.")); ``` Next, we will individually send each prompt to the `Chain` in turn, printing out the response from each one. Note that at step 4, we should receive an answer that includes the names of the previous three people we just made a personalized greeting for (Joe, Jane and Alice). ```rust // Execute the conversation steps. let res1 = chain.send_message(step1, ¶meters!(), &exec).await?; println!("Step 1: {}", res1.to_immediate().await?.primary_textual_output().unwrap()); let res2 = chain.send_message(step2, ¶meters!(), &exec).await?; println!("Step 2: {}", res2.to_immediate().await?.primary_textual_output().unwrap()); let res3 = chain.send_message(step3, ¶meters!(), &exec).await?; println!("Step 3: {}", res3.to_immediate().await?.primary_textual_output().unwrap()); let res4 = chain.send_message(step4, ¶meters!(), &exec).await?; println!("Step 4: {}", res4.to_immediate().await?.primary_textual_output().unwrap()); ``` Running this should get an output that looks something like this: ```bash Step 1: Hello, Joe! I hope you are having a fantastic day filled with positivity and joy. Keep shining bright and making a difference in the world with your unique presence. Wishing you continued success and happiness in all that you do! Step 2: Hello, Jane! Sending you warm greetings and positive vibes today. May your day be as wonderful and vibrant as you are. Remember to keep being your amazing self and always believe in the incredible things you are capable of achieving. Wishing you endless happiness and success in all your endeavors! Step 3: Hello, Alice! I hope this message finds you well and thriving. You are such a remarkable individual with a heart full of kindness and a spirit full of strength. Keep inspiring those around you with your grace and resilience. May your day be filled with love, laughter, and countless blessings. Stay amazing, Alice! Step 4: We just created personalized greetings for Joe, Jane, and Alice. ``` ## Using embeddings with llm-chain In terms of using embeddings with `llm-chain`, it provides a helper method for using Qdrant as a vector store. It abstracts over the `qdrant_client` crate, providing an easy way to embed documents and carry out similarity search. Note that the `Qdrant` struct will assume your collection(s) that you want to use have already been created! ### Basic usage While we can use `qdrant_client` to manually create our own embeddings, `llm-chain` also has an integration for easy access. We will be required to create our own client through `qdrant_client` - which we can then use with the `Qdrant` struct to be able to parse stuff. First, let's define a couple of passages that we want to insert into our Qdrant collection: ```rust const DOC_DOG_DEF: &str = r#"The dog (Canis familiaris[4][5] or Canis lupus familiaris[5]) is a domesticated descendant of the wolf. Also called the domestic dog, it is derived from the extinct Pleistocene wolf,[6][7] and the modern wolf is the dog's nearest living relative.[8] Dogs were the first species to be domesticated[9][8] by hunter-gatherers over 15,000 years ago[7] before the development of agriculture.[1] Due to their long association with humans, dogs have expanded to a large number of domestic individuals[10] and gained the ability to thrive on a starch-rich diet that would be inadequate for other canids.[11] The dog has been selectively bred over millennia for various behaviors, sensory capabilities, and physical attributes.[12] Dog breeds vary widely in shape, size, and color. They perform many roles for humans, such as hunting, herding, pulling loads, protection, assisting police and the military, companionship, therapy, and aiding disabled people. Over the millennia, dogs became uniquely adapted to human behavior, and the human-canine bond has been a topic of frequent study.[13] This influence on human society has given them the sobriquet of "man's best friend"."#; const DOC_WOODSTOCK_SOUND: &str = r#"Sound for the concert was engineered by sound engineer Bill Hanley. "It worked very well", he says of the event. "I built special speaker columns on the hills and had 16 loudspeaker arrays in a square platform going up to the hill on 70-foot [21 m] towers. We set it up for 150,000 to 200,000 people. Of course, 500,000 showed up."[48] ALTEC designed marine plywood cabinets that weighed half a ton apiece and stood 6 feet (1.8 m) tall, almost 4 feet (1.2 m) deep, and 3 feet (0.91 m) wide. Each of these enclosures carried four 15-inch (380 mm) JBL D140 loudspeakers. The tweeters consisted of 4x2-Cell & 2x10-Cell Altec Horns. Behind the stage were three transformers providing 2,000 amperes of current to power the amplification setup.[49][page needed] For many years this system was collectively referred to as the Woodstock Bins.[50] The live performances were captured on two 8-track Scully recorders in a tractor trailer back stage by Edwin Kramer and Lee Osbourne on 1-inch Scotch recording tape at 15 ips, then mixed at the Record Plant studio in New York.[51]"#; ``` The `Qdrant` struct will automatically assume you have your collection set up and have a `QdrantClient` that already exists, along with the collection name. We'll pass these as arguments into a new function that does the following: - Create embeddings using `llm-chain-openai` - Insert the embeddings into Qdrant - Conduct a similarity search using the prompt Firstly, we'll want to define a method for creating our `Qdrant` struct so that we can re-use it later on: ```rust use llm_chain_openai::embeddings::Embeddings; use llm_chain_qdrant::Qdrant; use llm_chain::schema::EmptyMetadata; use qdrant_client::prelude::{QdrantClient, QdrantClientConfig}; // note that the URL must connect to port 6334 - qdrant_client uses GRPC! // feel free to replace this wit async fn create_qdrant_client(url: String) -> QdrantClient { let mut config = QdrantClientConfig::from_url(url); // this part is only required if you're running Qdrant on the cloud // if running locally, no api key is required config.api_key = std::env::var("QDRANT_API_KEY").ok(); QdrantClient::new(Some(config)) } async fn create_qdrant_struct(qdrant_client: QdrantClient, collection_name: String) -> Qdrant { let embeddings = Embeddings::default(); // Storing documents Qdrant::new( qdrant_client, collection_name, embeddings, None, None, None, ) } ``` Next, we can use the `Qdrant` struct to carry out a similarity search! We'll add our documents to our collection, then conduct a similarity search and print out the stored documents: ```rust async fn similarity_search_qdrant(qdrant: Qdrant) -> Result<(), Box> { // embed and upsert the documents into Qdrant let doc_ids = qdrant .add_documents( vec![ DOC_DOG_DEF.to_owned(), DOC_WOODSTOCK_SOUND.to_owned(), ] .into_iter() .map(Document::new) .collect(), ) .await?; println!("Documents stored under IDs: {:?}", doc_ids); // conduct similarity search and find similar vectors let response = qdrant .similarity_search( "Sound engineering is involved with concerts and music events".to_string(), 1, ) .await?; // print out the stored documents with payload, embeddings etc println!("Retrieved stored documents: {:?}", response); } ``` After this, we can then send it into a `Chain` or whatever else we need. ### Usage within a prompt template In isolation, the Qdrant struct is not particularly helpful and mainly provides convenience methods for embedding things. However, we can also add it as part of a `ToolCollection` which lets the pipeline know that it is able to use embeddings. ```rust let qdrant = create_qdrant_struct( , "mycollection".to_string()).await; let exec = executor!().unwrap(); let mut tool_collection = ToolCollection::::new(); tool_collection.add_tool( QdrantTool::new( qdrant, "factual information and trivia", "facts, news, knowledgebuild_local_qdrant().await; or trivia", ) .into(), ); let task = "Tell me something about dogs"; let prompt = ChatMessageCollection::new() .with_system(StringTemplate::tera( "You are an automated agent for performing tasks. Your output must always be YAML.", )) .with_user(StringTemplate::combine(vec![ tool_collection.to_prompt_template().unwrap(), StringTemplate::tera("Please perform the following task: {{task}}."), ])); let result = Step::for_prompt_template(prompt.into()) .run(¶meters!("task" => task), &exec) .await .unwrap(); println!("{}", res.to_immediate().await?.as_content()); ``` ## Processing data using llm-chain While `llm-chain` provides tooling for creating LLM pipelines, another important part of Langchain and libraries like it is being able to process and transform data. Prompts (and prompt engineering) are important to get right. However if we're also feeding data into our pipeline, we'll also want to make sure it's as easy as possible to find the most relevant context. Below are a couple of useful use cases that you may want to check out. ### Scraping search results `llm-chain` provides a convenience struct for scraping Google Results using the `GoogleSerper` struct - using the Serper.dev service. ```rust use llm_chain::tools::{tools::GoogleSerper, Tool}; #[tokio::main(flavor = "current_thread")] async fn main() { let serper_api_key = std::env::var("SERPER_API_KEY").unwrap(); let serper = GoogleSerper::new(serper_api_key); let result = serper .invoke_typed(&"Who was the inventor of Catan?".into()) .await .unwrap(); println!("Best answer from Google Serper: {}", result.result); } ``` As well as this, there is also support for Bing Search API which provides 1000 free searches a month. Below is a code snippet of how you can use the API: ```rust use llm_chain::tools::{tools::BingSearch, Tool}; #[tokio::main(flavor = "current_thread")] async fn main() { let bing_api_key = std::env::var("BING_API_KEY").unwrap(); let bing = BingSearch::new(bing_api_key); let result = bing .invoke_typed(&"Who was the inventor of Catan?".into()) .await .unwrap(); println!("Best answer from bing: {}", result.result); } ``` Should you need to change between one or the other, both are quite easy to use. ### Extracting labelled text `llm-chain` also has some convenience methods for extracting labelled text. If you have a string of bullet points for instance, you can use `extract_labeled_text()` to be able to extract the text. ```rust use llm_chain::parsing::extract_labeled_text; fn main() { let text = r" - Title: The Matrix - Actor: Keanu Reeves - Director: The Wachowskis "; let result = extract_labeled_text(text); println!("{:?}", result); } ``` Running this code should result in an output that looks like this: ```bash [("Title", "The Matrix"), ("Actor", "Keanu Reeves"), ("Director", "The Wachowskis")] ``` You can find out more about the `parsing` module for `llm-chain` [here](https://docs.rs/llm-chain/latest/llm_chain/parsing/index.html) as well as [some of the examples](https://github.com/sobelio/llm-chain/tree/main/crates/llm-chain/examples). ## Conclusion Thanks for reading! With the power of `llm-chain`, you can easily leverage AI for your applications. Read more: - [Building a RAG agent workflow](https://www.shuttle.dev/blog/2024/05/23/building-agentic-rag-rust-qdrant) - [Using Huggingface with Rust](https://www.shuttle.dev/blog/2024/05/01/using-huggingface-rust) - [Building an Axum web server to use with llm-chain](https://www.shuttle.dev/blog/2024/03/13/simple-web-server-rust) --- # Implementing Semantic Caching with Qdrant & Rust Source: https://www.shuttle.dev/blog/2024/05/30/semantic-caching-qdrant-rust Date: 30 May 2024 Author: josh Tags: rust, ai, rag, guide Using semantic caching for RAG in a Rust web service context and deploying it Hello world! Today we're going to learn about semantic caching with Qdrant, in Rust. By the end of this tutorial, you'll have a Rust application that can do the following: - Ingest a CSV file, turn it into an embedding with the help of an LLM and insert it into Qdrant - Create two collections in Qdrant - one for regular usage and one for caching - Utilize semantic caching for quicker access Interested in deploying or got lost and want to find a repository with the code? You can find that [here](https://github.com/joshua-mo-143/shuttle-qdrant-semantic-caching) ## What is semantic caching, and why use it? In a regular data cache, we store information to enable faster retrieval later on. For example, you might have in a web service that's served behind Nginx. We can have Nginx cache either all responses, or only the most accessed endpoints. This improves performance and reduces load on your web server. Semantic caching in this regard is quite similar. Using vector databases, we can create database collections that store the queries themselves. For example, these two questions semantically carry the same meaning: - What are some best practices for writing the Rust programming language? - What are some best practices for writing Rustlang? We can store a copy of the query in a cache collection, with the answer as a JSON payload. If users then ask a similar question, we can retrieve the embedding and fetch the answer from the payload. This avoids us having to use an LLM to get our answer. There are a couple of benefits to semantic caching: - Prompts that require long responses can see serious cost savings. - It's pretty easy to implement and fairly cheap - the only cost is in storage and using the embedding model - You can use a cheaper model than your regular embedding Semantic caching is normally used with RAG - Retrieval Augmented Generation. RAG is a framework to allow context retrieval from pre-embedded materials. For example, CSV files or documents can be turned into embeddings using models and stored in a database. Whenever a user wants to find similar documents to a given prompt, they embed the prompt and search against it in a given database. Of course, there are good reasons **not** to use semantic caching. Prompts that need differing, varied answers won't find any use for semantic caching. This is particularly relevant in generative AI usage. Fetching a stored query will reduce the creativity of the response. Regardless, if part of your pipeline is able to capitalise on semantic caching, it's a good idea to do so. ## Project setup ### Getting started To get started, don't forget to use `shuttle init`, with the Axum framework. We'll install our dependencies using the shell snippet below: ```bash cargo add qdrant-client@1.7.0 anyhow async-openai serde serde-json \ shuttle-qdrant uuid -F uuid/v4,serde/derive ``` You can find our quickstart docs [here.](https://docs.shuttle.dev/getting-started/quick-start) ### Setting up secrets To set up our secrets, we'll use a `Secrets.toml` file located in our project root (you will need to create this manually). You can then add whatever secrets you need using the format below: ```toml OPENAI_API_KEY = "" QDRANT_URL = "" QDRANT_API_KEY = "" ``` ## Setting up Qdrant ### Creating collections Now that we can get started, we will add some more general methods for creating a regular collection as well as a cache collection, to simulate a real-world scenario (as well as a `new()` function to make creating the `RAGSystem` struct itself). We'll create the struct first: Note here that although we're using vectors with 1536 dimensions, the number of dimensions you'll need may depend on the model you are using. ```rust use qdrant_client::prelude::QdrantClient; use async_openai::{config::OpenAIConfig, Client}; struct RagSystem { qdrant_client: QdrantClient, openai_client: Client } static REGULAR_COLLECTION_NAME: &str = "my-collection"; static CACHE_COLLECTION_NAME: &str = "my-collection-cached"; impl RAGSystem { fn new(qdrant_client: QdrantClient) -> Self { let openai_api_key = env::var("OPENAI_API_KEY").unwrap(); let openai_config = OpenAIConfig::new() .with_api_key(openai_api_key) .with_org_id("qdrant-shuttle-semantic-cache"); let openai_client = Client::with_config(openai_config); Self { openai_client, qdrant_client, } } } ``` Now we'll create the methods for initialising our regular collection. Note that we'll only need to use these once. After the collections have already been created, if we try to initialise them again we'll get an error. ```rust use qdrant_client::prelude::CreateCollection; use qdrant_client::qdrant::{ vectors_config::Config,VectorParams, VectorsConfig, WithPayloadSelector, }; impl RagSystem { async fn create_regular_collection(&self) -> Result<()> { self.qdrant_client .create_collection(&CreateCollection { collection_name: REGULAR_COLLECTION_NAME.to_string(), vectors_config: Some(VectorsConfig { config: Some(Config::Params(VectorParams { size: 1536, distance: Distance::Cosine.into(), ..Default::default() })), }), ..Default::default() }) .await?; Ok(()) } } ``` Next, we'll create our cache collection. When creating this collection, note that we use `Distance::Euclid` instead of `Distance::Cosine`. Both of these can be defined as follows: - `Distance::Cosine` (or "cosine similarity") measures how closely two vectors are pointing in the same direction. If we plot two vectors on a graph, for example, a vector located at [2,1] would be much closer to [1,1] than it would be [-1, -2]. Cosine similarity is overwhelmingly used in measuring document similarity in text analysis. - `Distance::Euclid` (or "Euclidean distance") measures how closely two vectors are from each other - i.e., the distance from A to B where A and B are two points on a graph. Rather than trying to determine similarity, here we want to determine whether something is mostly or exactly the same. ```rust impl RagSystem { async fn create_cache_collection(&self) -> Result<()> { self.qdrant_client .create_collection(&CreateCollection { collection_name: CACHE_COLLECTION_NAME.to_string(), vectors_config: Some(VectorsConfig { config: Some(Config::Params(VectorParams { size: 1536, distance: Distance::Euclid.into(), hnsw_config: None, quantization_config: None, on_disk: None, ..Default::default() })), }), ..Default::default() }) .await?; Ok(()) } } ``` ### Creating embeddings Next, we need to create an embedding from a file input - using a CSV file as an example. To do so, we'll need to do the following: - Read the file inputs and parse it to a string (`std::fs::read_to_string()` parses to a String type automatically) - Chunk the file contents into appropriate amounts (here we'll do it per-row naively, for illustration) - Bulk embed the embeddings and add them to Qdrant Here we're using the `async-openai` library to be able to create the embedding - but if you don't want to use OpenAI, you can always use `fastembed-rs` as an alternative or another crate of your choice that allows embedding creation. ```rust use std::path::PathBuf; use async_openai::types::{CreateEmbeddingRequest, EmbeddingInput}; use anyhow::Result; impl RagSystem { async fn embed_and_upsert_csv_file(&self, file_path: PathBuf) -> Result<()> { let file_contents = std::fs::read_to_string(&file_path)?; // note here that we skip 1 because CSV files typically have headers // if you don't have any headers, you can remove it let chunked_file_contents: Vec = file_contents.lines().skip(1).map(|x| x.to_owned()).collect(); let embedding_request = CreateEmbeddingRequest { model: "text-embedding-ada-002".to_string(), input: EmbeddingInput::StringArray(chunked_file_contents.to_owned()), encoding_format: None, // defaults to f32 user: None, dimensions: Some(1536), }; let embeddings = Embeddings::new(&self.openai_client) .create(embedding_request) .await?; if embeddings.data.is_empty() { return Err(anyhow::anyhow!( "There were no embeddings returned by OpenAI!" )); } let embeddings_vec: Vec> = embeddings.data.into_iter().map(|x| x.embedding).collect(); // note that we create the upsert_embedding function later on for embedding in embeddings_vec { self.upsert_embedding(embedding, file_contents.clone()) .await?; } Ok(()) } } ``` We'll then need to embed any further inputs to search for any matching embeddings. The `embed_prompt` function will look quite similar to the embedding part of our `embed_and_upsert_csv_file` function. However, it will instead return a `Vec` as we'll want to use this later to search our collection. ```rust impl RagSystem { pub async fn embed_prompt(&self, prompt: &str) -> Result> { let embedding_request = CreateEmbeddingRequest { model: "text-embedding-ada-002".to_string(), input: EmbeddingInput::String(prompt.to_owned()), encoding_format: None, // defaults to f32 user: None, dimensions: Some(1536), }; let embeddings = Embeddings::new(&self.openai_client) .create(embedding_request) .await?; if embeddings.data.is_empty() { return Err(anyhow::anyhow!( "There were no embeddings returned by OpenAI!" )); } Ok(embeddings.data.into_iter().next().unwrap().embedding) } } ``` ### Upserting embeddings Once we've created our embeddings, we'll create a method for adding embeddings into Qdrant called `upsert_embedding`. This will deal with creating the payload for our embedding and insert it into the database. Once added to the collection, we can search our collection later on and get the associated JSON payload alongside the embedding! The function will look like this: ```rust use qdrant_client::prelude::PointStruct; impl RAGSystem { async fn upsert_embedding(&self, embedding: Vec, file_contents: String) -> Result<()> { let payload = serde_json::json!({ "document": file_contents }) .try_into() .map_err(|x| anyhow::anyhow!("Ran into an error when converting the payload: {x}"))?; let points = vec![PointStruct::new( uuid::Uuid::new_v4().to_string(), embedding, payload, )]; self.qdrant_client .upsert_points(REGULAR_COLLECTION_NAME.to_owned(), None, points, None) .await?; Ok(()) } } ``` Here, we use a `uuid::Uuid` as a unique identifier for our embedding(s). You can also do the same thing by having a `u64` counter that increases with every embedding. However, you'll want to make sure you don't accidentally overwrite your own embeddings! Inserting a new embedding with the same ID as a currently existing embedding in the collection will **overwrite** the embedding. Of course, we also need to create a method for adding things to our cache. Note that our payload is different here. Instead of using the `document` payload field, we use `answer` since the payload will hold a pre-generated answer to the question. ```rust impl RagSystem { pub async fn add_to_cache(&self, embedding: Vec, answer: String) -> Result<()> { let payload = serde_json::json!({ "answer": answer }) .try_into() .map_err(|x| anyhow::anyhow!("Ran into an error when converting the payload: {x}"))?; let points = vec![PointStruct::new( uuid::Uuid::new_v4().to_string(), embedding, payload, )]; self.qdrant_client .upsert_points(CACHE_COLLECTION_NAME.to_owned(), None, points, None) .await?; Ok(()) } } ``` ### Searching Qdrant collections Having made something we can search against in Qdrant, we'll need to implement some methods for our `VectorDB`. We'll split this up into two methods: - `search_regular_collection` - `search_cache_collection` When searching for an embedding, we should attempt to search our semantic cache using `search_cache_collection` - if it doesn't find anything, we should then use the regular `search_regular_collection` method to get the document, prompt OpenAI with it and then return the result as To make our methods a little bit more error-resistant, we have used `.into_iter().next()` on the results. This tries to find the first item in the vector by only going to the first item in the vector. This works because we're only looking for one single embedding, but you can increase or decrease the limit as you'd like. Once we find a match, we need to get the `document` key from our JSON payload associated with the embedding match and return it. We'll be using this as context in our RAG prompt later on! ```rust use qdrant_client::qdrant::{ with_payload_selector::SelectorOptions, SearchPoints, WithPayloadSelector }; impl RagSystem { pub async fn search(&self, embedding: Vec) -> Result { let payload_selector = WithPayloadSelector { selector_options: Some(SelectorOptions::Enable(true)), }; let search_points = SearchPoints { collection_name: REGULAR_COLLECTION_NAME.to_owned(), vector: embedding, limit: 1, with_payload: Some(payload_selector), score_threshold: Some(0.35f32), ..Default::default() }; let search_result = self .qdrant_client .search_points(&search_points) .await .inspect_err(|x| println!("An error occurred while searching for points: {x}")) .unwrap(); let result = search_result.result.into_iter().next(); let Some(result) = result else { return Err(anyhow::anyhow!("There's nothing matching.")); }; Ok(result.payload.get("document").unwrap().to_string()) } } ``` Of course, you'll also want to implement a function for searching your cache collection. Note that although the functions are _mostly_ the same, we get the `answer` field from the payload instead of `document` for semantics. ```rust impl RagSystem { pub async fn search_cache(&self, embedding: Vec) -> Result { let payload_selector = WithPayloadSelector { selector_options: Some(SelectorOptions::Enable(true)), }; let search_points = SearchPoints { collection_name: CACHE_COLLECTION_NAME.to_owned(), vector: embedding, limit: 1, with_payload: Some(payload_selector), ..Default::default() }; let search_result = self .qdrant_client .search_points(&search_points) .await .inspect_err(|x| println!("An error occurred while searching for points: {x}"))?; let result = search_result.result.into_iter().next(); let Some(result) = result else { return Err(anyhow::anyhow!("There's nothing matching.")); }; Ok(result.payload.get("answer").unwrap().to_string()) } } ``` ### Prompting Of course, now that everything else is done, the last thing to do is prompting! Here, you can see below that we generate a prompt that basically consists of the prompt we want, as well as the provided context. We then grab the first result from OpenAI and return the message content. ```rust use async_openai::types::{ ChatCompletionRequestMessage, ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs }; impl RagSystem { pub async fn prompt(&self, prompt: &str, context: &str) -> Result { let input = format!( "{prompt} Provided context: {context} " ); let res = self .openai_client .chat() .create( CreateChatCompletionRequestArgs::default() .model("gpt-4o") .messages(vec![ ChatCompletionRequestMessage::User( ChatCompletionRequestUserMessageArgs::default() .content(input) .build()?, ), ]) .build()?, ) .await .map(|res| { // We extract the first result match res.choices[0].message.content.clone() { Some(res) => Ok(res), None => Err(anyhow::anyhow!("There was no result from OpenAI")), } })??; println!("Retrieved result from prompt: {res}"); Ok(res) } } ``` ### Using Qdrant in a Rust web service Let's have a quick look at a real world example. Below is a HTTP endpoint for the Axum framework that takes our `RAGSystem` as application state. It'll embed the prompt and attempt to search the cache. If there's no result, it searches in the regular collection for a match. The resulting document payload is added to an augmented prompt, and the question and answer are added to the cache. Finally, a response is returned from the endpoint. ```rust use axum::{Json, extract::State, response::IntoResponse, http::StatusCode}; use serde::Deserialize; #[derive(Deserialize)] struct Prompt { prompt: String, } async fn prompt( State(state): State, Json(prompt): Json, ) -> Result { let embedding = match state.embed_prompt(&prompt.prompt).await { Ok(embedding) => embedding, Err(e) => { return Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("An error occurred while embedding the prompt: {e}"), )) } }; if let Ok(answer) = state.search_cache(embedding.clone()).await { return Ok(answer); } let search_result = match state.search(embedding.clone()).await { Ok(res) => res, Err(e) => { return Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("An error occurred while prompting: {e}"), )) } }; let llm_response = match state.prompt(&prompt.prompt, &search_result).await { Ok(prompt_result) => prompt_result, Err(e) => { return Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong while prompting: {e}"), )) } }; if let Err(e) = state.add_to_cache(embedding, &llm_response).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong while adding item to the cache: {e}"), )); }; Ok(llm_response) } ``` The last thing to do is setting up our main function. Note that we add the `shuttle_qdrant::Qdrant` annotation to our main function, allowing us to provision a Qdrant instance locally with Docker automatically on a local run. In production though, we'll need the `cloud_url` and `api_key` keys filled out. ```rust #[shuttle_runtime::main] async fn main( #[shuttle_qdrant::Qdrant( cloud_url = "{secrets.QDRANT_URL}", api_key = "{secrets.QDRANT_API_KEY}" )] qdrant: QdrantClient, #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { secrets.into_iter().for_each(|x| env::set_var(x.0, x.1)); let rag = RAGSystem::new(qdrant); let setup_required = true; if setup_required { rag.create_regular_collection().await?; rag.create_cache_collection().await?; rag.embed_csv_file("test.csv".into()).await?; } let rtr = Router::new().route("/prompt", post(prompt)).with_state(rag); Ok(rtr.into()) } ``` ## Deploying To deploy, all you need to do is use `shuttle deploy` (with the `--ad` flag if on a Git branch with uncommitted changes) and wait for it to deploy! Once you've deployed, any further deploys needed will only need to re-compile your application (and any extra dependencies if added) then it'll be done much, much faster. ## Extending this example Want to extend this example? Here's a couple ways you can do that. ### Use a cheaper model for semantic caching While using a high-performance model is great and all, one thing that we want to save on in particular is costs. One thing that we can do here to save tokens is by using a cheaper model and asking the model if one question is semantically the same as another. Here's a prompt you can use: ``` Are these two questions semantically the same? Answer either 'Yes' or 'No'. Do not answer with anything else. If you don't know the answer, say 'I don't know'. Question 1: Question 2: ``` ### Smaller payload indexes It should be noted of course that while our example _does_ work, one thing you might need to think about is payload indexes or associated data connected to a particular embedding. If you're inserting the whole file contents as the payload for every single embedding in a large file, chances are you are going to increase your resource usage quite rapidly. You can mitigate this by only inserting a relevant slice of the file per embedding (so for example in this case, it might be the row). ## Finishing up Thanks for reading! By using semantic caching, we can create a much more performant RAG system that saves on both time and costs. Read more: - [Building a RAG agent workflow](https://www.shuttle.dev/blog/2024/05/23/building-agentic-rag-rust-qdrant) - [Parallelize your data processing using Rayon](https://www.shuttle.dev/blog/2024/04/11/using-rayon-rust) - [Using Huggingface with Rust](https://www.shuttle.dev/blog/2024/05/01/using-huggingface-rust) --- # Building Agentic RAG with Rust, Qdrant & OpenAI Source: https://www.shuttle.dev/blog/2024/05/23/building-agentic-rag-rust-qdrant Date: 23 May 2024 Author: josh Tags: rust, ai, rag, guide Using GPT-4o, Qdrant and Rust to build an agentic RAG workflow in a web service and deploy it Hey there! In this article, we're gonna talk about building an agentic RAG workflow with Rust! We'll be building an agent that can take a CSV file, parse it and embed it into Qdrant, as well as retrieving the relevant embeddings from Qdrant to answer questions from users about the contents of the CSV file. Interested in deploying or just want to see what the final code looks like? You can find the repository [here.](https://github.com/joshua-mo-143/shuttle-agentic-rag) ## What is Agentic RAG? Agentic RAG, or Agentic Retrieval Augmented Generation, is the concept of mixing AI agents with RAG to be able to produce a workflow that is even better at being tailored to a specific use case than an agent workflow normally would be. Essentially, the difference between this workflow and a regular agent workflow would be that each agent can individually access embeddings from a vector database to be able to retrieve contextually relevant data - resulting in more accurate answers across the board in an AI agent workflow! ## Getting Started To get started, use `shuttle init` to create a new project. Next, we'll add the dependencies we need using a shell snippet: ```bash cargo add anyhow cargo add async-openai cargo add qdrant-client cargo add serde -F derive cargo add serde-json cargo add shuttle-qdrant cargo add uuid -F v4 ``` We'll also need to make sure to have a Qdrant URL and an API key, as well as an OpenAI API key. Shuttle uses environment variables via a `SecretStore` macro in the main function, and can be stored in the `Secrets.toml` file: ```toml OPENAI_API_KEY = "" ``` Next, we'll update our main function to have our Qdrant macro and our secrets macro. We'll iterate through each secret and set it as an environment variable - this allows us to use our secrets globally, without having to reference the `SecretStore` variable at all: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_qdrant::Qdrant] qdrant_client: QdrantClient, #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { secrets.into_iter().for_each(|x| { set_var(x.0, x.1); }); let router = Router::new() .route("/", get(hello_world)); Ok(router.into()) } ``` ## Building an agentic RAG workflow ### Setting up our agent The agent itself is quite simple: it holds an OpenAI client, as well as a Qdrant client to be able to search for relevant document embeddings. Other fields can also be added here, depending on what capabilities your agent requires. ```rust use async_openai::{config::OpenAIConfig, Client as OpenAIClient}; use qdrant_client::prelude::QdrantClient; pub struct MyAgent { openai_client: OpenAIClient, qdrant_client: QdrantClient, } ``` Next we'll want to create a helper method for creating the agent, as well as a system message which we'll feed into the model prompt later. ```rust static SYSTEM_MESSAGE: &str = " You are a world-class data analyst, specialising in analysing comma-delimited CSV files. Your job is to analyse some CSV snippets and determine what the results are for the question that the user is asking. You should aim to be concise. If you don't know something, don't make it up but say 'I don't know.'. " impl MyAgent { pub fn new(qdrant_client: QdrantClient) -> Self { let api_key = std::env::var("OPENAI_API_KEY").unwrap(); let config = OpenAIConfig::new().with_api_key(api_key); let openai_client = OpenAIClient::with_config(config); Self { openai_client, qdrant_client, } } } ``` ### File parsing and embedding into Qdrant Next, we will implement a `File` struct for CSV file parsing - it should be able to hold the file path, contents as well as the rows as a `Vec` (string array, or more accurately a vector of strings). There's a few reasons why we store the rows as a `Vec`: - Smaller chunks improve the retrieval accuracy, one of the biggest challenges that RAG has to deal with. Retrieving a wrong or otherwise inaccurate document can hamper accuracy significantly. - Improved retrieval accuracy leads to enhanced contextual relevance - which is quite important for complex queries that require specific question. - Processing and indexing smaller chunks ```rust pub struct File { pub path: String, pub contents: String, pub rows: Vec, } impl File { pub fn new(path: PathBuf) -> Result { let contents = std::fs::read_to_string(&path)?; let path_as_str = format!("{}", path.display()); let rows = contents .lines() .map(|x| x.to_owned()) .collect::>(); Ok(Self { path: path_as_str, contents, rows }) } } ``` While the above parsing method _is_ serviceable (collecting all the lines into a `Vec`), note that it is a naive implementation. Based on how your CSV files are delimited and/or if there is dirty data to clean up, you may want to either prepare your data so that it is already well-prepared, or include some form of data cleaning or validation. Some examples of this might be: - `unicode-segmentation` - [a library crate for splitting sentences](https://github.com/unicode-rs/unicode-segmentation) - `csv_log_cleaner` - [a binary crate for cleaning CSVs](https://github.com/ambidextrous/csv_log_cleaner) - `validator` - [a library crate for validating struct/enum fields](https://github.com/Keats/validator) Next, we'll go back to our agent and implement a method for embedding documents into Qdrant that will take the `File` struct we defined. To do this, we need to do the following: - Take the rows we created earlier and add them as the input for our embed request. - Create the embeddings (with openAI) and create a payload for storing alongside the embeddings in Qdrant. Note that although we use a `uuid::Uuid` for unique storage, you could just as easily use numbers by adding a number counter to your struct and incrementing it by 1 after you've inserted an embedding. - Assuming there are no errors, return `Ok(())` ```rust use async_openai::types::{ CreateEmbeddingRequest, EmbeddingInput }; use async_openai::Embeddings; use qdrant_client::prelude::{Payload, PointStruct}; static COLLECTION: &str = "my-collection"; // text-embedding-ada-002 is the model name from OpenAI that deals with embeddings static EMBED_MODEL: &str = "text-embedding-ada-002"; impl MyAgent { pub async fn embed_document(&self, file: File) -> Result<()> { if file.rows.is_empty() { return Err(anyhow::anyhow!("There's no rows to embed!")); } let request = CreateEmbeddingRequest { model: EMBED_MODEL.to_string(), input: EmbeddingInput::StringArray(file.rows.clone()), user: None, dimensions: Some(1536), ..Default::default() }; let embeddings_result = Embeddings::new(&self.openai_client).create(request).await?; for embedding in embeddings_result.data { let payload: Payload = serde_json::json!({ "id": file.path.clone(), "content": file.contents, "rows": file.rows }) .try_into() .unwrap(); println!("Embedded: {}", file.path); let vec = embedding.embedding; let points = vec![PointStruct::new( uuid::Uuid::new_v4().to_string(), vec, payload, )]; self.qdrant_client .upsert_points(COLLECTION, None, points, None) .await?; } Ok(()) } } ``` ### Document searching Now that we've embedded our document, we'll want a way to check whether our embeddings are contextually relevant to whatever prompt the user gives us. For this, we'll create a `search_document` function that does the following: - Embed the prompt using `CreateEmbeddingRequest` and get the embedding from the results. We'll be using this embedding in our document search. Because we've only added one sentence to embed here (the prompt), it will only return one sentence - so we can create an iterator from the vector and attempt to find the first result. - Create a parameter list for our document search through the `SearchPoints` struct (see below). Here we need to set the collection name, the vector that we want to search against (ie the input), how many results we want to be returned if there are any matches, as well as the payload selector. - Search the database for results - if there are no results, return an an error; if there is a result, then return the result back. ```rust use qdrant_client::qdrant::{ with_payload_selector::SelectorOptions, SearchPoints, WithPayloadSelector, }; impl MyAgent { async fn search_document(&self, prompt: String) -> Result { let request = CreateEmbeddingRequest { model: EMBED_MODEL.to_string(), input: EmbeddingInput::String(prompt), user: None, dimensions: Some(1536), ..Default::default() }; let embeddings_result = Embeddings::new(&self.openai_client).create(request).await?; let embedding = &embeddings_result.data.first().unwrap().embedding; let payload_selector = WithPayloadSelector { selector_options: Some(SelectorOptions::Enable(true)), }; // set parameters for search let search_points = SearchPoints { collection_name: COLLECTION.to_string(), vector: embedding.to_owned(), limit: 1, with_payload: Some(payload_selector), ..Default::default() }; // if the search is successful // attempt to iterate through the results vector and find a result let search_result = self.qdrant_client.search_points(&search_points).await?; let result = search_result.result.into_iter().next(); match result { Some(res) => Ok(res.payload.get("contents").unwrap().to_string()), None => Err(anyhow::anyhow!("There were no results that matched :(")), } } } ``` Now that everything we need to use our agent effectively is set up, we can set up a prompt function! ```rust use async_openai::types::{ ChatCompletionRequestMessage, ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs, }; static PROMPT_MODEL: &str = "gpt-4o"; impl MyAgent { pub async fn prompt(&self, prompt: &str) -> anyhow::Result { let context = self.search_document(prompt.to_owned()).await?; let input = format!( "{prompt} Provided context: {} ", context // this is the payload from Qdrant ); let res = self .openai_client .chat() .create( CreateChatCompletionRequestArgs::default() .model(PROMPT_MODEL) .messages(vec![ //First we add the system message to define what the Agent does ChatCompletionRequestMessage::System( ChatCompletionRequestSystemMessageArgs::default() .content(SYSTEM_MESSAGE) .build()?, ), //Then we add our prompt ChatCompletionRequestMessage::User( ChatCompletionRequestUserMessageArgs::default() .content(input) .build()?, ), ]) .build()?, ) .await .map(|res| { //We extract the first one res.choices[0].message.content.clone().unwrap() })?; println!("Retrieved result from prompt: {res}"); Ok(res) } } ``` ## Hooking the agent up to our web service Because we separated the agent logic from our web service logic, we just need to connect the bits together and we should be done! Firstly, we'll create a couple of structs - the `Prompt` struct that will take a JSON prompt, and the `AppState` function that will act as shared application state in our Axum web server. ```rust #[derive(Deserialize)] pub struct Prompt { prompt: String, } #[derive(Clone)] pub struct AppState { agent: MyAgent, } ``` We'll also introduce our prompt handler endpoint here: ```rust async fn prompt( State(state): State, Json(json): Json, ) -> Result { let prompt_response = state.agent.prompt(&json.prompt).await?; Ok((StatusCode::OK, prompt_response)) } ``` Then we need to parse our CSV file in the main function, create our `AppState` and embed the CSV, as well as setting up our router: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_qdrant::Qdrant] qdrant_client: QdrantClient, #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { secrets.into_iter().for_each(|x| { set_var(x.0, x.1); }); // note that this already assumes you have a file called "test.csv" // in your project root let file = File::new("test.csv".into())?; let state = AppState { agent: MyAgent::new(qdrant_client), }; state.agent.embed_document(file).await?; let router = Router::new() .route("/", get(hello_world)) .route("/prompt", post(prompt)) .with_state(state); Ok(router.into()) } ``` ## Deploying To deploy, all you need to do is use `shuttle deploy` (with the `--ad` flag if on a Git branch with uncommitted changes), sit back and watch the magic happen! ## Finishing Up Thanks for reading! With the power of combining AI agents and RAG, we can create powerful workflows to be able to satisfy many different use cases. With Rust, we can leverage performance benefits to be able to run our workflows safely and with a low memory footprint. Read more: - [Using Huggingface with Rust](https://www.shuttle.dev/blog/2024/05/01/using-huggingface-rust) - [Building a RAG web service with Qdrant & Rust](https://www.shuttle.dev/blog/2024/02/28/rag-llm-rust) - [Prompting AWS Bedrock with the AWS Rust SDK](https://www.shuttle.dev/blog/2024/05/10/prompting-aws-bedrock-rust) --- # Building AI Agents with Rust Source: https://www.shuttle.dev/blog/2024/05/16/building-ai-content-writer-rust-gpt4o Date: 16 May 2024 Author: josh Tags: rust, ai, guide Using GPT-4o and Rust to build AI agents in a web service and deploy them Hello world! In this guide, we're going to talk about how you can get started with using AI agents to create a content writer that will use the [Serper.dev](http://Serper.dev) API to search Google for results on your query, then use the results together with GPT-4o to create a summary of the results and finally create an article about it. Interested in just deploying or got stuck during the tutorial? [Have a look at the repository.](https://github.com/joshua-mo-143/shuttle-content-writer) ## Setting up To get started, we'll create a new project using `cargo-shuttle init`, making sure to pick Axum as the framework. After that, we'll install the dependencies we need: ```bash cargo add async-openai cargo add reqwest -F json cargo add serde -F derive cargo add serde_json cargo add thiserror ``` You will also additionally need API keys from Serper and OpenAI, which you will put in a new `Secrets.toml` file in your project root: ```toml SERPER_API_KEY = "" OPENAI_API_KEY = "" ``` ## Error handling Before we get started, let's quickly define some error types. We can add this to an `errors.rs` file and use `thiserror`. The reason why we'll do this is for error propagation: instead of manually pattern matching, if we want the error to be propagated we can use this enum as the error return type then use error propagation: ```rust use async_openai::error::OpenAIError; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use thiserror::Error; #[derive(Error, Debug)] pub enum ApiError { #[error("OpenAI error: {0}")] OpenAI(#[from] OpenAIError), #[error("Reqwest error: {0}")] Reqwest(#[from] reqwest::Error), #[error("De/serialization error: {0}")] SerdeJson(#[from] serde_json::Error), } impl IntoResponse for ApiError { fn into_response(self) -> Response { let (status, body) = match self { Self::OpenAI(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), Self::Reqwest(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), Self::SerdeJson(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), }; println!("An error happened: {body}"); (status, body).into_response() } } ``` This is enabled by `ApiError` implementing `From`, allowing easy propagation. ## Building AI Agents Our first step will be defining a generic interface that all of our autonomous agents will work with. We'll be creating two agents: - A researcher (takes some data from a Google search, feeds it into ChatGPT and asks it to summarize the information) - A writer (takes the summary and writes an article about it) It will look something like this: ```rust use async_openai::{config::OpenAIConfig, Client as OpenAIClient}; pub trait Agent { fn name(&self) -> String; fn client(&self) -> OpenAIClient; fn system_message(&self) -> String; // to be given a default implementation later async fn prompt(&self, input: &str, data: String) -> Result; } ``` Why do we need the other 3 methods if `prompt()` already uses `self`? This is because as part of the `Agent` trait, the prompt function **cannot** reference types that it doesn't know about. If we have a struct that has the `async_openai::Client` type already, we need to create a method from the `Agent` trait to be able to access the client. If you're only creating one agent, typically you don't need a specific trait. However, if you wanted to create more agents in the same library or application, it would be a good idea to have a generic interface to hold all of the relevant methods! Let's define our `Researcher` struct, which will hold the data and methods for us to query the Serper API and then use the data to prompt a model: ```rust #[derive(Clone)] pub struct Researcher { http_client: reqwest::Client, system: Option, openai_client: OpenAIClient, } ``` Next, we can implement the `Agent` trait for our struct: ```rust impl Agent for Researcher { fn name(&self) -> String { "Researcher".to_string() } fn client(&self) -> OpenAIClient { self.openai_client.clone() } fn system_message(&self) -> String { if let Some(message) = &self.system { message.to_owned() } else { "You are an agent. You will receive a question that may be quite short or does not have much context. Your job is to research the Internet and to return with a high-quality summary to the user, assisted by the provided context. The provided context will be in JSON format and contains data about the initial Google results for the website or query. Be concise. Question: ".to_string() } } } ``` Note that here, the `name()` and `client()` functions are mostly boilerplate. If you wanted to extend this even further, you could use a macro to get rid of this totally. The system message will be for pre-prompting the model. When we prompt the model, the system message will be passed in and then the bot will respond to the prompt according to the system message. In models where system messages don't exist, this would simply represent the text before you put your prompt. We also implement two methods for `impl Researcher`: one to initialise the struct itself, and then one for preparing the data to send into our agent pipeline: ```rust use std::env; use reqwest::header::HeaderMap; impl Researcher { pub fn new() -> Self { let api_key = env::var("OPENAI_API_KEY").unwrap(); let config = OpenAIConfig::new().with_api_key(api_key); let openai_client = OpenAIClient::with_config(config); let mut headers = HeaderMap::new(); headers.insert( "X-API-KEY", env::var("SERPER_API_KEY").unwrap().parse().unwrap(), ); headers.insert("Content-Type", "application/json".parse().unwrap()); let http_client = reqwest::Client::builder() .default_headers(headers) .build() .unwrap(); Self { http_client, system: None, openai_client, } } pub async fn prepare_data(&self, prompt: &str) -> Result { let json = serde_json::json!({ "q": prompt }); let res = self .http_client .post("") .json(&json) .send() .await .unwrap(); let json = res.json::().await?; Ok(serde_json::to_string_pretty(&json)?) } } ``` The `Writer` half of our AI agents will mostly be the same, save the `reqwest::Client`. We need to implement `Agent` alongside it, however, ```rust #[derive(Clone)] pub struct Writer { system: Option, client: OpenAIClient, } impl Writer { pub fn new() -> Self { let api_key = env::var("OPENAI_API_KEY").unwrap(); let config = OpenAIConfig::new().with_api_key(api_key); let client = OpenAIClient::with_config(config); Self { system: None, client, } } } impl Agent for Writer { fn name(&self) -> String { "Writer".to_string() } fn client(&self) -> OpenAIClient { self.client.clone() } fn system_message(&self) -> String { if let Some(message) = &self.system { message.to_owned() } else { "You are an agent. You will receive some context from another agent about some Google results that a user has searched. Your job is to research the Internet and to write a high-quality article that a user has written. The article must not appear to be AI written. The article should be SEO optimised without overly compromising the quality of the article. You are free to be as creative as you wish. However, each paragraph must have the following: - The point you are trying to make - If there is a follow up action point - Why the follow up action point exists (or why the user needs to carry it out) Search query: ".to_string() } } } ``` Finally, we'll go back and fill the `prompt` method back in on the default `Agent` trait so that we have a default method implementation (and therefore don't need to keep re-implementing it): ```rust use async_openai::types::{ ChatCompletionRequestMessage, ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs, }; async fn prompt(&self, input: &str, data: String) -> Result { let input = format!( "{input} Provided context: {} ", serde_json::to_string_pretty(&data)? ); let res = self .client() .chat() .create( CreateChatCompletionRequestArgs::default() .model("gpt-4o") .messages(vec![ //First we add the system message to define what the Agent does ChatCompletionRequestMessage::System( ChatCompletionRequestSystemMessageArgs::default() .content(&self.system_message()) .build()?, ), //Then we add our prompt ChatCompletionRequestMessage::User( ChatCompletionRequestUserMessageArgs::default() .content(input) .build()?, ), ]) .build()?, ) .await .map(|res| { //We extract the first one res.choices[0].message.content.clone().unwrap() })?; println!("Retrieved result from prompt: {res}"); Ok(res) } ``` ### Writing our web service Now that the hard work is over - we can implement the AI agents in our web application! To get started, we'll create an `AppState` struct that implements `Clone`. Typically, this is a trait bound set by Axum or pretty much any Rust-based framework that you use. In it we'll have our `Researcher` and `Writer` struct: ```rust use crate::agent::{Researcher, Writer}; #[derive(Clone)] pub struct AppState { pub researcher: Researcher, pub writer: Writer, } impl AppState { pub fn new() -> Self { let researcher = Researcher::new(); let writer = Writer::new(); Self { researcher, writer } } } ``` Next, we will write our handler endpoint that will take in a JSON input, run the agent pipeline and then return the end result: ```rust #[derive(Deserialize, Serialize)] pub struct Prompt { q: String, } #[axum::debug_handler] async fn prompt( State(state): State, Json(prompt): Json, ) -> Result { let data = state.researcher.prepare_data(&prompt.q).await?; let resarcher_result = state.researcher.prompt(&prompt.q, data).await?; let writer_result = state.writer.prompt(&prompt.q, res).await?; Ok(writer_result) } ``` And that's basically it! We can then hook it all up by adding our endpoint to the router: ```rust let router = Router::new() .route("/", get(hello_world)) .route("/prompt", post(prompt)) .with_state(state); ``` ## Deployment Now all we need to do is use `shuttle deploy` (adding the `--ad` flag if on an uncommitted Git branch) and watch the magic happen! ## Extending this project ### Making a Pipeline struct for your agents So let's say you've built this example, and want to go even further. What about pulling in another agent that generates a Twitter post or a LinkedIn post. At this point, you probably want to build a pipeline that holds all your agents, then you can just write `.run_pipeline()` and it'll do everything for you. To do this, you could create a `Pipeline` trait does two things: - Initialise the agents set (and return it as a `Vec>`) - Run the vector as a pipeline, where the results of the previous agent gets fed into the next one However, you may run into an issue with your Agents needing to implement `Sized`. This is because `Clone` requires an object to be a known size at compile-time - otherwise it won't work! To fix this, we can wrap the `dyn` type in a `Box`, allocating it on the heap. This works because the `Clone` trait requires the type to have a known, static size at compile-time. Additionally, you might also receive an error with not being able to compile because of the `prompt()` method being async. You can fix this by adding the `async_trait` crate, then using the attribute macro above your code: ```rust #[async_trait::async_trait] trait Agent { // .. } ``` Note that for every `impl Agent for T`, you'll also need to remember to add the async trait macro. Otherwise, you'll get an error about the lifetime annotations not matching! ### Updating the prompt If you're not happy with the prompt results, don't forgetyou can always update the message prompt that gets sent to your model! ## Finishing Up By leveraging Rust with the power of GPT-4o, you can develop a robust AI-powered content writer. ## Additional Resources For further learning and details, refer to: - [Building Your First AI Tool in Rust - www.shuttle.dev](https://www.shuttle.dev/blog/2024/04/29/building-your-first-ai-tool-rust) - [Using Huggingface with Rust - www.shuttle.dev](https://www.shuttle.dev/blog/2023/03/01/getting-started-with-rust-and-gpt) --- # Prompting AWS Bedrock with Rust Source: https://www.shuttle.dev/blog/2024/05/10/prompting-aws-bedrock-rust Date: 10 May 2024 Author: josh Tags: rust, ai, aws, guide Prompting AWS Bedrock with Rust, looking at outputting both static and streamed GPT responses Hey there! In this article we're going to talk about using AWS Bedrock with Rust. At the end of this article, you'll have a API that can take a JSON prompt from a HTTP request and return an answer from AWS Bedrock that can be streamed or returned as a full response. Interested in the full code? You can find the repository [here.](https://github.com/joshua-mo-143/shuttle-bedrock-ex) ## What is AWS Bedrock? AWS Bedrock is one of the AI-based services that Amazon offers. It allows you to use models directly for inference and generative AI. Compared to other AWS offerings like SageMaker, you only pay for each API call. This makes it much cheaper to use in a real application compared to SageMaker, which charges you based on instance uptime and can snowball costs very quickly. Bedrock also comes with tools like guard rails to allow you to customise topic/word filters (to mitigate model abuse) and add your own training data. ## Getting Started ### Setting up your foundational model Before we get started, you'll need to set up access to the foundational model you want to use and an IAM user. We'll use the Titan Text G1 Express model as an example. To request access to a model, do the following: - Log into AWS Console and go to the AWS Bedrock section (it can also be found using the search bar) - Click "Model Access" on the left hand side - it's somewhere near the bottom. - Click "Manage Model access" (top right hand side of the table). - Find the model(s) you want access to, click the appropriate checkbox then click Request Access. Note that some models require you to elaborate on your use case before AWS will approve access. The Titan Text models will generally grant you immediate access to use them. Once done, you'll need to find (and save) the name of the model ID that you're using! You can find the endpoint URL you need [here,](https://docs.aws.amazon.com/general/latest/gr/bedrock.html) as well as the model ID [here.](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns) ### Setting up an IAM user You'll also need an access key ID and secret access key from an IAM user: - `AWS_ACCESS_KEY_ID` (your Access Key) - `AWS_SECRET_ACCESS_KEY` (your Secret Access Key) Both can be found in your IAM user credentials if you've already set a user up. If you don't have an appropriate user with policies, you can get started quickly by doing the following: - Go to the Users menu - Start creating a user and go to the "Attach policies" section (then search "Bedrock") - Here you can either use the "AmazonBedrockFullAccess" policy which gives you full access to Bedrock on that user, or you can create a custom policy. Select one and finish creating your user. **Access to Bedrock is required**, as otherwise you won't be able to use it! - Go back to the Users menu and click on your newly created user - Go to "Access keys" and follow the prompt (clicking "Application outside of AWS"). Don't forget to store your Access Key ID and Secret Access Key! In production, you may want to go a step further and create a Group that you can then attach policies and users to. ### Initialisation To get started, we're going to use `shuttle init` (requires `cargo-shuttle` installed) to initialise our template, picking Axum as the framework. Next, we're going to add our dependencies: ```bash cargo add aws-config -F behavior-version-latest cargo add aws-credential-types -F hardcoded-credentials cargo add aws-sdk-bedrockruntime -F behavior-version-latest cargo add serde -F derive cargo add serde-json ``` You'll also want to store your secrets in a `Secrets.toml` file (in the root of your project) like so: ```rust AWS_ACCESS_KEY_ID = "your-access-key-id" AWS_SECRET_ACCESS_KEY = "your-secret-access-key" AWS_URL = "your-endpoint-url" ``` ## Setting up the AWS config To get started, we'll create a function that will take secrets from our Secrets.toml file that we created earlier. This can be done by adding the `#[shuttle_runtime::Secrets]` macro to our main function: ```rust use shuttle_runtime::SecretStore; #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore ) -> shuttle_axum::ShuttleAxum { // .. your code here } ``` On a local or deployment run, the secrets macro will allow the Shuttle runtime to read the `Secrets.toml` file! Next we'll grab our secrets, then create our AWS `Credentials` struct then create an `aws_config` Config type. This will allow us to create the AWS Bedrock Runtime client, as well as any other client from the AWS Rust SDK that we need. ```rust use cargo_shuttle::SecretStore; use aws_credential_types::Credentials; use aws_sdk_bedrockruntime::Client; async fn create_client(secrets: SecretStore) -> Client { let access_key_id = secrets .get("AWS_ACCESS_KEY_ID") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let secret_access_key = secrets .get("AWS_SECRET_ACCESS_KEY") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let aws_url = secrets .get("AWS_URL") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); // note here that the "None" is in place of a session token let creds = Credentials::from_keys(access_key_id, secret_access_key, None); let cfg = aws_config::from_env() .endpoint_url(aws_url) // Note: Don't forget to set this to the appropriate region! .region(Region::new("us-east-1")) .credentials_provider(creds) .load() .await; Client::new(&cfg) } ``` Onto using the runtime itself! ## Using the Bedrock runtime ### Pre-requisites Before we start using the Bedrock runtime, you'll need: - a model ID (for a model that you have access to) - the identifier of the guardrail you want to use (if you're using one) - the content type (JSON by default) You can check out the model IDs [here.](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns) ### Getting a Prompt Response For this example, we'll be using the Titan Text G1 Lite model. Although some models may differ in their request bodies and/or response body shape, the process is largely the same. Before we can write our endpoint, we'll need to define a few structs: - A JSON input (that contains the prompt) - A struct that models the HTTP response from Bedrock for our model - A struct that models the HTTP request to Bedrock for our model ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] struct Prompt { prompt: String, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct TitanResponse { input_text_token_count: i32, results: Vec, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct TitanTextResult { token_count: i32, output_text: String, completion_reason: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct TextGenConfig { temperature: f32, top_p: f32, max_token_count: i32, stop_sequences: Vec, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct TitanRequest { input_text: String, text_generation_config: TextGenConfig, } impl TitanRequest { fn new(prompt: String) -> Self { Self { input_text: prompt, text_generation_config: TextGenConfig { // higher temperature allows for more LLM creativity // the minimum value, 0.0, allows for a 100% predictable // response temperature: 0.2, // nucleus sampling probability - aka sampling the smallest // set of words that exceed the "top_p" threshold for a // response top_p: 0.0, // note here that 1 token is between 1 to 4 words // we have kept the max token count low here // to avoid high costs max_token_count: 100, stop_sequences: vec!["|".to_string()], }, } } } ``` Now onto making our prompt handler! We'll set up a function like so (note that we use destructuring to get access to the inner variable from our JSON prompt struct): ```rust use axum::response::IntoResponse; async fn prompt( State(state): State, Json(Prompt { prompt }): Json, ) -> Result { // .. code below } ``` Next, we'll need to use our client from shared state to send a request to Bedrock. ```rust use aws_sdk_bedrockruntime::primitives::Blob; use axum::http::StatusCode; let titan_req = TitanRequest::new(prompt); let Ok(prompt) = serde_json::to_vec(&titan_req) else { return Err(StatusCode::BAD_REQUEST); }; let blob = Blob::new(prompt); let res = state.client .invoke_model() .body(blob) .model_id("amazon.titan-text-lite-v1:0:4k") .send().await .unwrap(); ``` After this, we need to get the response, convert the response body to a `&[u8]` and deserialize it back into a response body struct. Because the text results come back as a `Vec`, we'll then use `.first()` to then get the first results and immediately return the text as a HTTP string: ```rust let res: &[u8] = &res.body.into_inner(); let Ok(response_body) = serde_json::from_slice::(res) else { return Err(StatusCode::INTERNAL_SERVER_ERROR); }; let Some(TitanTextResult { output_text, .. }) = response_body.results.first() else { return Err(StatusCode::INTERNAL_SERVER_ERROR); }; Ok(output_text.to_owned()) ``` ### Streamed Prompt Responses Often, it can be better to get a streamed response from a model. Models can often take a long time to formulate a full answer, so having a streamed response can greatly assist with user retention by not requiring them to wait for Bedrock to fully finish processing the tokens. As you can see below, adding the method for a response stream does not require much change for requesting something from Bedrock: ```rust let res = state.client .invoke_model_with_response_stream() .body(blob) .model_id("amazon.titan-text-lite-v1:0:4k") .send().await .unwrap(); ``` However, for our response we do need to make some changes. To be able to create a stream, we need to declare a variable that is compatible with the `futures::stream::Stream` type. We can use the `stream::unfold` function that takes a variable, then puts the mutable state in a closure and then we can do as we like within our stream. The only requirement for this is that we return the item we want to output, as well as the state itself (so the stream can progress). This would look something like this: ```rust use futures::stream; use aws_sdk_bedrockruntime::types::ResponseStream; let stream = stream::unfold(res.body, |mut state| async move { let message = state.recv().await.unwrap(); match message { Some(ResponseStream::Chunk(chunk)) => { let Ok(response_body) = serde_json::from_slice::( &chunk.bytes.unwrap().into_inner() ) else { println!("Unable to deserialize response body :("); return None; }; let Some(TitanTextResult { output_text, .. }) = response_body.results.first() else { println!("No results :("); return None; }; Some((output_text.clone(), state)) } _ => None, } }); Ok(axum_streams::StreamBodyAs::text(stream)); ``` ## Interacting with your API To make sure that the previous endpoint works, you can use curl on your service: ```bash curl http://localhost:8000/prompt \ -H 'Content-Type: application/json' \ -d '{"prompt":"Hello world!"}' ``` Note that the above snippet is for a non-streamed response. If you want to receive a streamed response using curl, you need to add the `--no-buffer` flag: ```bash curl --no-buffer http://localhost:8000/prompt/streamed \ -H 'Content-Type: application/json' \ -d '{"prompt":"Hello world!"}' ``` The reason why you need to do this is because curl by default stores the response in a buffer. By removing the buffer (via the flag), you can immediately receive the text as it comes. If you want to serve your response from a HTTP webpage, you need to set up a new `TextDecoder` using JavaScript. This is the whole frontend HTML file that you need: ```html

Shuttle AWS Bedrock Prompt

``` If you want to add this file to your Axum service, you'll want to install `tower-http` with the `fs` flag enabled: ```bash cargo add tower-http -F fs ``` This will allow you to serve a whole directory (or file) on your web service! We can do this by adding the HTML file to a subfolder of our project root and then declaring it in `Shuttle.toml`. Generally, we can use a wildcard - if we have a folder called `static` (aptly named to hold all our static assets), we can declare it like so: ```bash assets = ["static/*"] ``` Then in our router, we would add `tower_http::services::ServeDir` as a `tower` layer for our Axum application: ```rust use tower_http::services::ServeDir; let router = Router::new() .route("/prompt", post(prompt)) .route("/prompt/streamed", post(streamed_prompt)) .nest_service("/", ServeDir::new("static")) .with_state(appstate); ``` ## Wrapping it all up Now it's time to hook it all up! We'll change our `axum::Router` so that it should only have the prompt routes, as well as including the static assets we talked about earlier (you can remove this if you're not using them). Your Shuttle main function should look like this: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore ) -> shuttle_axum::ShuttleAxum { // create AWS client let client = create_client(secrets).await; // create application shareable state from AWS client let appstate = AppState::new(client); // set up the router let router = Router::new() .route("/prompt", post(prompt)) .route("/prompt/streamed", post(streamed_prompt)) .with_state(appstate); Ok(router.into()) } ``` ## Deployment To deploy, all you need to do is `shuttle deploy` (with the `--allow-dirty` flag if working from a Git branch with uncommitted changes) and watch the magic happen! Once finished, you'll get a message containing information about your deployment as well as where you can reach the deployment URL. Shuttle will also cache your dependencies, so if you need to re-deploy you won't have to worry about waiting to re-compile! ## Finishing up Thanks for reading! By integrating Rust with AWS Bedrock, you can harness the power of both technologies to build scalable, reliable, and maintainable systems. Read more: - [Building a RAG web service with Qdrant and OpenAI](https://www.shuttle.dev/blog/2024/02/28/rag-llm-rust) - [Building AI agents](https://www.shuttle.dev/blog/2024/04/30/building-ai-agents-rust) - [Using Huggingface with Rust](https://www.shuttle.dev/blog/2024/05/01/using-huggingface-rust) --- # Using Huggingface with Rust Source: https://www.shuttle.dev/blog/2024/05/01/using-huggingface-rust Date: 1 May 2024 Author: josh Tags: rust, ai, guide Using Huggingface with Rust Hello world! Today, we're going to talk about Huggingface with Rust. We're going to cover the following: - Downloading a repo from Huggingface - Tokenizing input and outputting tokens using Huggingface via Candle - Generating text from tokens - Using Candle in a web service By the end of this article, we'll have a fully working web server for running our model on. Interested in the final code? Check it out [here.](https://www.github.com/joshua-mo-143/shuttle-candle) ## What is Huggingface? Huggingface describes itself simply as: > The AI community, building the future. Huggingface is both a company, as well as a platform that contributes heavily to the AI and NLP fields through open source and open science. A couple of examples of how they do this: - Offering a free platform for free to upload AI models, try other peoples' AI models and gain a lot of insight generally about how LLMs work - They have an in-depth NLP course teaching you how to create and use transformers, datasets and tokenizers (although it's using Python) - Giving back to the community by creating frameworks like Candle (Rust) Huggingface is a huge driving force within the AI community. As you can see, their platform is a great way to be able to start using AI at home, without paying for anything (initially). ## Using Huggingface ### Getting started To use Huggingface with your Rust application requires the `hf_hub` crate to be added to your Cargo.toml: ```bash cargo add hf_hub ``` You'll also want the following dependencies which you can grab from this shell snippet: ```bash cargo add anyhow candle-core candle-nn candle-transformers serde serde_json tokenizers -F serde/derive ``` Before we start initialising the model, let's talk about models. Models typically have a weights map, which determine the strength of connections within your neural network. For example if we were to go to [a HuggingFace repo](https://huggingface.co/mistralai/Mistral-7B-v0.1) (assuming we're logged in and have been granted access), you might see a bunch of files regarding tensors (see below), a JSON file containing model weights as well as some other stuff. But first before we continue, let's have a quick look at what tensors actually are - as they're quite important to know about! From Wikipedia: > a **tensor** is an algebraic object that describes a multilinear relationship between sets of algebraic objects related to a vector space. Practically speaking for us, this just means multidimensional arrays of numbers (an array with multiple indexes, if you will). We can manipulate tensors and shape them to get output. A real-world example of using a tensor might be a picture. A picture can have height, width and color - which we can model as a 3D tensor. If we feed this into a model by turning it into a data representation, the model can find similar photos by comparing values from its training data and output a relevant response. To deserialize the weight map file, we'll want to implement a custom deserialization function as the file can contain an unknown amount of keys and values. meaning that we should deserialize it to a `serde_json::Value` first. If you try to just deserialize it directly, will simply tell you that it got a sequence but expected a map (or something similar). ```rust #[derive(Debug, Deserialize)] struct Weightmaps { #[serde(deserialize_with = "deserialize_weight_map")] weight_map: HashSet, } // Custom deserializer for the weight_map to directly extract values into a HashSet fn deserialize_weight_map<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let map = serde_json::Value::deserialize(deserializer)?; match map { serde_json::Value::Object(obj) => Ok(obj .values() .filter_map(|v| v.as_str().map(ToString::to_string)) .collect::>()), _ => Err(serde::de::Error::custom( "Expected an object for weight_map", )), } } ``` Next, we'll write a function to load tensors from a file in the "safetensors" format into our program. Safetensors is a data serialization format for storing tensors safely. Created by Hugging Face, it was made to replace the pickle format. In Python, you may have a PyTorch weight model that gets saved (or "pickled") into a `.bin` file with the Python pickle utility. However, this is unsafe and said files may hold malicious code to be executed on un-pickling. Note that `repo.get()` returns `Result`: ```rust pub fn hub_load_safetensors( repo: &hf_hub::api::sync::ApiRepo, json_file: &str, ) -> Result> { let json_file = repo.get(json_file).map_err(candle_core::Error::wrap)?; let json_file = std::fs::File::open(json_file)?; let json: Weightmaps = serde_json::from_reader(&json_file).map_err(candle_core::Error::wrap)?; let pathbufs: Vec = json .weight_map .iter() .map(|f| repo.get(f).unwrap()) .collect(); Ok(pathbufs) } ``` Next, we'll add some code to our program for downloading a `mistral-7b` model with the current latest revision at the time of writing. Note that the repository download is quite large, clocking in at around 13.4gb! If you're running this locally, you will want some space to store the model on. You can find the latest version of a repository by going to the repo, clicking on Files and Versions and then going to the commit history. The Mistral-7B-v0.1 repo can be found [here](https://huggingface.co/mistralai/Mistral-7B-v0.1). To make this easier to follow, we'll first write a function that initialises the `ApiRepo` for us to download files from. We can do this like so, by creating an `ApiBuilder` that takes our token. ```rust fn get_repo(token: String) -> Result { let api = ApiBuilder::new().with_token(Some(token)).build()?; let model_id = "mistralai/Mistral-7B-v0.1".to_string(); api.repo(Repo::with_revision( model_id, RepoType::Model, "26bca36bde8333b5d7f72e9ed20ccda6a618af24".to_string(), )) } ``` Next, we'll need to set up our tokenizer. This function will use `repo.get()` to download the tokenizer file (returning a path) and then we use `Tokenizer::from_file` to create it: ```rust fn get_tokenizer(repo: &ApiRepo) -> Result { let tokenizer_filename = repo.get("tokenizer.json")?; Ok(Tokenizer::from_file(tokenizer_filename).map_err(E::msg)?) } ``` Finally, we'll create the model itself: ```rust fn initialise_model(token: String) -> Result { let repo = get_repo(token)?; let tokenizer = get_tokenizer(&repo)?; let device = Device::Cpu; let filenames = hub_load_safetensors(&repo, "model.safetensors.index.json")?; // note that here, we'd set this to true if we were using Flash Attention // Flash Attention requires the CUDA feature flag to be enabled, but speeds // up inference let config = Config::config_7b_v0_1(false); let model = { let dtype = DType::F32; let vb = unsafe { VarBuilder::from_mmaped_safetensors(&filenames, dtype, &device)? }; Mistral::new(&config, vb)? }; Ok((model, device, tokenizer).into()) } ``` ### Creating a Token Output Stream So we've downloaded our model, loaded the weight map files, loaded them all in and created our model. But how do we use it? Internally, when you feed an input to a model, it encodes the input into tokens. Tokens can represent words, characters, data - anything. The main idea is that by converting the data to tokens, it allows a model to parse the data more easily. Most of the more popular pretrained models will often have billions, if not trillions of tokens in training data that allow it to produce sophisticated answers. The training data gives words meaning and allows the model to produce an answer according to the input by comparing the input to its training data. To get started, we will create a stream for encoding our tokens and outputting a stream of tokens: ```rust pub struct TokenOutputStream { tokenizer: tokenizers::Tokenizer, tokens: Vec, prev_index: usize, current_index: usize, } impl TokenOutputStream { pub fn new(tokenizer: tokenizers::Tokenizer) -> Self { Self { tokenizer, tokens: Vec::new(), prev_index: 0, current_index: 0, } } } ``` Next, we'll implement some methods to do the following: - Decode tokens into UTF-8 strings - Advance the internal index and return a string if there is any text ```rust impl TokenOutputStream { fn decode(&self, tokens: &[u32]) -> String { match self.tokenizer.decode(tokens, true) { Ok(str) => Ok(str), Err(err) => candle_core::bail!("cannot decode: {err}"), } } pub fn next_token(&mut self, token: u32) -> Result> { // if there's nothing there, return an empty string let prev_text = if self.tokens.is_empty() { String::new() } else { // otherwise, use the previous decode method to decode tokens to a String let tokens = &self.tokens[self.prev_index..self.current_index]; self.decode(tokens)? }; // add this token to the current list of tokens already processed self.tokens.push(token); let text = self.decode(&self.tokens[self.prev_index..])?; if text.len() > prev_text.len() && text.chars().last().unwrap().is_alphanumeric() { let text = text.split_at(prev_text.len()); self.prev_index = self.current_index; self.current_index = self.tokens.len(); Ok(Some(text.1.to_string())) } else { Ok(None) } } } ``` We'll also need some auxiliary methods on this struct which we'll call later on when generating our response text. Some comments have been added below to show what these are used for: ```rust impl TokenOutputStream { // resets self-state so as not to del fn clear(&mut self) { self.tokens.clear(); self.prev_index = 0; self.current_index = 0; } // get access to inner tokenizer as a reference pub fn tokenizer(&self) -> &tokenizers::Tokenizer { &self.tokenizer } // uses get_vocab() to get a hashmap of tokens to indexes // then grabs the index value with .get() pub fn get_token(&self, token_s: &str) -> Option { self.tokenizer.get_vocab(true).get(token_s).copied() } } ``` ### Generating text from tokens As the final piece of the puzzle, we'll create a struct for generating text from our tokens. This is arguably the most important part. There is some terminology here that you may want to be acquainted here before we continue, as otherwise you may be confused: - `temp` (or temperature) - lets the model know how accurate we want it to be. The lower the temperature, the less likely the model will hallucinate. - `top_k` - This argument allows the model to only sample from the "top K tokens", then samples based on probability. A lower K value makes the model more predictable and consistent. - `top_p` - This allows the model to choose from a subset of tokens whose combined probability reaches or exceeds a threshold `p`. This allows you to create diverse responses while still having relevant context. - `repeat_penalty` - We can decide how much we want to punish repetitive or redundant output here. - `repeat_last_n` - This allows us to choose the size of the context window that we want for our repeat penalty. Note that a token can be anywhere between 1 to 4 words, so don't make your context window size too large! To start with generating tokens from text, we can declare our text generator below, like so. ```rust struct TextGeneration { model: Mistral, device: Device, tokenizer: TokenOutputStream, logits_processor: LogitsProcessor, repeat_penalty: f32, repeat_last_n: usize, } impl TextGeneration { #[allow(clippy::too_many_arguments)] fn new( model: Mistral, tokenizer: Tokenizer, seed: u64, temp: Option, top_p: Option, _top_k: Option, repeat_penalty: f32, repeat_last_n: usize, device: &Device, ) -> Self { let logits_processor = LogitsProcessor::new(seed, temp, top_p); Self { model, tokenizer: TokenOutputStream::new(tokenizer), logits_processor, repeat_penalty, repeat_last_n, device: device.clone(), } } } ``` Next, we need to write a `run` function to actually run our simple pipeline. This will do the following: - Turn a prompt into encoded tokens - Ensures that the `` token exists (a sort of "end of tokens" marker) - Loops over the sample length, creates a tensor, applies a repeat penalty and attempts to get the next token To start with, let's set up our function - we'll start by clearing the tokenizer of any leftover tokens from previous prompts, then take the prompt and encode it. We then turn the tokens into a vector so that we can process it later on: ```rust impl TextGeneration { fn run(mut self, prompt: String, sample_len: usize) -> Result> { // clear the tokenizer of any previous input here self.tokenizer.clear(); let mut tokens = self.tokenizer .tokenizer() .encode(prompt, true) .unwrap() .get_ids() .to_vec(); println!("Got tokens!"); // .. more code here! } } ``` Next, we need to check whether or not the tokenizer has a `` token - this signals the end of the output. If this isn't present, the model might try to run infinitely! Definitely not good. Let's add it in: ```rust let eos_token = match self.tokenizer.get_token("") { Some(token) => token, None => panic!("cannot find the token"), }; ``` The next step is to go enumerate through from a range of 0 to `sample_len`, get the correct token from the start position of where we want to process the tokens from. We then create a new `Tensor` and unsqueeze it. This adds an additional dimension to the tensor and allows tensor multiplication. ```rust let mut string = String::new(); for index in 0..sample_len { let context_size = if index > 0 { 1 } else { tokens.len() }; let start_pos = tokens.len().saturating_sub(context_size); let ctxt = &tokens[start_pos..]; let input = Tensor::new(ctxt, &self.device).unwrap().unsqueeze(0).unwrap(); // more code to come here } ``` We then perform a forward pass to get the value of the output layer from the input data. In a neural network, this would mean traversing through all of the nodes from first to last and doing a calculation based on the output. This allows us to then compute the `logits` (outputs of a neural network pre-activation function). ```rust let logits = self.model.forward(&input, start_pos).unwrap(); ``` Next, we squeeze the logits. This decreases the number of dimensions in a tensor and allows us to grab the value we want. However, if the repeat penalty is over `1.0`, we need to make sure to apply it! (Note here that we haven't accounted for repeat penalty values under `1.0`). ```rust let logits = logits.squeeze(0).unwrap().squeeze(0).unwrap().to_dtype(DType::F32).unwrap(); let logits = if self.repeat_penalty == 1.0 { logits } else { let start_at = tokens.len().saturating_sub(self.repeat_last_n); candle_transformers::utils ::apply_repeat_penalty(&logits, self.repeat_penalty, &tokens[start_at..]) .unwrap() }; ``` We then get our next token by sampling the logits, pushing it to our tokens list and checking if it's the end-of-input token: if it is, break the loop immediately. If not, we add our converted String to the final output! Then we return the output. ```rust fn run(mut self, prompt: String, sample_len: usize) -> Result> { // .. previous code here for index in 0..sample_len { let next_token = self.logits_processor.sample(&logits).unwrap(); tokens.push(next_token); if next_token == eos_token { break; } if let Some(t) = self.tokenizer.next_token(next_token).unwrap() { println!("Found a token!"); string.push_str(&t); } } string } ``` This is quite a long function - if you get stuck, you can find the final code here: ```rust impl TextGeneration { fn run(mut self, prompt: String, sample_len: usize) -> Result> { self.tokenizer.clear(); let mut tokens = self .tokenizer .tokenizer() .encode(prompt, true) .unwrap() .get_ids() .to_vec(); println!("Got tokens!"); let eos_token = match self.tokenizer.get_token("") { Some(token) => token, None => panic!("cannot find the token"), }; let mut string = String::new(); for index in 0..sample_len { let context_size = if index > 0 { 1 } else { tokens.len() }; let start_pos = tokens.len().saturating_sub(context_size); let ctxt = &tokens[start_pos..]; let input = Tensor::new(ctxt, &self.device).unwrap().unsqueeze(0).unwrap(); let logits = self.model.forward(&input, start_pos).unwrap(); let logits = logits.squeeze(0).unwrap().squeeze(0).unwrap().to_dtype(DType::F32).unwrap(); let logits = if self.repeat_penalty == 1. { logits } else { let start_at = tokens.len().saturating_sub(self.repeat_last_n); candle_transformers::utils::apply_repeat_penalty( &logits, self.repeat_penalty, &tokens[start_at..], ).unwrap() }; let next_token = self.logits_processor.sample(&logits).unwrap(); tokens.push(next_token); if next_token == eos_token { break; } if let Some(t) = self.tokenizer.next_token(next_token).unwrap() { println!("Found a token!"); string.push_str(&t); } }; Ok(string) } } ``` ## Using Candle with a web service Using the previous functions we've made, we can create a web service using `axum` and `tokio` with an endpoint to run our prompt! No local work required. To install Axum and Tokio you can use this shell snippet: ```bash cargo add axum tokio -F tokio/macros,tokio/rt-multi-thread ``` To get started, we'll create an `AppState` that implements `Clone` and some helper methods to shorten down code in application-related functions: ```rust #[derive(Clone)] pub struct AppState { model: Mistral, device: Device, tokenizer: Tokenizer } impl From<(Mistral, Device, Tokenizer)> for AppState { fn from(e: (Mistral, Device, Tokenizer)) -> Self { Self { model: e.0, device: e.1, tokenizer: e.2 } } } impl From for TextGeneration { fn from(e: AppState) -> Self { Self::new( e.model, e.tokenizer, 299792458, // seed RNG Some(0.), // temperature None, // top_p - Nucleus sampling probability stuff None, // Only sample along the top K samples 1.1, // repeat penalty 64, // context size to consider for the repeat penalty &e.device, ) } } ``` Next, we'll create a `Prompt` struct that can be deserialized from JSON, then an Axum function endpoint: ```rust #[derive(Deserialize)] pub struct Prompt { prompt: String } async fn run_pipeline( State(state): State, Json(Prompt{prompt}): Json ) -> impl IntoResponse { let textgen = TextGeneration::from(state); textgen.run(prompt, 5).unwrap() } ``` Then we add the endpoint to our main function and it's done: ```rust #[tokio::main] async fn main() -> Result<()> { let Ok(api_token) = std::env::var("HF_TOKEN") else { return Err(anyhow::anyhow!("Error getting HF_TOKEN env var")) }; let state = initialise_model(api_token)?; let router = Router::new() .route("/", get(hello_world)) .route("/prompt", post(run_pipeline)) .with_state(state); let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap(); axum::serve(tcp_listener, router).await.unwrap(); Ok(()) } ``` ### Performance considerations Of course, when you try to run this locally you may notice that CPU performance is somewhat sub-optimal compared to using a high-performance GPU. Generally speaking, while optimal performance can be attained on a GPU, there are times where this is infeasible. GPU usage is gated by the use of the "CUDA" - Compute Unified Device Architecture - toolkit (and is a feature flag of `candle_core`). However, this would be difficult to use on the web unless you can have CUDA preinstalled. Nevertheless, it unlocks performance enhancing features outside of just your GPU. For example, Flash Attention, a technique that improves inference speeds with better parallelism and work partitioning. You can find the Arvix paper [here.](https://tridao.me/publications/flash2/flash2.pdf) This is even more noticeable when you're running in debug mode. To alleviate this, you can run in release mode using `shuttle run --release` (or without Shuttle: `cargo run --release`). This will run the release profile of your application, which is much more optimised. ## Finishing up Thanks for reading! Huggingface makes it much easier to deploy your own and other peoples' models for absolutely free (disregarding electricity usage). Taking advantage of AI is becoming easier than ever! --- # AI Agents: Building AI Primitives with Rust Source: https://www.shuttle.dev/blog/2024/04/30/building-ai-agents-rust Date: 30 April 2024 Author: ian Tags: rust, ai, guide Building AI agents with OpenAI and Rust Remember The Matrix? Yes, that Matrix, the one with leather trenchcoats, bullet-time and simulated steaks. It was something special at the time - and it also had one of the coolest representations of a computer virus: the infamous Agent Smith, the self-replicating virus embodying the consciousness of a single agent, duplicating itself over and over again, chasing our heroes across the simulation. His superpower lied in his ability to create new instances of himself at any moment, creating and dispatching them to do whatever task needed to be done. And in this new era of Generative AI, there is one pattern that has consistently proved itself as a core primitive of the LLM world - the Agent. If you haven't heard of the Agent pattern yet, you can think of it just like you'd think of Agent Smith - it's an embodied instance of your LLM - specialising in whatever task you need it to perform, be it writing code, translating inputs, invoking functions or writing haikus about Rust. The Agent pattern allows us to perform more accurate LLM calls, parallelise work and even more important - orchestrate agents by letting one "main" agent dispatch work to others. So let's take a dive and look at how we can harness the power of LLMs and build agents of our own using Rust. ## Why build them in Rust? You mean except the speed, efficiency, portability? Because when developing with agents - especially letting them run functions and code on our machines - we want to make sure we're running it as safe as possible. The memory safety provided by Rust can work as a nice isolation to common attacks, leaving our services safe from any potential exploits via code injection (And of course, if the AI breaks out of the box, having it sandboxed inside a Rust program might just prevent the AI-pocalypse from happening). But besides safety, the error handling and pattern matching abilities Rust provides can ensure that our code works correctly, which when working with AI agents is quite important. As LLMs can often hallucinate and make mistakes, catching them on time and handling them in a proper manner is extremely important for any production usecase - _you don't want your users to get wrong data, or even worse - someone else's data_. For such things, Rust's powerful error handling really comes handy, allowing us to build safety latches into our product easily. All of this, when combined with the powerful trait & macro abilities, allows us to create and combine our abstractions safely and enabling us to scale our agents into complex networks with ease. When building real-life usecases that depend on multiple agents working together in sync, you'll find that the number of agents can grow quite fast, especially when adding different evaulation or format translation agents, making it of paramount importance to choose a language that enables you to easily build strong abstractions. ## Agents 101 While Agents might sound like some super complex thing, especially with multiple frameworks and libraries offering similar abstractions, building them is quite simple - they usually consist of a few things: - The System message - system message is in a way the "core" of your agent, defining its purpose and behaviour. It primes your LLM with instructions on how to behave and answer to your messages. - The model definition - usually agents also contain reference to the model used, allowing us to switch between more powerful models for accuracy or smaller and faster models for efficiency. - The supported functions - especially useful if you want to allow your agents to invoke functions from your code, using tools like OpenAPI's function calling to invoke different functions depending on the data provided. So let's create some agents of our own. Start up your favorite editor, create a Rust project and let's go and define a simple `Agent` struct that will take care of these things for us: ```rust pub(crate) struct Agent { pub(crate) system: String, pub(crate) model: String, } ``` Now, we also need a way to actually use our agents, so let's add some extras to the struct. First off, we need to be able to prompt the agent and give it a task to perform. For this, we'll create a `prompt` method that simply takes in a string and returns a string. So let's create our prompt function: ```rust impl Agent { async fn prompt(&mut self, input: String) -> Result } ``` Usually, to make these agents even more useful, we'd also want them to be able to remember their conversation history, allowing us to build some kind of context through the conversation. So let's also add a `history` field that will contain the list of messages. Messages can be created either by the agent itself or by the user, so we'll create an enum to differentiate between them - we'll also add an enum for the `System` message, since that is the first message all agents will receive. To make them future-proof, we'll also expose the `history` to the outside world, so we can imbue our agents with predetermined messages or fork new agents from existing ones. While for our example usecase this won't be relevant, in larger and more complex usecases you will probably encounter the need to use message history to remember the context of the previous executions. So let's add those bits of code too: ```rust pub(crate) enum Role { AGENT, USER, SYSTEM } pub(crate) struct Message { pub(crate) content: String, pub(crate) role: Role } pub(crate) struct Agent { pub(crate) system: String, pub(crate) model: String, pub(crate) history: Vec } ``` And that is it - the basic building blocks for our agents are in place. Now, how to actually use them? ## Defining the agent's purpose To actually use our agents, we need to give them a purpose, otherwise they will only be acting like generic LLM models - okay, but not great. By giving them a proper purpose through the system message, we prime them to act a certain way, giving us increased accuracy on tasks. So for this article, we'll make two simple agents that can be used both together or independently: - First, we'll create one Agent that will receive a YouTube video and summarise the main talking points for us. - Then, we'll create another agent that will take that summary and actually make it usable, by translating it to JSON we'll consume inside our code. ![Diagram of how AI agents work](/images/blog/ai-agent-diagram.png) Now, you might be wondering why use two separate agents when this could be rolled into one? Well, while LLM's are quite capable, they're also quite fickle machines - their outputs are quite non-deterministic, so every time we provide the same input, we might get a different output. By trying to squeeze both tasks into a single agent, we increase the chance of having erroneous, inaccurate and unwanted results. For example, if we can achieve 80% accuracy on one task, then accuracy for two tasks could fall to 64% (80% x 80%) - but since these are LLM's, the accuracy can fall even more due to the first task priming the LLM into a specific local minima, i.e. it being more focused on the first task. That is why when developing with LLM agents, the best philosophy to follow is the UNIX philosophy: > Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface. Incredibly, this nearly 50-year old philosophy serves us well even today and fits perfectly with the LLM world. So let's create our two agents by following it. First, we'll create our Summarising agent. To do that, we need to implement our Agent trait. For this, we'll be using the most popular of the LLM providers - OpenAI - and we'll access it using the `async-openai` crate, giving us some simple abstractions over it. So pop open your terminal and add the dependency: `cargo add async-openai` To be able to use the `async-openai` crate, we also need an API key from OpenAI - you can find it [here](https://platform.openai.com/api-keys). To use it, just add it to your environment variables or open your terminal and hit: `export OPENAI_API_KEY='your-key-here'` Now, let's create an implementation of the prompt method - it's quite simple, we'll just add the OpenAI `Client` to our agent and map the prompt method to the `async_openai`'s prompt: ```rust pub(crate) struct Agent { pub(crate) system: String, pub(crate) model: String, pub(crate) history: Vec, pub(crate) client: Client } impl Agent { pub(crate) async fn prompt(&mut self, input: String) -> Result { //If you are remembering the message history, you can restore it from here. self.client.chat().create( CreateChatCompletionRequestArgs::default() .model(self.model.clone()) .messages(vec![ //First we add the system message to define what the Agent does ChatCompletionRequestMessage::System( ChatCompletionRequestSystemMessageArgs::default() .content(&self.system) .build() .unwrap(), ), //Then we add our prompt ChatCompletionRequestMessage::User( ChatCompletionRequestUserMessageArgs::default() .content(input) .build() .unwrap(), ), ]) .build() .unwrap(), ).await.map(|res| { //We extract the first one res.choices[0].message.content.clone().unwrap() }) //Now here, you can save the prompt and agent response to the history if needed } } ``` With that out of the way, we can create the actual first instance of our agent by defining its system prompt and passing it in. So let's write down some instructions that will go in the system prompt. ```rust static SUMMARY_PROMPT : &str = r#"You are an agent dedicated to summarising video transcripts. You will receive a transcript and answer with main talking points of the video first, followed by a complete summary of the transcript. Answer only in this format: Talking points: 1. .. 2. .. N. .. Summary: Summary of the transcript "# ``` But first - we need the transcript. To fetch the transcript itself, we'll be using the `youtube-captions` crate. While getting the transcript can be quite straightforward using the API, we'll do it the lazy way and let the crate do the job for us. To do the HTTP request, we'll also need the famous `reqwest` crate, together with `serde` and `serde-json` to deserialize the JSON into a struct. So head on to your Cargo.toml or terminal and add the crates: ```toml serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" youtube-captions = "0.1.0" reqwest = "0.11" ``` To keep it nice and tidy, let's create a `get_transcript(video: &str) -> String` method. First, we'll use the `youtube-captions` scraper to get the captions: ```rust async fn get_transcript(video: &str) -> String { let digest = DigestScraper::new(reqwest::Client::new()); // Fetch the video let scraped = digest.fetch(video, "en").await.unwrap(); // Find our preferred language - in this case, english let language = LanguageTag::parse("en").unwrap(); let captions = scraped.captions.into_iter() .find(|caption| language.matches(&caption.lang_tag)) .unwrap(); let transcript_json = captions.fetch(Format::JSON3).await.unwrap(); } ``` Since youtube doesn't return us the pure caption text but JSON, we'll need to deserialize the captions. To do that we need some structs matching the format - one for the whole transcript, one for the video `events` and one for the captioned `segment` itself, which will contain the captioned UTF-8. So let's make some and mark them with serde's `Deserialize` macro: ```rust #[derive(Deserialize)] struct Transcript { events: Vec, } #[derive(Deserialize)] struct Event { segs: Option>, } #[derive(Deserialize)] struct Segment { utf8: String, } ``` With this, we can deserialize the received object and extract all the captions, joining them together into one giant string. So open up the `get_transcript` method and let's flatten them all together: ```rust async fn get_transcript(video: &str) -> String { // ... // after fetching the transcript let root: Transcript = serde_json::from_str(transcript_json.as_str()).unwrap(); // Collect all utf8 fields from all events and all segments let transcript: String = root.events.iter() .filter_map(|event| event.segs.as_ref()) .flatten() .map(|segment| segment.utf8.clone()) // Extract the utf8 field of each segment .collect::>() .join(" "); return transcript } ``` Now, we're ready to build our agents and put them to work! Let's create a `summarize_video` function that will wrap it all up - take in the video ID and return us a nice summary of the text. First, we'll use the `get_transcript` method to get the video's transcript, then we'll pass it on to our agent to summarise it for us. ```rust async fn summarize_video(video: &str) -> String { let client = Client::with_config( OpenAIConfig::default(), ); //First, we fetch the transcript for the video let transcript = get_transcript(video).await; // Then we create our summary agent and have it summarize the video for us let mut summarize_agent = Agent { system: SUMMARY_PROMPT.to_string(), model: "gpt-4".to_string(), history: vec![], client: client.clone(), }; let summary = summarize_agent.prompt(transcript).await.unwrap(); } ``` Next, we'll create another agent - one that will take the summary and translate it into the JSON format of our choice. So let's write a new system message for our Agent. To make sure it follows a specific format, it's best to include the format example and some basic rules: ```rust static SUMMARY_TO_JSON_PROMPT = r#"You are an agent dedicated to translating text to JSON. You will receive the text and return it in JSON format. The format is as follows: { "summary": "Whole video summary goes here", "talking_points": [ { "title" : "Title of the point", "description: "Talking point summary" }, ... ] } Rules: - Follow the specified JSON format closely - Wrap the JSON in a code block - Skip prose, return only the JSON "# ``` And now we can wrap it all together: ```rust //In summarize_video ... let mut summary_to_json_agent = Agent { system: SUMMARY_TO_JSON_PROMPT.to_string(), client: client.clone(), model: "gpt-4".to_string(), history: vec![], }; let json = summary_to_json_agent.prompt(summary).await.unwrap(); return json; ``` But hold on, we're not done yet - here's the thing - since LLM's can be fickle, we cannot be certain that the output will contain only the JSON, so we'll add an extra utility method that will extract the codeblock from the response. ````rust fn extract_codeblock(text: &str) -> String { if !text.contains("```") { return text.to_string(); } let mut in_codeblock = false; let mut extracted_lines = vec![]; for line in text.lines() { if line.trim().starts_with("```") { in_codeblock = !in_codeblock; continue; } if in_codeblock { extracted_lines.push(line); } } extracted_lines.join("\n") } ```` We can use this on the received response and extract the codeblock if it exists - otherwise, it will return the whole text. ```rust //... let json = summary_to_json_agent.prompt(summary).await.unwrap(); let result = extract_codeblock(&json); return result ``` ## Bringing the agents live Finally, we're ready to use our agents. But uhhh wait, how do we actually use them? If we plan to use them from a frontend, we'll need to create an API around that. Luckily, we can use [Shuttle and Axum together](https://docs.shuttle.dev/examples/axum) to get an API up and running quickly. First, you need to [install Shuttle](https://docs.shuttle.dev/getting-started/installation) - just hit `cargo binstall cargo-shuttle` and it should install in no time. So let's open up that Cargo.toml and add the required dependencies - we'll add `axum` and `shuttle-axum` to build the server, `tokio` for async work and the `shuttle-runtime` to run in Shuttle's cloud. ``` axum = "0.7.3" shuttle-axum = "0.44.0" shuttle-runtime = "0.44.0" tokio = "1.28.2" ``` Now, pop open the main.rs and let's create a simple server we can deploy to Shuttle: ```rust async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new(); Ok(router.into()) } ``` Yes, that's really everything you need to start & deploy a server - quite amazing, isn't it? Now, let's add to the router an endpoint that will wrap it all together. We'll keep it simple, passing in the video ID through the path itself - so you can replace the `youtube.com/watch?v=VIDEO_ID` url with just `ourservice.com/VIDEO_ID`, keeping the API simple and concise. With Axum, adding endpoints is super easy - we just define the endpoint path and write the function. For this one, we'll use a `GET /:video_id`and pass it in the video ID in the path itself. So let's add a function that will get this path and invoke our `summarize_video`: ```rust async fn summarize_endpoint(Path(video): Path) -> String { let summary = summarize_video(video.as_str()).await; return summary; } ``` And add it to our Axum configuration: ```rust #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/:video", get(summarize_endpoint)); Ok(router.into()) } ``` With this we can finally deploy - just hit `cargo shuttle project start && cargo shuttle deploy --ad`, grab a coffee and you'll find your agents spinning in Shuttle's cloud, ready to be used. So let's test it out - we'll use the [Shuttle AI announcement video](https://www.youtube.com/watch?v=6sHo-2ddw3U) as an example. For now, we'll keep it simple and just use curl - later, you can also build a frontend for it, using something like [Next.js](https://www.shuttle.dev/blog/2023/03/23/nextjs-and-rust) or [one of Shuttle's starter templates](https://docs.shuttle.dev/templates/overview). So let's bring up your terminal and curl it: `curl "yourprojectname.shuttleapp.rs/6sHo-2ddw3U"` If you've done everything right, you should receive back the summary and key points as a JSON - if you see something about Chateau instead of Shuttle, don't worry, nothing's broken - except Youtube's auto-caption service. Maybe they should rewrite it in Rust? :) But now that you're done, give yourself a high-five - you deserved it - then go build that frontend. And feel free to [share it with us on Discord](https://discord.com/invite/shuttle) - we're eager to see what you'll build! --- # Building your first AI tool in Rust Source: https://www.shuttle.dev/blog/2024/04/29/building-your-first-ai-tool-rust Date: 29 April 2024 Author: ivan Tags: rust, ai, guide Writing a simple AI helper with Rust using llm-chain In this tutorial, we'll build a command-line application in Rust that can analyze data from a CSV file. The app will prompt the user to enter a question, and it will provide an answer based on the data in the CSV file. At the end, we'll be able to ask our helper various questions, and get various answers! Here's a short video that shows its powers: ![Video demo GIF](/images/blog/your-first-ai-tool-demo.gif) The full code of the Rust AI helper can be found [here.](https://github.com/joshua-mo-143/shuttle-blog-tutorials/tree/main/shuttle-first-ai-tool) ## Prerequisites Before we get started, make sure you have the following installed: - Rust (you can install it from [rustup.rs](https://rustup.rs)) - The `csv` and `llm_chain` crates (we'll install these later) You'll also need an OpenAI API key which you can [get here](https://platform.openai.com/api-keys). ## Setting up the Project First, create a new Rust project: ```bash cargo new data-analysis-app cd data-analysis-app ``` Next, open the `Cargo.toml` file and add the required dependencies: ```toml [dependencies] csv = "1.1" llm-chain = "0.13.0" llm-chain-openai = "0.13.0" tokio = { version = "1.25.0", features = ["macros", "rt-multi-thread"] } ``` These lines add the required dependencies for our project: `csv` for reading CSV files, [`llm_chain`](https://github.com/sobelio/llm-chain) for natural language processing, and [`tokio`](https://tokio.rs) for async runtime functionality. We also use `llm-chain-openai` (for integration with the OpenAI API). ## What is llm-chain? Here is the description from their [official GitHub repository](https://github.com/sobelio/llm-chain): > llm-chain is a collection of Rust crates designed to help you create advanced LLM applications such as chatbots, agents, and more. `llm-chain` is effectively an LLM orchestration library that allows you to "chain" prompts together, executing one after the other - with extra features: - Templates are supported, allowing you to not need to manually chain steps - We can carry out complex tasks that LLMs cannot handle in a single step - `llm-chain` also supports vector storage, allowing long term memory and subject matter knowledge. This benefits us in several ways: - Being able to chain steps allows us to get much closer to a useful output, rather than only being able to use one step. For example, you may want several pre-instruction prompts to set up a context for your application. - By operating on data in steps, we can get much better insight from our data. LLM orchestration is a crucial tool for enterprise AI, as business use cases often require advanced contexts. ## Diving into it Let's update our `main.rs` file by importing all the neccessary crates we will need. I've added comments to the snippet below that describe what each crate/module is used for. ```rust use std::env; use std::error::Error; use std::fs::File; use std::io::{self, Write}; use csv::Reader; use llm_chain::{executor, parameters, prompt, step::Step}; ``` Next, we will mark our `main` function with the `async` keyword and add the `#[tokio::main]` macro right on-top of it. This allows us to use the Tokio asynchronous runtime for async functionality and automatic polling of futures (values that have may or may not finished work). P.S. If you'd like to learn more about Async in Rust, you can check out our [Async Rust in a Nutshell](https://www.shuttle.dev/blog/2024/02/29/async-rust) article! ```rust #[tokio::main] async fn main() -> Result<(), Box> { // ... } ``` The return type is set to `Result<(), Box>` to enable error propagation with `?` and overall error handling across different error types. ### Adding our API Key Find your API key from OpenAI and add it to your current shell session's environment variables: `export OPENAI_API_KEY=your_api_key_here` Make sure to replace `your_api_key_here` with your actual API key which you can [get here](https://platform.openai.com/api-keys). Now when we use our program, we won't have to worry about storing our environment variable anywhere in the program! ### Creating an Executor We create an executor instance using the `executor!` macro from the `llm_chain` crate. The macro makes it easy for us to create a new executor for a specific model without having to directly call the constructor functions of the respective executor structs. In short; it allows you to call an LLM with a pre-defined input and output, using multiple steps to refine the output. ```rust let exec = executor!()?; ``` ### Reading the CSV file For this example, we'll be reading data from a CSV file because we are building a data-helper tool which will allows us to ask it various questions regarding the data at-hand. The snippet below opens a CSV file named "data.csv" in the root folder and reads its contents into a string variable `csv_data`, where each row is represented as a comma-separated string with a newline character at the end. It also uses the `csv` crate to handle the CSV parsing. In short, it makes sure that we can the `csv` data for further actions. ```rust let file = File::open("data.csv")?; let mut reader = Reader::from_reader(file); let mut csv_data = String::new(); for result in reader.records() { let record = result?; csv_data.push_str(&record.iter().collect::>().join(",")); csv_data.push('\n'); } ``` The contents of the CSV file (make sure to create a `data.csv` file in your root directory and copy-paste the contents below): ```csv Name,Age,Occupation,City,FavoriteSport,AnnualIncome Samantha,28,Entrepreneur,New York,Skydiving,$120000 Michael,35,Software Engineer,San Francisco,Rock Climbing,$95000 Emily,42,Chef,Chicago,Surfing,$65000 David,25,Artist,Los Angeles,Parkour,$30000 Sophia,31,Pilot,Miami,Bungee Jumping,$85000 Daniel,47,Doctor,Boston,Snowboarding,$180000 Olivia,22,Student,Seattle,Skateboarding,$12000 William,39,Marketing Manager,Austin,Mountain Biking,$110000 Ava,27,Photographer,Portland,Kayaking,$45000 Jacob,33,Teacher,Denver,Hiking,$55000 Isabella,40,Lawyer,Washington D.C.,Scuba Diving,$200000 Ethan,29,Musician,Nashville,Bouldering,$25000 Mia,36,Graphic Designer,Atlanta,Skiing,$75000 Benjamin,44,Engineer,Houston,Surfing,$125000 Abigail,23,Writer,Minneapolis,Rock Climbing,$18000 ``` Example of what it looks like as a table: | Name | Age | Occupation | City | FavoriteSport | AnnualIncome | | -------- | --- | ----------------- | ------------- | ------------- | ------------ | | Samantha | 28 | Entrepreneur | New York | Skydiving | $120000 | | Michael | 35 | Software Engineer | San Francisco | Rock Climbing | $95000 | | Emily | 42 | Chef | Chicago | Surfing | $65000 | | David | 25 | Artist | Los Angeles | Parkour | $30000 | ### Creating the user input loop The user input loop is the loop which the user uses to continuosly ask questions to our helper. The next couple of sections are all happening within this loop -- prompting, executing, outputting the result, etcetera. To start thing off; we'll be asking the user to enter their question and when the user is done with asking questions, they can type in `quit` to exit the helper. ```rust loop { println!("Enter your prompt (or 'quit' to exit):"); io::stdout().flush()?; let mut user_prompt = String::new(); io::stdin().read_line(&mut user_prompt)?; user_prompt = user_prompt.trim().to_string(); if user_prompt.to_lowercase() == "quit" { break; } // ... } ``` ### Setting the prompt Now, we'll create a prompt string that includes the user's question and the CSV data. This prompt will be used by the `llm_chain` crate to generate a response. > 💡 **TIP**: When defining prompts, be clear and concise about the task you want the language model to perform. Provide any necessary context or input data (like the CSV example) and be specific about the desired output (eg, a summary, analysis, code, or text generation). ```rust let prompt_string = format!( "You are a data analyst tasked with analyzing a CSV file containing information about individuals, including their name, age, occupation, city, favorite sport, and annual income. Your goal is to provide clear and concise answers to the given questions based on the data provided. Question: {}\n\nCSV Data:\n{}", user_prompt, csv_data ); ``` ### Creating a Step instance We create a Step instance from the `llm_chain` crate, passing in the prompt string we created earlier. Steps are individual LLM invocations in a chain. They are a combination of a prompt and a configuration and we use them to set the per-invocation setting for a prompt. This comes in very handy when we want to change the settings for a specific prompt in a chain. ```rust let step = Step::for_prompt_template(prompt!("{}", &prompt_string)); ``` ### Connecting everything together We run the analysis by calling the `run` method on the `Step` instance, passing in the parameters and the executor we created earlier. ```rust let res = step.run(¶meters!(), &exec).await?; ``` ### Outputting the result This one is as simple as it gets; we invoke the `println!()` macro print the result of the analysis to the console. ```rust println!("{}", res.to_immediate().await?.as_content()); ``` That's the end of our loop and now we need to wrap it all up by adding `Ok(())` at the end of the main function to indicate that the function executed successfully without any errors. ### Running our App To run the app, navigate to the project directory and execute `cargo run`. You should see the following message in your terminal: ```bash Enter your prompt (or 'quit'` to exit): ``` Enter your question related to the data in the data2.csv file, and the app will provide a concise answer based on the analysis. Here are some CSV-specific questions you can ask your helper: 1. "Who has the highest annual income, and what is their occupation?" 2. "What is the most popular extreme sport among the individuals in the data?" 3. "Which city has the most individuals represented in the data?" 4. "What is the average age of the individuals whose favorite sport is rock climbing?" 5. "Which occupation has the highest average annual income?" ### Challenge Sharing a CLI app with your friends or team mates might not be too straight-forward. As a challenge, try building an API around your helper with endpoints that'll allow users to interact with it. Next, build a simple frontend with a small prompt box that users can use to ask your helper various questions, it should communicate with your API. And finally, hook it all up together and try deploying it with Shuttle! P.S. Tweet `#shuttleai` when you are done and we'll check out and share your creations! ## Summary In this tutorial, we built a command-line application in Rust that can analyze data from a CSV file. The app uses the `csv` and `llm_chain` crates to read the CSV data and generate a response based on the user's question. If you are up for another challenge; try making the data source dynamic, allowing users to upload their own `csv`, or even better, other file formats such as `pdf` or `json`! Read more: - [Creating a RAG-assisted web service with Axum](https://www.shuttle.dev/blog/2024/02/28/rag-llm-rust) - [More than you've ever wanted to know about error handling](https://www.shuttle.dev/blog/2022/06/30/error-handling) --- # Event driven Microservices using Kafka and Rust Source: https://www.shuttle.dev/blog/2024/04/25/event-driven-services-using-kafka-rust Date: 25 April 2024 Author: josh Tags: rust, kafka, guide Building an event-driven microservice using Apache Kafka and Rust When it comes to real-time data handling, there are certain challenges that are imposed upon regular HTTP-based services. Resiliency, consistency and reliability are all qualities that a production-grade system handling real-time data processing should have. Event driven architecture and microservices is one way to become more resilient against this kind of issue. In this post, we'll take a deep dive into writing an event driven microservice and how we can use Change Data Capture to assist with taking functionality from a monolith and putting it in a microservice. Interested in deploying or got stuck somewhere in the article? You can find the final repository [here.](https://github.com/joshua-mo-143/kafka-shuttle) ## What does "event-driven" mean? The term "event-driven" simply means that the web service is not driven by HTTP requests, but consumes events from event sources and executes logic based on the event type. This is in contrast to HTTP request based interactions. In practicality, events may be driven by a message queue (like AMQP) or event store like Kafka where messages are sent and received by one or more services. Event-driven microservices are often used as part of an event-driven architecture, where the whole system is based on passing of published messages and using them as a source of truth. The advantages of event-driven architecture are clear: - Ideal for handling real time data in large quantities - Because event driven services don't rely on synchronous HTTP calls, instead using events, there is less chance of a cascading failure across your architecture if something fails. - Using a message broker promotes loose component coupling, meaning you are not required to know the specific implementation of how your component works with other components. Of course, with advantages there are also new challenges that need to be overcome: - How do you deal with out-of-order messages? - How do you migrate functionality from a monolith to an event-driven microservice? - How do you test that your system actually works? We'll be diving into all this and more in this article! ## Getting started ### Pre-requisites If you want to connect locally to a Kafka instance, you'll either need Apache Kafka and Zookeeper with both running, or Docker installed so that you can spin up Kafka and Zookeeper instances.. If you want to deploy this service to the web somewhere, you'll also want to make use of a service that provides Kafka. Shuttle currently doesn't provide it, but you can make use of services like Upstash to be able to provide it. You'll need to store your environment variables in the `Secrets.toml` file as described later on. ### Project setup To get started, we'll want to make a new project using `shuttle init` - don't forget to select Axum as the framework! You will need `cargo-shuttle` installed for this. If you don't have it installed, you can use `cargo install cargo-shuttle`. We'll then want to use the following shell snippet to install our project dependencies: ```rust cargo add serde@1.0.198 -F derive cargo add serde-json@1.0.116 cargo add shuttle-service cargo add shuttle-shared-db -F postgres,sqlx cargo add sqlx -F runtime-tokio-rustls,postgres,macros cargo add thiserror@1.0.59 cargo add rdkafka@0.36.2 -F cmake-build cargo add futures@0.3.30 cargo add pretty_env_logger@0.5.0 --dev cargo add testcontainers@0.15.0 --dev cargo add testcontainers-modules@0.3.7 --dev -F kafka ``` Then we'll add our macro annotations to our application: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool, #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { // your function code here } ``` With only three lines, we've added a database (provisioned locally by Docker and provisioned by Shuttle servers in deployment!), our Secrets file and deployment metadata to the application! We will also be using `sqlx` to handle our database migrations and queries. This means we'll also want to use `sqlx-cli` to help manage our migrations. We'll get started by installing it (the following command adds all features): ```rust cargo install sqlx-cli ``` Next, you can use `sqlx migrate add init` to generate a new migration file located in the `migrations` folder (a subfolder of your root project). We'll generate a simple table: ```sql -- Add migration script here create table if not exists messages ( message_id int primary key, name varchar not null, message varchar not null, last_updated date not null default current_date ); ``` We'll be expanding on this later, but for the basics this is all we need. We'll also be making use of SQLx's macros to enable type-checked queries. For this, we'll need to spin up a database, use `sqlx migrate run` to run the migrations against the database then use `cargo sqlx prepare` to generate our `.sqlx` folder. Once this folder gets checked into version control, we won't need a database anymore when compiling our query macros. Here's a shell snippet you can use to accomplish this quickly (requires Docker to be running): ```rust docker run -d -t -p 8081:5432 --name kafka-shuttle-pg postgres sqlx migrate run --database-url postgres://postgres:postgres@localhost:8081/postgres DATABASE_URL=postgres://postgres:postgres@localhost:8081/postgres cargo sqlx prepare docker rm -f kafka-shuttle-pg ``` You'll want to make sure to keep your Kafka endpoint URL somewhere, as we'll be putting this in a `Secrets.toml` file: ```rust KAFKA_URL = "" ``` Because we're deploying via Shuttle, we will be using Secrets rather than raw environment variables. When storing our environment variables later, you'll want to iterate through the `SecretStore` type like in the snippet below: ```rust secret_store.into_iter().for_each(|x| std::env::set_var(x.0, x.1)); ``` When you deploy or run locally, the secrets will get taken from this file. ### Set up Kafka using Docker A simple Kafka setup can easily be done using Docker Compose: ```yaml # docker-compose.yml version: "3" services: # kafka zookeeper-1: container_name: zookeeper-1 image: zookeeper restart: always ports: - 2181:2181 environment: - ZOOKEEPER_CLIENT_PORT=2181 volumes: - ./config/zookeeper-1/zookeeper.properties:/kafka/config/zookeeper.properties kafka-1: container_name: kafka-1 image: bitnami/kafka restart: on-failure depends_on: - zookeeper-1 ports: - 9092:9092 environment: - KAFKA_ZOOKEEPER_CONNECT=zookeeper-1:2181 - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 - ALLOW_PLAINTEXT_LISTENER=yes - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_AUTO_CREATE_TOPICS_ENABLE=true - KAFKA_CREATE_TOPICS=messages:1:3 healthcheck: test: ["CMD-SHELL", "kafka-topics.sh --bootstrap-server kafka:9092 --list"] interval: 5s timeout: 10s retries: 5 networks: net: name: "net" driver: bridge ``` If you run this `docker-compose.yml` setup, it should automatically spawn a Zookeeper and Kafka instance for you. There are a lot of initial logs created when spawning both instances, so you may want to either create a detached instance (using `-d` flag) or opening a separate terminal window for this. You may find on startup that the topic is not initialised properly, in which case you can use this shell snippet to auto-create a topic in the Docker container: ```bash #!/usr/bin/env sh docker exec kafka-1 /opt/bitnami/kafka/bin/kafka-topics.sh \ --bootstrap-server localhost:9092 --create --if-not-exists --topic messages \ --replication-factor 1 --partitions 1 ``` ## Building ### Error Handling Before we get started, we should think about the kinds of errors we can get from using our application. There are a few that immediately come to mind: - An RDKafka error - An error from using Kafka itself - `serde_json` errors (particularly when serializing and deserializing messages) - Oneshot messages being cancelled (from `futures::channel`) We can represent all of these errors using an enum that uses the `thiserror` crate to easily derive error messages: ```rust #[derive(Debug, thiserror::Error)] pub enum ApiError { #[error("RDKafka error: {0}")] RDKafka(#[from] rdkafka::error::RDKafkaError), #[error("Kafka error: {0}")] Kafka(rdkafka::error::KafkaError), #[error("De/serialization error: {0}")] SerdeJson(#[from] serde_json::Error), #[error("Oneshot message was canceled")] CanceledMessage(#[from] futures::channel::oneshot::Canceled), } ``` Note that while three of our types use the `#[from]` attribute macro to quickly derive the `From` implementation, converting a `KafkaError` into our enum variant is a little bit more tricky. The methods that return this error will normally return the error as a tuple containing both the error and the record where the error occurred. We can thus implement it like this: ```rust impl<'a> From<( rdkafka::error::KafkaError, rdkafka::producer::FutureRecord<'a, str, std::vec::Vec>, )> for ApiError { fn from( e: ( rdkafka::error::KafkaError, rdkafka::producer::FutureRecord<'a, str, std::vec::Vec>, ), ) -> Self { Self::Kafka(e.0) } } impl From<(rdkafka::error::KafkaError, rdkafka::message::OwnedMessage)> for ApiError { fn from(e: (rdkafka::error::KafkaError, rdkafka::message::OwnedMessage)) -> Self { Self::Kafka(e.0) } } ``` To use this error type with our Axum service, we need to implement the `IntoResponse` trait. This trait specifically represents a type that can be turned into a HTTP response. We can do so by pattern matching the enum like so: ```rust impl IntoResponse for ApiError { fn into_response(self) -> Response { let (status, body) = match self { Self::Kafka(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::RDKafka(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::SerdeJson(e) => (StatusCode::BAD_REQUEST, e.to_string()), Self::CanceledMessage(e) => (StatusCode::BAD_REQUEST, e.to_string()), }; (status, body).into_response() } } ``` ### Setting up a producer and consumer To get started with `rdkafka`, we need to create a publisher and a consumer. We can do this with these two functions: ```rust pub fn create_kafka_producer(secrets: &SecretStore) -> FutureProducer { let url = secrets.get("KAFKA_URL").unwrap(); let log_level: FutureProducer = ClientConfig::new() .set("bootstrap.servers", url) .set("message.timeout.ms", "5000") .set("allow.auto.create.topics", "true") .create() .expect("Producer creation error"); log_level } pub fn create_kafka_consumer(secrets: &SecretStore) -> StreamConsumer { let url = secrets.get("KAFKA_URL").unwrap(); ClientConfig::new() .set("group.id", "shuttle-kafka") .set("bootstrap.servers", url) .set("enable.partition.eof", "false") .set("session.timeout.ms", "6000") .set("enable.auto.commit", "true") // only store offset from the consumer .set("enable.auto.offset.store", "false") .set_log_level(RDKafkaLogLevel::Debug) .create() .expect("Consumer creation failed") } ``` These settings may be changed according to your liking. You can find a full list of configuration properties [here.](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md) You may note that above, we've enabled auto commit while only enabling storing offset from the consumer. The reason for this is that it allows us to rely on the underlying Kafka logic to commit regularly while only allowing the consumer to commit a message after it's been fully processed. This enables us to prevent any loss of messages! This is also called **At Least Once** delivery. Note that the producer has permissions to automatically create topics. In production, you may want to remove this and create topics manually. Allowing a producer to create topics freely may result in some unexpected behaviour! Additionally, some hosted Kafka services will require SASL or SSL authentication. You can find more about the dependencies in the `rdkafka-rust` repo [here.](https://github.com/fede1024/rust-rdkafka?tab=readme-ov-file#installation) Note that if you are unable to install dependencies, `rdkafka` also has feature flags for vendored versions of the required dependencies. Next, we'll want to create our `AppState` which will hold the `FutureProducer` created by the `create_kafka_producer` function: ```rust // src/state.rs use crate::kafka; use rdkafka::producer::FutureProducer; use shuttle_runtime::SecretStore; use shuttle_service::Environment; #[derive(Clone)] pub struct AppState { kafka_producer: FutureProducer, } impl AppState { pub fn new(secrets: &SecretStore) -> Self { let kafka_producer = kafka::create_kafka_producer(secrets), Self { kafka_producer } } } impl<'a> AppState { pub fn producer(&'a self) -> &'a FutureProducer { &self.kafka_producer } } ``` To tie this all together, we'll add it to our main function: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool, #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&db).await.unwrap(); let state = AppState::new(&secrets); let rtr = Router::new().route("/", get(hello_world)).with_state(state); Ok(rtr.into()) } ``` ### Using a Kafka producer To use our producer, we'll first need something to send. Let's create a simple struct that will have an action, as well as a message ID, name and the message itself: ```rust use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] struct CustomMessage { name: String, message: String, } #[derive(Debug, Serialize, Deserialize)] pub struct KafkaMessage { action: Action, message_id: i32, data: Option, } #[derive(Debug, Serialize, Deserialize)] enum Action { Create, Update, Delete, } ``` Here we have modeled a message that can take three different forms: A create action, an update action and a delete action. The `data` field has been left as an `Option`, as with the `Delete` action there is no data required. We can then use these in a new Axum handler function endpoint like so: ```rust async fn send_message( State(state): State, Json(message): Json, ) -> Result<&'static str, ApiError> { let msg = serde_json::to_vec(&message)?; let record: FutureRecord> = FutureRecord::to("messages").payload(&msg).key("1"); state.producer().send_result(record)?.await??; tracing::info!("Message sent with data: {message:?}"); Ok("Message sent!") } ``` This is pretty much the only endpoint we need at the moment. We need to add this endpoint to our router like so to use it: ```rust let rtr = Router::new() .route("/", get(hello_world)) .route("/send", post(send_message)) .with_state(state); ``` If you use `shuttle run` to start your application (assuming Kafka is running) and run this curl command: ```bash curl localhost:8000/send -H 'Content-Type: application/json' \ -d '{"action":"Create","message_id":4,"data":{"name":"Josh","message":"Hello world!"}}' ``` You should see "Message sent!" as a response. ### Using a Kafka consumer Next, the important part: receiving our messages! As a basic example, we will spawn a Tokio task to handle this. Here is a short example of how we can use a `StreamConsumer` to subscribe to a channel, then loop while waiting for the message stream to receive a message: ```rust // src/kafka.rs #[tracing::instrument(skip_all)] pub async fn kafka_consumer_task(con: StreamConsumer, db: sqlx::PgPool) { con.subscribe(&["messages"]).expect("Failed to subscribe to topics"); tracing::info!("Starting the consumer loop..."); loop { match con.recv().await { Err(e) => tracing::warn!("Kafka error: {}", e), Ok(m) => { let Some(payload) = m.payload() else { tracing::error!("Could not find a payload :("); continue; }; // here we use `from_slice()` as we initally send it as &[u8] let message: KafkaMessage = match serde_json::from_slice(payload) { Ok(res) => res, Err(e) => { // if there is a deserialization error, print an error // and go to the next loop iteration tracing::error!("Deserialization error: {e}"); continue; } }; // print out our payload tracing::info!("Got payload: {message:?}"); let _ = con .store_offset_from_message(&m) .inspect_err(|e| tracing::warn!("Error while storing offset: {}", e)); } }; } } ``` Currently, we just get a message and do nothing with it. However, in production when your app is communicating with other applications you probably want your consumer to do something! Let's do something with our message payloads by carrying out an SQL query for each action: ```sql use crate::kafka::KafkaMessage; #[tracing::instrument] pub async fn create_message(message: KafkaMessage, db: &sqlx::PgPool) { let _ = sqlx::query!( "INSERT INTO MESSAGES (message_id, name, message) VALUES ($1, $2, $3) ON CONFLICT (message_id) DO NOTHING", message.message_id(), message.data().name(), message.data().message() ) .execute(db) .await .inspect_err(|e| tracing::error!("Error while inserting message: {e}")); } #[tracing::instrument] pub async fn update_message(message: KafkaMessage, db: &sqlx::PgPool) { let _ = sqlx::query!( "UPDATE MESSAGES SET name = $1, message = $2 where message_id = $3", message.data().name(), message.data().message(), message.message_id() ) .execute(db) .await .inspect_err(|e| tracing::error!("Error while updating message: {e}")); } #[tracing::instrument] pub async fn delete_message(message: KafkaMessage, db: &sqlx::PgPool) { let _ = sqlx::query!( "DELETE from messages where message_id = $1", message.message_id() ) .execute(db) .await .inspect_err(|e| tracing::error!("Error while deleting message: {e}")); } ``` Next, we can add a short pattern-matching snippet to our consumer task function: ```rust tracing::info!("Got payload: {message:?}"); match message.action { Action::Create => queries::create_message(message, &db).await, Action::Update => queries::update_message(message, &db).await, Action::Delete => queries::delete_message(message, &db).await, } ``` Now whenever we receive a message, the following should happen: - We attempt to get the message payload - We attempt to deserialize the payload to `KafkaMessage` - Depending on the action, we either create a new record, update an existing record or delete a record ## Beyond the basics ### Introducing Change Data Capture If you're considering migrating from a monolith to a microservice that uses a database, there are probably a few concerns that you have: - How do I maintain data consistency? - How do I deal with out of order messages? To solve your issues, one idea that you can leverage is Change Data Capture (or CDC). CDC typically refers to the tracking of data in a data source - for example, a database or data warehouse - so it can be captured in destination systems. CDC is useful because it allows us to track changes across a database reliably: for example, let's say you have a record that was updated, and then another event of the same type is sent and the same field gets updated again. We can track these changes such that we'll know which version of the record the field should actually be updated to! How you can do this typically depends on the database that you're using. Since we're using Postgres in our application, we'll look at a great way you can carry out Change Data Capture using Postgres using triggers. ### Postgres Trigger-based Change Data Capture Given our current Postgres migrations, we can also create a table for CDC logs that holds a generic row ID, the message ID, operation type, timestamp, pre-operation values and post-operation values: ```sql create table if not exists messages_cdc ( cdc_id SERIAL PRIMARY KEY, message_id INT, operation_type VARCHAR(10), timestamp TIMESTAMP, name_before VARCHAR, message_before VARCHAR, name_after VARCHAR, message_after VARCHAR ); ``` Next, we'll need to create an SQL function that returns a trigger. ```sql CREATE OR REPLACE FUNCTION capture_changes() RETURNS TRIGGER AS $$ BEGIN END ; IF (TG_OP = 'DELETE') THEN Log DELETE operation INSERT INTO messages_cdc (message_id, operation_type, timestamp, name_before, message_before) VALUES ( OLD.message_id, 'DELETE', NOW(), OLD.name, OLD.message ) ; ELSIF (TG_OP = 'UPDATE') THEN -- Log UPDATE operation INSERT INTO users_cdc (message_id, operation_type, timestamp, name_before, email_before, name_after, message_after) VALUES ( NEW.message_id, 'UPDATE', NOW(), OLD.name, OLD.message, NEW.name, NEW.message ) ; ELSIF (TG_OP = 'INSERT') THEN Log INSERT operation INSERT INTO users_cdc (message_id, operation_type, timestamp, name_after, message_after) VALUES ( NEW.message_id, 'INSERT', NOW(), NEW.name, NEW.message ) ; END IF; RETURN NEW; $$ LANGUAGE plpgsql; ``` Finally, we need to create a trigger: ```sql CREATE TRIGGER messages_trigger AFTER INSERT OR UPDATE OR DELETE ON MESSAGES FOR EACH ROW EXECUTE FUNCTION capture_changes(); ``` And we're done! You can put this in a new migration file, start your application up and you'll have added both the new function and trigger to your database. As you can see, there is a non-insignificant amount of code required to implement this and you're also required to know some SQL beyond the fundamentals. However in exchange for this, Postgres triggers are very reliable, comprehensive and also enable instantaneous data capture. You can also create triggers for lots of different types of events! However, this also additionally puts extra strain onto the database. If you're finding that your database table is starting to slow down, you may need to create a read-only replica to minimise resource usage. ### Telemetry Of course, while writing a microservice architecture you will probably want a way to track events across your service. For example, when a payload is retrieved successfully or an error occurs while trying to insert a new record in your database. With the `tracing` libraries, this can be as simple as adding the `#[tracing::instrument]` macro to your function and then using any one of the event macros. Below is our consumer task loop, fully instrumented: ```sql #[tracing::instrument(skip(con, db))] pub async fn kafka_consumer_task(con: StreamConsumer, db: sqlx::PgPool) { con.subscribe(&["messages"]) .expect("Failed to subscribe to topics"); tracing::warn!("Starting the consumer loop..."); loop { match con.recv().await { Err(e) => tracing::warn!("Kafka error: {}", e), Ok(m) => { let payload = match m.payload() { Some(payload) => payload, None => { tracing::error!("Could not find a payload :("); continue; } }; let message: KafkaMessage = match serde_json::from_slice(payload) { Ok(res) => res, Err(e) => { tracing::error!("Deserialization error: {e}"); continue; } }; tracing::info!("Got payload: {message:?}"); match message.action { Action::Create => queries::create_message(message, &db).await, Action::Update => queries::update_message(message, &db).await, Action::Delete => queries::delete_message(message, &db).await, } con.commit_message(&m, CommitMode::Async).unwrap(); } }; } } ``` Note that we skip adding the consumer to our traces. In order for something to be logged in traces, it needs to implement `std::fmt::Debug` - which the consumer doesn't. ### Rate limiting As mentioned previously, event-driven architecture may be considered naturally more resilient than other types of microservice architectures. Because the message queue acts as a natural barrier between microservices, event-driven service won't immediately fall over in the event of a failed message. This is in comparison to HTTP request driven services, which can fail almost immediately if a request fails for any reason (without re-try code). However, this doesn't make it invincible. Naturally, we should make every effort possible to avoid load peaks and causing services to fall over. A few different things can cause a Kafka consumer to lag, which can cause eventually falling-over of services: - A producer starts producing much more number of messages than the consumer can handle - Slow consumer processing - Consumer doesn't have high enough capacity One way to solve these issues is through rate limiting. In event-driven services, generally speaking rate limiting can be effectively implemented using backpressure: a mechanism that signals to the upstream system to either slow down, or stop producing messages. One way to implement this may be controlling the polling interval (`max.poll.interval.ms`) disabling auto-commits (the `enable.auto.commit` property) so that we only commit on processing completion. This slows down the consumer, but allows Kafka much better control over how much is being processed and alleviates memory load. More specifically for Kafka, you can also pause and resume collections as well as adjusting the **`max.poll.records`** option for our consumer. On the HTTP side, you may have a public-facing web service. Should you need to add it, HTTP rate limiting is also made easy with `tower-governor`. You can find more about this in our article [here](https://www.shuttle.dev/blog/2024/02/22/api-rate-limiting-rust) where we talk about how you can implement a naive sliding window rate limiter, as well as more production-ready rate limiting. ### Testing To avoid having to spin up a Kafka container manually every time you want to test Kafka, you can use Testcontainers to be able to be able to spin up a container without any external input. Since we already installed `testcontainers` and `testcontainers-modules` (with the `kafka` feature flag), we can get coding immediately. We can put this code snippet in a test: ```rust let docker = clients::Cli::default(); let kafka_node = docker.run(kafka::Kafka::default()); let bootstrap_servers = format!("127.0.0.1:{}", kafka_node.get_host_port_ipv4(kafka::KAFKA_PORT) ); ``` The address will link directly to the Kafka node which we can connect to and create a `FutureProducer`, `StreamConsumer` etc from. `testcontainers` is primarily designed around isolated testing - if you want a persistent connection over several tests, you probably need to either reconfigure your tests or maybe want to think about a different approach using `bollard` to power your automated testing. ## Deploying To deploy, simply write `shuttle deploy --ad` and watch the magic happen! Once you've compiled the first time, you'll benefit from incremental deployment compilations. ## Finishing up Thanks for reading! Hopefully with this article, you've gained a better understanding of how to use Kafka and when it would be useful to do so in an application. Read more: - [Everything you need to know about testing in Rust](https://www.shuttle.dev/blog/2024/03/21/testing-in-rust) - [Building an uptime monitor in Rust](https://www.shuttle.dev/blog/2024/02/08/uptime-monitoring-rust) - [An intro to advanced Rust traits and generics](https://www.shuttle.dev/blog/2024/04/18/using-traits-generics-rust) --- # An introduction to advanced Rust traits and generics Source: https://www.shuttle.dev/blog/2024/04/18/using-traits-generics-rust Date: 18 April 2024 Author: josh Tags: rust, guide All about Rust traits, generics, trait bounds and implementing advanced trait bounds Hello world! In this post we're going to give a quick refresher course on Rust traits and generics, as well as implementing some more advanced trait bounds and type signatures. ## A quick refresher on Rust traits Writing a Rust trait is as simple as this: ```rust pub trait MyTrait { fn some_method(&self) -> String; } ``` Whenever a type implements `MyTrait`, you can guarantee that it will implement the `some_method()` function. To implement a trait simply requires that you implement the required methods (the ones with a semi-colon at the end). ```rust struct MyStruct; impl MyTrait for MyStruct { fn some_method(&self) -> String { "Hi from some_method!".to_string() } } ``` You can also implement traits you don't own on types you do own, or traits you do own on a type you don't own - but not both! The reason you can't do this is because of trait coherence. We want to make sure that we don't accidentally have conflicting trait implementations: ```rust // implementing Into, a trait we don't own, on MyStruct impl Into for MyStruct { fn into(self) -> String { "Hello world!".to_string() } } // implementing MyTrait for a type we don't own impl MyTrait for String { fn some_method(&self) -> String { self.to_owned() } } // You can't do this! impl Into for &str { fn into(self) -> String { self.to_owned() } } ``` A common workaround for this is to create a newtype pattern - that is, a one-field tuple struct encapsulating the type we want to extend. ```rust struct MyStr<'a>(&'a str); // note here that implementing From also implements Into - so we can use .into() as well as String::from() impl<'a> From> for String { fn from(string: MyStr<'a>) -> String { string.0.to_owned() } } fn main() { let my_str = MyStr("Hello world!"); let my_string: String = my_str.into(); println!("{my_string}"); } ``` If you have multiple traits that have the same method name, you need to manually declare what trait implementation you're calling the type from: ```rust pub trait MyTraitTwo { fn some_method(&self) -> i32; } impl MyTraitTwo for MyStruct { fn some_method(&self) -> i32 { 42 } } fn main() { let my_struct = MyStruct; println("{}", MyTraitTwo::some_method(&my_struct); } ``` Sometimes, you might want the user to be able to have a default implementation as it may otherwise be quite tricky to do so. We can do this by simply defining the method within the trait. ```rust trait MyTrait { fn some_method(&self) -> String { "Boo!".to_string() } } ``` Traits can also require other traits! Take the `std::error::Error` trait for example: ```rust trait Error: Debug + Display { // .. re-implement the provided methods here if you want } ``` Here, we explicitly tell the compiler that our type must implement both `Debug` and `Display` traits before it can implement `Error`. ## An introduction to marker traits Marker traits are used as a "marker" for the compiler to understand that when a marker trait is implemented for a type, certain guarantees can be upheld. They have no methods or specific properties but are often used to ensure certain behaviors by the compiler. There's a couple of reasons why you would want marker traits: - The compiler needs to know if something can be guaranteed to do something - They're an implementation-level detail that you can also implement manually Two marker traits in particular, in conjunction with other lesser-used marker traits, are quite important to us: `Send` and `Sync`. `Send` and `Sync` are unsafe to implement manually - this is typically because you need to manually ensure that it is implemented safely. `Unpin` is also another example of this. You can find more about why it's unsafe to manually implement these traits [here.](https://doc.rust-lang.org/nomicon/send-and-sync.html) In addition to this, marker traits are also (generally speaking) auto traits. If a struct has fields that all implement an auto trait, the struct itself will also implement the auto trait. For example: - If all field types within a struct are `Send`, the struct is now automatically marked `Send` by the compiler with no input required from the user. - If all but one of your struct fields implement `Clone` but one doesn't, your struct now cannot derive `Clone` anymore. You can get around this by wrapping the relevant type in an `Arc` or `Rc` - but this depends on your use case. In certain cases, this is not possible and you may need to think about an alternative solution. ## Why do marker traits matter in Rust? Marker traits in Rust form the core of the ecosystem and allows us to provide garuantees that may not be possible in other languages. For example, Java has marker interfaces which are analogous to Rust's marker traits. However, marker traits in Rust are not just for behavior like `Cloneable` or `Serializable`; they also ensure that types can be sent across threads, for example. This is a subtle but far-reaching difference within the Rust ecosystem. With `Send` types for example, we can ensure that it's always safe to send the type across a thread. This makes the problem of concurrency much easier to handle. Marker traits can also affect other things: - The `Copy` trait is required to duplicate things by performing a bitwise copy (although this requires Clone). Attempting to copy a pointer bitwise only returns the address! This is also the same reason why String is unable to be copied and must be cloned: Strings in Rust are smart pointers. - The `Pin` trait which allows us to "pin" a value to a static place in memory - The `Sized` trait allows us to define a type as having a constant size at compile-time - however, this is already implemented for most types automatically There are also marker traits like `?Sized`, `!Send` and `!Sync`. In comparison to `Sized`, `Send` and `Sync` they are negative trait bounds and do the absolute opposite: - `?Sized` allows a type to be unsized (or in other words, dynamically sized) - `!Send` tells the compiler that an object absolutely cannot be sent to other thread - `!Sync` tells the compiler that an object's references absolutely cannot be shared between threads Marker traits can also improve the ergonomics of library crates. For example, let's say you have a type that implements `Pin` because your application or library requires it (Futures being a huge example of this). This is great because you can use the type safely now, but it's much more difficult to use your `Pin` type with things that don't care about pinning. Implementing `Unpin` allows you to use the type with things that don't care about pinning, making your developer experience that much better. ## Object traits and dynamic dispatch In addition to all of the above, traits can also make use of dynamic dispatch. Dynamic dispatch is essentially moving the process of selecting which implementation of a polymorphic function to use at runtime. While Rust does favour static dispatch for performance reasons, there are benefits to using dynamic dispatch through trait objects. The most common pattern for using trait objects would be `Box`, where we are required to wrap the trait object in `Box` to make it implement the `Sized` trait. Because we're moving the polymorphism process to runtime, the compiler can't know what size the type is. Wrapping the type in a pointer (or "boxing" it) puts it on the heap instead of the stack. ```rust // a struct with your object trait in it struct MyStruct { my_field: Box } // this works! fn my_function(my_item: Box) { // .. some code here } // this doesn't! fn my_function(my_item: dyn MyTrait) { // .. some code here } // an example of a trait with a Sized bound trait MySizedTrait: Sized { fn some_method(&self) -> String { "Boo!".to_string() } } // an illegal struct that won't compile because of the Sized bound struct MyStruct { my_field: Box } ``` The object type will then be computed during runtime, as opposed to generics which use compile-time. The main advantages of dynamic dispatch are that your function doesn't need to know the concrete type; as long as the type implements the trait, you can use it as a trait object (as long as it's trait object safe). This is similar to the concept of duck-typing in other languages, where the functions and properties available to an object determine the typing. Typically from a user standpoint, the compiler doesn't care what the underlying concrete type is - just that it implements the trait. There are cases however where it _does_ matter - in which case Rust offers ways to determine concrete type, although tricky to use. You also save some code bloat, which depending on your use can be a good thing. Errors are also easier to understand from a library user perspective. From a library developer's point of view this is not such an issue, but if you need to use a generics-heavy library, you can get some very confusing errors! Axum and Diesel are two libraries that can sometimes be guilty of this and have workarounds for this (Axum's `#[debug_handler]` macro and Diesel's documentation, respectively). Because you're moving the dispatch process to runtime, you also save compilation time. The downsides are that you need to ensure object trait safety. The conditions you need to satisfy for object safety include: - your type doesn't require `Self: Sized` - your type must use some type of "self" in function arguments (whether it's `&self`, `self`, `mut self` etc...) - your type must not return Self Find out more [here.](https://doc.rust-lang.org/reference/items/traits.html#object-safety) Note that if you have a trait that doesn't require `Self: Sized` but the trait has a method that requires it, you can't call that method on a `dyn` object. This stems from the fact that by moving dispatch to runtime, the compiler can't guess the size of your type - object traits don't have a constant size at compile-time. This is also why we need to box dynamically dispatched objects and put them on the heap as mentioned earlier. Because of this, your application also takes a performance hit - although of course this depends on how many dynamically dispatched objects you're using and how large they are! To illustrate these points further, there are two HTML templating libraries that come to mind: - Askama, which uses macros and generics for compile-time checking - Tera, which uses dynamic dispatch for getting filters and testers at runtime Both of these libraries, while they can be used somewhat interchangeably for most use cases, have different trade-offs. Askama takes longer to compile and any errors will show in compile-time, but Tera only throws compilation errors at runtime and takes a performance hit due to dynamic dispatch. Zola, the Static Site Generator, uses Tera specifically because of certain design conditions that are unable to be satisfied by Askama. You can see [here](https://github.com/Keats/tera/blob/3b2e96f624bd898cc96e964cd63194d58701ca4a/src/tera.rs#L61) that the Tera framework uses `Arc`. ## Combining traits and generics ### Getting started Traits and generics synergise very well together and are easy to use. You can write a struct that implements generics like this without much trouble: ```rust struct MyStruct { my_field: T } ``` However, to be able to use our struct with types from other crates, we will need to ensure that our struct can garuantee certain behavior. This is where we add trait bounds: conditions that a type must satisfy in order for the type to compile. A common trait bound you may find would be `Send + Sync + Clone`: ```rust struct MyStruct { my_field: T } ``` Now we can use any value we want for `T` as long as that type implements the `Send`, `Sync` and `Clone` traits! As a more complex example of using traits with generics that you may occasionally need to re-implement for your own types, take the `FromRequest` trait from Axum for example (the below code snippet is a simplification of the original trait to illustrate the point): ```rust use axum::extract::State; use axum::response::IntoResponse; trait FromRequest where S: State { type Rejection: IntoResponse; fn from_request(r: Request, _state: S) -> Result; } ``` Here we can also add trait bounds by using the `where` clause. This trait simply tells us that `S` implements `State`. However, `State` also requires the inner object to be `Clone`. By using complex trait bounds, we can create framework systems that make heavy use of traits to be able to do what some might refer to as "trait magic". Take a look at this trait bound, for example: ```rust use std::future::Future; struct MyStruct where B: Future, T: Fn() -> B { my_field: T } #[tokio::main] async fn main() { let my_struct = MyStruct { my_field: hello_world }; let my_future = (my_struct.my_field)(); println!("{:?}", my_future.await); } async fn hello_world() -> String { "Hello world!".to_string() } ``` The above one-field struct stores a function closure that returns `impl Future`, that we store `hello_world` in and then call it in the main function. We then wrap parenthesis around the field to be able to call it, then await the future. Note that we **do not** have brackets at the end of the field. This is because adding `()` at the end actually invokes the function! You can see where we invoke the function after declaring the struct, then awaiting it. ### Usage in libraries Combining traits and generics like this is extremely powerful. One use case where this is effectively leveraged is in HTTP frameworks. Actix Web for example, has a trait called `Handler` that takes a number of arguments, calls itself and then has a function called `call` that produces a Future: ```rust pub trait Handler: Clone + 'static { type Output; type Future: Future; fn call(&self, args: Args) -> Self::Future; } ``` This then allows us to extend this trait to a handler function. We can tell the web service that we have a function that has an inner function, some arguments and implements `Responder` (Actix Web's HTTP response trait): ```rust pub fn to(handler: F) -> Route where F: Handler, Args: FromRequest + 'static, F::Output: Responder + 'static { // .. the actual function code here } ``` Note that other frameworks like Axum also follow this same methodology to provide an extremely ergonomic developer experience. ## Finishing up Thanks for reading! While traits and generics can be a confusing topic to understand, hopefully this guide to using Rust traits and generics has shed some light on the subject! Read more: - [Why Enums in Rust feel so much better](https://www.shuttle.dev/blog/2023/11/23/enums-in-rust) - [Async Rust in a nutshell](https://www.shuttle.dev/blog/2024/02/29/async-rust) - [Everything you need to know about testing in Rust](https://www.shuttle.dev/blog/2024/03/21/testing-in-rust) --- # Building with AWS S3 using Rust Source: https://www.shuttle.dev/blog/2024/04/17/using-aws-s3-rust Date: 17 April 2024 Author: josh Tags: rust, aws, guide Guide to using AWS S3 in a Rust web service application Hello world! This time, we're going to go a little more in-depth when it comes to writing web services. We're going to create a web service that uses AWS S3 to store and retrieve images. We will also add telemetry via tracing, look at tests and other common things for productionising a Rust web application. Interested in deploying or just want to see what the final code looks like? Check it out [here.](https://github.com/joshua-mo-143/shuttle-s3-example) ## Pre-requisites ### Setting up your S3 bucket Before we get started, you'll need to set up an S3 Bucket and an IAM user. We'll go through this below. To create a bucket, do the following: - Log into AWS Console and go to the S3 section (it can also be found using the search bar) - Click "Create Bucket" and follow the prompt. If this is your first time (and you aren't handling sensitive data), it is safe to leave the defaults as they are. Public bucket access is turned off by default. When using S3, your bucket endpoint will look like the following: ```bash http://[bucket_name].s3.amazonaws.com/ ``` You'll want to make sure this is kept somewhere safe as we'll be using this later on. ### Setting up an IAM user You'll also need two variables which will be found in S3 or any S3-compatible API: - `AWS_ACCESS_KEY_ID` (your Access Key) - `AWS_SECRET_ACCESS_KEY` (your Secret Access Key) The first two can be found in your IAM user credentials if you've already set a user up. If you don't have an appropriate user with policies, you can get started quickly by doing the following: - Go to the Users menu - Start creating a user and go to the "Attach policies" section (then search "S3") - Here you can either use the "AmazonS3FullAccess" policy which gives you full access to S3 on that user, or you can create a custom policy. Select one and finish creating your user. **Access to S3 is required**, as otherwise you won't be able to use it! - Go back to the Users menu and click on your newly created user - Go to "Access keys" and follow the prompt (clicking "Application outside of AWS"). Don't forget to store your Access Key and Secret Access Key! In production, you may want to go a step further and create a Group that you can then attach policies and users to! When using S3-compatible APIs, this may look different depending on the service you're using. However, the documentation should provide enough information for you to create an S3 client for their service. ## Getting started To get started, we'll create a Shuttle service via `shuttle init`, making sure to pick the Axum framework. Make sure you have `cargo-shuttle` installed! Next, you'll want to install the following Rust dependencies using the following shell snippet: ```bash cargo add aws-config@1.1.8 -F behavior-version-latest cargo add aws-credential-types@1.1.8 -F hardcoded-credentials cargo add aws-sdk-s3@1.23.0 -F behavior-version-latest cargo add axum -F multipart cargo add image@0.25.1 cargo add serde@1.0.197 -F derive cargo add thiserror@1.0.58 cargo add tower-http@0.5.2 -F timeout ``` We'll want to add our secrets to a `Secrets.toml` file located in the project root folder: ```toml AWS_ACCESS_KEY_ID = "" AWS_SECRET_ACCESS_KEY = "" AWS_URL = "" ``` ## Error handling Before we get started, we'll want to create an error type that can represent all the kinds of errors we can encounter while using the service. There's several reasons to do this: - It allows error propagation instead of having to manually handle an error every time - We can use the `From` trait to convert error types from our libraries to our API's error type - It saves time debugging! In this snippet we use the `thiserror::Error` derive macro to be able to quickly derive `Display`, `Error` and `From` all in one by using attribute macros in conjunction with the derive macro. ```rust use aws_sdk_s3::error::SdkError; use aws_sdk_s3::operation::delete_object::DeleteObjectError; use aws_sdk_s3::operation::get_object::GetObjectError; use aws_sdk_s3::operation::put_object::PutObjectError; use axum::extract::multipart::MultipartError; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use image::ImageError; use std::io::Error as IoError; use thiserror::Error; #[derive(Debug, Error)] pub enum ApiError { #[error("Error while deleting object: {0}")] DeleteObjectError(#[from] SdkError), #[error("Error while getting image: {0}")] GetObjectError(#[from] SdkError), #[error("Error while inserting image: {0}")] PutObjectError(#[from] SdkError), #[error("Error while manipulating image bytes: {0}")] ImageError(#[from] ImageError), #[error("Error while getting data from multipart: {0}")] Multipart(#[from] MultipartError), #[error("IO error: {0}")] IO(#[from] IoError), #[error("Body is empty")] EmptyBody, // the user tried to send an empty body while uploading } ``` Next, we implement `axum::response::IntoResponse` for our error type. This allows it to be turned into a HTTP response: ```rust impl IntoResponse for ApiError { fn into_response(self) -> Response { let response = match self { Self::DeleteObjectError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::GetObjectError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::PutObjectError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::ImageError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::Multipart(e) => (StatusCode::BAD_REQUEST, e.to_string()), Self::IO(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::EmptyBody => (StatusCode::BAD_REQUEST, self.to_string()), }; response.into_response() } } ``` ## Building the base of our S3 microservice ### Setting up AWS SDK To get started, we'll set up some code in our main function that allows us to create an AWS client. ```rust use shuttle_runtime::SecretStore; use aws_config::Region; use aws_credential_types::Credentials; use aws_sdk_s3::Client; #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore ) -> shuttle_axum::ShuttleAxum { let access_key_id = secrets .get("AWS_ACCESS_KEY_ID") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let secret_access_key = secrets .get("AWS_SECRET_ACCESS_KEY") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let aws_url = secrets .get("AWS_URL") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); // note here that the "None" is in place of a session token let creds = Credentials::from_keys(access_key_id, secret_access_key, None); let cfg = aws_config::from_env() .endpoint_url(aws_url) .region(Region::new("eu-west-2")) .credentials_provider(creds) .load().await; let s3 = Client::new(&cfg); // rest of your code goes down here } ``` For our region we've used `eu-west-2` as the Shuttle servers are in `eu-west-2`, which reduces latency. However, feel free to use whichever region you'd like! We'll also additionally create a shared state struct which will hold the client. When we need to access the client, we can simply add the `State` extractor to our functions and it will work. ```rust #[derive(Clone, Debug)] pub struct AppState { s3: Client, } ``` ### Creating a custom response type To make it easier for ourselves when writing our code, we'll create our own enum return type that will implement `axum::response::IntoResponse`. While you _can_ use `impl IntoResponse` itself as the return type, it is often better to declare a specific type for a couple of reasons: - While using `impl IntoResponse`, every response type is required to be the same - Using an enum allows you to be more flexible in your response type As a short illustration, we'll create an enum with two variants and implement `IntoResponse`: ```rust pub enum Image { Filename(String), File(String, Vec), } impl IntoResponse for Image { fn into_response(self) -> Response { match self { Self::Filename(name) => (StatusCode::OK, name).into_response(), Self::File(filename, data) => { let filename_header_value = format!("attachment; filename=\"{filename}\""); Response::builder() .header("Content-Disposition", filename_header_value) .header("Content-Type", "image/jpeg") .body(Body::from(data)) .unwrap() } } } } ``` Now we can avoid writing our types directly out into the functions! However, we can also take this a step further by implementing `Into` for our types as well as creating helper functions to create our `Image` enum easily. Let's implement `Into` for `String` and a function to convert a filename with a `Vec` to an image: ```rust impl Into for (String, Vec) { fn into(self) -> Image { Image::File(self.0, self.1) } } impl Into for String { fn into(self) -> Image { Image::Filename(self) } } impl Into for &str { fn into(self) -> Image { Image::Filename(self.to_owned()) } } ``` ### Routing We will get started with a handler function for uploading an image. We will need to deal with multipart form upload data, and as such we'll want to use `axum::extract::Multipart` here. There is a small footnote here: if you're operating with variables that need to function outside of the multipart loop, you need to declare them beforehand as a `None` option and re-assign them. This is primarily due to scoping - if you declare it inside the loop, you can't suddenly use it outside the loop again. Whether you need to do this however depends on your use case. ```rust // src/routing.rs use axum::extract::Multipart; use crate::AppState; use axum::response::IntoResponse; use crate::errors::ApiError; pub async fn upload_image( State(state): State, mut multipart: Multipart, ) -> Result { let mut field: Option> = None; while let Some(formitem) = multipart.next_field().await.unwrap() { field = Some(formitem.bytes().await?.to_vec()); } let Some(data) = field else { tracing::error!("User tried to upload an empty body"); return Err(ApiError::EmptyBody); }; let filename = "my_file.jpeg"; // rest of function code goes here Ok(filename.into()) } ``` Next, we'll add the code for inserting an object into your S3 object into the comment area: ```rust let _res = state.s3 .put_object() .bucket("my-bucket") .key(&filename) .body(new_vec.into()) .send().await?; ``` It is important to note here that we've generated our own filename. It is always more secure to generate your own file names rather than taking the user's filenames, as you may accidentally end up overwriting your own files. Users may also maliciously try to upload files with known names! Strictly speaking, we don't _need_ the file extension at the end of our file key. However, when you're using said files outside of image storage, it's best to preserve them for future usage. To retrieve an image, we can write the following route: ```rust pub async fn retrieve_image( State(state): State, Path(filename): Path, ) -> Result { let res = state .s3 .get_object() .bucket("my-bucket") .key(&filename) .send() .await?; let body: Vec = res.body.collect().await?.to_vec(); Ok((filename, body).into()) } ``` Note here that we're setting the filename dynamically. You can also set your `Content-Type` header according to the kind of image you're trying to serve from S3. The handler function for deleting the image is by far the simplest to write: we just need to delete the image from S3. ```rust pub async fn delete_image( State(state): State, Path(filename): Path, ) -> Result { state .s3 .delete_object() .bucket("my-bucket") .key(&filename) .send() .await?; tracing::info!("Image deleted with filename: {filename}"); Ok(filename.into()) } ``` To wrap it all up, let's add it to our main function: ```rust let state = AppState { s3 }; fn init_router(state: AppState) -> Router { Router::new() .route("/", get(hello_world)) .route("/images/upload", post(routing::upload_image)) .route("/images/:filename", get(routing::retrieve_image).delete(routing::delete_image)) .with_state(state) } ``` ## Extending our web service While what we've currently got works well, we can much do better. In its current state, it's not super production ready. Let's have a look at what we can do to assist with ensuring production readiness. ### Timeout layer Although we've written our base service and now it works perfectly fine, there are a couple of issues that we'd need to deal with in production: - We need to stop slow loris attacks (flooding a server with opened connections) - We need to stop people who want to upload unexpectedly large files, which saves on egress costs The first point is a rather big deal, as most Rust web frameworks do not deny long-running requests by themselves. It just needs to be added like below, specifying a timeout duration. ```rust use std::time::Duration; use tower_http::timeout::TimeoutLayer; let router = Router::new() .route("/", get(hello_world)) .route("/images/upload", post(routing::upload_image)) .route("/images/:filename", get(routing::retrieve_image).delete(routing::delete_image)) .with_state(state) .layer(TimeoutLayer::new(Duration::from_secs(20))); ``` Simple and easy! Our service will now automatically return a timeout error to any request taking longer than 20 seconds (returning the 408 Timeout error). ### Tracing To add tracing to our service, we only need to add the `#[tracing::instrument]` macro to our handler functions. ```rust #[tracing::instrument] pub async fn upload_image( State(state): State, mut multipart: Multipart, ) -> Result { // function code } ``` Now whenever anything gets printed out from this endpoint, the whole function will get printed out - application state included! If you're holding any sensitive data in your application state, you can use the `skip` attribute to skip printing it out in logs: ```rust #[tracing::instrument(skip(state))] pub async fn upload_image( State(state): State, mut multipart: Multipart, ) -> Result { // function code } ``` Adding events to our handler functions that then get triggered will automatically send the output to our logs: ```rust #[tracing::instrument] pub async fn delete_image( State(state): State, Path(filename): Path, ) -> Result { // .. your other code tracing::info!("Image deleted with filename: {filename}"); // .. your other code } ``` Note that Shuttle automatically starts the subscriber from `tracing_subscriber` for you. If you want to create your own custom subscriber, you can do that by turning off all default features: ```bash cargo add shuttle-runtime --no-default-features ``` ### Testing We can test S3 by using the `s3-server` crate. To get started, you only need to install it: ```rust cargo install s3-server --features binary ``` This crate will additionally require the `http` crate. Since we're only using it in tests, we can add it as a dev dependency like so: ```rust cargo add http --dev ``` We can then set up a common function in our project to be able to create an S3 server: ```rust fn setup_s3_testing() -> Client { let conf = aws_config::load_from_env().await; let ep = Endpoint::immutable(Uri::from_static("http://localhost:8543")); let s3_conf = aws_sdk_s3::config::Builder::from(&conf).endpoint_resolver(ep).build(); Client::from_conf(s3_conf); } ``` Of course, you'll want to make sure `s3-server` is running in the background. Because Axum itself integrates with most things in the Tower ecosystem, you can either send oneshot requests to your server to test it (requires `hyper` installed as dev dependency) or you can start a `TcpListener` and start your Axum server up in the usual manner. You would then use `reqwest` or a similar library to send HTTP requests to your server: ```rust #[tokio::test] async fn my_test() { let state = AppState { s3: setup_s3_testing() }; let router = init_router(state); let tcp_listener = TcpListener::bind("127.0.0.1:8000").await.unwrap(); tokio::spawn(async { axum::serve(tcp_listener, router).await.unwrap(); }); // whatever requests you want to make down here, using hyper or reqwest } ``` If you want to do a oneshot request however, you can do so like this (test assumes you have a "Hello, World!" route at `/`): ```rust #[tokio::test] async fn my_test() { let state = AppState { s3: setup_s3_testing() }; let router = init_router(state); let response = router .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()).await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); assert_eq!(&body[..], b"Hello, world!"); } ``` ## Deploying To deploy our web service, all we need to do now is `shuttle deploy`! Make sure to add the `--allow-dirty` flag if on a Git branch with uncommitted changes. If you've added tests, make sure to add the `--no-test` flag, as they may not work while deploying. One quick workaround for this is to add a test workflow before deployment. ## Finishing up Thanks for reading! Using the AWS SDK can be difficult. However, hopefully this tutorial on using S3 with Rust can shed some light on writing a fully functioning service that uses S3! Read more: - [Building a notification service with Rust & AWS SNS](https://www.shuttle.dev/blog/2024/03/20/notification-service-rust) - [Learn more about Axum](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - [Using OpenTelemetry with Rust](https://www.shuttle.dev/blog/2024/04/10/using-opentelemetry-rust) --- # Data Parallelism with Rust and Rayon Source: https://www.shuttle.dev/blog/2024/04/11/using-rayon-rust Date: 11 April 2024 Author: josh Tags: rust, rayon, guide Speeding up data processing with Rayon and Rust using the power of parallelism Hello world! Today we'll be talking about using the [Rayon](https://github.com/rayon-rs/rayon) crate to speed up synchronous data processing. Rayon is a library crate that allows you to ergonomically parallelize your computations with high-level methods. You can also use the more low level API to divide the work up yourself! ## What makes rayon good? Parallel execution is hard. When writing a library that can carry out parallel execution, you are likely to induce a significantly difficult-to-fix bug or a data race. Rayon's APIs guarantee data race freedom and use an ergonomic API via traits that means if your code compiles normally, it will still do mostly the same thing as it did before. If your iterators have side effects however, they may occur in a different order. Rayon's core primitive is called `join`, which essentially just "joins" two functions in `FnOnce` closures that may or may not run in parallel, depending on if there is an idle core available. This approach of potential parallelism rather than forced parallelism can also provide an upside to performance as there are times when sequential work can be more value. It is also difficult to predict when parallelism is a good thing, which adds to the complexity. The `join` function is implemented using work stealing (like the Tokio async runtime). Rayon uses a global thread pool to be able to take advantage of this. Essentially this means that if you have a thread doing work that finishes, it will then look for other units of work. For example, let's have task A and task B. A thread might execute task A, while adding B to a local queue of work to be done. Threads from the thread pool will actively look for work to execute - so another idle thread from the thread pool might try to execute task B. The great thing about `join` is that the way it is set up is inherently safe. Let's take a look at the below code which doesn't compile and if it did, would cause bad things to happen: ```rust fn share_rc(rc: Rc) { // In the closures below, the calls to `clone` increment the // reference count. These calls MIGHT execute in parallel. // Would not be good! rayon::join(|| something(rc.clone()), || something(rc.clone())); } ``` You can't have two closures that are simultaneously in scope and access the same `&mut` data. `&mut` types themselves can only be borrowed once; by trying to use it over both closures, we've violated this rule. Additionally, the `Rc` type doesn't actually implement the `Send` marker trait - which is required to send values across threads! ## Using Rayon ### Getting started To get started, add `rayon` to your Rust application: ```bash cargo add rayon ``` ### Parallelizing array work The simplest way to use [Rayon](https://github.com/rayon-rs/rayon) is to convert your iterators into parallel iterators. If you're using `.iter()`, you simply change the method to `.par_iter()` and you're done! No change required. ```rust use rayon::prelude::*; fn sum_of_squares(input: &[i32]) -> i32 { input.par_iter() .map(|i| i * i) .sum() } ``` You can also parallelise extending a vector or array by using the `ParallelExtend` trait. This also requires `IntoParallelIterator`, the parallel version of the `IntoIterator` trait from the standard library. If you require indexes, you can use the `IndexParallelIterator` trait. It supports random access, allowing you to split an array at arbitrary indices and draw data from a given point of your choosing. ### Turn an iterator into a parallel iterator Some types may implement `Iterator` but are otherwise impossible or extremely difficult to implement `ParallelIterator` for. This is where the `par_bridge()` function comes in. By using this function on an iterator, it lets you bridge an `Iterator` type to `IterBridge` (which then allows conversion to `ParallelIterator`). A quick example might look something like this: ```rust use rayon::iter::ParallelBridge; use rayon::prelude::ParallelIterator; use std::sync::mpsc::channel; let rx = { let (tx, rx) = channel(); tx.send("one!"); tx.send("two!"); tx.send("three!"); rx }; let mut output: Vec<&'static str> = rx.into_iter().par_bridge().collect(); output.sort_unstable(); assert_eq!(&*output, &["one!", "three!", "two!"]); ``` Note that while the final iterator type generated from this is generally not as good as implementing `ParallelIterator` yourself, it can be a great deal faster than purely sequential work. ### Using your own thread pool For even more low level work, you may want to use your own thread pool for work or customize the global thread pool for `rayon`. A quick way to get started would be using it like this: ```rust // as a variable let pool = rayon::ThreadPoolBuilder::new().num_threads(8).build().unwrap(); // globally rayon::ThreadPoolBuilder::new().num_threads(8).build_global().unwrap(); ``` When using the ThreadPool, you can do a few things: - Carry out a function on every single thread using `.broadcast()` - Use `.join()` to start some parallel work - Use `install()` which takes a `join` type Interested in learning more? You can find more about the `ThreadPool` type [here.](https://docs.rs/rayon/latest/rayon/struct.ThreadPool.html) ## Use cases for rayon ### Log analysis Text processing is a great area where [Rayon](https://github.com/rayon-rs/rayon) can provide a big performance bonus! Here we can analyse a log file that has been extracted from somewhere by filtering for any lines that contain the word "ERROR" in uppercase. ```rust use std::fs::read_to_string; use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; fn main() { // read a csv file to a string let my_string = read_to_string("my_file.txt").unwrap(); let my_vec = my_string.lines().collect::>(); let my_vec: Vec = my_vec.into_par_iter() .filter(|x| x.contains("ERROR")) .map(|x| x.to_owned()) .collect(); // this should now print a vec of vecs // where every single value is the "Hello world!" string println!("{:?}", my_vec); } ``` ### CSV processing Of course, where there's mountains of data, data parallelism will always be a good thing. You can parse a file, split it then parallelise the reading. In the below example, we parse a CSV file and turn every value into a "Hello world!" string: ```rust use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use std::fs::read_to_string; use std::str::Split; fn main() { // read a csv file to a string let my_string = read_to_string("my_file.csv").unwrap(); let my_vec: Vec = my_string .lines() .filter(|x| *x != String::new()) .map(|x| x.to_string()) .collect(); println!("{my_vec:?}"); let my_vec: Vec> = my_vec .into_par_iter() .map(|x| { let x = x.split(",").map(|x| x.to_string()).collect::>(); let res: Vec = x .into_par_iter() .map(|_| "Hello world!".to_string()) .collect(); res }) .collect(); // this should now print a vec of vecs // where every single value is the "Hello world!" string println!("{:?}", my_vec); } ``` For many use cases, the more sensible option here most of the time would be to use the `csv` crate. However, that doesn't stop us from implementing our own CSV parser - particularly if we're not using the `serde` crate to deserialize and serialize our CSV records. ## Pitfalls Rayon is a very powerful crate. However, there are some pitfalls that can happen while using it if you're not careful. ### Blocking rayon threads (deadlocks) Work done in parallel rayon threads that block each others' threads are something you absolutely want to avoid here. One example of this may be a mutex that gets locked on every iteration. ```rust fn main() { let arcmutex = Arc::new(Mutex::new(0)); let res = (0..5000).par_iter().for_each|x| { let mut mutex_locked = arcmutex.lock().unwrap(); mutex_locked += x; }.collect(); println!("{res}"); } ``` Here, you can see that the mutex gets locked on every iteration! That's a terrible way to do it. Clearly, we need a better way. We can avoid this totally by simply collecting the parallelized work, and **then** adding it to our wrapped number: ```rust fn main() { let arcmutex = Arc::new(Mutex::new(0)); let res = (0..5000).par_iter().sum(); let mut locked_mutex = arcmutex.lock().unwrap(); *locked_mutex += res; println!("{locked_mutex:?}"); } ``` ### Workload too small If your workload is too small, the overhead from using the `rayon` crate can neutralise any performance gains. This is particularly because of the thread pool management and dealing with work-stealing. However, whether this matters to you will typically depend on your use case. ## Finishing up Thanks for reading! Rayon is an awesome crate to help you speed up your data processing. Read more: - [Using the Reqwest library for web scraping](https://www.shuttle.dev/blog/2023/09/13/web-scraping-rust-reqwest) - [Sending your logs to Grafana with Rust](https://www.shuttle.dev/blog/2024/03/28/grafana-rust) - [Parsing JSON with Rust](https://www.shuttle.dev/blog/2024/01/18/parsing-json-rust) --- # Working with OpenTelemetry using Rust Source: https://www.shuttle.dev/blog/2024/04/10/using-opentelemetry-rust Date: 10 April 2024 Author: josh Tags: rust, opentelemetry, guide Adding OpenTelemetry to a Rust application and using the OpenTelemetry collector ## What is OpenTelemetry? From OpenTelemetry.io: > High-quality, ubiquitous, and portable telemetry to enable effective observability OpenTelemetry is a framework for effective observability based on creating and managing metrics, traces and logs. It is open source and tool-agnostic, meaning that you can use it with a huge amount of tools. When using OpenTelemetry, you also completely own your data and you are only required to conform to one set of APIs. OpenTelemetry itself is focused on the creation and management of aforementioned metrics, traces and logs. This means you will typically need to provide an appropriate observability backend yourself. In contrast to an observability framework, an observability backend can be more accurately described as a platform for storing and analysing metrics, traces and logs. The most prominent examples would be open-source observability backends like Jaeger and Prometheus, as well as paid services like Datadog and Grafana. ## A quick introduction to observability Observability is the ability to understand the events of a program through its outputs. A simple example of this is logging: something happens, then we log to `stdout` or a log file what happened. This is observability at its most basic form (if you can call it that!). However, we can do much better. By "instrumenting" our application (augmenting it to emit metrics, traces and logs) and sending traces to a platform that we can then use to store and analyse them, we can provide a high level of observability into our application. Why is this important? - In production, observability saves time and money on solving bugs because events can be easily traced back - It allows us to validate that a program is working as intended ## How OpenTelemetry works OpenTelemetry relies upon the idea of combining logs, spans and traces all together to create a cohesive system. Let's explore these concepts as they are vital to being able to understand how OpenTelemetry works. ### Logs Logs are typically timestamped messages emitted by services or other components. A quick example of this may be logging something to stdout for debugging purposes with the `println!()` macro. A more sophisticated example of this might be using the `log` crate with an initialised logger to log messages to stdout. While logs can provide important context, by themselves they also typically miss metadata that can be extremely helpful for observability purposes. ### Spans Spans can be described as a period of time over which operations or actions are recorded in a given context. If you've used the `tracing` libraries at all, you may be familiar with this concept. Spans typically have a given name, time-related data, structured logging messages as well as other related metadata ("attributes"). Spans typically wrap around logs so that the logs can be given context. You can check out more about how spans work in relation to OpenTelemetry [here.](https://opentelemetry.io/docs/concepts/signals/traces/#spans) ### Traces Traces typically record the path of a request, and can span across multiple services with propagation. A huge advantage of traces is that they can be used with microservice or serverless architecture. This is a big deal as there is a lot to keep track of within these kinds of architectures! Tracing is also additionally essential in distributed systems, where problems can be locally difficult to reproduce. Traces can consist of one or more spans, under which more child spans are typically created to illustrate different units of work being completed within a trace. ### OpenTelemetry collector To be able to aid in collecting metrics and traces, OpenTelemetry has a collector that we can send our traces to. The collector offers a vendor-neutral web service for receiving, processing, and exporting telemetry data. In addition, it removes the need to run, operate, and maintain multiple agents/collectors in order to support open-source telemetry data formats. The collector behavior can be modified by using a YAML file and then executed using the `--config` flag when running it. This makes it exceptionally easy to use, as there is [a GitHub repository full of examples.](https://github.com/open-telemetry/opentelemetry-collector-contrib) ## Using OpenTelemetry with Rust ### Setting up OpenTelemetry in an application A basic pipeline with OpenTelemetry can be set up via `opentelemetry_otlp`. A `tracing_opentelemetry` layer that uses the pipeline is then created, then added to a tracing subscriber that gets initialised. ```rust // note that here, localhost:4318 is the default HTTP address // for a local OpenTelemetry collector let tracer = opentelemetry_otlp ::new_pipeline() .tracing() .with_exporter(opentelemetry_otlp::new_exporter().http().with_endpoint("localhost:4318")) .install_batch(Tokio) .unwrap(); // log level filtering here let filter_layer = EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new("info")) .unwrap(); // fmt layer - printing out logs let fmt_layer = fmt::layer().compact(); // turn our OTLP pipeline into a tracing layer let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer); // initialise our subscriber subscriber .with(filter_layer) .with(fmt_layer) .with(otel_layer) // The error layer needs to go after the otel_layer, because it needs access to the // otel_data extension that is set on the span in the otel_layer. .with(ErrorTracingLayer::new()) .init(); ``` Once we start our app, we can start instrumenting our application! Because we're using the `tracing` library, we can instrument our functions through the `#[tracing::instrument]` macro, which saves a lot of work setting up spans manually (though for more complex use cases, you may want to go more in-depth into customising your spans). A basic instrumented function may look something like this: ```rust #[tracing::instrument] async fn hello_world() -> &'static str { info!("Received a request!"); "Hello world!" } ``` ### Set up the OpenTelemetry collector So you've written your application - next you'll want to start the collector. If you just want to get something going, you can use the shell snippet below that runs the collector in an attached terminal and outputs the collector ```bash docker run \ -p 127.0.0.1:4318:4318 \ -p 127.0.0.1:55679:55679 \ otel/opentelemetry-collector-contrib:0.97.0 \ 2>&1 | tee collector-output.txt # Optionally tee output for easier search later ``` Note that the OpenTelemetry collector YAML file can be found at `/etc//config.yaml`. This means we can create a Dockerfile that copies in an external YAML file into the config! Let's have a look at what this would look like: ```docker ARG OTEL_TAG= FROM docker.io/otel/opentelemetry-collector-contrib:${OTEL_TAG} # copy an existing .yaml file from the directory where the dockerfile is COPY otel-collector-config.yaml /etc/otel-collector-config.yaml # Reset the user to allow reading from the docker.sock USER 0 CMD ["--config=/etc/otel-collector-config.yaml"] ``` When we start the docker container, we add the `--config` flag to enable usage of the new collector config file. The `opentelemetry-collector-contrib` repo contains a lot of different exports you can use, each with a YAML file you can examine. You can find this [here.](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter) Note that OpenTelemetry collector utilities are not currently available on Shuttle. ## Finishing up Thanks for reading! With OpenTelemetry, making your Rust applications observable has never been easier. Read more: - [Get started with the tracing libraries](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) - [Deploying Rust to the web](https://www.shuttle.dev/blog/2024/02/07/deploy-rust-web) - [Building an uptime monitor with Rust, askama, axum and htmx](https://www.shuttle.dev/blog/2024/02/08/uptime-monitoring-rust) --- # Working with OpenAPI using Rust Source: https://www.shuttle.dev/blog/2024/04/04/using-openapi-rust Date: 4 April 2024 Author: josh Tags: rust, openapi, guide Adding OpenAPI to a Rust web service and generating Rust libraries from OpenAPI Hello world! In this article we're going to talk about how you can make the most of OpenAPI with Rust, by learning all the different ways we can use OpenAPI in a Rust context. By the end of this article, you'll learn the following: - Adding OpenAPI to a Rust web service - How to generate API client libraries from OpenAPI specifications - Working with the OpenAPI spec directly ## What is OpenAPI? From Swagger.io: > The OpenAPI specification (OAS) defines a standard, language-agnostic interface to HTTP APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code. No matter what language you're using, OpenAPI allows you to easily read the specification and understand how to use an API without needing specific documentation. Here is a simple example of what an OpenAPI specification file may look like: ```yaml swagger: "3.0" info: version: "1.0" title: "Hello World API" paths: /hello/{user}: get: description: Returns a greeting to the user! parameters: - name: user in: path type: string required: true description: The name of the user to greet. responses: 200: description: Returns the greeting. schema: type: string 400: description: Invalid characters in "user" were provided. ``` There are several benefits of using OpenAPI: - You can improve cross-team collaboration by allowing teammates to quickly experiment with endpoints by providing a frontend - You can quickly get an understanding of endpoints that your teammates have made - It's widely used, so you can get an understanding of official APIs that utilise it much faster - We can generate code from it as it's machine parseable! ## Adding OpenAPI to a Rust API ### utoipa Adding an OpenAPI specification to a Rust API can be done with the `utoipa` family of crates. `utoipa` is a crate that primarily uses macros to set up the OpenAPI specification. There is also support for frontend GUIs like Swagger UI, Redoc and Rapidoc that allow you to visualise working with your API A simple Axum example that shows a JSON representation of your OpenAPI specification looks like this: ```rust use std::net::SocketAddr; use axum::{routing::get, Json}; use utoipa::OpenApi; #[derive(OpenApi)] #[openapi(paths(openapi))] struct ApiDoc; /// Return JSON version of an OpenAPI schema #[utoipa::path( get, path = "/api-docs/openapi.json", responses( (status = 200, description = "JSON file", body = ()) ) )] async fn openapi() -> Json { Json(ApiDoc::openapi()) } #[tokio::main] async fn main() { let socket_address: SocketAddr = "127.0.0.1:8080".parse().unwrap(); let listener = tokio::net::TcpListener::bind(socket_address).await.unwrap(); let app = axum::Router::new().route("/api-docs/openapi.json", get(openapi)); axum::serve(listener, app.into_make_service()) .await .unwrap() } ``` Let's break this down. We have the following: - An `ApiDoc` struct that takes the `OpenApi` derive macro and sets all the attributes required to serve the OpenAPI specification from your API. - We have an attribute macro above our function handler. This macro will document the given information about a handler endpoint and show it in the OpenAPI spec when we open it. - We have a "list" of responses in the macro. Note that because we have no exact type to give to OpenAPI, we leave the body as `()`. Interested in checking out what attributes the `utoipa::path` macro can take? Have a look [here.](https://docs.rs/utoipa/latest/utoipa/attr.path.html) If you run this code and visit `[localhost:8080/api-docs/openapi.json](http://localhost:8080/api-docs/openapi.json)` you should see a JSON response of the API specification. This is typically good enough for just providing a basic representation. However, for internal exploration you may want to add a GUI to your OpenAPI specification. You can do this by installing the `utoipa_swagger_ui` crate and changing your `Router` to the following: ```rust let app = Router::new().merge( SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()) ); ``` If you run your code again and go to `localhost:8080/swagger-ui`, you'll get a Swagger UI menu that should look something like this: ![Swagger UI tab unexpanded](/images/blog/openapi-rust-article/utoipa-1.png) Two small things to note here are that `utoipa-service` is taken from the crate name and `crate` is the module where our route comes from. Here because our function is taken from the top level, it says `crate` - but we can fix this later on if we wanted by putting the route in a module. If we then expand this section by clicking on the route, it will allow us to then execute the endpoint! Pretty helpful, huh? ![Swagger UI tab expanded](/images/blog/openapi-rust-article/utoipa-2.png) The `utoipa` crate is quite comprehensive, so you can be assured that it will support mostly everything you want to do. If you'd like to check out their documentation, you can do so [here.](https://docs.rs/utoipa/latest/utoipa/) When it comes to having an API with hundreds or thousands of endpoints though, this may not be entirely ergonomic. You'll be spending quite a lot of time writing macros which can bloat your files. To that end, you can also use the [utoipauto](https://github.com/ProbablyClem/utoipauto) crate which lets you automate all of the work with only one macro. However, this adds additional compilation time. Whether you'll want to use it depends on your use case. ### poem-openapi Should you be happening to use the [Poem](https://github.com/poem-web/poem) framework, you can also use the `poem-openapi` crate to add OpenAPI functionality to your Poem service. Similarly to the `utoipa` crate, `poem-openapi` also uses macros to get OpenAPI documentation. ```rust use poem::{listener::TcpListener, Route}; use poem_openapi::{param::Query, payload::PlainText, OpenApi, OpenApiService}; struct Api; #[OpenApi] impl Api { #[oai(path = "/hello", method = "get")] async fn index(&self, name: Query>) -> PlainText { match name.0 { Some(name) => PlainText(format!("hello, {}!", name)), None => PlainText("hello!".to_string()), } } } #[tokio::main] async fn main() -> Result<(), std::io::Error> { let api_service = OpenApiService::new(Api, "Hello World", "1.0").server("http://localhost:3000/api"); let ui = api_service.swagger_ui(); let app = Route::new().nest("/api", api_service).nest("/", ui); poem::Server::new(TcpListener::bind("0.0.0.0:3000")) .run(app) .await } ``` Running this code will also generate a Swagger UI GUI as above, but at a different endpoint (`localhost:3000/api`). ## Generating Rust from OpenAPI specifications Now let's talk about generating Rust code from OpenAPI specifications. The OpenAPI collective have made a tool to generate a server client library - Rust support included! We'll be using `npm` to install the OpenAPI generator. You can install it with the following shell snippet: ```bash npm install @openapitools/openapi-generator-cli -g ``` There are also alternative ways to install the OpenAPI generator, which you can check out [here.](https://openapi-generator.tech/docs/installation/) Next, we'll need a specification to generate a client library from. The generator takes YAML or JSON files as input. Thankfully for us, we already have a JSON filefrom the OpenAPI service we just built. If we head to `localhost:8080/api-docs/openapi.json`, we can select the Raw Text option, prettify it and then put it all into a JSON file for input. Our file name will be `utoipa-client.json`. Next, we'll actually generate the client code. This can be done with the following shell snippet: ```bash npx @openapitools/openapi-generator-cli generate -i utoipa-client.json -g rust -o ./utoipa-client ``` This looks like quite a long command! What's happening here? - The `-i` flag is our input file - The `-g` flag is for the generator we should use (in this case, the `rust` one). Generators are not necessarily one-per-language, hence the flag convention. - The `-o` flag is for the output directory. If the directory doesn't exist, the generator will attempt to create it. Once done, you should see a new Rust crate in the automatically generated `utoipa-client` folder. At this point in time, the generator is mostly correct. However, your generated code can have syntactical errors if your OpenAPI specification input is malformed. You can find out more about this [here.](https://docs.rs/openapi_lib_generator/latest/openapi_lib_generator/) Interested in further customization? You can check out more OpenAPI generator configuration options [here.](https://openapi-generator.tech/docs/generators/rust) ## Working with OpenAPI specifications directly in Rust Interested in building tools that use the OpenAPI specification? There's a crate for that! Using the `openapiv3` crate, you can also deserialize (and serialize) to and from the OpenAPI spec format. Below, we deserialize it from a JSON string - you would additionally need `serde_json` installed: ```rust use serde_json; use openapiv3::OpenAPI; fn main() { let data = include_str!("openapi.json"); let openapi: OpenAPI = serde_json::from_str(data) .expect("Could not deserialize input"); println!("{:?}", openapi); } ``` There are several crates for handling this; notably, the `openapiv3` crate only supports the V3 specification. For V3.1 you want to use the `oas3` crate, which can take both YAML and JSON: ```rust fn main() { match oas3::from_path("path/to/openapi.yaml") { Ok(spec) => println!("spec: {:?}", spec), Err(err) => println!("error: {}", err) } } ``` ## Finishing up Thanks for reading! With this article, you should be able to tackle OpenAPI with Rust no problem. Read more: - [Get started with Axum](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - [Learn about sending your logs to Datadog](https://www.shuttle.dev/blog/2024/03/27/datadog-rust) - [Learn about tools to help your Rust productivity](https://www.shuttle.dev/blog/2024/02/15/best-rust-tooling) --- # Send logs to Grafana Loki with Rust Source: https://www.shuttle.dev/blog/2024/03/28/grafana-rust Date: 28 March 2024 Author: josh Tags: rust, grafana, guide Exploring how to send logs to a Grafana Loki instance using Rust, without Promtail Hello world! We will look at how you can leverage Grafana Loki for log storage and analysis. Application monitoring tools are a crucial part of monitoring and observability. They allow you to examine exactly how your application works, what is going in/out, and what is going wrong. Using application monitoring tools saves time and money by being able to fix production issues faster and retaining users. At the end of this article, you'll have a Rust web service deployed freely to Shuttle that logs traces to Grafana. Interested in checking out the final repository? You can find that [here.](https://github.com/joshua-mo-143/shuttle-grafana-example) ## Why should I use Grafana Loki? Loki is a logging service for Grafana designed with scalability and cost-effectiveness in mind. Rather than indexing the contents of your logs, it only stores the metadata and labels. A set of labels for each log stream is also used. Every unique set of labels represents each new stream - so if you add or remove any labels from a stream for example, you create an entirely new stream. With Grafana, you can also additionally visualize all of your data quite easily using either pre-created dashboards or making your own. ## Getting started ### Pre-requisites To get the most out of this article, you need to either self-host Grafana services or sign up to Grafana Cloud. For this article we'll mostly be referencing Grafana Cloud as this is the easiest way to use their services (without manual setup). To get started, you will want an API token that has the `write:logs` permissions. This can be done from Grafana Cloud user management. Make sure you save your Grafana user and API token variables, as you'll need them in just a little bit. For initializing our service, we'll use `shuttle init --template axum` (requires `cargo-shuttle` installed) to create a new Shuttle project with the Axum template. We will then add the following dependencies with this shell snippet: ```bash cargo add url cargo add tracing-subscriber -F fmt,env-filter cargo add tracing-loki cargo add tracing cargo add base64 cargo add shuttle-runtime --no-default-features ``` We'll also additionally need to add the `#[shuttle_runtime::Secrets]` annotation macro to our main function. This allows us to automatically grab secrets from our Secrets.toml file when we use `shuttle run` to run our Shuttle service. It should look like this: ```rust [..] use shuttle_runtime::SecretStore; #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore ) -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/", get(hello_world)); Ok(router.into()) } ``` The Secrets.toml file will be located at the project folder root and takes a key-value format. ```rust GRAFANA_USER = "" GRAFANA_API_KEY = "" ``` ## Building To get started, we will be using the `tracing_loki` crate to build the base of our tracing subscriber. `tracing_loki` allows us to create a `tracing_subscriber` layer that collects and exports logs to a Grafana Loki instance. On creation of a Grafana Cloud account we are given a free data source by default for Loki, which `tracing_loki` is compatible with. The default Grafana data source uses the basic authentication scheme, which you can find more about [here.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme) Later on when we create the `tracing_loki` tracing layer, we will add this as a HTTP header. We'll start by grabbing our secrets and making a Base64 string out of them: ```rust fn init_grafana_subscriber(store: SecretStore) { let grafana_user = store.get("GRAFANA_USER").unwrap(); let grafana_password = store.get("GRAFANA_API_KEY").unwrap(); let basic_auth = format!("{grafana_user}:{grafana_password}"); let encoded_basic_auth = BASE64_STANDARD.encode(basic_auth.as_bytes()); // .. rest of code } ``` Next, we'll need to create the `tracing_loki` layer which sends the logs to a Grafana instance. A few things are going on in the below snippet: - We add a label (with a key and a value) - We add an extra field for the Process ID (PID). This also takes a key-value format, so if you wanted to add anything else, you would do it here. - We set the HTTP header using the `.http_header()` function, setting the `Authorization` header as required. ```rust use url::Url; let url = Url::parse("https://logs-prod-012.grafana.net").expect("Failed to parse Grafana URL"); let (layer, task) = tracing_loki ::builder() .label("application", "shuttle-grafana") .unwrap() .extra_field("pid", format!("{}", process::id())) .unwrap() .http_header("Authorization", format!("Basic {encoded_basic_auth}")) .unwrap() .build_url(url) .unwrap(); ``` Additionally, we will want to create an `EnvFilter`. Having the tracing subscriber set at the default `trace` logging level can be useful. However, due to parsing headers and other similar actions that all use `trace!` spans, there are quite a lot of them. This can lead you to go over the free tier limits unintentionally. Here, we will set the default directive so that we only get errors where the logging level is `DEBUG` or above (i.e. warning or an error). ```rust use tracing_subscriber::filter{EnvFilter, LevelFilter}; let filter = EnvFilter::builder() .with_default_directive(LevelFilter::DEBUG.into()) .parse("").unwrap(); ``` The last thing to do is to create a tracing subscriber with the layers that we've created, and then initialise it! You may have noticed earlier that the `tracing_loki::builder()` method also generates a task that deals with log aggregation. We will need to spawn a Tokio task to handle this. ```rust // We need to register our layer with `tracing`. tracing_subscriber::registry() .with(filter) .with(tracing_subscriber::fmt::Layer::new()) .with(layer) // One could add more layers here, for example logging to stdout: // .with(tracing_subscriber::fmt::Layer::new()) .init(); // The background task needs to be spawned so the logs actually get // delivered. tokio::spawn(task); ``` Now the function is done! We can add it to our `fn main` at the start of the function. ```rust #[tracing::instrument] async fn hello_world() -> &'static str { tracing::debug!("An event happened!"); "Hello, world!" } #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore ) -> shuttle_axum::ShuttleAxum { setup_tracing(&secrets); let router = Router::new().route("/", get(hello_world)); Ok(router.into()) } ``` When using `shuttle run` now, your terminal will become populated with traces. If you visit `localhost:8000` in the browser, you should see a debug tracing event with the description `An event happened!` in your traces. Note that it may take Grafana 5-10 minutes to receive your traces. ## Reading your logs To read your logs, you need to create a dashboard for your data source. Head over to your Grafana Cloud instance, find the Grafana Cloud data source and create a dashboard for it. Then once you're on the dashboard, you can query your logs! A best practices guide for building Grafana dashboards can be found [here](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/best-practices/). The label we added to our logs should show up under the `Labels` column. It will contain the extra label we put in (the `application` label), as well as the logging level of the trace. The trace will also contain exactly what was in the trace message. If you need to add fields in the `#[tracing::instrument]` macro, you can do so and it will show up in Grafana. You can also find a guide for understanding labels [here.](https://grafana.com/docs/loki/latest/get-started/labels/) ## Deploying To deploy, simply use `shuttle deploy` and watch the magic happen! ## Finishing up Thanks for reading! Application monitoring is just one step to ensuring our Rust web services are performing better than ever. By using instrumentation, we can reduce the need for manual debugging. Read more: - [Read our guide to the tracing libraries](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) - [Send logs to Datadog with Rust](https://www.shuttle.dev/blog/2024/03/27/datadog-rust) - [8 tools to help you be more productive with Rust](https://www.shuttle.dev/blog/2024/02/15/best-rust-tooling) --- # Sending Logs to Datadog with Rust Source: https://www.shuttle.dev/blog/2024/03/27/datadog-rust Date: 27 March 2024 Author: roberto Tags: rust, datadog, guide Sending logs to Datadog with Rust, without Datadog Agent ## Some words about observability As we all know, being able to '_see_' what's going on in our services can be critical in many ways. We can easily find bugs or identify undesired behaviors, and it's certainly an invaluable tool at our disposal. Observability, in software, refers to the **ability to understand the state of a system and its behavior** by collecting, analyzing, and presenting data about its various components and interactions. This enables engineers to diagnose and resolve issues and make informed decisions about system health and performance. Observability is **critical for ensuring the reliability, scalability, and performance** of modern systems, and is becoming increasingly important as software continues to play a larger role in our daily lives. Fortunately, in the Rust ecosystem, we have [Tokio Tracing](https://docs.rs/tracing/latest/tracing/) which is a powerful framework for **instrumenting** Rust programs to collect structured, event-based diagnostic information. It provides a convenient and flexible API for collecting and viewing traces of events in your application and you can easily **add context and structure to your traces**, making it easier to identify bottlenecks and debug issues. ## Shuttle logs A while ago, I wrote a [post](https://robertohuertas.com/2023/01/09/shuttle-rust-backend-deployment/) about [Shuttle](https://www.shuttle.dev/), where I explained how ridiculously easy it is to deploy a Rust backend to the cloud by using their [CLI tool](https://docs.shuttle.dev/introduction/quick-start). [Shuttle](https://www.shuttle.dev/) is still in beta, and although its observability features are not really polished yet, they offer [support](https://docs.shuttle.dev/introduction/telemetry) for [Tokio Tracing](https://docs.rs/tracing/latest/tracing/) and a way to [view logs](https://docs.shuttle.dev/introduction/telemetry#viewing-logs) by using their CLI tool. By simply running `cargo shuttle logs --follow`, you will be able to see something like this: ![shuttle logs](https://robertohuertas.com/assets/images/shuttle-datadog/shuttle-cli-logs.png) This is great for simple applications, but what if you want to send your logs to a **more powerful tool** like [Datadog](https://datadoghq.com)? Well, in this post, **I'll show you how to do it**. ## Datadog [Datadog](https://datadoghq.com) is a **monitoring and observability platform** that provides a **single pane of glass** for your infrastructure and applications. It is a **cloud-based** service that allows you to **collect, aggregate and analyze** your data, and it is **extremely powerful**. > As a disclaimer, I must say that I'm currently working at [Datadog](https://datadoghq.com), so I'm a bit biased, but I'm also a huge fan of the product and I think it's a great tool for developers 😅. Most of the time, the easiest way to send anything to the [Datadog platform](https://www.datadoghq.com/observability-platform/) is by using the [Datadog Agent](https://docs.datadoghq.com/agent/), but in this case, as **we cannot install it** in any way, we will use a **small library I created for the occasion** called [dd-tracing-layer](https://docs.rs/dd-tracing-layer/latest/dd_tracing_layer/), which happens to be using the [Datadog HTTP API](https://docs.datadoghq.com/api/latest/logs/) under the hood to send logs to the [Datadog platform](https://www.datadoghq.com/observability-platform/). ## How to use tracing with Shuttle If we check the [Shuttle documentation](https://docs.shuttle.dev/configuration/logs), we can read this: > Shuttle will record anything your application writes to stdout, e.g. a tracing or log crate configured to write to stdout, or simply println!. By default, Shuttle will set up a global tracing subscriber behind the scenes. ```rust // [...] use tracing::info; #[shuttle_runtime::main] async fn axum(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleAxum { info!("Running database migration"); pool.execute(include_str!("../schema.sql")) .await .map_err(CustomError::new)?; // [...] } ``` So, as you can see, it seems that the Shuttle macro is already instantiating and initializing a [tracing subscriber](https://docs.rs/tracing/latest/tracing/trait.Subscriber.html) for us. This is pretty **convenient for most of the simple cases**, but unfortunately, it's not enough for our purposes. Ideally, if we had access to the underlying infrastructure, we could probably install the [Datadog Agent](https://docs.datadoghq.com/agent/) and configure it to send our logs directly to [Datadog](https://datadoghq.com), or even use [AWS Lambda functions](https://docs.datadoghq.com/logs/guide/send-aws-services-logs-with-the-datadog-lambda-function/?tab=awsconsole) or [Azure Event Hub + Azure Functions](https://docs.datadoghq.com/integrations/azure/?tab=azurecliv20#log-collection) in case we were facing some specific cloud scenarios. > You can check the [Datadog docs for log collection and integrations](https://docs.datadoghq.com/logs/log_collection/) if you want to learn more. Those solutions are generally great because they allow us to remove the burden of sending our logs to [Datadog](https://datadoghq.com) from our application, thus becoming the **responsibility of the platform** itself. If we could do something like that with [Shuttle](https://www.shuttle.dev/), it would be great. But, as we just mentioned, in the case of [Shuttle](https://www.shuttle.dev/), we **don't have access to the underlying infrastructure**, so we need to find a way to send our logs to [Datadog](https://datadoghq.com) from our application. And that's what we are going to try to do in this post. ## Getting access to the subscriber So, the basic idea is to add a new [tracing layer](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/) to the subscriber which will be responsible for sending our logs to [Datadog](https://datadoghq.com). But for that, we'll need to get **access to the subscriber instance prior to its initialization**, and it turns out that [Shuttle](https://www.shuttle.dev/) provides a way to do that just by disabling the default features on `shuttle-runtime` crate. ```toml shuttle-runtime = { version = "*" default-features = false } ``` ## Creating our project As a walkthrough, we are going to create a new [Shuttle](https://www.shuttle.dev/) project from scratch. The idea is to build a simple REST API using [Axum](https://docs.rs/axum/latest/axum/) and send our logs to [Datadog](https://datadoghq.com) using the [dd-tracing-layer](https://crates.io/crates/dd-tracing-layer) crate. Although I'm going to describe all the steps you need to take to make this work, you can see the **final state of the project** in this [GitHub repository](https://github.com/robertohuertasm/shuttle-datadog-logs). Feel free to use it as a reference. ### Initializing the project First of all, we need to create a new [Shuttle](https://www.shuttle.dev/) project. You can do that by using the [Shuttle CLI](https://docs.shuttle.dev/getting-started/cli): ```bash cargo shuttle init --template axum ``` Follow the instructions and you should have a new project ready to go. I called mine `shuttle-datadog-logs`, but use the name you want. ### Adding some dependencies In our example, we are going to be using [Shuttle Secrets](https://docs.shuttle.dev/resources/shuttle-secrets), [Tokio Tracing](https://docs.rs/tracing/latest/tracing/) and [dd-tracing-layer](https://crates.io/crates/dd-tracing-layer). Make sure you have the following dependencies in your `Cargo.toml` file: ```toml [dependencies] axum = "0.7.4" shuttle-axum = "0.42.0" shuttle-runtime = { version = "0.42.0", default-features = false } tokio = "1" # tracing tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "time"] } dd-tracing-layer = "0.1" ``` ### Instrumenting a little bit the default project Now that we have our dependencies ready, we can **start instrumenting** our project a little bit. Note that we have added the `#[instrument]` macro to the `hello_world` function and added a `tracing::info!` and a `tracing::debug!` log to it. We have also added an info log to the `axum` function. ```rust // [...] use tracing::instrument; #[instrument] async fn hello_world() -> &'static str { tracing::info!("Saying hello"); tracing::debug!("Saying hello for debug level only"); "Hello, world!" } #[shuttle_runtime::main] async fn axum() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/", get(hello_world)); tracing::info!("Starting axum service"); Ok(router.into()) } ``` At this point, if you try to run the project locally by using the `shuttle run` command, you should see none of our logs. That's ok, as we haven't initialized a [tracing subscriber](https://docs.rs/tracing/latest/tracing/trait.Subscriber.html) yet. ### Adding our tracing subscriber The first thing we're going to do is to add a [tracing subscriber](https://docs.rs/tracing/latest/tracing/trait.Subscriber.html) to our application. Then we will add several [layers](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html) to it: - [EnvFilter layer](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) to set the tracing level according to a variable's value. - [Format layer](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/index.html) to format the logs. We will be using JSON format. - [Datadog Tracing layer](https://docs.rs/dd-tracing-layer/) to send our logs to [Datadog](https://datadoghq.com). Apart from that, we're also going to add support for [Shuttle Secrets](https://docs.shuttle.dev/resources/shuttle-secrets). Let's do it! Make sure your `axum` function looks like this: ```rust use axum::{routing::get, Router}; use dd_tracing_layer::{DatadogOptions, Region}; use shuttle_runtime::SecretStore; use tracing::instrument; use tracing_subscriber::prelude::*; // version of our app to be sent to Datadog const VERSION: &'static str = "version:0.1.0"; // [...] #[shuttle_runtime::main] async fn axum(#[shuttle_runtime::Secrets] secret_store: SecretStore) -> shuttle_axum::ShuttleAxum { // getting the Datadog Key from the secrets let dd_api_key = secret_store .get("DD_API_KEY") .expect("DD_API_KEY not found"); // getting the Datadog tags from the secrets let tags = secret_store .get("DD_TAGS") .map(|tags| format!("{},{}", tags, VERSION)) .unwrap_or(VERSION.to_string()); // getting the log level from the secrets and defaulting to info let log_level = secret_store.get("LOG_LEVEL").unwrap_or("INFO".to_string()); // datadog tracing layer let dd_layer = dd_tracing_layer::create( DatadogOptions::new( // first parameter is the name of the service "shuttle-datadog-logs", // this is the Datadog API Key dd_api_key, ) // this is the default, so it can be omitted .with_region(Region::US1) // adding some optional tags .with_tags(tags), ); // filter layer let filter_layer = tracing_subscriber::EnvFilter::try_new(log_level).expect("failed to set log level"); // format layer let fmt_layer = tracing_subscriber::fmt::layer() .with_ansi(true) .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339()) .json() .flatten_event(true) .with_target(true) .with_span_list(true); // starting the tracing subscriber tracing_subscriber::registry() .with(filter_layer) .with(fmt_layer) .with(dd_layer) .init(); // starting the server let router = Router::new().route("/", get(hello_world)); tracing::info!("Starting axum service"); Ok(router.into()) } ``` There are many things going on in this code, so take your time to go through it. ### Secrets Before running our project, there's still a thing we have to deal with: **secrets**. As you can see in the code above, we are using the `#[shuttle_runtime::Secrets]` macro to get the [Datadog](https://datadoghq.com) API key, the tags and the log level. [Shuttle Secrets](https://docs.shuttle.dev/resources/shuttle-secrets) relies on having a `Secrets.toml` file in the root of our project containing all the secrets, and it also supports having a `Secrets.dev.toml` file for local development. You can learn more about this convention in the [Shuttle Secrets documentation](https://docs.shuttle.dev/resources/shuttle-secrets#local-secrets). So, let's create two files in the root of our project: `Secrets.dev.toml` ```toml DD_API_KEY = "your-datadog-api-key" DD_TAGS = "env:dev,service:shuttle-datadog-logs" # setting info as the default log level, but debug for our project LOG_LEVEL = "INFO,shuttle_datadog_logs=DEBUG" ``` and `Secrets.toml` ```toml DD_API_KEY = "your-datadog-api-key" DD_TAGS = "env:prod,service:shuttle-datadog-logs" LOG_LEVEL = "INFO" ``` > Remember to add these files to your `.gitignore` file! ### Running the project Now, run `cargo shuttle run` and go to `http://localhost:8000` in your browser to see our "Hello, world!" message. Alternatively, you can also use `curl` to test the endpoint: ```bash curl -i http://localhost:8000 ``` You should be able to see the logs in your terminal now. But remember... this endpoint was instrumented! So, if everything went well, we should be able to see the logs in [Datadog](https://app.datadoghq.com/logs). Let's check it out! 👀 ![Datadog logs](https://robertohuertas.com/assets/images/shuttle-datadog/datadog-logs-local.png) It works! 🎉 ## Conclusion As you can see, it's pretty easy to send your logs to [Datadog](https://datadoghq.com) from your [Shuttle](https://www.shuttle.dev/) powered backend. Again, you can see the full code in [this GitHub repository](https://github.com/robertohuertasm/shuttle-datadog-logs). I hope you've enjoyed it! 😁 --- # Everything you need to know about testing in Rust Source: https://www.shuttle.dev/blog/2024/03/21/testing-in-rust Date: 21 March 2024 Author: josh Tags: rust, testing, guide Talking about everything testing in Rust, crates and tools included Testing is an important tool. It cuts down on production errors and allows us to check for regressions. It's easy to see the value of testing - it saves time (and money!) trying to find regressions later on. By the end of this article, you'll have a comprehensive understanding of implementing different types of testing in Rust. ## Rust unit testing ### Set up a simple test To get started, we just need to define a module for our tests: ```rust #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!("hello world!", "hello world!"); } } ``` If we add this to any of our Rust files in `src` and then run `cargo test`, it will compile all of our dependency and run this test module. The attribute macros ensure that this module only gets run when using `cargo test`. The `#[test]` macro declares the function as a test, enabling it to be run by cargo test. There are several macros we can use to help assist with our testing: - `assert!()` which attempts to assert that the variable given equates to `true` and failing if not (for example, checking whether a Result is the `Ok` variant with `result.is_ok()`). A second variable can be added at the end for a custom message. - `assert_eq!()` as used above which compares two items and panics if not equating to `true`. A third variable can be added at the end for a custom message. - `debug_assert!()` which does the same thing as `assert!()` but not in `--release` mode. A second variable can be added at the end for a custom message. ### Testing in an async context Sometimes, you might need to test in an async context (for example, while using `tokio`). You can do this by simply using the following macro: ```rust #[tokio::test] async fn my_test() { assert_eq!("hello world!", "hello world!", "Somehow this failed? :("); } ``` `tokio::test` provides a convenient abstraction for testing with the Tokio runtime. A full explanation of macro attributes can be found [here.](https://docs.rs/tokio-macros/latest/tokio_macros/attr.test.html) ## Dev dependencies What about adding crates but only for testing? We can do that with the `--dev` flag. For example, if we wanted to add the `hyper` crate for HTTP request testing we could add it with the following shell snippet: ```rust cargo add hyper --dev -F client ``` Now when we run our crate, `hyper` will only be built if we need to do testing! In `Cargo.toml`, the dev dependencies section will look like this: ```rust [dev-dependencies] hyper = { version = "1.2.0", features = ["client"] } ``` ## Sharing common functions Some unit tests may want to share common functions. For example, setting up required functionality across a bunch of tests. As a starting point, this can be done by including the function in the same module as your test, or in a different module. However, this can get quite messy. Typically the best way to organise this is to put shared functionality in a parent mod. If you have quite a lot of setup for your application testing, a more idiomatic way to do this would be to have a local unpublished crate that has all the testing utilities you need. Then import the local crate as a dev dependency and work from there. For example, let's say you have a crate called `test_utilities` with the following code: ```rust // src/lib.rs async fn do_a_thing() { println!("This function does a thing!"); } ``` Let's say you have a project folder that looks like this: ``` ├── Cargo.toml ├── src │ └── lib.rs ├── tests │ └── integration.rs └── utilities ├── Cargo.toml └── src └── lib.rs ``` You would want to make sure that you are importing `utilities` as a dev dependency in `Cargo.toml`: ```rust [dev-dependencies] utilities = { path = "utilities" } ``` ## Rust integration testing To set up integration tests, create a new folder in your project root called `tests`. You can then create a `.rs` file named anything you like. We'll call our example file `integration_tests.rs`. Imagine you have a function called `return_one()` in your main Rust application called `returner` that simply returns the number 1 as an `i32`. ```rust // src/lib.rs fn return_one() -> i32 { 1 } ``` We then import the crate using the crate name in our test and reference the method. ```rust // tests/integration_tests.rs #[test] fn test_returns_one() { assert_eq!(returner::return_one(), 1); } ``` The tests folder isn't purely for tests, however! As mentioned before, we can create a module folder in our tests to add extra utility functions. We'll create a folder called `common` that should have two files: a `mod.rs` file and a `postgres.rs` file. The contents of these files should be as follows. ```rust // tests/common/mod.rs mod postgres; // tests/common/postgres.rs use sqlx::PgPool; async fn setup() -> PgPool { let pool = PgPool::connect("postgres://postgres:postgres@localhost:5432/postgres") .await.unwrap(); pool } ``` Now we can use it back in our test function: ```rust // importing common module. mod common; #[tokio::test] fn test_add() { // using common code. let db = common::setup(); let query = sqlx::query("SELECT 'hello world!'") .execute(&db) .await; assert!(query.is_ok()) } ``` It's generally suggested to group similar tests together. This allows you to be able to find tests easily. ## Rust testing library crates ### pretty_assertions With a simple macro, `pretty_assertions` will make your assertion fails much easier to read. ```rust use pretty_assertions::assert_eq; ``` Assertion fails will now look something like this: ![Pretty assertions diff](/images/blog/pretty-assertions-testing-rust.png) All in all, a simple crate that does one thing to make your life much easier. Particularly if you need to parse large objects or strings! ### tempfile While [tempfile](https://docs.rs/tempfile/latest/tempfile/) is not strictly a testing dependency library, it does make testing much easier! It allows setup and teardown of temporary file directories and files via `tempfile::TempDir` and `tempfile::tempfile()` respectively. You can then extract the `PathBuf` and use it wherever you want (for example, in functions that See below for a small example taken from the docs: ```rust use tempfile::tempdir; use std::fs::File; use std::io::{self, Write}; fn run() { // Create a directory inside of `std::env::temp_dir()`. let dir = tempdir()?; let file_path = dir.path().join("my-temporary-note.txt"); let mut file = File::create(file_path)?; writeln!(file, "Brian was here. Briefly.")?; // By closing the `TempDir` explicitly, we can check that it has // been deleted successfully. If we don't close it explicitly, // the directory will still be deleted when `dir` goes out // of scope, but we won't know whether deleting the directory // succeeded. drop(file); dir.close()?; } ``` This is quite useful for situations that require filesystem handling (for example, if you're downloading or generating files somewhere). You can find out more [here.](https://docs.rs/tempdir/latest/tempdir/) ### rstest [rstest](https://github.com/la10736/rstest) is a Rust library aimed at making testing easier by allowing fixtures to be passed in as function arguments. Here is a short snippet showing how you can easily use it to create fixtures, then use the names of the fixture functions in your `#[rstest]` tests: ```rust #[fixture] fn my_fixture() -> i32 { 1 } #[rstest] fn assert_that_one_equals_one(my_fixture: i32) { assert_eq!(my_fixture, 1); } ``` Additionally, we can also add attribute macro for cases with support for async. This avoids needing to create extra functions. ```rust use rstest::*; use std::future::Future; #[rstest] #[case(2, async { 4 })] #[case(21, async { 42 })] #[tokio::test] async fn my_async_test(#[case] a: u32, #[case] #[future] result: u32) { assert_eq!(2 * a, result.await); } ``` As you can see here, the second variable is an async function that requires a future. We can simply add the `#[future]` attribute to make it async-friendly. ### proptest Property testing is as important a facet in Rust as any other. Simply put: it's testing against the properties of an object or function until it crashes (or the input finishes). For example, consider an enum with a non-standard `to_string()` implementation (granted from the `std::fmt::Display` trait), the property testing library may try to use non-UTF8 strings. This can fail the test depending on whether or not you've accounted for non-UTF8 strings. [proptest](https://docs.rs/proptest/latest/proptest/) allows us to carry out property testing by matching random inputs against a test function. Check out the snippet below: ```rust use proptest::prelude::*; proptest! { #[test] fn i64_abs_is_never_negative_above_min(a in 1..1000i32) { assert!(a.abs() >= 0); } } ``` This short snippet runs 1000 tests and asserts that the absolute value is above or equal to 0. Similarly, we can also use regex to be able to find out whether or not a function properly covers all use cases: ```rust use proptest::prelude::*; proptest! { #[test] fn number_can_be_parsed_from_string(a in "[0-9]{0-8}") { assert!(a.parse::().is_ok()); } } ``` As you can see, the inputs can be very powerful. Interested in more? Check out the mdbook for proptest [here.](https://proptest-rs.github.io/proptest/intro.html) ## Test tooling for Rust The Rust base testing tools are sufficient for most basic use cases. However, there are certain cases where you will absolutely want the most up to date tooling. Here we will talk about a number of tools that you can use to speed up your testing productivity. ### cargo-nextest `cargo-nextest` is a test runner for Rust that improves a lot of the core testing functionality. To install, you can do so with the following snippet: ```bash cargo add cargo-nextest ``` For regular usage, you can get started by using `cargo nextest run` to run all of the tests in a given workspace. You can also use `cargo nextest list` to list all of the tests that you need to run! Detection for tests that are slow or leaky is supported by default by `cargo-nextest`. Retries are also supported via the `--retries` flag. Interested in checking out all of the things you can do with `cargo-nextest`? You can do that [here.](https://nexte.st/book/running.html#options-and-arguments) ### testcontainers [`testcontainers`](https://github.com/testcontainers/testcontainers-rs) is a tool that automatically spins up localised infrastructure for you to test on. The project is completely open source and free to use, as well as having a Rust SDK. You can install with this snippet: ```rust cargo add testcontainers cargo add testcontainers-modules ``` Spinning up infrastructure is gated by features on `testcontainers-modules`; for example, if you wanted a Postgres database you need to add the `postgres` feature. To use the test container, you would need to set up a setup command that sets up the container for you: ```rust use sqlx::PgPool; async fn setup() -> PgPool { let docker = Cli::default(); let node = docker.run(Postgres::default()); // prepare connection string let connection_string = &format!( "postgres://postgres:postgres@127.0.0.1:{}/postgres", node.get_host_port_ipv4(5432) ); let db: PgPool = PgPool::connect(&connection_string).await.unwrap(); db } ``` `testcontainers` automatically takes care of setup and teardown for you. Nothing else is required. Note that `testcontainers` is primarily built for isolated testing. If you wanted to run a test that requires a long-running instance, you may want to use an end-to-end test. For usage across multiple tests, you can use the `once_cell` crate and store it in a `once_cell::static::Lazy`: ```rust static TEST_CONTAINER: Lazy = Lazy::new(|| { let docker = Cli::default(); let node = docker.run(Postgres::default()); // prepare connection string let connection_string = &format!( "postgres://postgres:postgres@127.0.0.1:{}/postgres", node.get_host_port_ipv4(5432) ); let db: PgPool = PgPool::connect(&connection_string).await.unwrap(); db }); ``` ### cargo-fuzz `cargo-fuzz` is a crate designed to help you carry out fuzz testing on Rust projects. Fuzzing is an automated testing method that tries to find function inputs that fail and then finds the minimal version of the test case that can fail. Fuzzing is also often coupled with property-based testing, as they complement each other very well. While the crate itself isn't a fuzzer and actually invokes a fuzzer, it's still a very useful tool to have. It supports `libFuzzer` and can be extended to support others. Running `cargo fuzz init` will create a directory called `fuzz_targets` which contains a list of fuzzing targets. To use the `cargo-fuzz` library in your application, you need to use the macro. The below example shows how you can fuzz the `Url::parse()` method from the `url` crate. ```rust use url::Url; fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { let _ = url::Url::parse(s); } }); ``` Note here that we are aiming for a minimal implementation. Generally speaking, you want to test at the lowest possible level to be able to To run fuzzing against a generated target from the `fuzz_targets` directory, you can do so like this: ```bash cargo fuzz run ``` All in all, a pretty useful crate. You can also combine this with `proptest` for very efficient testing strategies. Interested in more? You can find a tutorial for using `cargo-fuzz` and `afl` (another fuzzing crate) [here.](https://rust-fuzz.github.io/book/introduction.html) ### cargo-mutants Finally: mutation testing! Mutation testing allows you to test your code coverage by scanning your code, then changing some variables and expecting some tests to fail or succeed depending on what has been changed. With [cargo-mutants](https://mutants.rs/), this can be done easily. To get started, you'll need to install it: ```bash cargo install cargo-mutants ``` For cargo-mutants to give useful results, your Rust project must already 1. Be built with `cargo build`, and 2. Have reliable non-flaky tests that run under either `cargo test` or `cargo nextest`. Flaky tests can invalidate the `cargo-mutants` result insights. Assuming tests pass normally, cargo-mutants will generate every mutant it can (subject to filters) and then runs `cargo build` and `cargo test` on each of them. Each mutant results in one of the following outcomes: - **caught** — A test failed with this mutant applied. This is a good sign about test coverage. - **missed** — No test failed with this mutation applied, which seems to indicate a gap in test coverage. Or, it may be that the mutant is undistinguishable from the correct code. You may wish to add a better test, or mark that the function should be skipped. - **unviable** — The attempted mutation doesn't compile. This is inconclusive about test coverage and no action is needed, but indicates an opportunity for cargo-mutants to either generate better mutants, or at least not generate unviable mutants. - **timeout** — The mutation caused the test suite to run for a long time, until it was eventually killed. You might want to investigate the cause and potentially mark the function to be skipped. Interested? You can find their docs [here](https://mutants.rs/welcome.html) - they're quite comprehensive! The main thing to take into account here is that mutation testing in Rust can be somewhat expensive. If you change one line of code, you need to scan and re-compile your program again which can be very expensive in terms of time. If you need to use `cargo-mutants` with CI, this can result in a very expensive CI bill. ### insta-rs Snapshot testing (also called approval testing) is also another form of testing you can do to help build up tests for a legacy system. It generally works as follows: - You bring your code into a test - Throw a variety of inputs at the tested function - Capture the output This builds up a "net" of snapshots that you can use to check for any regressions. With `insta`, you can capture inputs and insta will automatically manage the snapshotting for you. A snapshot capture can be as simple as this (taken from the docs): ```rust #[test] fn test_simple() { insta::assert_yaml_snapshot!(calculate_value()); } ``` When actually running your tests, you can use `cargo test` to run tests normally. However, if you have multiple snapshot assertions in one test, you may want to use `cargo insta test` instead which takes care of this for you. Once you've done the captures, you can use `cargo insta review` to review your snapshots! Snapshots can either be stored in `.snap` files, or inside of inline string literals in your Rust files. If you're interested in learning more, you can find the crate docs [here.](https://insta.rs/docs/) ## Finishing up Thanks for reading! Hopefully you've gotten a better understanding of how to test within a Rust context. With so many types of testing being available to us, it's never been easier to ensure our Rust applications are working as intended. Read more: - Check out our guide for [getting started with Tracing](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) - Learn more about [writing a REST API with Rust](https://www.shuttle.dev/blog/2024/01/31/write-a-rest-api-rust) - Check out our [recently updated article about using OAuth2 with Rust](https://www.shuttle.dev/blog/2023/08/30/using-oauth-with-axum) --- # Building a Notification Service in Rust with AWS SNS Source: https://www.shuttle.dev/blog/2024/03/20/notification-service-rust Date: 20 March 2024 Author: josh Tags: rust, aws, guide Building and deploying a notification service in Rust with Axum and AWS SNS Notifications are a very helpful tool for notifying services. This is particularly relevant if you're using microservice architecture. One notification mechanism we can use is AWS SNS (Simple Notification Service), which lets us create topics, subscribe to them and push messages to topics. By the end of this article, you'll have two fully functioning web services with the following overall functionality: - One web service will have a single endpoint that can receive a message from AWS SNS - One web service will handle the job of sending messages Interested in just deploying? You can find the repository [here](https://github.com/joshua-mo-143/shuttle-sns-ex), complete with instructions on how to deploy. ## Getting started ### Pre-requisites To get started, you will need two keys from AWS: - Your access key ID - Your secret access key ## Receiving AWS SNS messages To start with, let's have a look at how we can receive SNS messages. We can do this with `shuttle init`, picking Axum as our framework. This service will have a single endpoint that takes the SNS messages and prints the message out. To make this idiomatic, we'll want to create a struct called `SnsMessage` that implements `axum::FromRequest`. This will allow us to use it as an extractor, rather than trying to parse the POST requests from SNS manually. ```rust use axum::{routing::{get, post}, Router, response::{IntoResponse, Response}, http::StatusCode, extract::{Request, FromRequest}, Json, RequestExt }; use serde::Deserialize; #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] pub struct SnsMessage { #[serde(rename = "Type")] kind: String, message_id: String, topic_arn: String, subject: String, message: String, timestamp: String, signature_version: String, signature: String, signing_cert_url: String, unsubscribe_url: String } #[axum::async_trait] impl FromRequest for SnsMessage { type Rejection = Response; async fn from_request(req: Request, state: &S) -> Result { let headers = req.headers(); // if none of these headers are sent in the request // automatically send a BAD_REQUEST status code if !headers.contains_key("x-amz-sns-message-type") | !headers.contains_key("x-amz-message-id") | !headers.contains_key("x-amz-topic-arn") | !headers.contains_key("x-amz-subscription-arn") { return Err((StatusCode::BAD_REQUEST).into_response()) } let Json(payload): axum::Json = req.extract() .await.map_err(|_| (StatusCode::BAD_REQUEST).into_response())?; Ok(payload) } } ``` Note that this `FromRequest` implementation is primarily to show the fundamentals. In production use cases, you would want to verify the signature sent from AWS to make sure it's accurate. You can find out more about the basics of this [here.](https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html) To use our new struct, you can use `SnsMessage` as an extractor for a handler function like this: ```rust async fn receive_sns( sns_message: SnsMessage ) -> StatusCode { println!("{}", sns_message.message); StatusCode::OK } ``` You can find out more about receiving POST requests from AWS SNS [here.](https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html) To extend this, you may want to think about verifying the signature sent by AWS [here.](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) We'll tie this all together by adding the handler function to our router: ```rust use axum::routing::post; #[shuttle_runtime::main] async fn axum() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/", get(hello_world)) .route("/sns", post(receive_sns)); Ok(router.into()) } ``` Now it's complete! ### Deploying To deploy, you can use `shuttle deploy` (with `--allow-dirty` if on a Git branch and uncommitted changes). Take note of the deployment URL! We will need this later on. ## Sending AWS SNS messages Now for the second part of our notification service: sending messages to our receiver! ### Setup We'll get started by using `shuttle init` once again, picking Axum as our choice of framework. Make sure to follow the prompt until the end to finalise your project. Once done, we can install our dependencies with this script: ```rust cargo add aws-config@1.1.8 -F behavior-version-latest cargo add aws-credential-types@1.1.8 -F hardcoded-credentials cargo add aws-sdk-sns@1.18.0 cargo add anyhow@1.0.81 cargo add serde@1.0.197 -F derive cargo add serde-json@1.0.114 cargo add thiserror@1.0.58 ``` Remember your AWS keys from earlier? We'll add them to `Secrets.toml` file in the root of our project folder. It should look like this: ```rust AWS_ACCESS_KEY_ID = "your-access-key-id-here" AWS_SECRET_ACCESS_KEY = "your-secret-access-key-here" ``` ## Setting up our AWS config To get started, we'll want to add our resource annotations to our main function - which should look like this: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { // .. your code here } ``` When we use `shuttle run`, our secrets will now get provisioned to us without needing to do anything! Before we start adding more code, let's write our `AppState` struct to hold Axum state. This will allow our handler functions to access our AWS SNS client whenever we want. ```rust use aws_sdk_sns::Client; #[derive(Clone)] pub struct AppState { sns: Client, topic_arn: String } ``` Next, we'll want to add code to our main function that gets the secrets, creates `aws_credential_types::Credentials` and creates an AWS config from the credentials, as well as adding a region. We'll use `eu-west-02` to reduce latency (because the Shuttle servers are hosted on `eu-west-02`). We then create a new client, initialize our app state and then add it to the `axum::Router`; ```rust let access_key_id = secrets.get("AWS_ACCESS_KEY_ID").expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let secret_access_key = secrets.get("AWS_SECRET_ACCESS_KEY").expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let creds = Credentials::from_keys( access_key_id, secret_access_key, None ); let cfg = aws_config::from_env() .region(Region::new("eu-west-02")) .credentials_provider(creds) .load() .await; let sns = Client::new(&cfg); let state = AppState { sns }; let router = Router::new().route("/", get(hello_world)).with_state(state); ``` ## Error handling ### Error handling for the ShuttleAxum type The `shuttle_axum::ShuttleAxum` return type defaults to `anyhow::Error` for user errors. This doesn't affect handler functions. For functions used in the main function however, they will need to either be able to convert to `anyhow::Error` or be of the same type. ### Error handling for API route functions To create errors easily, we'll use the `thiserror` crate we installed to generate `From` implementations for our error type, as well as the error messages. Let's have a look. ```rust use thiserror::Error; use aws_sdk_sns::error::SdkError; use aws_sdk_sns::operation::publish::PublishError #[derive(Debug, Error)] pub enum ApiError { #[error("Error while publishing message: {0}")] PublishMessage(#[from] SdkError), } ``` We use the `thiserror::Error` derive macro to enable the attribute macros. A few things are happening here: - The `#[from]` implementation automatically derives `From` so that the given type will automatically turn into a given enum variant. Note however that if the error type is an enum, it will convert it regardless of the enum variant. If you want more custom error handling, you may want to implement `From` manually. - `#[error("..")]` automatically generates the `std::fmt::Display` implementation for our error type. Note that we need to use this on every enum variant, otherwise we will receive compile-time errors. - Using `thiserror::Error` automatically implements `std::error::Error` for `ApiError` so there's no need for us to re-implement it! Next, we'll want to implement `axum::response::IntoResponse` so that we can return `ApiError` from our Axum handler functions. ```rust use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; impl IntoResponse for ApiError { fn into_response(self) -> Response { (StatusCode::INTERNAL_SERVER_ERROR, self.to_string().into_response()) } } ``` Currently we just have all of our error variants return internal server errors. To go further into this, you could create a response depending on the internal error enum variant or source (via pattern matching). ### Topics To create a topic, we can use the AWS SNS client to create a builder object and then add tags or attributes to it depending on what we want. For the endpoint, it is mostly about creating the builder, appending the attributes and using `send()` to create it. ```rust async fn create_topic(sns: &Client, name: &str) -> AnyhowResult { let topic = sns.create_topic().name(name); let output = topic.send().await.unwrap(); println!("Topic created: {output:?}"); Ok(output.topic_arn.unwrap()) } ``` The output from sending the topic will contain the ARN. We need to store this to be able to retrieve a topic later on. Note that attempting to create two topics with the same name will return the current ARN instead of creating a new resource. If you're interested in customising this further, you can check out more about the `CreateTopicFluentBuilder` [here.](https://docs.rs/aws-sdk-sns/latest/aws_sdk_sns/operation/create_topic/builders/struct.CreateTopicFluentBuilder.html) ### Subscriptions Receiving published messages requires a topic subscription. Creating a subscription requires declaring an endpoint, the topic ARN as well as the protocol used. We can refer to the `SubscribeFluentBuilder` for this [here.](https://docs.rs/aws-sdk-sns/latest/aws_sdk_sns/operation/subscribe/builders/struct.SubscribeFluentBuilder.html) The function will look something like this: ```rust async fn subscribe_to_topic(sns: &Client, url: &str, arn: &str) -> AnyhowResult<()> { let sub = sns .subscribe() .protocol("https".to_string()) .endpoint(url) .topic_arn(arn); let output = sub .send() .await .map_err(|e| anyhow::anyhow!("error: {e}"))?; println!("New subscriber created: {output:?}"); Ok(()) } ``` There is not much to talk about here as it's primarily just setting it up and then sending it. If we wanted to, we could also create multiple subscriptions to the same topic ARN, giving us the benefit of being able to fan-out our notifications. Because we're setting up an HTTPS endpoint, we need to confirm the subscription. When we create the subscription, SNS will send a subscription confirmation message to our receiver. This requires our receiver to already be deployed. Because we already print out the confirmation message in our logs, we can easily access and visit the URL to confirm the subscription. You can find out more about this [here.](https://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.confirm.html) To check that the subscription exists, we can do this: ```rust async fn subscription_exists(sns: &Client, url: &str, arn: &str) -> AnyhowResult { let subscribers = sns .list_subscriptions() .send() .await .map_err(|e| anyhow::anyhow!("error: {e}"))?; if let Some(subs) = subscribers.subscriptions { if subs.iter().any(|sub| { sub.clone().endpoint.unwrap() == *url && sub.clone().topic_arn.unwrap() == *arn }) { return Ok(true); } return Ok(false); } Ok(false) } ``` ### Publishing messages To be able to publish messages, we'll need the topic ARN we want to subscribe to, the message as well as the subject. ```rust #[derive(Deserialize)] struct PublishMessageParams { message: String, subject: String } async fn publish_message( State(state): State, Json(json): Json ) -> Result { let res = state.sns.publish() .topic_arn(state.arn) .message(json.message) .subject(json.subject) .send().await?; Ok(StatusCode::OK) } ``` All of the endpoints who are subscribed to the topic will now automatically receive the message! Interested in customising this further? You can check out more about the `PublishFluentBuilder` [here.](https://docs.rs/aws-sdk-sns/latest/aws_sdk_sns/operation/publish/builders/struct.PublishFluentBuilder.html) ### Connecting it all together To connect it all, let's revisit our main function and fill it in with the new functions: ```rust #[shuttle_runtime::main] async fn main(#[shuttle_secrets::Secrets] secrets: SecretStore) -> shuttle_axum::ShuttleAxum { let access_key_id = secrets .get("AWS_ACCESS_KEY_ID") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let secret_access_key = secrets .get("AWS_SECRET_ACCESS_KEY") .expect("AWS_ACCESS_KEY_ID not set in Secrets.toml"); let creds = Credentials::from_keys(access_key_id, secret_access_key, None); let cfg = aws_config::from_env() .region(Region::new("eu-west-02")) .credentials_provider(creds) .load() .await; let sns = aws_sdk_sns::Client::new(&cfg); let topic_arn = create_topic(&sns, "my_topic").await?; // NOTE: Change this to your deployment URL for your receiver service! // The SNS receiver route should be on `/sns` as instructed previously let url = "https://sns-receiver.shuttleapp.rs/sns"; if !subscription_exists(&sns, url, &topic_arn).await? { subscribe_to_topic(&sns, url, &topic_arn).await?; } let state = AppState { sns, topic_arn }; let router = Router::new().route("/", get(hello_world)).with_state(state); Ok(router.into()) } ``` ## Deploying To deploy, we just need to use `shuttle deploy` (with the `--allow-dirty` flag if on an uncommitted Git branch) and watch the magic happen! ## Finishing up Thanks for reading! AWS SNS is a powerful tool to be able to get going with notifying services, particularly if you need fan-out from something like AWS SQS or other message queues. Read more: - Learn more about how to [competently write an API with Axum](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - Secure your API with [JWT authentication](https://www.shuttle.dev/blog/2024/02/21/using-jwt-auth-rust) - Learn about [getting started with logging in Rust](https://www.shuttle.dev/blog/2023/09/20/logging-in-rust) --- # Using PostHog with Rust Source: https://www.shuttle.dev/blog/2024/03/14/using-posthog-rust Date: 14 March 2024 Author: josh Tags: rust, posthog, guide Exploring how to use PostHog in a Rust application to be able to improve product analytics. Hello world! In this article, we're going to talk about how you can use Posthog with Rust to be able to track user behavior across an application. Analytics are more important than ever to be able to improve your product; Posthog makes it easy to do exactly that. ## How does PostHog work? Posthog works by allowing you to capture events and then convert those events into "insights" by aggregating the data. PostHog allows you to then query the data using HogQL (a DSL made by PostHog for querying data,, similar to SQL). Alhough there isn't an officially supported Rust SDK for PostHog, they do have a community-supported one (`posthog-rs`) that can capture events in your application. We'll also look at using `reqwest` to set up our own custom client for sending requests to the PostHog API! ## Capturing events We can get started with the `posthog-rs` client by creating it: ```rust use posthog::{ client, Event }; use std::env; let posthog_api_key = env::var("POSTHOG_API_KEY").expect("POSTHOG_API_KEY env var missing"); let client = client(posthog_api_key); ``` The event essentially allows us to add anything we want - for example, we can add user IDs, user cookies or timestamps and whatever else we need. Next, we'll want to make a function for sending an event to the PostHog API: ```rust fn send_posthog_event(client: &posthog::Client) { let mut event = Event::new("my_database_event", "1234"); event.insert_prop("action", "insert").unwrap(); client.capture(event).unwrap(); } ``` Now whenever we need to track an action, we can do so. Below is an example of using the client in an Axum handler function to be able to send an event: ```rust use axum::{http::StatusCode, extract::State}; #[derive(Clone)] struct AppState { db: sqlx::PgPool, posthog: posthog::Client } async fn add_to_database( State(state): State ) -> StatusCode { let res = sqlx::query("SELECT * FROM USERS") .fetch_all(&state.db) .await .unwrap(); send_posthog_event(&state.client); StatusCode::OK } ``` ### Using the posthog-rs client in an async context It should be noted that the `posthog-rs` client is not async-friendly due to using the `blocking` feature of the `reqwest` crate. This means that if you want to use it with async (for example in a web service), you will need to use `tokio::task::block_in_place()` like so: ```rust use axum::{Extension, StatusCode, response::IntoResponse}; async fn my_example_axum_handler( Extension(client): Extension ) -> impl IntoResponse { tokio::task::block_in_place(|| move { let mut evt = Event::new("my_database_event", "Hello world!"); evt.insert_prop("action", "insert").unwrap(); client.capture(evt).unwrap(); }); StatusCode::OK } ``` Alternatively, Shuttle has a fork of a fork of `posthog-rs` that contains an async client. You can find out more [here.](https://github.com/shuttle-hq/posthog-rs) Due to being a fork it's not an officially published crate, but you can add it to your Rust project with this shell snippet: ```bash cargo add posthog-async --git https://github.com/shuttle-hq/posthog-rs.git --branch main ``` Now when you refer to it in your Rust files, you can use `posthog_async` as the dependency name. ## Using a custom client In this section, we're going to create a custom client for working with Posthog. We'll get started by installing `reqwest`, which is a library for writing HTTP requests and is async by default (requiring a feature to be blocking): ```bash cargo add reqwest ``` Next, we will want to write a function that we can use to make our request client easily. We'll need to add bearer authorization on the header, which we can do like so: ```bash let posthog_api_key = std::env::var("POSTHOG_API_KEY") .expect("Could not find POSTHOG_API_KEY environment variable"); let client = reqwest::Client::builder() .header("Authorization", format!("Bearer {}")) .build().unwrap(); ``` Now whenever we use `client`, it will always have the appropriate header attached. ## Using PostHog insights ### Creating insights Insights in Posthog are the main building blocks of dashboards and allow you to visualise how users use your product. To create insights, we can use our `reqwest::Client` to send an API key manually. There's quite a few endpoints - we'll be making POST requests to `/api/projects/:project_id/insights/` for this example. To be able to set the JSON body, we will need to make use of the `serde_json` crate which we can install with this shell snippet: ```bash cargo add serde_json ``` Next, we'll want to make a JSON map - which we can do below with the `serde_json::json!()` macro: ```rust use serde_json::json; fn my_json_body() -> serde_json::Value { json!({ "name": "my_insight", }) } ``` You can find a more extensive list of request body parameters [here.](https://posthog.com/docs/api/insights#post-api-projects-project_id-insights) Once you're finished creating the request body you want to send to Posthog, you can then create the request using the client we created. ```rust async fn create_insight( client: request::Client, project_id: String ) -> Result<(), Box> { let url = format!( "https://app.posthog.com/api/projects/{project_id}/insights/" ); let json_body = my_json_body(); let response = client.post(&url).json(json_body).send.await?; } ``` However, this is just the start of insight creation. To make our insight do anything, we need to add a funnel to it. Similarly to the last API endpoint where we attached some basic values, we'll create a new JSON body using the `json!()` macro and then send it: ```rust async fn create_insight_funnel( client: request::Client, project_id: String ) -> Result<(), Box> { let url = format!( "https://app.posthog.com/api/projects/{project_id}/insights/" ); let json_body = json!({ events: [{"id":"my_database_event"}], date_from: "-1m" }) let response = client.post(&url).json(json_body).send.await?; } ``` Here, we've specified in the JSON body we want to get events from up to a month ago and the name of the event we want to get is `my_database_event`, which we created earlier. Note that the `$pageview` event signifies any time a user views a page that PostHog is added to. ### Retrieving an insight Once you have some events and insights built up, it's time to retrieve them! You can do this by making a GET request to the insights URL. See below: ```rust async fn get_insights( client: request::Client, project_id: String ) -> Result<(), Box> { let url = format!( "https://app.posthog.com/api/projects/{project_id}/insights/" ); let response = client.get(&url).send.await?; } ``` When you get the response, you will need to turn it into a struct that implements `Deserialize`. Thankfully, `reqwest` provides a method for turning the response into JSON-compatible structs. Let's make a struct that represents some of the properties of the JSON response from PostHog. `serde` completely ignores undeclared fields by default, allowing us to only take what we need. ```rust #[derive(Deserialize, Debug)] struct PosthogResponse { count: i32, next: String, previous: String, results: Vec } #[derive(Deserialize, Debug)] struct PosthogResult { id: String, name: String, deleted: bool } ``` Now we can expand our previous function to include the struct conversion: ```rust async fn get_insights( client: request::Client, project_id: String ) -> Result<(), Box> { let url = format!( "https://app.posthog.com/api/projects/{project_id}/insights/" ); let response = client.get(&url).send.await?; let json: PosthogResponse = response.json().await?; println!("{json:?}"); Ok(()) } ``` ## Finishing Up Posthog makes it easy to track how users are using your application and find easy wins. While the Rust SDK is not complete, hopefully you've found some clarity in communicating with the Posthog API with Rust! Read more: - Get started with using Rust's most popular framework [here](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - Learn about logging for your Rust application [here](https://www.shuttle.dev/blog/2023/09/20/logging-in-rust) - Check out our guide for using Stripe with Rust [here](https://www.shuttle.dev/blog/2024/03/07/stripe-payments-rust) --- # Building a Simple Web Server in Rust Source: https://www.shuttle.dev/blog/2024/03/13/simple-web-server-rust Date: 13 March 2024 Author: josh Tags: rust, guide Building and deploying a simple web server in Rust with the Axum framework Building a web server with Rust doesn't need to be complex. With frameworks like Axum, you can write a web server without hassle. Leveraging Rust allows you to easily take on the job of web services written in other languages and more. In this post we're going to talk about how you can build and deploy a simple web server using Axum. ## What Rust framework should I use? While there's a lot of options we can use, our personal choice is Axum for a few reasons: - Axum uses generics and traits. This allows you to leverage Rust language tooling in ways that other frameworks may not. - Axum has familiar syntax (using handler functions for routing). - It has an extremely high level of compatibility with `tower` crates. This allows you to go very low-level if required. Additionally, we also have an article about what framework you should use [here.](https://www.shuttle.dev/blog/2023/08/23/rust-web-framework-comparison) ## Getting started To get started, you'll want Rust installed on your system. Don't have it installed? You can get it from [this install page.](https://www.rust-lang.org/tools/install) Next, you'll want to use `shuttle init` (requires `cargo-shuttle` installed). and then pick `Axum` for our framework. ## Hello world! When creating a project with `cargo init`, you will need to manually create (or copy!) the initial boilerplate. This may look something like this: ```rust use axum::{Router, routing::get}; use std::net::SocketAddr; use tokio::net::TcpListener; async fn hello_world() -> &'static str { "Hello world!" } #[tokio::main] async fn main() { let router = Router::new().route("/", get(hello_world)); let addr = SocketAddr::from(([127,0,0,1], 8000)); let tcp = TcpListener::bind(&addr).await.unwrap(); axum::serve(tcp, router).await.unwrap(); } ``` As you can see from the above, we do a few things: - We set up a router with given routes and the functions that need to be called. - We define an address for our web server to receive requests at, bind it to a TCP listener. - The TCP listener then responds to requests and responds accordingly. If you used `cargo run` to load this program up then go to `[localhost:8000](http://localhost:8000)` in the browser, it would return "Hello world!" as a raw text response. When initialising a project with Shuttle, all of this gets set up for you. The basic project comes with a native integration so that you don't need to set up the socket binding manually. The integration code is quite short and revolves around the usage of the `shuttle_runtime::Service` trait. If you have a non-standard service you want to run, you can run the service in a struct that implements the `Service` trait and you'll be ready to go! See below for an example of how a Shuttle "Hello World" project looks like: ```rust use axum::{Router, routing::get}; async fn hello_world() -> &'static str { "Hello world!" } #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/", get(hello_world)); Ok(router.into()) } ``` ## Routing HTTP responses from routing within Rust frameworks can be done by anything that implements a trait that represents a HTTP response. In Axum, this would be the `axum::response::IntoResponse` trait (or `axum::response::IntoResponseParts` for headers and other non-response body parts). Web servers can only return things that are valid HTTP responses. Implementing `IntoResponse` (and `IntoResponseParts` respectively) ensures this! It is possible to use `impl IntoResponse` as the return type for a function (for convenience). However, we would need to make sure all of the responses are of the same type. This can lead to confusion later down the line, particularly if you're working in a team. For JSON responses, Axum provides the handy `Json` struct we can use as a response type by wrapping a type with it. For example, this snippet below shows how you can return some raw JSON: ```rust use serde_json::{json, Value}; use axum::Json; async fn return_some_json() -> Json { let json = json!({"hello":"world"}) Json(json) } #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new() .route("/", get(hello_world)) .route("/json/", get(return_some_json)); Ok(router.into()) } ``` However, a more likely situation is that you'll want to return data that follows a schema. We can use the `Deserialize` and `Serialize` traits from the `serde` crate to do this. We can easily apply these traits by adding the `derive` feature and then adding it as a derive macro to a struct: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] struct MyStruct { my_field: String } ``` Now we can wrap it in `axum::Json` and return the struct in our HTTP response. ```rust async fn return_a_struct_as_json() -> Json { let my_struct = MyStruct { my_field: "Hello world!".to_string() }; Json(my_struct) } ``` ## Extractors Extractors are handler function arguments. They extract parts of the HTTP request and turn it into simple variables that we can use with our application. We can use extractors for a lot of things: - Accessing shared mutable state (by adding state to our application then accessing it in the function) - Extracting a typed header for authorization purposes - Consuming the request body to extract a JSON or Form body, depending on what you want. - If there's nothing readily available for our use case, we can just implement it ourselves! Here is an example of how you can use extractors. Note here that we use de-structuring to automatically get the inner variable in `Json` as it looks cleaner. ```rust use axum::routing::post; async fn function_with_extractors( Json(json): Json, ) -> impl IntoResponse { format!("The contents of my_field is: {}", json.my_field) } #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new() .route("/", get(hello_world)) .route("/json/", get(return_some_json)) .route("/json-struct/", post(function_with_extractors)); Ok(router.into()) } ``` ## Databases Using databases generally isn't much different in Rust than any other language. The main difference is Rust web frameworks generally use shared mutable state to pass around data. This means that you'll want to first initialise your database connection (pool) and pass it around using state. In Axum, state is required to implement `Clone`. If your type cannot implement `Clone` because one or more types don't implement Clone, you can wrap the state struct in a `std::sync::Arc` which does implement `Clone`. Here's an example of how you can do exactly that, using SQLx with Postgres as example. We initialise the `PgPool`, initialise our state struct and attach it to the router. ```bash cargo add sqlx -F postgres ``` ```rust use sqlx::PgPool; use axum::{extract::State, Router, routing::get, http::StatusCode}; #[derive(Clone)] struct MyState { db: PgPool } #[tokio::main] async fn main() { let db: PgPool = PgPool::connect("").await.unwrap(); let state = MyState { db }; let router = Router::new().route("/", get(hello_world)).with_state(state); // the rest of your code... } ``` With Shuttle, we can provision a database by simply adding a database macro. This saves time both locally and in production! To get started, we'll need to add the `shuttle_shared_db` dependency. ```bash cargo add shuttle-shared-db -F postgres,sqlx ``` Then we simply add it to our main function and initialise the state struct again like before. ```rust use sqlx::PgPool; use axum::{Router, routing::get}; #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool, ) -> shuttle_axum::ShuttleAxum { let state = MyState { db }; let router = Router::new().route("/", get(hello_world)).with_state(state); Ok(router.into()) } ``` To use our state struct, we can use the `axum::extract::State` extractor: ```rust use axum::{extract::State, http::StatusCode}; async fn hello_world( State(state): State ) -> StatusCode { sqlx::query("SELECT 'Hello world!'") .execute(&state.db) .await .unwrap(); StatusCode::OK } ``` ## Static files To get started with static files on Axum, we'll create a folder in the root of our project called `static`. We'll then define it in a file named `Shuttle.toml` in the project root: ```toml assets = ["static/*"] ``` Using the wildcard tag allows us to serve any file contained within the folder by using the file path. We can write a HTML file in the static folder called `index.html`: ```html

Hello world

``` To serve this static folder on Axum, we'll need to import some things from `tower-http`. We will run the following shell snippet: ```bash cargo add tower-http -F fs ``` Then we can add it like below: ```rust use tower_http::services::{ServeDir}; let router = Router::new() .route_service("/", ServeDir::new("static")); ``` If you are using a SPA framework like React or Vue for your static files, you will want to additionally set up a `.not_found_service()` to be able to serve `index.html`: ```rust let router = Router::new() .route_service("/", ServeDir::new("static") .not_found_service(ServeFile::new("static/index.html") ) ); ``` ## Deployment To deploy Rust to Shuttle, you can use `shuttle deploy` and watch the magic happen! Don't forget to add the `--allow-dirty` flag if on a Git branch with uncommitted changes. Besides web server development, Shuttle is a lifesaver when it comes to easily deploying and making your web service public. Interested? You can find our docs [here.](https://docs.shuttle.dev/introduction/welcome) ## Finishing up Web development doesn't have to be complicated. With Rust, we can achieve our goal of building a simple web server quickly and easily. Read more: - Get more in-depth with Axum [here.](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - Learn more about tracing libraries and improve application logging [here.](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) --- # Using Stripe Payments with Rust Source: https://www.shuttle.dev/blog/2024/03/07/stripe-payments-rust Date: 7 March 2024 Author: josh Tags: rust, stripe, guide Exploring how to use Stripe Payments in a Rust application so you can get paid. Hello world! In this article we're going to talk about how to integrate Stripe Payments into a Rust application. Stripe is a hassle-free, easy to use payments provider that makes commercializing your Rust web services hassle-free. We're going to cover: - One off payments - Setting up customers - Products and prices, plus subscriptions (creating, updating and deleting) - Webhooks ## Getting started To get started, we'll grab a Stripe API key from the dashboard. Stripe is free to sign up for and try out in the Test Mode. You can find out more about this [here.](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) To use Stripe with Rust, we'll add the following dependency ([`async-stripe`](https://github.com/arlyon/async-stripe)) to our project with this shell snippet: ```bash cargo add async-stripe ``` ## Using Stripe ### Products Before we can do anything meaningful, we are required to create [Stripe products](https://docs.stripe.com/api/products). The product data should be stored on Stripe as a source of truth for us to be able to generate purchases and/or product prices programmatically. On our side, we will only store the product and price object IDs. This gives us two advantages: - The IDs aren't useful by themselves; if our database gets compromised, the information cannot be meaningfully used. - We can rely on Stripe as a source of truth instead of having two sources of information (though if there's anything you want to store for convenience like product names, etc, that's probably a good idea!). Let's have a look at creating a product: ```rust let stripe_key = std::env::var("STRIPE_API_KEY").expect("STRIPE_API_KEY env var not found"); let client = stripe::Client::new(stripe_key); // create a new example project let product = { let mut create_product = CreateProduct::new("T-Shirt"); create_product.metadata = Some( std::collections::HashMap::from([(String::from("async-stripe"), String::from("true"))]) ); Product::create(&client, create_product).await.unwrap() }; ``` Note here that you can add any metadata you want for your product (as long as it can be a String). If you need to add things like net weight, nutritional information, or other kinds of information this is where you'd put it. Additionally, you can add images to the `CreateProduct` struct - up to 8 using URLs. This would mean using something like Cloudflare R2 or AWS S3 buckets to store your images and accessing them from there. You can read more about the `CreateProduct` struct [here.](https://docs.rs/async-stripe/0.34.1/stripe/generated/core/product/struct.CreateProduct.html#structfield.default_price_data) Prices in Stripe are essentially separate objects from products themselves. This allows us to create multiple prices for a project and price tiering. we'll need to do use a `CreatePrice` struct that requires an additional product. Because the price and product objects are separate, we can create multiple prices for the same product! ```rust let price = { let mut create_price = CreatePrice::new(Currency::USD); create_price.product = Some(IdOrCreate::Id(&product.id)); create_price.metadata = Some( std::collections::HashMap::from( [(String::from("async-stripe"), String::from("true"))] ) ); create_price.unit_amount = Some(1000); create_price.expand = &["product"]; Price::create(&client, create_price).await.unwrap() }; ``` Note that here, we have the currency as US dollars. The unit amount is the smallest possible currency for a given currency, meaning that the total amount for this price is actually $10.00 (or "1000 cents"), not $1000! As before with the product information itself, we can add whatever metadata we want to the price object. We are also required to input a product ID as the price object depends on a product ID being present. You can additionally set things up like tiered pricing (based on unit volume!) programmatically. You can find more about the price object from this docs page [here.](https://docs.stripe.com/api/prices/object) ### Subscriptions [Subscriptions](https://docs.stripe.com/billing/subscriptions/overview) are worth a special mention. Although they are technically just "prices with a recurring period" and you can use them just like a regular price object in Stripe, you may want to structure them differently. For example: How do you set your subscription tiers up properly? To create a subscription price, when creating the price object you need to add the `recurring` property like so: ```rust async fn create_product_price( client: &Client, product: &Product, ) -> Result { let price = { let mut create_price = CreatePrice::new(Currency::USD); create_price.product = Some(IdOrCreate::Id(&product.id)); create_price.metadata = Some(std::collections::HashMap::from([( String::from("async-stripe"), String::from("true"), )])); create_price.unit_amount = 1000; create_price.recurring = Some(CreatePriceRecurring { interval: CreatePriceRecurringInterval::Month, ..Default::default() }); create_price.expand = &["product"]; Price::create(client, create_price).await? }; Ok(price) } ``` Some things you may want to keep in mind for subscriptions: - Do you have multiple subscription tiers? If so, it may be a good idea to keep them as separate products - You can also have multiple prices for said tiers. This means you can set things up like having a monthly recurring bill and then offer a discount on a yearly recurring bill. Cancelling a user subscription is fairly easy. To do so, you can use `Subscription::cancel`: ```rust async fn cancel_subscription(subscription_id: String) -> Result<(), stripe::Error> { let _ = Subscription::cancel( &client, &SubscriptionId::from_str(subscription_id).unwrap(), CancelSubscription { cancellation_details: None, invoice_now: Some(true), prorate: Some(true), }, ) .await?; Ok(()) } ``` Note that here, we've additionally told Stripe we want to invoice now with [prorating](https://docs.stripe.com/billing/subscriptions/prorations) and no additional cancellation details. You can simply set it to None if you don't want instant invoicing (or if you don't want prorating). Updating subscription tiers or the details of a subscription however, is somewhat more complicated. To do so, you need to retrieve a user's subscription ID somewhere (this ID would ideally be stored in your database) and then adjust the relevant item in your subscription items list. See below for an example of a subscription with only one item on the subscription list (the base subscription price): ```rust async fn update_subscription( user_subscription_id: String, old_item_id: String, new_item_id: String, new_price_id: String ) -> Result<(), stripe::Error> { let subscription_item = Subscription::retrieve( &client, &SubscriptionId::from_str(&user_subscription_id).unwrap(), &["items"], ) .await? .items; let subscription_item = &subscription_item.data[0]; let _ = Subscription::update( &client, &SubscriptionId::from_str(&user_subscription_id).unwrap(), UpdateSubscription { items: Some(vec![UpdateSubscriptionItems { id: Some(old_item_id), deleted: Some(true), ..Default::default() }, UpdateSubscriptionItems { id: Some(new_item_id), price: Some(new_price_id) ..Default::default() }, ]), ..Default::default() }, ) .await?; Ok(()) } ``` As you can see, although somewhat complicated it is not too difficult. We simply delete the old item, then add a new one with the relevant price and item ID. ### Creating Checkout Sessions Now for the fun part: getting paid! We can create a checkout session with a customer ID, then add a checkout with a price ID. Then once the session has been created, we can send the URL back to the user! Note that we've added a cancel and success URL to explicitly tell Stripe where we want to send our user to after; if this isn't set, they will stay on the Stripe page. ```rust async fn create_checkout_session(customer_id: String) -> String { let checkout_session = { let mut params = CreateCheckoutSession::new(); params.cancel_url = Some("http://test.com/cancel"); params.success_url = Some("http://test.com/success"); params.customer = Some(customer_id); params.mode = Some(CheckoutSessionMode::Payment); params.line_items = Some( vec![CreateCheckoutSessionLineItems { quantity: Some(1), price: Some(price.id.to_string()), ..Default::default() }] ); params.expand = &["line_items", "line_items.data.price.product"]; CheckoutSession::create(&client, params).await.unwrap() }; let line_items = checkout_session.line_items.unwrap(); checkout_session.url.unwrap() } ``` When we return the URL to the user (assuming they visit the URL), once the checkout is completed, they'll now have purchased a product or service from us! If we have webhooks set up (see the later [webhooks section](/#webhooks)), we will receive a `CheckoutSessionCompleted` event which will hold the user subscription ID that we can save and use in other scenarios (for example, updating/cancelling subscriptions). Because the ID by itself cannot be used for anything without an API key, it is not considered sensitive data and is therefore safe to store without much additional consideration. ### Setting up customers To set up a customer on Stripe, you can easily create one like so: ```rust let secret_key = std::env::var("STRIPE_API_KEY").expect("Missing STRIPE_API_KEY in env"); let client = Client::new(secret_key); let customer = Customer::create(&client, CreateCustomer { name: Some("Josh"), email: Some("test@async-stripe.com"), description: Some("A fake customer that is used to illustrate the examples in async-stripe."), metadata: Some( std::collections::HashMap::from([(String::from("async-stripe"), String::from("true"))]) ), ..Default::default() }).await.unwrap(); ``` Here we have added a test customer to Stripe, with some metadata, an email and a description. You can find out more about the `CreateCustomer` struct [here](https://docs.rs/async-stripe/latest/stripe/struct.CreateCustomer.html), which shows all of the fields/methods that can be used. Note that while it is possible to additionally add a payment method for a customer through the API, you are required to be [PCI compliant](https://stripe.com/ie/guides/pci-compliance) while using Stripe; processing or handling of card information of any kind through an API requires you to meet much more stringent criteria than if you just set up a Stripe checkout session and allow the user to input their details into the checkout session. If you're still interested in adding a payment method manually through your API, see below: ```rust let payment_method = { let pm = PaymentMethod::create( &client, CreatePaymentMethod { type_: Some(PaymentMethodTypeFilter::Card), card: Some(CreatePaymentMethodCardUnion::CardDetailsParams( CardDetailsParams { number: "4242424242424242".to_string(), // test card number exp_year: 2025, exp_month: 1, cvc: Some("123".to_string()), ..Default::default() })), ..Default::default() }, ) .await .unwrap(); PaymentMethod::attach( &client, &pm.id, // this customer ID is taken from earlier when creating the customer AttachPaymentMethod { customer: customer.id.clone() }, ) .await .unwrap(); pm }; ``` ### Webhooks Stripe additionally offers [webhooks](https://docs.stripe.com/webhooks) that we can use to receive events! Whenever someone sets up a new subscription, updates or cancels one, we can have an event sent to a web service that we own! While we can do this manually, `async-stripe` has examples that we can use in extractors. The way that we do this is by wrapping `stripe::Event` in a struct that then implements the relevant extractor trait, checking the `stripe-signature` header to ensure HTTP request integrity and construct the payload. Below is a snippet of an Axum custom extractor that wraps `stripe::Event`: ```rust struct StripeEvent(Event); #[async_trait] impl FromRequest for StripeEvent where String: FromRequest, S: Send + Sync { type Rejection = Response; async fn from_request(req: Request, state: &S) -> Result { let signature = if let Some(sig) = req.headers().get("stripe-signature") { sig.to_owned() } else { return Err(StatusCode::BAD_REQUEST.into_response()); }; let payload = String::from_request(req, state).await.map_err(IntoResponse::into_response)?; Ok( Self( stripe::Webhook ::construct_event(&payload, signature.to_str().unwrap(), "whsec_xxxxx") .map_err(|_| StatusCode::BAD_REQUEST.into_response())? ) ) } } ``` We can then use it in a function like this, where `StripeEvent` is passed in as a function argument (enabled by it implementing `FromRequest`): ```rust async fn my_function( StripeEvent(event): StripeEvent, ) -> impl IntoResponse { match event.type_ { EventType::CheckoutSessionCompleted => { // .. do some stuff here } EventType::SubscriptionScheduleCanceled => { // .. do some stuff here } _ => {} } StatusCode::OK } ``` Webhooks normally require HTTPS to test - if you're trying to make this work locally, you can use Cloudflare Tunnel, Ngrok or a similar service to receive webhooks. Interested in checking out the examples for Actix Web and Rocket? The `async-stripe` GitHub repo has those examples [here.](https://github.com/arlyon/async-stripe/tree/master/examples) ## Finishing up Thanks for reading! Using Stripe in Rust web services is a great way to start being able to make money from Rust. By reading this guide to using Stripe with Rust, you should be one step closer to exactly that. - Learn how you can use Stripe with a full-stack Loco template [here.](https://www.shuttle.dev/blog/2024/02/29/fullstack-loco-rust) - Learn about rate limiting your API [here.](https://www.shuttle.dev/blog/2024/02/22/api-rate-limiting-rust) - Have a look at getting started with Tracing logging libraries [here.](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) --- # Writing & Compiling WASM in Rust Source: https://www.shuttle.dev/blog/2024/03/06/writing-wasm-rust Date: 29 February 2024 Author: josh Tags: rust, wasm, guide Exploring how to write and compile WASM from Rust Hello world! In today's post we're going to talk about how you can write a WebAssembly module in Rust. WebAssembly is a portable compilation target for programming languages to be able to conveniently inter-op with JavaScript on the web. Rust being able to take advantage of this has made it extremely useful for many use cases, such as: - CPU intensive workloads (encryption) - GPU intensive workloads (image/video processing, image recognition) This article will focus on writing a WASM module for image processing that can be used on the backend, as well as exploring common ways to deploy WASM and its targets. ## Getting started To get started, you'll need Rust installed. If you don't, you can install it [here](https://www.rust-lang.org/tools/install). We'll be focusing on trying out writing a WASM module in three different ways: - Using the `wasm-bindgen` CLI - Using `wasm-pack` - Using `napi-rs` We will initially use `wasm-bindgen-cli` to create our application, then look at using `wasm-pack`. The focus of this article will be creating a simple module for image processing. Byte array manipulation and data processing is an area where Rust can significantly speed up your application. Before we start, make sure you have the `wasm32-unknown-unknown` target installed. If you don't, you can add it like so: ```bash rustup target add wasm32-unknown-unknown ``` Note that for trying out our module, you'll also additionally want `npm` (or any alternatives) installed. ## Writing a WASM module ### The basics To set up our project, we'll get started with using `cargo init --lib wasm-example` to create a new library project named `wasm-example`. We will then install our dependencies with a small shell snippet: ```bash cargo add wasm-bindgen@0.2.91 cargo add js-sys@0.3.68 cargo add image@0.24.9 ``` We will also want to add the dynamic library flag to our `Cargo.toml` file. Normally, it lets Cargo know that we want to make a dynamic system library - but when using it with the WebAssembly target, it simply means "make a `*.wasm` file without a `start` function". To do this, we can add this small snippet below: ```toml [lib] crate-type = ["cdylib"] ``` ### JavaScript types in Rust To be able to use JavaScript types in Rust, we need to use `extern C` in addition to using the `wasm-bindgen` macros. This allows us to import functions straight out of JavaScript and into Rust! The Hello World application in WASM looks like this (from the book): ```rust use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { fn alert(s: &str); } #[wasm_bindgen] pub fn greet(name: &str) { alert(&format!("Hello, {}!", name)); } ``` Note that the `alert` function in the extern C is taken directly from JavaScript, and is what allows us to call it in our Rust function. If we were to compile this and execute it in a JavaScript file, it would be the same as if you had called `alert()` from regular JavaScript. We can apply the same logic to be able to work with other types and functions - namely, buffers. `Vec` in JavaScript can either be represented in one of two ways: - The `Uint8Array` type (the direct JavaScript equivalent of `Vec`) - A `Buffer` type `Buffer` is a subclass of `Uint8Array`. This is because when Node.js first released, there was no Uint8Array type - which is what led to the creation of the `Buffer` type. Later down the line when Uint8Arrays were introduced with ES6, both were eventually merged as it made sense to do so. Many JavaScript libraries still use `Buffer`. By using `js-sys`, we can get interoperability between JavaScript and Rust - which we can see below by defining the `Buffer` type and providing a method with the `buffer()` method: ```rust use js_sys::ArrayBuffer; // This defines the Node.js Buffer type #[wasm_bindgen] extern "C" { pub type Buffer; #[wasm_bindgen(method, getter)] fn buffer(this: &Buffer) -> ArrayBuffer; #[wasm_bindgen(method, getter, js_name = byteOffset)] fn byte_offset(this: &Buffer) -> u32; #[wasm_bindgen(method, getter)] fn length(this: &Buffer) -> u32; } ``` Now when we write our WASM function, we can refer to the `Buffer` type directly! Let's write our Rust function for converting our image file format. We'll make it require our `Buffer` and then have it return `Vec` - when we compile it through `wasm-pack` or another compiler, it will automatically get converted to a `Uint8Array`. ```rust use js_sys::{ArrayBuffer, Uint8Array}; use wasm_bindgen::prelude::wasm_bindgen; use image::ImageFormat; use image::io::Reader; use std::io::Cursor; // .. extern C stuff goes here #[wasm_bindgen] pub fn convert_image(buffer: &Buffer) -> Vec { // This converts from a Node.js Buffer into a Vec let bytes: Vec = Uint8Array::new_with_byte_offset_and_length( &buffer.buffer(), buffer.byte_offset(), buffer.length() ).to_vec(); let img2 = Reader::new(Cursor::new(bytes)).with_guessed_format().unwrap().decode().unwrap(); let mut new_vec: Vec = Vec::new(); img2.write_to(&mut Cursor::new(&mut new_vec), ImageFormat::Jpeg).unwrap(); new_vec } ``` ### Building via wasm-bindgen-cli Here, we need to compile from Rust to WASM by building our package for the `wasm32-unknown-unknown` target, which we can do like so: ```bash cargo build --target=wasm32-unknown-unknown ``` Next, we need to use `wasm-bindgen` to generate the JS glue code to make it all work. We'll use the `nodejs` target which will generate a CommonJS module and put it in the `./pkg` folder which we can then implant anywhere we want. ```bash wasm-bindgen --target nodejs --out-dir ./pkg \ ./target/wasm32-unknown-unknown/release/wasm_example.wasm ``` Now we can either publish our WASM code as a package or implant it anywhere we want to use it! ### I don't want to use CommonJS! If you don't want to use CommonJS because you're using ESM (EcmaScript modules, or ES6 modules), that's cool! The CLI currently allows several targets: - `bundler` (produces code for usage with bundlers like Webpack) - `web` (directly loadable in a web browser) - `nodejs` (loadable via `require` as a CommonJS Node.js module) - `deno` (usable as a Deno module) - `no-modules` (like the `web` target but doesn't use ES Modules). There are specific docs for usage with ES. The easiest way to do it in terms of what compiler to use is typically with Webpack as it's the most compatible. You can also compile to ES6 modules without a bundler, though it involves initializing the WASM module manually before running which adds some overhead. ## Test driving our new module Now that we've written our code, let's try it out! We will spin up a JavaScript backend server using Express.js. We will assume you're running the following in the same folder as your Rust project (for convenience purposes). We'll get started with the following shell snippet: ```bash npm init -y npm i express express-fileupload ``` Next, we'll create a `server.js` file in our root directory and insert the following code: ```jsx const fileUpload = require("express-fileupload"); const express = require("express"); const { convert_image } = require("./pkg/wasm_example"); const app = express(); app.use(fileUpload()); const port = 3030; app.get("/", (req, res) => { res.send(`

With "express" npm package

Text field title:
File:
`); }); app.post("/api/upload", (req, res, next) => { const image = convert_image(req.files.file.data); res.setHeader("Content-disposition", 'attachment; filename="meme.jpeg"'); res.setHeader("Content-type", "image/jpg"); res.send(image); }); app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); ``` This snippet does the following: - We set up an Express server at port 3030 - We have a route at `/` that will give us a HTML form when we visit it in the browser - We have an API route that will grab the data from our file upload, convert it to a new format, set the correct headers and return the new image. If we use `node server.js`, head to `[http://localhost:3030](http://localhost:3030)` in our browser then fill the form out and attach an image, we should get an image download response back! Note that depending on the settings you are using for your image file format conversion, your file size may increase post-conversion; this is because you may be using lossless conversion. If you want to use lossy conversion to decrease your file size, you want the `new_with_quality` method while instantiating the image encoder in your Rust code. ## Building our app with alternative CLIs While `wasm-bindgen-cli` is useful, it's also the most low level CLI out of our options and you can run into spurious issues while using it such as `wasm-bindgen` version incompatibility issues. There are some additional quality of life changes that we could benefit from, such as automatic versioning and `wasm-opt` usage. Let's take a quick look at some of these other options and see how they compare. ### Wasm-pack `wasm-pack` is a tool that aims to be a one-stop-shop for compiling Rust to WASM. It includes a CLI that you can install. In comparison to using `wasm-bindgen-cli`, it has a number of quality of life upgrades: - Comes with `wee_alloc`, a WebAssembly allocator with a (pre-compression) 1kB code footprint. - Comes with a panic hook that allows you to debug Rust panic messages in the browser To initialise our project, we can use `wasm-pack new wasm-example` which will do everything for us. Code-wise, our main function (and C/JS bindings) will remain the same as `wasm-pack` provides primarily tooling additions to make compilation easier and does not have any library code we can use. ### napi-rs `napi-rs` is a framework for building pre-compiled Node.js addons in Rust. If you find using `wasm-bindgen` too complicated to use and just want to write Node.js stuff, this is a good alternative. To use it, Node v0.10.0 or later is required. You can install it with the following shell snippet (requires npm or its alternatives): ```bash npm install -g @napi-rs/cli ``` Once done, you can then use `napi new wasm-example` to build your new NAPI project! `napi-rs` does come with some code changes, which you'll be able to see below: we can finally get rid of the `extern C` block and instead use napi's `bindgen_prelude` to include whatever we need. ```rust use napi::bindgen_prelude::*; use image::io::Reader; use image::ImageFormat; use image::ImageOutputFormat; use std::io::Cursor #[macro_use] extern crate napi_derive; #[napi] pub fn convert_image(buffer: Buffer) -> Result { let bytes: Vec = buffer.into(); let img2 = Reader::new(Cursor::new(bytes)).with_guessed_format().unwrap().decode().unwrap(); let mut new_vec: Vec = Vec::new(); img2.write_to(&mut Cursor::new(&mut new_vec), ImageFormat::Jpeg).unwrap(); Ok(new_vec.into()) } ``` The advantages of this are clear: - We don't need to manually import anything using `extern C` - We can easily use Node.js internals without trouble Of course for all its advantages, `napi-rs` is only compatible with Node.js. If you want to write some WASM code for the browser, you would need to default to `wasm-pack` or `wasm-bindgen`. You additionally also need to use the Node ecosystem to keep your CLI updated, which from a Rust-first standpoint is a bit of a strange decision. Needless to say, `napi-rs` is a pretty easy way to start writing Node.js with Rust. ## Finishing up Thanks for reading! Rust has great inter-op with WASM, and there's no reason why we shouldn't be able to take advantage of this to help us when using other lanaugages. Read more: - We wrote a list of [our top 8 Rust tools to speed up your productivity](https://www.shuttle.dev/blog/2024/02/15/best-rust-tooling) - Here's our guide to [getting started with Axum, Rust's most popular framework](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - We wrote about our [top 8 tools to help you be more productive in Rust](https://www.shuttle.dev/blog/2024/02/15/best-rust-tooling) --- # A Full Stack SaaS Template with Loco Source: https://www.shuttle.dev/blog/2024/02/29/fullstack-loco-rust Date: 29 February 2024 Author: josh Tags: rust, fullstack, loco, guide Exploring how to use the Loco.rs framework to write a SaaS, complete with payments. Loco is a Rust framework that aims to do it all - authentication, tasks, migrations and more. While initially being more complex to use than using pure Axum, it can save a lot of time setting up boilerplate and provides a great platform to build on. In this guide, we'll deploying Loco on Shuttle via a fullstack template that also additionally includes SaaS subscription payments. By using their SaaS starter, we can leverage their pre-built authentication features to build payments out quickly and easily. Interested in deploying or trying out the final repo? You can check it out [here.](https://github.com/joshua-mo-143/shuttle-stripe-ex) Steps to deploy from cloning: - Run `shuttle init --from joshua-mo-143/shuttle-stripe-ex` and follow the prompt (requires `cargo-shuttle` installed) - Set up API keys (see below) - Use `shuttle deploy --allow-dirty` and watch the magic happen! ## Prerequisites Before we get started, you will need a Stripe API key. It's free to sign up with Stripe and you can turn on Test Mode before you do anything in production. Stripe also has docs on this if you need assistance [here.](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) Once you've created your project, make sure you create a `Secrets.toml` file in the root of your project and add it like so: ```rust STRIPE_API_KEY = "" ``` ### Getting started We're going to use the following command to initialise our project (requires `cargo-shuttle` installed), following the prompt to initialise a project with our name. The `--from` flag allows us to take the starter from ```bash shuttle init --from loco-rs/loco --subfolder starters/saas ``` We will then use `cargo loco generate deployment` to generate a Shuttle deployment for our Loco project! While we're here, make sure you're using the latest version of `cargo-shuttle` and Shuttle dependencies in your project. We released v0.40.0 today! We'll also add the following dependencies with a shell snippet: ```bash cargo add async-stripe@0.34.1 -F runtime-tokio-hyper-rustls cargo add shuttle-shared-db -F postgres cargo add shuttle-secrets ``` Note that you will get a blank Shuttle deployment with no database installed. To remedy this, we'll adjust the main function to provide our database annotation and make sure that the `Migrator` (from the migrations folder) is added: ```rust use migrations::Migrator; #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] conn_str: String, #[shuttle_metadata::ShuttleMetadata] meta: shuttle_metadata::Metadata, #[shuttle_secrets::Secrets] secrets: shuttle_secrets::SecretStore ) -> shuttle_axum::ShuttleAxum { std::env::set_var("DATABASE_URL", conn_str); let stripe_api_key = secrets .get("STRIPE_API_KEY") .expect("STRIPE_API_KEY not found in secrets"); std::env::set_var("STRIPE_API_KEY", stripe_api_key); let environment = match meta.env { shuttle_metadata::Environment::Local => Environment::Development, shuttle_metadata::Environment::Deployment => Environment::Production, }; let boot_result = create_app::(StartMode::ServerOnly, &environment) .await .unwrap(); let router = boot_result.router.unwrap(); Ok(router.into()) } ``` In the regular `src/bin/main.rs`, you may also need to adjust the app so that `Migrator` is also included. Once done, you can use `shuttle run` and it should just automatically work! You'll get a database connection URL (save this for later!). ### Migrations To get started, we'll make some migrations that we can then reference later on in the program. You can do this like so: ```bash cargo loco generate model --migration-only subscription_tiers tier:string! stripe_item_id:string! stripe_price_id:string! cargo loco generate model --migration-only user_subscriptions user:references stripe_customer_id:string! stripe_subscription_id:string! user_tier:string! ``` Then we'll use the following to migrate your database and generate entities: ```bash DATABASE_URL= cargo loco db migrate DATABASE_URL= cargo loco db entities ``` Note that you will need a database URL for this. If you don't have one yet, you can use `shuttle run` to automatically spin up a Postgres container with a provided connection string or spin up your own Docker container. These two commands will generate some files in the `migrations` folder as well as in `src/models`, which we'll be making heavy use of as they are the main way to interface with the database when using Loco. ## Frontend We won't be covering the frontend in this tutorial as there's a lot of different ways you can do it - however, if you'd like to look at the way that we've done it, feel free to check out the repo here! We use React as provided by Loco with `react-router-dom` for routing and `zustand` for state management, with vanilla CSS. The following pages in the repo have been provided: - A home page - Login and register pages - A dashboard page that allows users to downgrade/upgrade their subscription tier, cancel it and check what tier they are. - Pricing and payment checkout pages - A payment success/fail page ## Error handling Loco by default uses `anyhow` to be able to provide easy error handling. However, for our purposes, let's create our own error type. This will allow us a couple of things: - We know exactly what error is happening and where - We can customise the behavior of our error handling Let's start by using the `thiserror` crate we added earlier to add macros for automatically implementing `std::fmt::DIsplay` and `std::error::Error`. The `thiserror::Error` derive macro also allows us to add attribute macros to our struct for automatic `From` implementations, which saves a lot of time! That being said, you can also implement `From` manually if you want to create more than one enum variant for an error based on what the reason of the error is. ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum ApiError { #[error("Stripe error: {0}")] Stripe(#[from] stripe::StripeError), #[error("User already has this subscription tier!")] UserTierAlreadyExists, #[error("SQL error: {0}")] SQL(#[from] sea_orm::DbErr), #[error("Model error: {0}")] Model(#[from] loco_rs::model::ModelError), } ``` To be able to use this in our API, we will need to implement `axum::response::IntoResponse`. We can do this by simply matching each enum variant like below: ```rust use axum::{http::StatusCode, response::{Response, IntoResponse}}; impl IntoResponse for ApiError { fn into_response(self) -> Response { let res = match self { Self::Stripe(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), Self::SQL(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), Self::UserTierAlreadyExists => ( StatusCode::BAD_REQUEST, "User already has this tier!".to_string(), ), Self::Model(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), }; res.into_response() } } ``` ## Using Stripe To get started with using Stripe, we'll want to create a new loco controller file that will hold all of our routes. We can do this with `cargo loco generate controller stripe`, which will generate a new controller route at `src/controllers/stripe.rs` and inject some code into `src/app.rs` to make sure the controller gets automatically included. Looking inside `src/controllers/stripe.rs` should give you a function that returns `Routes` (a struct that builds on `axum::Router`) and a couple of routes for returning "Hello, world!" and the contents of a given request. Let's start first by defining what our user tiers are. Let's say we have the Pro tier, and the Team tier. We can write an enum with relevant impls like so: ```rust use std::fmt; use serde::{Deserialize, Serialize}; use sea_orm::{EnumIter, DeriveActiveEnum}; #[derive(EnumIter, DeriveActiveEnum, Clone, Deserialize, Debug, Serialize, PartialEq, Eq)] #[sea_orm(rs_type = "String", db_type = "String(StringLen::N(1))")] pub enum UserTier { #[sea_orm(string_value = "P")] Pro, #[sea_orm(string_value = "T")] Team, } impl UserTier { fn get_price(&self) -> Option { match self { Self::Pro => Some(1000), Self::Team => Some(2500), } } fn from_str(str: &str) -> Self { match str { "Pro" => Self::Pro, "Team" => Self::Team, _ => panic!("There should only be the Pro and Team tier!") } } } impl fmt::Display for UserTier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Pro => write!(f, "Pro"), Self::Team => write!(f, "Team"), } } } ``` Note that the macros we are importing from `sea_orm` will let the enum be stored as a `varchar` type in Postgres. ### Creating Stripe products and prices Before we do anything else, we will want to create a Stripe product that has some prices attached to it. To do that, we can create two functions; one for creating the Product item, and then one for adding a price (as products on Stripe can have multiple prices). Creating a price requires a product. To keep it simple, we'll keep one price attached to one item. Both functions will be used as part of other functions. ```rust use stripe::{Client, Product, Price, CreatePrice, CreateProduct, IdOrCreate, CreatePriceRecurring, CreatePriceRecurringInterval, Currency}; async fn create_product_item(client: &Client, user_tier: &UserTier) -> Result { let product = { let mut create_product = match user_tier { UserTier::Pro => CreateProduct::new("Pro User Subscription"), UserTier::Team => CreateProduct::new("Team Subscription"), }; create_product.metadata = Some(std::collections::HashMap::from([( String::from("async-stripe"), String::from("true"), )])); Product::create(client, create_product).await? }; Ok(product) } async fn create_product_price( client: &Client, user_tier: &UserTier, product: &Product, ) -> Result { let price = { let mut create_price = CreatePrice::new(Currency::USD); create_price.product = Some(IdOrCreate::Id(&product.id)); create_price.metadata = Some(std::collections::HashMap::from([( String::from("async-stripe"), String::from("true"), )])); create_price.unit_amount = user_tier.get_price(); create_price.recurring = Some(CreatePriceRecurring { interval: CreatePriceRecurringInterval::Month, ..Default::default() }); create_price.expand = &["product"]; Price::create(client, create_price).await? }; Ok(price) } ``` This then allows us to build a more higher-level function that can either simply retrieve the Stripe product ID if it already exists in the database, or create a product with price then save the details in the database. For simplicity (and not wanting to deal with securing financial information in the database), we will only be storing the IDs and relevant information, such as what product tier a user is. ```rust use sea_orm::DatabaseConnection; async fn retrieve_product( client: &Client, user_tier: &UserTier, db: &DatabaseConnection, ) -> Result { use crate::models::_entities::subscription_tiers; let product = subscription_tiers::Entity::find() .filter(subscription_tiers::Column::Tier.contains(user_tier.to_string())) .one(db) .await?; let price = match product { Some(product) => { Price::retrieve( client, &PriceId::from_str(&product.stripe_price_id.to_string()).unwrap(), &["product"], ) .await? } None => { let product = create_product_item(client, &user_tier).await?; let price = create_product_price(client, &user_tier, &product).await?; let tier_model = subscription_tiers::ActiveModel { tier: ActiveValue::Set(user_tier.to_string()), stripe_item_id: ActiveValue::Set(product.id.to_string()), stripe_price_id: ActiveValue::Set(price.id.to_string()), ..Default::default() }; subscription_tiers::Entity::insert(tier_model) .exec(db) .await?; price } }; Ok(price) } ``` Later on, when we run the web service, the API should automatically be able to know if it is required to remake the Stripe product or not. Let's write a function for creating a subscription. To get started, we will define what the JSON input should look like when the API receives a request: ```rust #[derive(Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct UserSubscription { name: String, email: String, card_num: String, exp_year: i32, exp_month: i32, cvc: String, user_tier: UserTier, } ``` Note that when sending the request, the variables must be in camel case and not snake case. To make things a little bit easier for ourselves later, we will add an `impl` for `UserSubscription` which will allow us to automatically turn it into some structs that we'll be using later on to create the customer and `CardDetailsParams`. ```rust impl UserSubscription { fn as_create_customer(&self) -> CreateCustomer { CreateCustomer { name: Some(&self.name), email: Some(&self.email), description: Some( "A paying user.", ), metadata: Some(std::collections::HashMap::from([( String::from("async-stripe"), String::from("true"), )])), ..Default::default() } } fn as_card_details_params(&self) -> CardDetailsParams { CardDetailsParams { number: self.card_num.to_string(), exp_year: self.exp_year, exp_month: self.exp_month, cvc: Some(self.cvc.clone()), ..Default::default() } } } ``` ### Creating subscriptions Next, we'll want to get started on writing an endpoint for creating user subscriptions! We'll create a `stripe::Client` here from the API key that we stored earlier. ```rust use crate::models::_entities::user_subscriptions; use crate::models::_entities::users; use loco_rs::controller::middleware::auth::JWTWithUser; pub async fn create_subscription( State(ctx): State, auth: middleware::auth::JWTWithUser, Json(json): Json, ) -> Result { let secret_key = std::env::var("STRIPE_API_KEY").expect("Missing STRIPE_API_KEY in env"); let client = Client::new(secret_key); // .. rest of your code } ``` Here, note that we specifically use the `JWTWithUser` middleware to extract a Bearer JWT from the Authorization header and return a user model. Next, we want to create a customer and add it to our Stripe account. We will also create the payment method and attach it to the customer. Note here that while we're using card payments as it's a very common form of payment, Stripe also has quite a few other types of payments you can try. ```rust use stripe::{Customer, PaymentMethod, PaymentMethodTypeFilter, CreatePaymentMethodCardUnion, AttachPaymentMethod}; let customer = Customer::create(&client, json.as_create_customer()).await?; let payment_method = { let pm = PaymentMethod::create(&client, CreatePaymentMethod { type_: Some(PaymentMethodTypeFilter::Card), card: Some(CreatePaymentMethodCardUnion::CardDetailsParams(json.as_card_details_params())), ..Default::default() }).await?; PaymentMethod::attach(&client, &pm.id, AttachPaymentMethod { customer: customer.id.clone(), }).await?; pm }; ``` Next, we'll want to add the part that creates the subscription. This part is relatively simple as there's only one item in our subscription list we want - which is the base price for our SaaS subscription (although if you want to extend it, there are options for adding more too!): ```rust use stripe::{CreateSubscription, CreateSubscriptionItems, Subscription, let mut params = CreateSubscription::new(customer.id.clone()); params.items = Some( vec![CreateSubscriptionItems { price: Some(price.id.to_string()), ..Default::default() }] ); params.default_payment_method = Some(&payment_method.id); params.expand = &["items", "items.data.price.product", "schedule"]; let subscription = Subscription::create(&client, params).await?; let subscription_activemodel = user_subscriptions::ActiveModel { user_id: ActiveValue::Set(auth.user.id), stripe_customer_id: ActiveValue::Set(customer.id.to_string()), stripe_subscription_id: ActiveValue::Set(subscription.id.to_string()), user_tier: ActiveValue::Set(json.user_tier), ..Default::default() }; user_subscriptions::Entity::insert(subscription_activemodel).exec(&ctx.db).await?; Ok(StatusCode::OK); ``` If you get stuck on errors while writing this function, you can find the function in the repo [here.](https://github.com/joshua-mo-143/shuttle-stripe-ex/blob/main/src/controllers/stripe.rs#L34) ### Cancelling Stripe Subscriptions Okay, now let's say you want your users to be able to use self-service to cancel the SaaS subscription. This primarily involves canceling the subscription and then making sure to update the database. In this case, we are choosing to outright delete the record from the database on successful cancellation. Firstly, we'll create the Stripe client again by grabbing our API key: ```rust pub async fn cancel_subscription( State(ctx): State, auth: middleware::auth::JWTWithUser, ) -> Result { let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; let secret_key = std::env::var("STRIPE_API_KEY").expect("Missing STRIPE_API_KEY in env"); let client = Client::new(secret_key); // .. rest of your code } ``` Next, we'll find our user subscription from the `user_subscriptions` table based on the user ID foreign key. We will then use the Stripe subscription ID to cancel the subscription. ```rust let subscription = user_subscriptions::Entity ::find() .filter(user_subscriptions::Column::UserId.eq(user.id)) .one(&ctx.db).await? .unwrap(); let _ = Subscription::cancel( &client, &SubscriptionId::from_str(&subscription.stripe_subscription_id).unwrap(), CancelSubscription { cancellation_details: None, invoice_now: Some(true), prorate: Some(true), } ).await?; ``` Once the subscription has been successfully canceled and there's nothing else we need to do with Stripe, we can go back and update our database to delete the record: ```rust let subscription_to_delete = user_subscriptions::Entity ::find() .filter(user_subscriptions::Column::UserId.eq(user.id)) .one(&ctx.db).await? .unwrap(); subscription_to_delete.delete(&ctx.db).await?; Ok(StatusCode::OK); ``` Note that there are quite a few different ways to handle this. You may find that you want to explicitly mark a customer's subscription as "expired" rather than outright deleting it from the database if you want users to be able to check their subscription history and other such details. ### Upgrading/Downgrading Subscription Tiers Finally, we will add the ability to upgrade and downgrade subscription tiers. In Stripe terms, we're grabbing information about a user's subscription and updating the price ID of an existing item on the subscription. As before, we'll start with creating the `stripe::Client`: ```rust pub async fn update_subscription_tier( State(ctx): State, auth: middleware::auth::ApiToken, Json(new_user_tier): Json, ) -> Result { use crate::models::_entities::subscription_tiers; use crate::models::_entities::user_subscriptions; let secret_key = std::env::var("STRIPE_API_KEY").expect("Missing STRIPE_API_KEY in env"); let client = Client::new(secret_key); // .. rest of your code } ``` After that, we will find the `user_subscription` based on the user ID. We will then retrieve the subscription data from Stripe and get the first item on the subscription list. Note that while we are using a vector index which can technically panic, there should always be at least one item so it is safe to use index 0. We will also return an error if the user's current tier is the same as the requested tier. ```rust let user_subscription = user_subscriptions::Entity ::find() .filter(user_subscriptions::Column::UserId.eq(auth.user.id)) .one(&ctx.db).await? .unwrap(); let subscription_item = Subscription::retrieve( &client, &SubscriptionId::from_str(&user_subscription.stripe_subscription_id).unwrap(), &["items"] ).await?.items; let subscription_item = &subscription_item.data[0]; if new_user_tier.user_tier == user_subscription.user_tier { return Err(ApiError::UserTierAlreadyExists); } ``` Once done, we can then find the subscription tier data from our database according to the requested tier change and update the subscription using the new tier's Stripe price object ID. ```rust let new_subscription = subscription_tiers::Entity ::find() .filter(subscription_tiers::Column::Tier.contains(new_user_tier.user_tier.to_string())) .all(&ctx.db).await?; let new_sub_tier: String = new_subscription .iter() .find(|x| x.tier == new_user_tier.user_tier.to_string()) .map(|x| x.stripe_price_id.to_string()) .unwrap(); let updated_subscription = Subscription::update( &client, &SubscriptionId::from_str(&user_subscription.stripe_subscription_id).unwrap(), UpdateSubscription { items: Some( vec![UpdateSubscriptionItems { id: Some(subscription_item.id.to_string()), price: Some(new_sub_tier), ..Default::default() }] ), ..Default::default() } ).await?; ``` Before we finish this function up, we will need to update the user tier in the `user_subscriptions` table. ```rust use sea_orm::IntoActiveModel; let mut updated_user_subscription = user_subscription.into_active_model(); updated_user_subscription.user_tier = ActiveValue::Set(new_user_tier.user_tier); let _ = updated_user_subscription.update(&ctx.db).await?; Ok(StatusCode::OK); ``` ### Grabbing a user's product tier Of course, for the frontend you'll probably want a quick way to be able to grab the user's product tier. You can do this like so: ```rust pub async fn get_current_tier( State(ctx): State, auth: middleware::auth::JWTWithUser, ) -> Result, ApiError> { let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; let subscription = user_subscriptions::Entity::find() .filter(user_subscriptions::Column::UserId.eq(user.id)) .one(&ctx.db) .await? .unwrap(); Ok(Json(UpdateUserTier { user_tier: subscription.user_tier })) } ``` ### Hooking it all up Now that we're done, we can add it back to our router file for the controller. We can add it back in, like so: ```rust pub fn routes() -> Routes { Routes::new() .prefix("stripe") .add("/get_current_tier", get(get_current_tier)) .add("/create", post(create_subscription)) .add("/update", post(update_subscription_tier)) .add("/cancel", delete(cancel_subscription)) } ``` ## Deployment To deploy, you just need to run `shuttle deploy --allow-dirty` and let Shuttle make the magic happen! Once done, you'll see information about your service. ## Finishing up Loco is a really strong framework to get started with, and by implementing subscription payments we've managed to slot in the final piece of the puzzle for a potential SaaS in the making. Interested in reading more? - Try using Qdrant and OpenAI to add a RAG-based LLM to your SaaS [here.](https://www.shuttle.dev/blog/2024/02/28/rag-llm-rust) - Add tracing to your project for better logging [here.](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) - Try implementing Oauth2-based authentication [here.](https://www.shuttle.dev/blog/2023/08/30/using-oauth-with-axum) --- # Async Rust in a Nutshell Source: https://www.shuttle.dev/blog/2024/02/29/async-rust Date: 29 February 2024 Author: stefan Tags: rust, async, guide Exploring how async Rust works, async primitives and using async in Rust traits Computers are just as fast as they can be. One way to speed up our programs is to do things in parallel, or concurrently. There is a fine distinction between those two terms. Parallel execution means that we execute two different tasks at the same time, on two different CPUs. Concurrent execution means that a single CPU makes progress on more than one task at the same time, by interleaving the execution of those tasks. The Rust standard library gives bindings and abstractions for the underlying operating system. This includes threads, a way to run code in parallel. The parallelism is managed by the operating system, you can have as many threads as CPU cores, but there can also be more, and the operating system decides when to execute what. This can be potentially very heavy and has lots of overhead. So we are stuck with two ways: Either run everything sequentially or use OS threads to execute in parallel, which can cause overhead. None of them might be the best solution for some domains, like web or networking applications. Async tries to fix those problems. Async is a way to write code sequentially, but execute it concurrently without you managing any threads or execution. The idea is to split up existing code into tasks then execute a portion of code, and have an async runtime pick the next task which needs to be executed. The runtime then decides when to execute what, and can do so in a very efficient way. It also takes advantage of the fact that most of the time, the CPU is waiting for something to happen, like a network request or a file to be read. Look at the following line of code. ```rust let mut socket = net::TcpStream::connect((host, port)).unwrap(); ``` All we do is establish a TCP connection. But this takes time. Not necessarily noticeable for you, but for computers, this means doing nothing, just waiting for the connection to be established. This is a time that we can use better. ## Async Primitives Concurrent execution is nothing new in the world of programming. Also, async programming has been around for a while, and you might have seen similar things in JavaScript or C#. But in Rust, things might look similar at first but are different if we take a closer look. One big difference is that Rust does not have an async runtime. We need an async runtime that manages the correct execution of tasks, but the involved Rust teams argue that there is no "one size fits all" async runtime, and developers should have the power to choose the runtime that fits their needs. Conceptually, this is different to e.g. Go, which comes with only one concurrency model: goroutines. And developers are stuck with that. In Rust, we can decide which one we use. Still, Rust gives us a way to prep our tasks for the async executor. This is done by an abstraction using the `Future` trait. The `Future` trait is the core of async programming in Rust. It is a trait that represents a value that is not yet available but will be available at some point in the _future_. This is very similar to `Promise` in JavaScript. Everything that implements a `Future` can be executed in an async runtime. The `Future` trait is defined as follows: ```rust pub trait Future { type Output; // Required method fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; } ``` It's pretty simple. It has one associated type `Output`, which, well, represents the future value. And it has one method called `poll`, which comes with a `Context` and returns a `Poll`. `Poll` is an enum with two states. Either `Pending`, which means that we wait for a value. Or `Ready`, meaning that the value is available. The `Ready` variant holds the output of type `Output`. ```rust pub enum Poll { Ready(T), Pending, } ``` `Context` currently only serves to provide access to a `Waker` object. The `Waker` is necessary to tell the runtime to poll this task again. Okay, okay, what's that? Polling, waking? Let's dig deeper. ## Execution As said before, the `Future` trait is used to abstract tasks that can be executed in an async runtime. But how does this work? To be fair, in detail, it depends on the async runtime in use, but a few basic concepts are the same for all of them. [Nick Cameron](https://www.ncameron.org/blog/what-is-an-async-runtime/) has written an overview on this topic, to sum it up: An asynchronous runtime has an executor. An executor typically has two key APIs: `spawn` and `block_on`. `block_on` is used to wait for a task to complete on the current thread. `spawn` is used to start a new task on the executor, but non-blocking. It returns immediately. The return value depends on the `Future`. Is there something asynchronous happening? Then polling the `Future` will return `Poll::Pending` immediately, but also sets up the rules for the executor to wake the task up when it's ready. This can be some IO event on the operating system, like a TCP connection that has been established. If there's nothing asynchronous happening, the `Future` will return `Poll::Ready` with the return value. Once an event has happened, the waker indicates to the executor to poll the same future again, there might already be a result. ## Sugar: `async` and `await` Okay, so all you need to have is functions or structs that implement `Future`, and you're done with it. Right? Right? Well, it's not that easy, literally. Implementing the `Future` trait can be very daunting, and it's not very ergonomic. That's why Rust has introduced the `async` and `await` keywords. To make something async, it needs to return a `Future`. So if you want to have a method `read_to_string`, and this is the synchronous version: ```rust fn read_to_string(&mut self, buf: &mut String) -> Result; ``` The async version would look like this: ```rust fn read_to_string(&mut self, buf: &mut String) -> impl Future>; ``` There is syntactic sugar for writing this. Instead of returning a `Future`, you can declare it as `async`. ```rust async fn read_to_string(&mut self, buf: &mut String) -> Result; ``` You also don't need to poll by yourself. You can use the `await` keyword to wait for the result of a `Future`. ```rust let result = fileread_to_string(&mut buf).await; ``` Under the hood, the Rust compiler creates the futures for you. It does so by splitting up your code into several tasks, with every `await` being a break-point separating the tasks. The compiler then creates a state machine for you, and the `Future` trait is implemented for you. With every `await`, the state machine is polled and potentially moves to the next state. The Tokio team shows wonderfully [how those Futures can be implemented or created by the compiler in this tutorial](https://tokio.rs/tokio/tutorial/async). Speaking of Tokio. Tokio is one of the most popular runtimes out there and has been designed for asynchronous execution of network applications. It has also been the playground for the early days of async and is stable, used in production, and most likely also the base of any web framework that you're using. It not only comes with the necessary abstractions for OS events, but a feature-rich runtime with different modes, and async representations of standard library IO and networking features. If you want to start out with Async in Rust, Tokio is a good choice. ## Async methods in Traits All that jazz leads up to one of the most wanted, yet most awaited features in async Rust: Defining async methods in traits. This feature has landed [recently in Rust](https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html) and still has some limitations. The problem underneath is that we as developers want to write in the beautiful `async` / `await` syntax, yet the compiler needs to prepare `Future` trait implementations for an automatically generated state machine, and this can get really complex. Let's look at an example that wants to define a writing interface for a chat application, called `ChatSink`. This is how I want to write it. ```rust pub trait ChatSink { type Item: Clone; async fn send_msg(&mut self, msg: Self::Item) -> Result<(), ChatCommErr>; } ``` Once we want to transfer this into something that uses `Future` implementations, things get a bit hairy. Instead of the `async` method, we need to define a `Future` return type, but we don't know which `Future` it's going to be! This will be defined by the implementor of the trait at some later stage. So all we can do is to say that whatever comes, it will implement the `Future` trait. This is done by using the `impl` keyword. ```rust pub trait ChatSink { type Item: Clone; fn send_msg(&mut self, msg: Self::Item) -> impl Future>; } ``` The interesting thing though is that `impl Trait` is also just syntactic sugar for an associated type. In reality, something like this would be generated. ```rust pub trait ChatSink { type Item: Clone; type $: Future>; fn send_msg(&mut self, msg: Self::Item) -> Self::$; } ``` But that's not all, we left out a very important detail. Compared to other `impl Trait` resolutions, a `Future` needs to add a lifetime parameter. This is related to how futures are handled internally: They don't execute code, they only pass the opportunity to execute code to another runtime environment, the executor we mentioned before! Async functions create futures like this, and they need to keep all references to the input parameters. Based on the ownership rules of Rust, all those references need to live as long as the future itself. To make sure that this information is available, we need to add a lifetime parameter to the `Future` trait. Which leads to a feature called _generic associated types_. An equivalent version of the `ChatSink` trait would look like this: ```rust pub trait ChatSink { type Item: Clone; type $<'m>: Future> + 'm; fn send_msg(&mut self, msg: Self::Item) -> Self::$<'_>; } ``` But until Rust 1.75, all of that was not possible. This has changed, but it still comes with a few limitations. `impl Trait` currently does not allow to add user-defined trait bounds, a feature that is necessary if you as a developer want to implement a trait from a library. Let alone if you want to decide that your async code only should work on a single thread or on a multi-threaded runtime, adding the `Send` and `Sync` marker traits is something you want to define on your own (more on those marker traits [here](https://doc.rust-lang.org/nomicon/send-and-sync.html)). ## Why do I need to know all of that? To be fair, this is a lot of information and goes into the nitty gritty details of what async Rust is all about. But there's a reason to that. Like everything in Rust, things appear easy and straightforward at first, but once you dig deeper, you'll find that there's a lot of complexity and a lot of things to consider. The same happens with async programming in Rust. Defining async methods is something that you definitely have done. You are reading the Shuttle blog after all, and async powers web development in Rust. At first, they are easy, but suddenly you might end up seeing error messages that you can't get a hold of. You define a resource in your async function, wrap it in a `std::sync::Mutex` and get it's `MutexGuard` once you lock it. Suddenly you decide to make a call to async API and pass an `.await` point. The compiler will scream at you, because a `MutexGuard` does not implement the `Send` trait, and you can't pass it to another thread. But why do you need to pass it to another thread? All you did was calling an async function? And this is where the runtime stuff comes in. Your runtime configuration might work multi-threaded, and you never know which one of the worker threads executes the current task. Since you need to keep all resources ready for the automatic `Future` implementation, all those references and resources need to be thread-safe. There are more pitfalls, but this is something for another time. ## Further Reading If you've gotten this far, you might be interested in the following resources: - [Nick Cameron writes a lot about Async Rust](https://www.ncameron.org/blog/), you might want to check it out. - So does [Without Boats](https://without.boats/), who gives a lot of insights into the design and development of async Rust. - The [Tokio Tutorial on Async in Depth](https://tokio.rs/tokio/tutorial/async) is nothing but excellent. - So is the Async chapter in ["Programming Rust"](https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/) by Jim Blandy, Jason Orendorff, and Leonora Tindall. - Also check out my talk at the first [Shuttle Labs](https://www.youtube.com/watch?v=PS-RywZP2_U). And of course, everything you read on this very blog. --- # Building a RAG Web Service with Qdrant and Rust Source: https://www.shuttle.dev/blog/2024/02/28/rag-llm-rust Date: 28 February 2024 Author: josh Tags: rust, qdrant, ai, guide Diving into Retrieval Augmented Generation to help enhance your web applications Hey there! Today, we're going to talk about creating a web application that utilises Retrieval Augmented Generation (RAG). By the end of this, you'll have a web service that can parse markdown files to create a small knowledge base that you can query. Interested in deploying or wanting to fiddle around with the code yourself? You can find the repo [here](https://github.com/joshua-mo-143/shuttle-qdrant-template). You can deploy it in three steps: - Use `shuttle init --from joshua-mo-143/shuttle-qdrant-template` and follow the prompt - Add your API keys (see Pre-requisites for this) - Use `shuttle deploy --allow-dirty` to deploy and wait for the magic to happen! ## What is Retrieval Augmented Generation? Retrieval Augmented Generation is an AI framework for improving the quality of LLM responses. This is done by providing documents or other kinds of data (for example, images) that improve the LLM's internal representation of information. This has a few benefits: - You can provide users with the latest facts instead of relying on outdated training data. - The chance of LLM hallucinations and sensitive data leakages are reduced. - RAG can work with loads of different types of data, as long as you can embed it. RAG has recently gained a lot of popularity in use cases that require knowledge bases, for example: - Support ticket work (being able to automate answering common questions) - Searching a database of business documents to generate a report/analysis Without further ado, let's get started! ## Getting started ### Pre-requisites Before we start, you need an OpenAI API key. You'll also need to sign up for Qdrant to get an API key and database URL. After initialising your project, you'll want to create a file named `Secrets.toml` in the root of your project with the following keys: ```rust OPENAI_API_KEY = "YOUR_OPENAI_API_KEY" QDRANT_URL = "YOUR_QDRANT_DATABASE_URL" QDRANT_TOKEN = "YOUR_QDRANT_API_KEY" ``` You'll also want to add the following to `Shuttle.toml` (again, create this file in the root of your project): ```rust assets = ["docs/*"] ``` ### Installing and project initialisation To get started, we'll generate a new application using `shuttle init` (requires `cargo-shuttle` installed). Make sure to pick Axum as the framework! We'll need to add some dependencies, which we can do with the following shell snippet: ```bash cargo add anyhow@1.0.71 cargo add axum-streams@0.12.0 -F text cargo add futures@0.3.28 cargo add openai@1.0.0-alpha.10 cargo add qdrant-client@1.2.0 cargo add serde@1.0.164 -F serde_derive cargo add serde_json@1.0.96 cargo add shuttle-qdrant cargo add shuttle-secrets cargo add tokio-stream@0.1.14 cargo add tower-http@0.5.0 -F fs cargo add uuid@1.3.3 ``` To start, we'll create a new struct called `File` that will hold the contents of a file, the pathname as well as individual sentences from the contents (as a `Vec`). Our shared application state will hold a `Vec` so that when we prompt our API, it will be able to easily load and search the file contents as it's already in memory. ```rust // src/contents.rs pub struct File { pub path: String, pub contents: String, pub sentences: Vec, } ``` We will also additionally set up a new struct called `VectorDB` so that we can easily extend the behavior of the `QdrantClient`. ```rust // src/vector.rs use qdrant_client::qdrant::QdrantClient; pub struct VectorDB { client: QdrantClient, id: u64, } impl VectorDB { pub fn new(client: QdrantClient) -> Self { Self { client, id: 0 } } } ``` To finish setting up, we'll add our macro annotations to our main function. On a local or deployment run, the Shuttle runtime will automatically provision any things we need from the macros. ```rust // src/main.rs mod vector; mod contents; use contents::File; use qdrant_client::qdrant::QdrantClient, struct AppState { files: Vec, vector_db: VectorDB, } async fn hello_world() -> &'static str { "Hello world!" } #[shuttle_runtime::main] async fn axum( #[shuttle_secrets::Secrets] secrets: shuttle_secrets::SecretStore, #[shuttle_qdrant::Qdrant( cloud_url = "{secrets.QDRANT_URL}", api_key = "{secrets.QDRANT_TOKEN}" )] qdrant_client: QdrantClient, ) -> shuttle_axum::ShuttleAxum { let router = Router::new() .route("/", get(hello_world)); Ok(router.into()) } ``` ### Setup Before we start, we'll need to set up a struct for dealing with errors while setting up. We can put this in a new `[error.rs](http://error.rs)` file and will be referencing this struct later on. ```rust // src/errors.rs #[derive(Debug)] pub struct SetupError(pub &'static str); impl std::error::Error for SetupError {} impl std::fmt::Display for SetupError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Error: {}", self.0) } } ``` Next, we'll want to set up a function that will globally set the OpenAI key for all of the `openai` crate functions. This will take the `SecretStore` provided to us from our secrets annotation macro. ```rust // src/open_ai.rs use anyhow::Result; pub fn setup(secrets: &SecretStore) -> Result<()> { // This sets OPENAI_API_KEY as API_KEY for use with all openai crate functions let openai_key = secrets .get("OPENAI_API_KEY") .ok_or(SetupError("OPENAI Key not available"))?; openai::set_key(openai_key); Ok(()) } ``` ### Embedding To get started, we need to find some files to embed! For our example we'll be using the Shuttle docs website, but you can use whatever you want. In this section, we'll be creating embeddings using an OpenAI-provided LLM and inserting/updating them in Qdrant as required. Before we start embedding, we'll want to quickly define some error types that will represent possible errors that can happen while doing this. Our first error type, `EmbeddingError`, will represent embedding errors that can happen while creating an embedding. Our second error type, `NotAvailableError`, will represent errors while trying to load file paths from the file directory we want to use for our knowledge base. ```rust // src/errors.rs #[derive(Debug)] pub struct EmbeddingError; impl std::error::Error for EmbeddingError {} impl Display for EmbeddingError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "Embedding error") } } impl From for EmbeddingError { fn from(_: anyhow::Error) -> Self { Self {} } } #[derive(Debug)] pub struct NotAvailableError; impl std::error::Error for NotAvailableError {} impl std::fmt::Display for NotAvailableError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "File 'not available' error") } } ``` To use `File`, we'll write a function called `load_files_from_dir` that will return `Result>`. Note that while `sub_files` calls the function recursively, the recursion depth is based on the folder depth of the document base you are embedding. Some `PathBuf` manipulation is done here to ensure that the paths are sanitized. ```rust // src/contents.rs use anyhow::Result; use std::path::PathBuf; // Load files from directory according to their file extension ("ending") pub fn load_files_from_dir(dir: PathBuf, ending: &str, prefix: &PathBuf) -> Result> { let mut files = Vec::new(); for entry in fs::read_dir(dir)? { let path = entry?.path(); if path.is_dir() { let mut sub_files = load_files_from_dir(path, ending, prefix)?; files.append(&mut sub_files); } else if path.is_file() && path.has_file_extension(ending) { println!("Path: {:?}", path); let contents = fs::read_to_string(&path)?; let path = Path::new(&path).strip_prefix(prefix)?.to_owned(); let key = path.to_str().ok_or(NotAvailableError {})?; let mut file = File::new(key.to_string(), contents); file.parse(); files.push(file); } } Ok(files) } ``` For each file and sentence, we need to create embeddings. These are used separately as our program will need to find relevant sentences from an embedded file. ```rust // src/open_ai.rs use openai::embeddings::{Embedding, Embeddings}; use anyhow::Result; use crate::errors::EmbeddingError; pub async fn embed_file(file: &File) -> Result { let sentence_as_str: Vec<&str> = file.sentences.iter().map(|s| s.as_str()).collect(); Embeddings::create("text-embedding-ada-002", sentence_as_str, "shuttle") .await .map_err(|_| EmbeddingError {}.into()) } pub async fn embed_sentence(prompt: &str) -> Result { Embedding::create("text-embedding-ada-002", prompt, "shuttle") .await .map_err(|_| EmbeddingError {}.into()) } ``` To be able to move the embedding to Qdrant easily, we'll create a method for `VectorDB` that allows us to easily upsert ("insert or update", depending on whether the point already exists or not) the points into the Qdrant database. We map the `Embedding` into a `Vec` and then upsert the points into the collection. We then increase the `VectorDB` ID by 1 to show that an embedding has been upserted. ```rust // src/vector.rs use crate::errors::EmbeddingError; const COLLECTION: &str = "docs"; impl VectorDB { // .. your other functions pub async fn upsert_embedding(&mut self, embedding: Embedding, file: &File) -> Result<()> { let payload: Payload = json!({ "id": file.path.clone(), }) .try_into() .map_err(|_| EmbeddingError {})?; println!("Embedded: {}", file.path); let vec: Vec = embedding.vec.iter().map(|&x| x as f32).collect(); let points = vec![PointStruct::new(self.id, vec, payload)]; self.client.upsert_points(COLLECTION, None, points, None).await?; self.id += 1; Ok(()) } } ``` Next, we need to embed the documentation by passing it into GPT, retrieving the embedding data, and upserting it into Qdrant. We'll use a function to embed the file and get a series of embeddings, then upsert the embeddings into Qdrant. ```rust // src/main.rs use contents::File; use vector::VectorDB; mod open_ai; mod vector; mod contents; async fn embed_documentation(vector_db: &mut VectorDB, files: &Vec) -> anyhow::Result<()> { for file in files { let embeddings = open_ai::embed_file(file).await?; println!("Embedding: {:?}", file.path); for embedding in embeddings.data { vector_db.upsert_embedding(embedding, file).await?; } } Ok(()) } ``` Now that we're done, we can move onto our next part: prompting our model! ### Prompting To get started, we'll need to create a chat stream. Receiving the chat as a streamed response allows us to output the stream to a webpage or CLI as it's being received. This decreases the amount of time a user needs to wait. We use the GPT `ChatCompletionBuilder` to create the prompt, using `gpt-3.5-turbo` as the model. We set a temperature of 0.0 so that we only get exactly what we want, as any higher may cause LLM hallucinations. ```rust // src/open_ai.rs use openai::{ chat::{ChatCompletion, ChatCompletionBuilder, ChatCompletionDelta, ChatCompletionMessage}, embeddings::{Embedding, Embeddings}, }; use shuttle_secrets::SecretStore; use tokio::sync::mpsc::Receiver; use anyhow::Result; use tokio::sync::mpsc::Receiver; type Conversation = Receiver; pub async fn chat_stream(prompt: &str, contents: &str) -> Result { let content = format!("{}\n Context: {}\n Be concise", prompt, contents); ChatCompletionBuilder::default() .model("gpt-3.5-turbo") .temperature(0.0) .user("shuttle") .messages(vec![ChatCompletionMessage { role: openai::chat::ChatCompletionMessageRole::User, content, name: Some("shuttle".to_string()), }]) .create_stream() .await .map_err(|_| EmbeddingError {}.into()) } ``` Now we can feed this into a function that embeds the prompt and then searches the database for embeddings with similar values. We map elements in the embedding vector to an `f32` , create a `SearchPoints` struct that includes the vector we mapped, and then search all of the embedding points in the database. Once done, we can get the first search result and return it. ```rust // src/vector.rs use openai::embeddings::Embedding; use qdrant_client::qdrant::{ vectors_config::Config, with_payload_selector::SelectorOptions, ScoredPoint, SearchPoints, WithPayloadSelector, }, }; impl VectorDB { // .. your other functions pub async fn search(&self, embedding: Embedding) -> Result { let vec: Vec = embedding.vec.iter().map(|&x| x as f32).collect(); let payload_selector = WithPayloadSelector { selector_options: Some(SelectorOptions::Enable(true)), }; let search_points = SearchPoints { collection_name: COLLECTION.to_string(), vector: vec, limit: 1, with_payload: Some(payload_selector), ..Default::default() }; let search_result = self.client.search_points(&search_points).await?; let result = search_result.result[0].clone(); Ok(result) } } ``` We can then write a function that includes getting the contents of the embedding and searches the database for similar embeddings and grabs the contents of the `File` from the `Vec` in our shared application state. To make it easier for us, let's implement a trait called `Finder` that helps us to find the embedding from the `ScoredPoint` struct. ```rust // src/finder.rs use qdrant_client::qdrant::{value::Kind, ScoredPoint}; use crate::contents::File; pub trait Finder { fn find(&self, key: &str) -> Option; fn get_contents(&self, result: &ScoredPoint) -> Option; } impl Finder for Vec { fn find(&self, key: &str) -> Option { for file in self { if file.path == key { return Some(file.contents.clone()); } } None } fn get_contents(&self, result: &ScoredPoint) -> Option { let text = result.payload.get("id")?; let kind = text.kind.to_owned()?; if let Kind::StringValue(value) = kind { self.find(&value) } else { None } } } ``` Now that we've written all of this up, we can write our final `get_contents()` function that gives us the final chat stream: ```rust // src/main.rs use crate::errors::PromptError; use anyhow::Result; use crate::{open_ai, AppState}; async fn get_contents( prompt: &str, app_state: &AppState, ) -> anyhow::Result> { let embedding = open_ai::embed_sentence(prompt).await?; let result = app_state.vector_db.search(embedding).await?; println!("Result: {:?}", result); let contents = app_state .files .get_contents(&result) .ok_or(PromptError {})?; open_ai::chat_stream(prompt, contents.as_str()).await } ``` Finally, we can provide an endpoint to do all of this in but a few lines, returning a streamed response. If there is no error, return the response from GPT; if there is an error, return a streamed response with the error. ```rust // src/main.rs use std::sync::Arc; use axum::{Json, extract::State}; use tokio_stream::wrappers::ReceiverStream; use tokio_stream::StreamExt; use futures::Stream; #[derive(Deserialize)] struct Prompt { prompt: String, } async fn prompt( State(app_state): State>, Json(prompt): Json, ) -> impl IntoResponse { let prompt = prompt.prompt; let chat_completion = get_contents(&prompt, &app_state).await; if let Ok(chat_completion) = chat_completion { return axum_streams::StreamBodyAs::text(chat_completion_stream(chat_completion)); } axum_streams::StreamBodyAs::text(error_stream()) } ``` ## Hooking it all up Now that everything's been written up, we can funnel it back into our main function! ```rust // src/main.rs #[shuttle_runtime::main] async fn axum( #[shuttle_secrets::Secrets] secrets: shuttle_secrets::SecretStore, #[shuttle_qdrant::Qdrant( cloud_url = "{secrets.QDRANT_URL}", api_key = "{secrets.QDRANT_TOKEN}" )] qdrant_client: QdrantClient, ) -> shuttle_axum::ShuttleAxum { let embedding = false; open_ai::setup(&secrets)?; let mut vector_db = VectorDB::new(qdrant_client); let files = contents::load_files_from_dir("./docs".into(), ".mdx", &".".into())?; println!("Setup done"); embed_documentation(&mut vector_db, &files).await?; println!("Embedding done"); let app_state = AppState { files, vector_db }; let app_state = Arc::new(app_state); let router = Router::new() .route("/prompt", post(prompt)) .nest_service("/", ServeDir::new("static")) .with_state(app_state); Ok(router.into()) } ``` Note that every time you spin this up, it will attempt to embed the documentation. You may want to comment this part out if you need to want to run the web service multiple times (for example, when testing). ## Deploying Now that we're at the end, we can deploy! Run `shuttle deploy` (with `--allow-dirty` attached) and `cargo-shuttle` will automatically make the magic happen. When finished, you'll receive some data about your deployment and a link to your deployed project. ## Finishing up Thanks for reading! Interested in extending this example? Here's a couple of ways you can extend this example: - Try using Candle as the LLM instead of OpenAI's GPT to reduce your costs. - Try parsing other types of files! - Add a frontend to your service (see the repo at the start for guidance on this) Here's a few suggestions for other articles you may be interested in: - Learn about implementing authentication for your application [here.](https://www.shuttle.dev/blog/2024/02/21/using-jwt-auth-rust) - Learn about rate limiting your API [here.](https://www.shuttle.dev/blog/2024/02/22/api-rate-limiting-rust) - Learn more about writing Axum [here.](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) --- # Implementing API Rate Limiting in Rust Source: https://www.shuttle.dev/blog/2024/02/22/api-rate-limiting-rust Date: 22 February 2024 Author: josh Tags: rust, rate-limiting, guide Exploring how to implement rate limiting manually in a Rust API as well as using crates Hello world! We're going to talk about implementing rate limiting for your API in Rust. When it comes to services in production, you want to ensure that bad actors aren't abusing your APIs - this is where API rate limiting comes in. For this tutorial we will be implementing the "sliding window" algorithm by having a dynamic period to check request histories over as well, using a basic in-memory hashmap to store users and their request times. We will also look at using `tower-governor` to configure rate limiting for you. ## Implementing a naive sliding window rate limiter To figure out how we can do this from the ground up, let's write a naive sliding window IP based rate limiter from scratch. To get started, we're going to initialise a regular project using `cargo init` and follow the prompt, picking Axum as our framework of choice. We're going to need some extra dependencies, so let's install them with this shell snippet: ```bash cargo add serde@1.0.196 -F derive cargo add chrono@0.4.34 -F serde,clock ``` We'll declare a new struct that holds a `HashMap` of `IpAddr` keys with the values being `Vec>` (a Vector of UTC-timezone timestamps). ```rust use std::sync::{Arc, Mutex}; use std::collections::HashMap; use std::net::IpAddr; use chrono::{DateTime, Utc}; // This will be the request limit (per minute) for a user to access an endpoint // If the user attempts to go beyond this limit, we should return an error const REQUEST_LIMIT: usize = 120; #[derive(Clone, Default)] pub struct RateLimiter { requests: Arc>>>>, } ``` To get started, we'll want to lock our hashmap by using `.lock()` which gives us write access. Then we'll want to check whether or not the hashmap contains a key containing the IP address we want to check for with the `.entry()` function, then modify it by retaining valid timestamps and pushing a new entry depending on whether or not the length is under the request limit. We then check if the entry length is a higher length than the request limit - if so, return an error; if not, return `Ok(())`. ```rust impl RateLimiter { fn check_if_rate_limited(&self, ip_addr: IpAddr) -> Result<(), String> { // we only want to keep timestamps from up to 60 seconds ago let throttle_time_limit = Utc::now() - std::time::Duration::from_secs(60); let mut requests_hashmap = self.requests.lock().unwrap(); let mut requests_for_ip = requests_hashmap // grab the entry here and allow us to modify it in place .entry(ip_addr) // if the entry is empty, insert a vec with the current timestamp .or_insert(Vec::new()); requests_for_ip.retain(|x| x.to_utc() > throttle_time_limit); requests_for_ip.push(Utc::now()); if requests_for_ip.len() > REQUEST_LIMIT { return Err("IP is rate limited :(".to_string()); } Ok(()) } } ``` Here is a basic example of how you might use this: ```rust use std::net::Ipv4Addr; fn main() { let rate_limiter = RateLimiter::default(); let localhost_v4 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); // here we request 120 times - our request limit for _ in 1..80 { assert!(rate_limiter.check_if_rate_limited(localhost_v4).is_ok()) } // wait 30 seconds std::thread::sleep(std::time::Duration::from_secs(30)); // make another 40 requests here to satisfy request quota for _ in 1..40 { assert!(rate_limiter.check_if_rate_limited(localhost_v4).is_ok()) } // wait another 30 seconds std::thread::sleep(std::time::Duration::from_secs(30)); // now we can make another 80 requests for _ in 1..80 { assert!(rate_limiter.check_if_rate_limited(localhost_v4).is_ok()) } } ``` From here if we wanted to extend this to work with Axum, we could. However, production-ready rate-limiting systems are typically much more advanced than this. We'll be discussing how you can utilize crates for rate limiting below, including usage of user-based rate limiting. ## Implementing user-based rate limiting For external-facing websites without a login, IP addresses are the only thing you can use (besides browser information) to track users. However, it can be much more useful to rate limit based on authenticated users rather than IP addresses. While working with IP addresses, you may run into the following issues: - Multiple users may have the same IP address - Users can simply change the IP address they use if you block them (via proxy or other methods) Working with user-based rate limiting allows us to solve these issues. While users can have more than one IP address, we can assign it all to the same user. ## Getting started To initialise our web service we'll use `shuttle init` (requires `cargo-shuttle` installed) to create our project, making sure to pick Axum as the framework. Before adding the rate limiter itself, we're going to create a custom header key! This will be used in routes where we require user authentication. We can also use the header when implementing our custom key extractor for the rate limiter later on. We'll want to start by adding `axum-extra` with the `typed-header` feature: ```rust cargo add axum-extra -F typed-header ``` Next we'll want to create a struct that will hold a String and implement the `axum_extra` re-export of `headers::Header`. You can see the `Header` implementation below, where it decodes the value by iterating over `HeaderValue` and creates the `CustomHeader` struct. We can start by defining a `HeaderName`: ```rust static X: HeaderName = HeaderName::from_static("x-custom-key"); static CUSTOM_HEADER: &HeaderName = &X; pub struct CustomHeader(String); impl CustomHeader { pub fn key(self) -> String { self.0 } } ``` Now that we've defined our custom header name (which will be used as the header key), we can implement `axum_extra::headers::Header` for `CustomHeader`: ```rust impl Header for CustomHeader { fn name() -> &'static HeaderName { CUSTOM_HEADER } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let value = values .next() .ok_or_else(axum_extra::headers::Error::invalid)?; Ok(CustomHeader(value.to_str().unwrap().to_owned())) } fn encode(&self, values: &mut E) where E: Extend, { let s = &self.0; let value = HeaderValue::from_str(s).unwrap(); values.extend(std::iter::once(value)); } } ``` To use `CustomHeader` as an Axum extractor, we need to wrap it in `TypedHeader` like so: ```rust async fn register( TypedHeader(header): TypedHeader, ) -> impl IntoResponse { // .. your code goes here } ``` This is all well and good, but how does this relate to rate limiting? While we can use this in middleware, a better alternative solution would be to use `tower_governor`. This crate is a Tower service backed by the `governor` crate (a crate for regulating data with rate limiting) and makes it much easier to implement rate limiting, The crate uses the Generic Cell Rate Algorithm (GCRA) which is a much more sophisticated version of a leaky bucket. You can read much more about GCRA [here.](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm) To get started, we'll add the crate to our Rust program: ```bash cargo add tower-governor ``` When we want to add it to our main function, we can do it by using `GovernorConfigBuilder` and then adding it into `GovernorLayer`. Note that while `GovernorConfigBuilder` doesn't implement `Clone`, adding a Tower service layer requires it to implement `Clone`. This means that we need to box the config builder and then later on, we can use `Box::leak` to leak the box to get a `&'static` lifetime `GovernorConfig` for usage with our `axum::Router`: ```rust use auxm::{Router, routing::get}; use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer}; #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let governor_conf = Box::new( GovernorConfigBuilder::default() .per_second(2) .burst_size(5) .finish() .unwrap(), ); let router = Router::new() .route("/", get(hello_world)) .layer(GovernorLayer { // We can leak this because it is created once and then never needs to be destructed config: Box::leak(governor_conf), }); Ok(router.into()) } ``` By default, `GovernorConfigBuilder` uses a type called `PeerIpKeyExtractor` which attempts to grab the IP key of a connecting client. However, to use our header as the extracted key we can implement `tower_governor::key_extractor::KeyExtractor`. To do this, we'll use a unit struct as when we add it to `GovernorConfigBuilder` later on, there aren't currently any extra variables we need: ```rust use tower_governor::GovernorError; use axum::http::Request; #[derive(Clone)] pub struct CustomHeaderExtractor; impl KeyExtractor for CustomHeaderExtractor { type Key = String; fn extract(&self, req: &Request) -> Result { let headers = req.headers(); match headers.get(CUSTOM_HEADER) { Some(res) => { let res = res.to_str() .map_err(|_| GovernorError::UnableToExtractKey)?; Ok(res.to_owned()) }, None => Err(GovernorError::UnableToExtractKey) } } } ``` This allows us to add `CustomHeaderExtractor` to our `GovernorConfigBuilder` in our main function. ```rust let governor_conf = Box::new( GovernorConfigBuilder::default() .per_second(2) .burst_size(5) .key_extractor(CustomHeaderExtractor) .finish() .unwrap(), ); ``` When a user attempts to access any route that is layered with the `GovernorLayer`, now it'll attempt to get a header with the header name `x-custom-key` - if it's not present, the route will return an error. Here we have set the limit to allow users to send 5 requests every 2 seconds. Note that in the builder, the `per_second()` function tells us exactly how many seconds the interval will be between replenishing the quota and `burst_size` tells us what the quota is before `tower-governor` will start blocking requests from a given IP address (or API key, in our case). We can also additionally set `per_millisecond()` and `per_nanosecond()` parameters so that if you want to replenish the quota every half a second for example, you can use `per_millisecond(500)` in the builder. ## Deploying Now that we're done, you can deploy using `shuttle deploy` (add `--ad` if on a dirty Git branch) and watch the magic happen. Once finished, Shuttle will output the details of your deployment in the terminal. ## Finishing Up Thanks for reading! With this guide, implementing rate limiting in a Rust web service should be much easier to do. Productionizing Rust web services has never been easier! Read more: - Try out implementing JWT authentication [here.](https://www.shuttle.dev/blog/2024/02/21/using-jwt-auth-rust) - Learn more about the Tracing ecosystem for logging [here.](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) --- # Implementing JWT Authentication in Rust Source: https://www.shuttle.dev/blog/2024/02/21/using-jwt-auth-rust Date: 21 February 2024 Author: josh Tags: rust, auth, jwt, guide Using JSON Web Tokens (JWTs) when implementing authentication in a Rust API Hey there! Following on from our ShuttleBytes talk which we held on Tuesday, we're going to talk about how you can implement authentication using JSON Web Tokens (JWTs) in Rust. ## What is a JWT? A JSON Web Token (JWT) is a compact, URL-safe way to transfer data ("claims") between two parties over the Web. The data is typically encoded using a JSON Web Signature or as part of a JSON Web Encryption (JWE) structure, and can be encrypted (and signed!). JWTs are a popular option when deciding on an auth strategy. The client stores all the information via the JWT, allowing for a stateless API. This can make user authentication much easier in some cases. While unencrypted and unsigned JWTs can be manipulated, it is simple for the server to be able to disregard manipulated JWTs as long as the secret key used to create them remains secret. ## Getting started To get started, let's initialise a project using `shuttle init`, making sure to pick Axum as the framework. Then we'll add our dependencies: ```bash cargo add axum-extra@0.9.2 -F typed-header cargo add chrono@0.4.34 -F serde,clock cargo add jsonwebtoken@9.2.0 cargo add once_cell@1.19.0 cargo add serde@1.0.196 -F derive cargo add serde-json@1.0.113 ``` ## Writing our web service ### Setting up keys To start with, we'll need to declare a struct that holds decoding and encoding key, with a method that can take a `&[u8]` (u8 slice) to generate the struct: ```rust use jsonwebtoken::{DecodingKey, EncodingKey}; struct Keys { encoding: EncodingKey, decoding: DecodingKey, } impl Keys { fn new(secret: &[u8]) -> Self { Self { encoding: EncodingKey::from_secret(secret), decoding: DecodingKey::from_secret(secret), } } } ``` This struct needs to be generated from a secret key as it's what we will use to generate the JWTs from. For this example, we'll be randomly generating our bytes from a String and then turning it into bytes. It will then be stored in a `once_cell::LazyCell` that can be accessed globally in our application: ```rust use once_cell::sync::Lazy; static KEYS: Lazy = Lazy::new(|| { let secret = Alphanumeric.sample_string(&mut rand::thread_rng(), 60); Keys::new(secret.as_bytes()) }); ``` Note that there's many different sources you can use to generate the byte array from for this - generating a random string and turning it into bytes is just one of them. For production usage, you may also want to use a cryptographically safe algorithm. ### Writing our JWT Claim The next step is to implement our claim. A claim (in JWT context) is the data transmitted by a JWT and gets encoded or decoded by the server. We can write our own Claim implementation by creating a struct that holds a username and expiry date, then implementing the `FromRequestParts` trait (from Axum) for the struct. This allows us to use it as an Axum extractor and saves us from having to implement any middleware! However before we write the actual implementation itself, `FromRequestParts` requires that we have a custom error type. We can write one that represents JWT failures and implement `IntoResponse` for it - which will then allow us to use it in the implementation. ```rust use axum::response::{ IntoResponse, Response }; use axum::http::StatusCode; use serde_json::json; pub enum AuthError { InvalidToken, WrongCredentials, TokenCreation, MissingCredentials, } impl IntoResponse for AuthError { fn into_response(self) -> Response { let (status, error_message) = match self { AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"), AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"), AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"), AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"), }; let body = Json(json!({ "error": error_message, })); (status, body).into_response() } } ``` Implementing `IntoResponse` for `AuthError` allows it to be used as the `Rejection` type in the `FromRequestParts` trait. Note that to be able to return `AuthError` in the `FromPartsRequest` trait, we use `map_err` to turn the error type into `AuthError` so that it can be propagated. We also use de-structuring here to extract the bearer struct from the `TypedHeader>` type as it's much easier to access. ```rust use serde::{ Serialize, Deserialize }; use axum::{ http::{ request::Parts }, extract::FromRequestParts, RequestPartsExt }; #[derive(Debug, Serialize, Deserialize)] pub struct Claims { username: String, exp: usize, } #[async_trait] impl FromRequestParts for Claims where S: Send + Sync { type Rejection = AuthError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { // Extract the token from the authorization header let TypedHeader(Authorization(bearer)) = parts .extract::>>().await .map_err(|_| AuthError::InvalidToken)?; // Decode the user data let token_data = decode::( bearer.token(), &KEYS.decoding, &Validation::default() ).map_err(|_| AuthError::InvalidToken)?; Ok(token_data.claims) } } ``` Now that we're done setting up our boilerplate for how the JWT should work, we can move onto creating our routes! ### Creating routes The next step is to write our endpoint when authorizing a user - when returning the token, we'll create a new `AuthBody` with the token in it and the type of token it is. We'll be using this later on: ```rust #[derive(Debug, Serialize)] struct AuthBody { access_token: String, token_type: String, } impl AuthBody { fn new(access_token: String) -> Self { Self { access_token, token_type: "Bearer".to_string(), } } } ``` Now that we've created our `AuthBody`, we can create an endpoint that will take a client ID and secret and verify it. Then it will create a claim, encode it and return it as JSON. ```rust use axum::Json; use chrono::Utc; #[derive(Debug, Deserialize)] struct AuthPayload { client_id: String, client_secret: String, } async fn authorize(Json(payload): Json) -> Result, AuthError> { // Check if the user sent the credentials if payload.client_id.is_empty() || payload.client_secret.is_empty() { return Err(AuthError::MissingCredentials); } // Here, basic verification is used but normally you would use a database if &payload.client_id != "foo" || &payload.client_secret != "bar" { return Err(AuthError::WrongCredentials); } // create the timestamp for the expiry time - here the expiry time is 1 day // in production you may not want to have such a long JWT life let exp = (Utc::now().naive_utc() + chrono::naive::Days::new(1)).timestamp() as usize; let claims = Claims { username: payload.client_id, exp, }; // Create the authorization token let token = encode(&Header::default(), &claims, &KEYS.encoding).map_err( |_| AuthError::TokenCreation )?; // Send the authorized token Ok(Json(AuthBody::new(token))) } ``` Now that we have a route to generate the JWT, we can create a route to try our token out! ```rust async fn protected(claims: Claims) -> String { // Send the protected data to the user format!("Welcome to the protected area, {}!", claims.username) } ``` Now that all of the routes have been written we can hook this all back up to our main function: ```rust use axum::{Router, routing::{get, post}}; #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new() .route("/", get(hello_world)) .route("/protected", get(protected)) .route("/login", post(authorize)); Ok(router.into()) } ``` ### Testing To test that the API works, we can use `shuttle run` to serve the API locally. To test our `/login` endpoint, we'll send a cURL request to it with the data: ```bash curl localhost:8000/login -H 'Content-Type: application/json/' \ -d '{"client_id":"foo","client_secret":"bar"}' ``` When we run this, it should output some JSON containing the JWT and type of token - which should be the `Bearer` type. This should be a Bearer token because in `FromRequestParts` we extract from the `Authorization: Bearer ...` header. To now test the protected route, we need to use the JWT token that was sent to us in the `Authorization` header: ```bash curl localhost:8000/protected -H 'Authorization: Bearer ' ``` You should receive a text response that looks like this: ```text Welcome to the protected area, foo! ``` ## Deploying Now that we've written our whole project, we can deploy! Type in `shuttle deploy` and press enter (add `--ad` flag if on a dirty Git branch to allow dirty deploys). When finished, our terminal should print out all of the data about our deployment and project and a link to the live project! ## Extending this project Interested in extending this project? Here's a couple ideas: - Use Postgres to store user logins. - Try using encryption and signing to strengthen your JWTs, as well as storing them in a cookie. - Try adding integration tests so you don't need manual testing! ## Finishing up Thanks for reading! I hope you enjoyed this guide to implementing JWT authentication in Rust. Interested in more? - Read more about using SQL with SQLx [here](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust). - Read more about session token based authentication [here](https://www.shuttle.dev/blog/2022/08/11/authentication-tutorial). --- # Rust Tooling: 8 tools that will increase your productivity Source: https://www.shuttle.dev/blog/2024/02/15/best-rust-tooling Date: 15 February 2024 Author: josh Tags: rust, opinion This article takes a look at Rust tooling that helps you ship faster by boosting your productivity. When it comes to Rust, there's an extensive ecosystem of libraries and packages to get you where you need to be. We've compiled a list of our favorite tools (written in Rust, of course!) to help you speed up your Rust-based productivity, whether they're small plugins to help round off a rough edge you've been having or to help enhance your debugging skills. This article dives into some of these crates and how they can help you ship faster. ## Cargo plugins Cargo has an extensive list of plugins that you can use to be able to speed up certain parts of development — for example, cutting out unused dependencies. They can be used as CLI commands, as well as CI actions (when installed into a CI workflow like GitHub Actions). Here is a list of the ones that we've found to be really useful: ### cargo-machete [`cargo-machete`](https://github.com/bnjbvr/cargo-machete) is a Cargo plugin for cutting out unused dependencies from your project. To install it, you can use `cargo install cargo-machete` then use `cargo machete` in a Rust project directory. The **return code** indicates whether unused dependencies have been found: - 0 if `cargo-machete` found no unused dependencies, - 1 if it found at least one unused dependency, - 2 if there was an error during processing (in which case there's no indication whether any unused dependency was found or not). That's it! That's the crate. ### cargo-nextest [`cargo-nextest`](https://nexte.st/) describes itself as a "next-generation Rust test runner". To install, you need to run `cargo install cargo-nextest`. After `cd`ing into a Rust project (or workspace), you can run all tests with `cargo nextest run`; or list all of them using `cargo nextest list`. Running tests will produce an output that looks like this (taken from the docs page): ![cargo-nextest test results](/images/blog/rust-tooling-article/nextest.png) The main difference between this test runner and the regular `cargo test` is that `cargo-nextest` will detect leaky and flaky tests. If you have a test that spawns a child subprocess that fails to clean up on a failed test, `cargo-nextest` will label it appropriately for you. Additionally, although you can configure things like delay and backoff (for tests using rate limited APIs, for example), environment variables and more by using the `cargo-nextest` configuration file! This is quite an extensive tool; if you'd like to learn more about `cargo-nextest`, you can find their docs page [here.](https://nexte.st/index.html) ### cargo-make [`cargo-make`](https://github.com/sagiegurari/cargo-make) aims to be an extensive Rust-written task runner that additionally lets you define workflows to execute your tasks. You can install it using `cargo install cargo-make`. To get started with, we'll create a `Makefile.toml` file that formats and runs the `clippy` linter: ```rust [tasks.format] install_crate = "rustfmt" command = "cargo" args = ["fmt", "--", "--emit=files"] [tasks.lint] install_crate = "rust-clippy" command = "cargo" args = ["clippy"] [tasks.fmtclip] dependencies = [ "format", "lint" ] ``` Now if we use `cargo make fmtclip`, it will automatically run `cargo fmt` and `cargo clippy`for us without needing anything! We can additionally add environment variables to our commands through the usual way (providing them before the command). For example, if we have a `Makefile.toml` that looks like this: ```toml # here we can also additionally set manual overrides for environment variables [env] ECHO_CMD = "echo" [tasks.expand] command = "${ECHO_CMD}" args = [ "VALUE: ${VALUE}" ] ``` If we run `VALUE=HELLO_WORLD cargo make expand`, it should print out `VALUE: HELLO_WORLD` to the terminal! `cargo-make` is quite useful when it comes to Rusty command runners - try it out! ### cargo-audit [`cargo-audit`](https://github.com/RustSec/rustsec/tree/main/cargo-audit) is a simple Cargo tool for detecting vulnerable Rust crates. You can install it with `cargo install cargo-audit`, use `cargo audit` and you're done! Any vulnerable crates will appear below, like so: ![cargo-audit discovering a vulnerable Rust crate](/images/blog/rust-tooling-article/audit.png) While not an "all-in-one" crate, `cargo-audit` fulfills a simple yet important role in making sure that vulnerable crates can be found and patched. ## Testcontainers Testing with added infrastructure can be quite tricky. [`testcontainers`](https://testcontainers.com/) aims to solve this by providing an open-source framework for providing local, lightweight containers for your application that can be immediately thrown away after use. It also has a [Rust SDK](https://github.com/testcontainers/testcontainers-rs)! We can add it to a project to get started: ```bash cargo add testcontainers ``` Then we will need to add another dependency called [`testcontainers-modules`](https://github.com/testcontainers/testcontainers-rs-modules-community) alongside the feature we want (for the container we want to use). For this example, let's use a Postgres database: ```bash cargo add testcontainers-modules -F postgres ``` When we're testing our application, we instantiate the `test_containers` client and run a Postgres instance by adding the following code to our tests: ```rust #[cfg(test)] mod tests { use testcontainers_modules::{postgres::Postgres, testcontainers::clients::Cli}; #[test] fn connect_to_database() { // startup the module let docker = Cli::default(); let node = docker.run(Postgres::default()); // prepare connection string let connection_string = &format!( "postgres://postgres:postgres@127.0.0.1:{}/postgres", node.get_host_port_ipv4(5432) ); // the rest of your code goes here } } ``` When you run this test, it will create a Postgres container for you (through Docker) and then you can give yourself the connection string by using the above format. From there, you can connect to your Postgres instance and execute any testing you need. Once the test is done, the container gets removed - this is to provide code isolation (so that your tests don't rely on other tests succeeding). ## tokio-console [`tokio-console`](https://github.com/tokio-rs/console) is a debugger for Rust async programs that use Tokio. To get started, add the `console-subscriber` crate to your project and add the following line which will initialise the subscriber and allow `tokio-console` to connect to it: ```bash console_subscriber::init(); ``` Note that currently, in order to collect task data from Tokio, the `tokio_unstable` cfg option must be enabled. You can either do this through Rustflags, or by adding it as an argument in your `.cargo/config.toml` file: ``` [build] rustflags = ["--cfg", "tokio_unstable"] ``` Then you want to install `tokio-console` with `cargo install tokio-console` and use `tokio console` from your terminal. By default it will attempt to connect on port 6669, but you can change this by passing in the URL string you want the console to connect to: ```bash cargo run -- http://localhost:8000 ``` On a successful connection, it should display something like this: ![tokio-console menu](/images/blog/rust-tooling-article/tokio-console.png) `tokio-console` is part of a wider effort to significantly improve Rust async debugging! We are highly looking forward to seeing how this area grows. If you're interested in learning more about `tokio-console` you can find out more [here.](https://github.com/tokio-rs/console) ## cargo-flamegraph [`cargo-flamegraph`](https://github.com/flamegraph-rs/flamegraph) is a program for generating flamegraphs, written in Rust! Flamegraphs are a visualisation of distributed request traces and were originally written in Perl but have now been ported to Rust. By using a flamegraph, it's much easier to see where a bug (for example, race conditions) may be originating from or where you might have a memory leak. Issues like unusually high latency or errors that are difficult to debug through regular logging can be solved much more easily this way as you can see a visual representation of the call stack. You can install `cargo-flamegraph` with `cargo install flamegraph`. There are some underlying requirements to be able to use `cargo-flamegraph`; you will want to take a look at the repo [here](https://github.com/flamegraph-rs/flamegraph) to make sure you have the right dependencies. Once done, you can use `cargo flamegraph` to generate a flamegraph - you can view the resulting svg file by using your favourite browser, or an SVG viewer program. It should look something like this: ![flamegraph example](/images/blog/rust-tooling-article/flamegraph.png) Interested in getting flamegraphs for web services? You can also use the `tracing_flame` package, which you can find more about [here.](https://docs.rs/tracing-flame/latest/tracing_flame/) It hooks up to the `tracing` ecosystem, which allows for instrumentation within your web application. We also have an article about `tracing` [here.](https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust) ## rust-analyzer Of course, no Rust tooling list is complete without `rust-analyzer`! `rust-analyzer` is a Language Server Protocol (LSP) that integrates with any editor that supports it and allows your editor to proactively point out errors for you by maintaining a connection with the LSP server. You can install it by either using `rustup component add rust-analyzer`, or it may be provided alternatively through an Extensions menu (for example, VSCode Extensions). While `rust-analyzer` does use quite a lot of memory (up to about 10 gigabytes of RAM!), it is very well worth having if your setup can handle it. Live debugging without required compilation, as well as autocomplete can make your life much easier. ## Finishing up Thanks for reading! As the Rust ecosystem expands, there are more and more tools coming to life which you can use to supercharge your workflow. We're looking forward to seeing what the future of the best Rust tooling looks like. Further reading: - Learn how to deploy a Rust API with Axum, Postgres and Shuttle [here](https://www.shuttle.dev/blog/2024/01/31/write-a-rest-api-rust) - Get started with logging in Rust [here](https://www.shuttle.dev/blog/2023/09/20/logging-in-rust) --- # Using Clerk authentication in Rust Source: https://www.shuttle.dev/blog/2024/02/13/clerk-in-rust Date: 13 February 2024 Author: sourabpramanik Tags: rust, actix, auth, clerk, guide Part 1: Building a Rust Actix Web backend with Clerk authentication. This two-part article covers how we can build an Issue tracking application using React for the frontend, and [Shuttle](https://www.shuttle.dev/) and [Actix Web](https://actix.rs/) for the backend. It also uses [Clerk](https://clerk.com/) for authentication in the frontend and protecting the Rest APIs in the backend. This article covers the Rust backend, and the [second part](https://www.shuttle.dev/blog/2024/02/13/clerk-in-react) covers the React frontend. Here is the [source code](https://github.com/sourabpramanik/issue-tracker) of the complete project if you want to follow along. To start this project we will use the Clerk template from Shuttle's [example repository](https://github.com/shuttle-hq/shuttle-examples) that has a barebone setup of Shuttle, Actix Web, Vite (React), and Clerk. The backend uses the [clerk-rs](https://github.com/DarrenBaldwin07/clerk-rs) community crate to create a connection between the backend application and Clerk project. This crate has many different APIs for different use cases. In the template there is an endpoint to fetch all user records using the `clerk-rs` user API. We can easily verify a user's identity when trying to access protected routes using the `ClerkMiddleware` from the crate. We will use Postgres for the database which will run in a docker container so make sure you also have docker [installed](https://docs.docker.com/engine/install/) in your local machine. ## Let's get started First, we need to clone the template from the Shuttle's example repository using Shuttle CLI: ```bash cargo shuttle init --from shuttle-hq/shuttle-examples --subfolder actix-web/clerk ``` You will get a few prompts to create a new project using this template. Make sure you provide a unique name because when you deploy this project on Shuttle this project name will become the sub-domain. And once a name is taken it cannot be used for any other projects After that, `cd` into the project directory. ## Setup a new application in Clerk Head on to Clerk's official website, sign in or sign up if you don't have an account, and create a new project in the dashboard. Give a name to your project and select the providers using which users can sign-in or sign-up in your application. After that, you will get a **Publishable Key** and a **Secret key** which we will use later in our application. ![clerk setup](https://gist.github.com/assets/61370770/7fb52565-605c-42e9-9a6a-3fc7e4855696) ## Build the backend Now we can start updating the existing backend code to write and build the Rest APIs for our application. `cd` into the backend, and rename the `Secrets.example.toml` file to `Secrets.toml`: ```bash cd backend mv Secrets.example.toml Secrets.toml ``` Copy the **Secret key** from the Clerk's dashboard and add it to the `Secrets.toml` file: ```bash CLERK_SECRET_KEY = "sk_test_vsWrxxxxxxxxxxxxxxxxxxxxxxxxxtfTr" ``` _⚠️ Do not commit this file, so make sure you have added this file in the `.gitignore`._ Now, we need to write a new migration in a `schema.sql` file which will create a new `issues` table in our Postgres database. Create `schema.sql` and add the schema: ```sql CREATE TABLE IF NOT EXISTS issues ( id SERIAL PRIMARY KEY, title VARCHAR(100) NOT NULL, description VARCHAR(300) NOT NULL, status VARCHAR(10) NOT NULL, label VARCHAR(10) NOT NULL, author VARCHAR NOT NULL ); ``` We can migrate this schema using **[SQLX](https://docs.rs/sqlx/latest/sqlx/)**. So add the SQLX crate to your dependency: ```bash cargo add sqlx ``` To connect to a shared Postgres database managed by Shuttle you need to add the crate with features specific to Postgres and SQLX to your dependencies: ```bash cargo add shuttle-shared-db -F shuttle-shared-db/postgres -F shuttle-shared-db/sqlx ``` ### Connect to the shared Postgres DB Let's edit the `main.rs` file. First, we need to bring SQLX into the scope: ```rust use sqlx::{Executor, FromRow, PgPool}; ``` Add the pool field to the `AppState` struct to share the Postgres pool instance in our whole application: ```rust struct AppState { client: Clerk, pool: PgPool, } ``` Next, pass the `#[shuttle_shared_db::Postgres] pool: PgPool` argument to your `shuttle_runtime::main` function and connect to Postgres using SQLX connect pool `PgPool`. ```rust #[shuttle_runtime::main] async fn actix_web( #[shuttle_secrets::Secrets] secrets: SecretStore, #[shuttle_shared_db::Postgres] pool: PgPool, ) -> ShuttleActixWeb { // DB Pool pool.execute(include_str!("../schema.sql")) .await .map_err(CustomError::new)?; // Clerk integration let clerk_secret_key = secrets .get("CLERK_SECRET_KEY") .expect("Clerk Secret key is not set"); let clerk_config = ClerkConfiguration::new(None, None, Some(clerk_secret_key), None); let client = Clerk::new(clerk_config.clone()); // Create new app state let state = web::Data::new(AppState { client, pool }); let app_config = move |cfg: &mut ServiceConfig| { cfg.service( web::scope("/api") .wrap(ClerkMiddleware::new(clerk_config, None, true)) .service(get_user) ) .service(actix_files::Files::new("/", "./frontend/dist").index_file("index.html")) .app_data(state); }; Ok(app_config.into()) } ``` The above function call will create the Postgres pool, and then SQLX will migrate the schema we have created before. The Clerk configuration is already defined in the template for us which uses the `CLERK_SECRET_KEY` we got from the Clerk project we have created earlier. The Clerk middleware protects all the paths under `/api`, so that only signed in users can access them. Try running the application using Shuttle CLI: ```bash cargo shuttle run ``` _🗨️ Make sure docker is running in your local machine because after you run the above command Shuttle will pull the Postgres docker image and start a docker container._ The backend application can now query the Postgres database using SQLX. Let's create some **CRUD** endpoints for our application. ### Issue structs To define the fields of an issue we will create an `Issue` struct that is used to define the response and request payload of our **Read, Update,** and **Delete** requests, and a `NewIssue` struct for the **Create** requests: ```rust #[derive(Serialize, Deserialize, FromRow)] struct Issue { id: i32, title: String, description: String, status: String, label: String, author: String, } #[derive(Serialize, Deserialize, FromRow)] struct NewIssue { title: String, description: String, status: String, label: String, author: String, } ``` ### Extract the Clerk JWT claim from a request In some of the endpoints we need to check for the user's authorization. For that we will look for a user id which can be found in the JWT claim `sub` property: ```rust async fn get_jwt_claim(service_request: &ServiceRequest, clerk_client: &Clerk) -> Option { let claim = clerk_authorize(service_request, clerk_client, true).await; match claim { Ok(value) => Some(value.1), Err(_) => None, } } ``` ### CRUD endpoints Add the create issue endpoint which can query our database to insert a new issue into the table like this: ```rust #[post("/issue")] async fn add_issue(payload: web::Json, state: web::Data) -> impl Responder { let create_query: Result = sqlx::query_as( "INSERT INTO issues (title, description, status, label, author) VALUES ($1, $2, $3, $4, $5) RETURNING *" ) .bind(&payload.title) .bind(&payload.description) .bind(&payload.status) .bind(&payload.label) .bind(&payload.author) .fetch_one(&state.pool) .await; if create_query.is_err() { return HttpResponse::InternalServerError().json(serde_json::json!({ "status":"FAILED", "message":"Failed to create an issue" })); } HttpResponse::Ok().json(serde_json::json!({ "status":"SUCCESS", "message":"Created the issue successfully" })) } ``` This endpoint will retrieve all the issues from the database: ```rust #[get("/issues")] async fn get_issues(state: web::Data) -> impl Responder { let query: Result, sqlx::Error> = sqlx::query_as("SELECT * FROM issues") .fetch_all(&state.pool) .await; let issues = match query { Ok(value) => value, Err(e) => { return HttpResponse::InternalServerError().json(serde_json::json!({ "status": "FAILED", "message": e.to_string(), })); } }; HttpResponse::Ok().json(issues) } ``` This endpoint function will extract the `issue_id` slug from the path using the [Path extractor](https://actix.rs/docs/extractors) from Actix Web and query our database where the issue `id` is equal to the `issue_id` slug of the path. ```rust #[get("/issue/{issue_id}")] async fn get_issue(state: web::Data, path: web::Path) -> impl Responder { let issue_id = path.into_inner(); let query: Result = sqlx::query_as("SELECT * FROM issues WHERE id=$1") .bind(issue_id) .fetch_one(&state.pool) .await; let issue = match query { Ok(value) => value, Err(_) => { return HttpResponse::InternalServerError().json(serde_json::json!({ "status":"FAILED", "message":"Something went wrong." })); } }; HttpResponse::Ok().json(serde_json::json!({ "status": "SUCCESS", "data": issue, })) } ``` To update an issue by id we will use slug from the path which will contain the `issue_id` to query our database and update the record if it exists in the database. Before updating the record we can query if the record exists in the database or not: ```rust #[patch("/issue/{issue_id}")] async fn update_issue( payload: web::Json, state: web::Data, path: web::Path, req: HttpRequest, ) -> impl Responder { let issue_id = path.into_inner(); let service_req = ServiceRequest::from_request(req); let claim = get_jwt_claim(&service_req, &state.client).await; if claim.is_none() { return HttpResponse::Forbidden().json(serde_json::json!({ "status":"FAILED", "message":"Not authorized to update the issue." })); } let query: Result = sqlx::query_as("SELECT * FROM issues WHERE id=$1") .bind(issue_id) .fetch_one(&state.pool) .await; match query { Ok(issue) => { if issue.author != claim.unwrap().sub { return HttpResponse::Unauthorized().json(serde_json::json!({ "status":"FAILED", "message":"Not authorized to update the issue." })); } } Err(_) => { return HttpResponse::NotFound().json(serde_json::json!({ "status":"FAILED", "message":"Issue does not exist." })); } } let update_query: Result = sqlx::query_as( "UPDATE issues SET title=$1, description=$2, status=$3, label=$4 WHERE id=$5", ) .bind(&payload.title) .bind(&payload.description) .bind(&payload.status) .bind(&payload.label) .bind(issue_id) .fetch_one(&state.pool) .await; if update_query.is_err() { return HttpResponse::InternalServerError().json(serde_json::json!({ "status":"FAILED", "message":"Failed to update the issue" })); } HttpResponse::Ok().json(serde_json::json!({ "status": "SUCCESS", "message":"Updated successfully" })) } ``` This endpoint is for deleting the issue by `id` from the database: ```rust #[delete("/issue/{issue_id}")] async fn delete_issue( state: web::Data, path: web::Path, req: HttpRequest, ) -> impl Responder { let service_req = ServiceRequest::from_request(req); let claim = get_jwt_claim(&service_req, &state.client).await; if claim.is_none() { return HttpResponse::Forbidden().json(serde_json::json!({ "status":"FAILED", "message":"Not authorized to delete the issue." })); } let issue_id = path.into_inner(); let query: Result = sqlx::query_as("SELECT * FROM issues WHERE id=$1") .bind(issue_id) .fetch_one(&state.pool) .await; match query { Ok(issue) => { if issue.author != claim.unwrap().sub { return HttpResponse::Unauthorized().json(serde_json::json!({ "status":"FAILED", "message":"No authorized to delete the issue." })); } } Err(_) => { return HttpResponse::NotFound().json(serde_json::json!({ "status":"FAILED", "message":"Issue does not exist." })); } } let delete_query: Result = sqlx::query_as("DELETE FROM issues WHERE id=$1") .bind(issue_id) .fetch_one(&state.pool) .await; if delete_query.is_err() { return HttpResponse::InternalServerError().json(serde_json::json!({ "status":"FAILED", "message":"Failed to delete the issue" })); } HttpResponse::Ok().json(serde_json::json!({ "status": "SUCCESS", "message":"Deleted successfully" })) } ``` ### User endpoints The template provides a `get_user` handler function and a `UserModel` struct. We will edit the handler function to use the `get_jwt_claim` function: ```rust #[get("/user/me")] async fn get_user(state: web::Data, req: HttpRequest) -> impl Responder { let service_req = ServiceRequest::from_request(req); let claim = get_jwt_claim(&service_req, &state.client).await; if claim.is_none() { return HttpResponse::Forbidden().json(serde_json::json!({ "status":"FAILED", "message":"Not authorized to update the issue." })); } let Ok(user) = User::get_user(&state.client, &claim.unwrap().sub).await else { return HttpResponse::InternalServerError().json(serde_json::json!({ "message": "Unable to retrieve user", })); }; HttpResponse::Ok().json(Into::::into(user)) } ``` We need one last endpoint which can fetch the user details by their `user_id`. This endpoint will be used to fetch issue author metadata from Clerk: ```rust #[get("/user/{user_id}")] async fn get_user_by_id(state: web::Data, path: web::Path) -> impl Responder { let user_id = path.into_inner(); let Ok(user) = User::get_user(&state.client, &user_id).await else { return HttpResponse::InternalServerError().json(serde_json::json!({ "status": "FAILED", "message": "Unable to retrieve all users", })); }; HttpResponse::Ok().json(Into::::into(user)) } ``` Now, let's add all these endpoints to our `main` function under the `/api` scope which is wrapped by Clerk middleware for protecting our endpoint routes and only allowing authenticated users to access these routes. After adding all the endpoints the `main` function you should have something like [this](https://github.com/sourabpramanik/issue-tracker/blob/main/backend/src/main.rs). ## Building the frontend and deploying One important thing to notice is that this piece of code ```rust .service(actix_files::Files::new("/", "./frontend/dist").index_file("index.html")) ``` is going the serve the static pages of the frontend application located in the `./frontend/dist` directory whenever we build our **Vite** application, and this final output is optimized for production. Follow along to [part 2](https://www.shuttle.dev/blog/2024/02/13/clerk-in-react) where we build the frontend and deploy. --- # Using Clerk authentication in React Source: https://www.shuttle.dev/blog/2024/02/13/clerk-in-react Date: 13 February 2024 Author: sourabpramanik Tags: javascript, react, auth, clerk, guide Part 2: Building a React frontend for the Issue Tracker app with Clerk. This two-part article covers how we can build an Issue tracking application using React for the frontend, and [Shuttle](https://www.shuttle.dev/) and [Actix Web](https://actix.rs/) for the backend. It also uses [Clerk](https://clerk.com/) for authentication in the frontend and protecting the Rest APIs in the backend. This article covers the React frontend, and the [first part](https://www.shuttle.dev/blog/2024/02/13/clerk-in-rust) covers the Rust backend. Here is the [source code](https://github.com/sourabpramanik/issue-tracker) of the complete project if you want to follow along. ## Overview In the `frontend` directory of the template we've been building from in [Part 1](https://www.shuttle.dev/blog/2024/02/13/clerk-in-rust), let's explore some of the React libraries that the template has provided us. **[Shadcn UI](https://ui.shadcn.com/docs)** Shadcn UI is a collection of reusable components that uses **Tailwind CSS** to build its components with great accessibility. **[SWR](https://swr.vercel.app/)** SWR is used to create hooks for data fetching and mutations by consuming the APIs we have created in the backend. **[Lucide React](https://lucide.dev/)** Lucide React has a wide range of icons that sits just right with Shadcn UI. ## Build the frontend ### Setting Environment Variable You can find a `.env.example` file copy the content, create a new file `.env` in the same path, and paste the content in it. From your Clerk dashboard get the `PUBLISHABLE_KEY` and assign it to the variable `VITE_CLERK_PUBLISHABLE_KEY` in the `.env` file: ```bash VITE_CLERK_PUBLISHABLE_KEY=pk_test_YW1xxxxxxxxxxxxxxxxxxxxxxxxxxxcy5kZXYk ``` ### Adding components from Shadcn UI We already have table and avatar components in the `/frontend/components/ui` directory but we need a few more components to build this application. Run this command to add the [components](https://ui.shadcn.com/docs/components): ```bash npx shadcn-ui@latest add badge button card dialog form input label select sonner ``` ### Schema We need to represent the structure of the data we are going receive in response from the backend to keep our frontend application type safe and more predictable. Create `/frontend/lib/schema.ts` and add `IssueSchema` and `UserSchema` interfaces: ```tsx export interface IssueSchema { id: string | undefined; title: string; description: string; status: "todo" | "inprogress" | "done" | "backlog"; label: "bug" | "feature" | "documentation"; author: string; } export interface UserSchema { id: string; first_name: string; last_name: string; username: string; profile_image_url: string; } ``` In the `IssueSchema`, we have predefined the label and status field value because this helps keep different parts of our application well aware of the incoming data and restricts invalid data input or output. ### Store We will need to create two stores using **[Zustand](https://docs.pmnd.rs/zustand/migrations/migrating-to-v4#create)**. First, install Zustand: ```bash npm install zustand ``` - Store for issue `id` that has to be updated/edited ```tsx import { create } from "zustand"; type IssueStore = { edit_issue_id: string; setEditIssueId: (id: string) => void; }; export const useIssueStore = create((set) => ({ edit_issue_id: "", setEditIssueId: (id: string) => set(() => ({ edit_issue_id: id })), })); ``` - Store for closing and opening the issue modal ```tsx type IssueModalStore = { isOpen: boolean; setOpen: () => void; setClose: () => void; }; export const useIssueModalStore = create((set) => ({ isOpen: false, setOpen: () => set({ isOpen: true }), setClose: () => set({ isOpen: false }), })); ``` ### Custom hooks We will create the custom hooks using `useSWR` and `userSWRMutation` so that we can revalidate the data after any mutation happens and cache the stale data. Create `/frontend/lib/hooks.ts` and let's start integrating all the APIs we have created in the backend. First let's bring all the required schema, hooks, and components into the scope: ```tsx import useSWR, { Fetcher, useSWRConfig } from "swr"; import useSWRMutation from "swr/mutation"; import { IssueSchema, UserSchema } from "./schema"; import { toast } from "sonner"; import { useIssueModalStore } from "./store"; ``` **Get user hook** This hook will consume the get user by `id` API we have created before. The first parameter of `useSWR` is the key which is used as an identifier to cache the data and revalidate the data using that key, this key can be a subset of the URL path which can be passed to the fetcher function to fetch or mutate data as well. To know more about the arguments follow this [documentation](https://swr.vercel.app/docs/arguments). The hook returns an object where the `isLoading` property is a boolean property that sets to true while data is being fetched and a data property that is the returned value of the hook. ```tsx export const useGetUser = (user_id: string | undefined) => { const fetcher: Fetcher = (url) => fetch(url).then((res) => res.json()); const { isLoading, data } = useSWR(`/api/user/${user_id}`, fetcher); return { isLoading, data }; }; ``` **Get issues hook** Fetches all the issues for the issues table, consuming get issues API, the response of this API on success will be an array of issues, that's why we have passed `IssuesSchema[]` to the `Fetcher` type: ```tsx export const useGetIssues = () => { const fetcher: Fetcher = (url) => fetch(url).then((res) => res.json()); const { isLoading, data } = useSWR("/api/issues", (url) => fetcher(url)); return { isLoading, data }; }; ``` **Get issue by `id` hook** This hook uses the get issue by `id` API and returns the one single record that matches that `id` additionally, we have added a condition to call the `fetcher` function only when `id` is not empty: ```tsx export const useGetIssue = (id: string) => { const fetcher: Fetcher<{ data: IssueSchema }, string> = (url) => fetch(url).then((res) => res.json()); const { data } = useSWR( () => (id !== "" ? `/api/issue/${id}` : null), fetcher, ); return { data: data?.data }; }; ``` **Create issue hook** This hook returns a `trigger` method from `useSWRMutation` hook which takes the data to be sent in the request body of the create issue API and an `isMutating` boolean property which we can use to show a loading animation and disable any mutation process while this property is true. We use the `setClose` method from the `useIssueModalStore` to close the issue modal once the issue has been created successfully. The data of the payload should match the `IssueSchema` but it should not have the `id` field as it is a new record, so to remove `id` field we use `Omit` which creates a new type with the `id` field omitted. ```tsx export const useCreateIssue = () => { const { setClose } = useIssueModalStore(); const create = (url: string, { arg }: { arg: Omit }) => fetch(url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify(arg), }).then((res) => res.json()); const { mutate: revalidateIssuesList } = useSWRConfig(); const { isMutating, trigger } = useSWRMutation("/api/issue", create, { onSuccess() { toast.success("Issue has been created."); setClose(); revalidateIssuesList("/api/issues"); }, onError(err) { if (err.message) { toast.error(err.message); } else { toast.error("Failed to create issue"); } }, }); return { isMutating, trigger }; }; ``` _🗨️ We need to specify the content type header as `application/json` in the fetch calls because by default fetch API sets the content type header to `text/plain` which causes an incorrect content error as the backend we have created expects a JSON payload._ **Update/Edit issue hook** This hook consumes the update issue by `id` API and similar to the create issue hook it also returns `isMutating` property and `trigger` method. We use the `setClose` method from the `useIssueModalStore` to close the issue modal once the issue has been updated successfully. ```tsx export const useEditIssue = () => { const { setClose } = useIssueModalStore(); const edit = (url: string, { arg }: { arg: IssueSchema }) => fetch(`${url}/${arg.id}`, { method: "PATCH", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify(arg), }).then((res) => res.json()); const { mutate: revalidateIssuesList } = useSWRConfig(); const { isMutating, trigger } = useSWRMutation("/api/issue", edit, { onSuccess() { toast.success("Issue has been updated."); setClose(); revalidateIssuesList("/api/issues"); }, onError(err) { if (err.message) { toast.error(err.message); } else { toast.error("Failed to update issue"); } }, }); return { isMutating, trigger }; }; ``` **Delete issue hook** One final API integration, the delete API takes the `id` and also returns an object with `isMutating` field and `trigger` method: ```tsx export const useDeleteIssue = () => { const { setClose } = useIssueModalStore(); const remove = (url: string, { arg }: { arg: { id: string } }) => fetch(`${url}/${arg.id}`, { method: "DELETE", }).then((res) => res.json()); const { mutate: revalidateIssuesList } = useSWRConfig(); const { isMutating, trigger } = useSWRMutation("/api/issue", remove, { onSuccess() { toast.success("Issue has been deleted."); setClose(); revalidateIssuesList("/api/issues"); }, onError(err) { if (err.message) { toast.error(err.message); } else { toast.error("Failed to delete issue"); } }, }); return { isMutating, trigger }; }; ``` ### Issue modal Create an issue modal in `/frontend/components/issue-modal.tsx`. The issue modal will have a form with title, description, status select, and label select input fields. **Using Zod** First, we need to install Zod to define the schema for the form and add rules with helper messages to validate the user input: ```bash npm install zod ``` Next, define the form schema: ```tsx import { z } from "zod"; //...rest of the imports const status = ["todo", "inprogress", "done", "backlog"] as const; const label = ["bug", "feature", "documentation"] as const; const formSchema = z.object({ title: z.string().min(3, { message: "title must be at least 3 characters.", }), description: z.string().min(5, { message: "title must be at least 5 characters.", }), status: z.enum(status, { required_error: "You need to select a status.", }), label: z.enum(label, { required_error: "You need to select a label.", }), author: z.string({ required_error: "Author is required" }), }); ``` In the above code, we have created a label and a status enum for the select input field option. It should be the same as the expected label and status field value of the `IssueSchema`. **Using React Hook Form** Now, we need to install [**React Hook Form**](https://react-hook-form.com/) for field validation and field state management: ```bash npm install react-hook-form ``` React Hook Form has a `useForm` hook which is used to handle the `onChange` event and validate the form on the fly. It needs a `type` parameter which will be the form definition consisting of the details of each field the form is having. We can infer the type by using the `formSchema` we have defined using Zod. `useForm` hook accepts two options a `resolver` and `defaultValues`. For the resolver, we need to integrate preferred schema validation. We will use Zod resolver for validation which will take the `formSchema` as a parameter to apply the validation rules. For the `defaultValues` for the form, but for the author we will not create a form element rather we will provide the user `id` which we can get from `useUser` hook from Clerk because we only want the user who is creating the issue to be the author of. ```tsx import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { useUser } from "@clerk/clerk-react"; //...rest of the imports function IssueCard() { const { user } = useUser(); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { title: "", description: "", status: "todo", label: "bug", author: user?.id, }, }); //...rest of the code } ``` **Conditional mutation and rendering for creation and update** So far we have added all the required hooks and components that can be used to create and update an issue. Now, we need to render the modal differently for the update than how it is for the creation. For the update, we need the issue `id` from `useIssueStore` hook and fetch the issue first using `useGetIssue` once we have retrieved the issue successfully we can update the state of the form using `useEffect` hook and only allow the author to update their own created issues: After that, we will define the `onSubmit` handler function to conditionally create an issue using `useCreateIssue` hook or update an issue using `useEditIssue` hook: ```tsx import { useIssueStore } from "@/lib/store"; import { useCreateIssue, useEditIssue, useGetIssue } from "@/lib/hooks"; //...rest of the imports function IssueCard() { const { edit_issue_id } = useIssueStore(); const { data: issue } = useGetIssue(edit_issue_id); const { isMutating: createMutating, trigger: createTrigger } = useCreateIssue(); const { isMutating: editMutating, trigger: editTrigger } = useEditIssue(); function onSubmit(values: z.infer) { edit_issue_id === "" ? createTrigger(values) : editTrigger({ id: edit_issue_id, ...values }); } //Check for the author is editing or not const noAuth = edit_issue_id !== "" && user?.id !== issue?.author; } ``` Finally, your issue modal will look like [this](https://github.com/sourabpramanik/issue-tracker/blob/main/frontend/src/components/issue-modal.tsx). ### Issues table Create a new directory inside the components directory `issues-table`, inside this directory create two files `index.tsx`, and `row.tsx`. **Edit the `row.tsx` file** We will define `Row` component which will take `props` having the type set to `IssueSchema`. Each row will have 6 cells for the author, title, description (truncated), status, label, and last cell for edit and delete buttons. The edit button will set the issue `id` to be edited using `setEditIssueId` from `useIssueStore` hook and open up the modal. The delete button will trigger the delete API in the `useDeleteIssue` hook. Both of these buttons is conditionally rendered based on the condition that if the signed-in user is an author of the issue then they can mutate the issue or else they can just view the issue. Please find the complete code [here](https://github.com/sourabpramanik/issue-tracker/blob/main/frontend/src/components/issue-table/row.tsx). **Edit the `index.tsx` file of the `issue-table` component** Here you can find the complete code for the [issue table](https://github.com/sourabpramanik/issue-tracker/blob/main/frontend/src/components/issue-table/index.tsx)\*\*. ### Update `App.tsx` file Now we can import our `IssuesTable` component and render it: ```tsx import { SignIn, SignedIn, SignedOut, UserButton } from "@clerk/clerk-react"; import IssuesTable from "@/components/issue-table"; function App() { return (
); } export default App; ``` ### Update `main.tsx` file Add the `Toaster` component from `sonner` and `IssueModal`: ```tsx import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; import { ClerkProvider } from "@clerk/clerk-react"; import { Toaster } from "@/components/ui/sonner"; import IssueModal from "@/components/issue-modal"; // Import your publishable key const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; if (!PUBLISHABLE_KEY) { throw new Error("Missing Publishable Key"); } ReactDOM.createRoot(document.getElementById("root")!).render( , ); ``` ## Run the application First, build the frontend application using this command: ```bash npm run build ``` This will generate an optimized build in the `/frontend/dist` directory. Finally run the backend using: ```bash shuttle run ``` Open the highlighted URL in the browser window: ![image screenshot](https://gist.github.com/assets/61370770/8a22b566-a930-494a-8ba6-cd92481a0113) ## Deployment Now you can deploy your app on the Shuttle platform by running `shuttle deploy` (with the `--allow-dirty` flag if you have uncommitted changes). One important note is that the deployment using the **development** keys from Clerk will work just fine, but if you change to use the **Production Instance** of Clerk then you will need to update the keys with **Live** keys. Now since you have moved to production you will also need to set up the domain or subdomain of your application in Clerk and add the CNAME records that you will get from the Clerk dashboard to your DNS record for managing sessions, loading portal using your domain, sending verification emails, and using custom callback URL. Since this process can be different based on your domain provider and management system, I will leave a link to the Clerks documentation on [**Deploy to production**](https://clerk.com/docs/deployments/overview). ## Closing in Thanks for reading!! That's a lot but I hope you have managed to deploy your application and had a good learning experience. --- # Building an Uptime Monitor in Rust Source: https://www.shuttle.dev/blog/2024/02/08/uptime-monitoring-rust Date: 8 February 2024 Author: josh Tags: rust, uptime-monitoring, guide This article explores how you can write and deploy an uptime monitoring web service in Rust. In this article, we're going to talk about building and deploying an uptime-monitoring web service in Rust! Interested in just deploying your uptime monitor? You can find that in 2 steps: 1. Open your terminal and run `shuttle init --from joshua-mo-143/shuttle-monitoring-template` (requires `cargo-shuttle` installed) and follow the prompt 2. Run `shuttle deploy --allow-dirty` and watch the magic happen! For everyone who wants to learn how to build it, let's get started - if you get lost, you can find the repo with the final code [here.](https://github.com/joshua-mo-143/shuttle-monitoring-template) ## Getting Started Firstly, we'll initialize our project using `shuttle init` (requires `cargo-shuttle` to be installed). Make sure you pick Axum as the framework! Now you'll want to install all of your dependencies. You can do that with the following shell snippet below: ```bash cargo add askama-axum cargo add askama -F with-axum cargo add chrono -F clock,serde cargo add futures-util cargo add reqwest cargo add serde -F derive cargo add shuttle-shared-db -F sqlx,postgres cargo add sqlx -F runtime-tokio-rustls,postgres,macros,chrono cargo add validator -F derive ``` Once that's done, we will want to install `sqlx-cli` to handle adding migrations. We can then run `sqlx migrate add init` and it will create a new migrations folder, along with an SQL file in it that we can use for migrations. Add the following text into the migration file: ```rust create table if not exists websites ( id serial primary key, url varchar not null, alias varchar(75) not null unique ); create table if not exists logs ( id serial primary key, website_id int not null references websites(id), status smallint, created_at timestamp with time zone not null default date_trunc('minute', current_timestamp), UNIQUE (website_id, created_at) ); ``` Note that `created_at` defaults to truncate the time to the current minute; this helps with timing later on as we want to be able to split the requests down into per-minute records. Additionally, adding both `website_alias` and `created_at` in the unique constraint makes it so that the constraint is only violated when a new record containing a combination of both the website alias and timestamp is inserted. Next, we'll want to add a database annotation to our program and add it as shared state to our Axum web service: ```rust #[derive(Clone)] struct AppState { db: PgPool, } impl AppState { fn new(db: PgPool) -> Self { Self { db } } } #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool ) -> shuttle_axum::ShuttleAxum { // carry out migrations sqlx::migrate!().run(&db).await.expect("Migrations went wrong :("); let state = AppState::new(db); let router = Router::new().route("/", get(hello_world)).with_state(state); Ok(router.into()) } ``` With one line of code, we've now given ourselves a database! Locally, Shuttle will use Docker to provision a Postgres container for us. In production, we are automatically provisioned one by Shuttle's servers with no input required on our part. Normally, it would be a bit of a pain to do manually but this has saved us some time. Lastly, here is the list of imports we'll be using - make sure to add this to the top of your `main.rs` file: ```rust use askama::Template; use chrono::Timelike; use askama_axum::IntoResponse as AskamaIntoResponse; use axum::{ extract::{Form, Path, State}, http::StatusCode, response::{IntoResponse as AxumIntoResponse, Redirect, Response}, routing::{get, post}, Router, }; use chrono::{DateTime, Utc}; use futures_util::StreamExt; use reqwest::Client; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio::time::{self, Duration}; use validator::Validate; ``` ## Building There are three main parts to this: The frontend, the backend, and the actual monitoring task itself. Let's start with the monitoring task first. ### Monitoring task The monitoring task itself is simple enough: we fetch the list of websites from the database, then sequentially send a HTTP request to each of them and record the results in Postgres. Firstly, we'll create the struct that we want to represent the `Website`: ```rust #[derive(Deserialize, sqlx::FromRow, Validate)] struct Website { #[validate(url)] url: String, alias: String } ``` Here we instantiate a `reqwest` client and then fetch all of the websites that we want to search for: ```rust async fn check_websites(db: PgPool) { let ctx = Client::new(); let mut res = sqlx::query_as::<_, Website>("SELECT url, alias FROM websites").fetch(&db); // .. rest of your code } ``` We _could_ use `fetch_all()`, but by using `fetch()` we get a `Stream` back and can skip dealing with `RowNotFound` SQL errors by checking whether or not there are any rows to fetch. Now that we have our list of websites (or lack thereof), we can try to send a request sequentially to each website that exists then store the results of the fetch in our database: ```rust while let Some(website) = res.next().await { let website = website.unwrap(); let response = ctx.get(website.url).send().await.unwrap(); sqlx::query( "INSERT INTO logs (website_alias, status) VALUES ((SELECT id FROM websites where alias = $1), $2)" ) .bind(website.alias) .bind(response.status().as_u16() as i16) .execute(&db).await .unwrap(); } ``` While we don't store the response body, at this point there's not much reason to do so. The response status should give us all the information we need. In the full function, we want to use `tokio::time::interval` to be able to accurately start the loop again. See below for the code block which includes this: ```rust async fn check_websites(db: PgPool) { let mut interval = time::interval(Duration::from_secs(60)); loop { interval.tick().await; let ctx = Client::new(); let mut res = sqlx::query_as::<_, Website>("SELECT url, alias FROM websites").fetch(&db); while let Some(website) = res.next().await { let website = website.unwrap(); let response = ctx.get(website.url).send().await.unwrap(); sqlx::query( "INSERT INTO logs (website_alias, status) VALUES ((SELECT id FROM websites where alias = $1), $2)" ) .bind(website.alias) .bind(response.status().as_u16() as i16) .execute(&db).await .unwrap(); } } } ``` ### Backend Before we start our backend, we should probably add an error handling enum. Let's add an enum with one arm, implement `axum::response::IntoResponse` for it (currently aliased in our program as `AxumIntoResponse`) and then implement `From` for easy error propagation: ```rust enum ApiError { SQL(sqlx::Error) } impl From for ApiError { fn from(e: sqlx::Error) -> Self { Self::SQLError(e) } } impl AxumIntoResponse for ApiError { fn into_response(self) -> Response { match self { Self::SQLError(e) => { ( StatusCode::INTERNAL_SERVER_ERROR, format!("SQL Error: {e}") ).into_response() } } } } ``` Now when we use any SQL queries, we can propagate the error by using `?` instead of unwrapping or having to manually deal with error handling! Of course, there will be certain situations where we can handle errors manually, but this will save a lot of pattern matching (or alternatively, `.unwrap()`or `.expect()`) within our code. We can also use it as a return type in our handler functions. To start working on our backend, we can create an initial route to add a URL to monitor. You may have noticed earlier we added the `Validate` derive trait. This allows us to validate the form data using preset rules and automatically return an error if the validation fails. In this case, we used `#[validate(url)]` - so if the string isn't in a URL format it will automatically break: ```rust // main.rs async fn create_website( State(state): State, Form(new_website): Form, ) -> Result { if new_website.validate().is_err() { return Err(( StatusCode::INTERNAL_SERVER_ERROR, "Validation error: is your website a reachable URL?", )); } sqlx::query("INSERT INTO websites (url, alias) VALUES ($1, $2)") .bind(new_website.url) .bind(new_website.alias) .execute(&state.db) .await .unwrap(); Ok(Redirect::to("/")) } ``` Now let's write a route to grab all the websites we're monitoring, as well as get a quick report on what the uptime was like in the last recorded 24 hours (filling in any gaps where required) for each website. Like before with the monitoring tasks, we need to grab a list of all of the websites we're currently tracking and add them to a vector of website data (except we will assume there are results there. If not, `askama` will handle it for us by automatically not rendering any records): ```rust #[derive(Serialize, Validate)] struct WebsiteInfo { #[validate(url)] url: String, alias: String, data: Vec, } #[derive(sqlx::FromRow, Serialize)] pub struct WebsiteStats { time: DateTime, uptime_pct: Option, } async fn get_websites(State(state): State) -> Result { let websites = sqlx::query_as::<_, Website>("SELECT url, alias FROM websites") .fetch_all(&state.db) .await?; let mut logs = Vec::new(); for website in websites { let data = get_daily_stats(&website.alias, &state.db).await?; logs.push(WebsiteInfo { url: website.url, alias: website.alias, data, }); } Ok(WebsiteLogs { logs }) } ``` Once this is done, we'll then need to make a new Vector. This will hold all the website URLs (and respective aliases) as well as a list of timestamps with the uptime percentage over the last 24 hours, calculated per hour from how many HTTP requests returned with `200 OK`. We then need to check if there's any gaps and fill them in with a `None`. This lets the person viewing the data know that no data was recorded at the time (for example if we're either developing locally, or if the service itself had an outage and was unable to record data). To start with, let's write a function for getting the daily stats for a given URL: ```rust #[derive(sqlx::FromRow, Serialize)] pub struct WebsiteStats { time: DateTime, uptime_pct: Option, } async fn get_daily_stats(alias: &str, db: &PgPool) -> Result, ApiError> { let data = sqlx ::query_as::<_, WebsiteStats>( r#" SELECT date_trunc('hour', created_at) as time, CAST(COUNT(case when status = 200 then 1 end) * 100 / COUNT(*) AS int2) as uptime_pct FROM logs LEFT JOIN websites on websites.id = logs.website_id WHERE websites.alias = $1 group by time order by time asc limit 24 "# ) .bind(alias) .fetch_all(db).await?; let number_of_splits = 24; let number_of_seconds = 3600; let data = fill_data_gaps(data, number_of_splits, SplitBy::Hour, number_of_seconds); Ok(data) } ``` Although the SQL function looks quite complicated, it basically retrieves a truncated date (down to the hour) and an uptime percentage based on the recorded data and what percentage recorded `200 OK` response status. The query then aggregates them by timestamp and limits it to the last 24 hours. Of course, this can lead to some awkward gaps in our data - for example, what if our monitoring web service goes down? We can't exactly just go back in time and record the data! We can, however, make up for this by filling in the gaps with a new `fill_data_gaps` function that will inform the webpage viewer that there's no data points for a given time. We can do this by declaring an enum: ```rust enum SplitBy { Hour, Day } ``` This enum will allow us to differentiate what period we want data over. We can abstract this block into its own function: ```rust fn fill_data_gaps( mut data: Vec, splits: i32, format: SplitBy, number_of_seconds: i32 ) -> Vec { // if the length of data is not as long as the number of required splits (24) // then we fill in the gaps if (data.len() as i32) < splits { // for each split, format the time and check if the timestamp exists for i in 1..24 { let time = Utc::now() - chrono::Duration::seconds((number_of_seconds * i).into()); let time = time .with_minute(0) .unwrap() .with_second(0) .unwrap() .with_nanosecond(0) .unwrap(); let time = if matches!(format, SplitBy::Day) { time.with_hour(0).unwrap() } else { time }; // if timestamp doesn't exist, push a timestamp with None if !data.iter().any(|x| x.time == time) { data.push(WebsiteStats { time, uptime_pct: None, }); } } // finally, sort the data data.sort_by(|a, b| b.time.cmp(&a.time)); } data } ``` Once done, we'll want to create a dynamic route for getting more information about a monitored URL. We can use this page to display things like past incidents/alerts. This function will follow most of the previous handler function except we're fetching one URL and additionally grabbing any records of HTTP requests that didn't return `200 OK` and labelling them as "Incidents". As you can see below it is _mostly_ the same as grabbing data for all websites, except now we also have last month's data (split by day) to add to our return results: ```rust #[derive(Serialize, sqlx::FromRow, Template)] #[template(path = "single_website.html")] struct SingleWebsiteLogs { log: WebsiteInfo, incidents: Vec, monthly_data: Vec, } #[derive(sqlx::FromRow, Serialize)] pub struct Incident { time: DateTime, status: i16, } async fn get_website_by_alias( State(state): State, Path(alias): Path, ) -> Result { let website = sqlx::query_as::<_, Website>("SELECT url, alias FROM websites WHERE alias = $1") .bind(&alias) .fetch_one(&state.db) .await?; let last_24_hours_data = get_daily_stats(&website.alias, &state.db).await?; let monthly_data = get_monthly_stats(&website.alias, &state.db).await?; let incidents = sqlx::query_as::<_, Incident>( "SELECT logs.created_at as time, logs.status from logs left join websites on websites.id = logs.website_id where websites.alias = $1 and logs.status != 200", ) .bind(&alias) .fetch_all(&state.db) .await?; let log = WebsiteInfo { url: website.url, alias, data: last_24_hours_data, }; Ok(SingleWebsiteLogs { log, incidents, monthly_data, }) } ``` Finally, we need to create a route for deleting a website. This will be a two-step process where we need to delete all of the website logs and then the URL itself, then return `200 OK` if everything went well. We can use a transaction to rollback if there's an error anywhere in this process and manually return `ApiError`: ```rust async fn delete_website( State(state): State, Path(alias): Path, ) -> Result { let mut tx = state.db.begin().await?; if let Err(e) = sqlx::query("DELETE FROM logs WHERE website_alias = $1") .bind(&alias) .execute(&mut *tx) .await { tx.rollback().await?; return Err(ApiError::SQLError(e)); }; if let Err(e) = sqlx::query("DELETE FROM websites WHERE alias = $1") .bind(&alias) .execute(&mut *tx) .await { tx.rollback().await?; return Err(ApiError::SQLError(e)); } tx.commit().await?; Ok(StatusCode::OK) } ``` ### Frontend Now that we've written our backend, we can use `askama` with `htmx` to write our frontend! If you'd like to skip over this part and just grab the files, you can do so from the repo - but make sure you don't forget to write the `styles` handler function below so your web server can find it! This function can be found at the bottom of this subsection or in the repo. We'll want to make four files: - A `base.html` file that will hold the head of our HTML so we don't need to write it in every file. - An `index.html` file. - A `single_website.html` file (for grabbing information about an individual monitored URL). - A `styles.css` file. We'll first want to make our `base.html` file: ```html Shuttle Status Monitor {% block head %}{% endblock %}
{% block content %}

Placeholder content

{% endblock %}
``` This will be extended in the rest of the templates so that we won't need to constantly copy and paste the HTML head every time we want to use HTMX or the Google fonts. Here is the HTML for our main page: ```html {% extends "base.html" %} {% block content %}

Shuttle Status Monitor

{% for log in logs %}

{{log.alias}} - {{log.url}}

Last 24 hours: {% for timestamp in log.data %} {% match timestamp.uptime_pct %} {% when Some with (100) %}
🟢 {{timestamp.time}} Uptime: {{timestamp.uptime_pct.unwrap()}}%
{% when None %}
{{timestamp.time}} No data here :(
{% else %}
🔴 {{timestamp.time}} Uptime: {{timestamp.uptime_pct.unwrap()}}%
{% endmatch %} {% endfor %}
View
{% endfor %}
``` As you can see here, we extend the `base.html` template and then loop through the websites we found earlier in our SQL query. We then display the timestamps as coloured circles depending on what the uptime percentage is (note that `None` means there's a data gap). Although we unwrap the uptime percentages here, we already match it beforehand to make sure it is a `Some` variant. You may have noticed we're using tooltips to display the time of day that the request was taken as well as displaying the uptime percentage on the webpage. In the CSS file, we add styling so that when you hover over a circle, a tooltip will display the timestamp the circle represents as well as the exact uptime percentage. The single URL HTML webpage is also mostly the same, except we're also adding an incident list: ```html {% extends "base.html" %} {% block content %}

Shuttle Status Monitor

Back to main page

{{log.alias}} - {{log.url}}

Last 24 hours: {% for timestamp in log.data %} {% match timestamp.uptime_pct %} {% when Some with (100) %}
🟢 {{timestamp.time}} Uptime: {{timestamp.uptime_pct.unwrap()}}%
{% when None %}
{{timestamp.time}} No data here :(
{% else %}
🔴 {{timestamp.time}} Uptime: {{timestamp.uptime_pct.unwrap()}}%
{% endmatch %} {% endfor %}
Last 30 days: {% for timestamp in monthly_data %} {% match timestamp.uptime_pct %} {% when Some with (100) %}
🟢 {{timestamp.time}} Uptime: {{timestamp.uptime_pct.unwrap()}}%
{% when None %}
{{timestamp.time}} No data here :(
{% else %}
🔴 {{timestamp.time}} Uptime: {{timestamp.uptime_pct.unwrap()}}%
{% endmatch %} {% endfor %}

Incidents

{% if incidents.len() > 0 %} {% for incident in incidents %}
{{incident.time}} - {{incident.status}}
{% endfor %} {% else %} No incidents reported. {% endif %}
{% endblock %} ``` Now it's time to add the CSS styling! The CSS file is extremely long; for the sake of not overwhelming you with code blocks, you can find it [here.](https://github.com/joshua-mo-143/shuttle-monitoring-template/blob/main/templates) However, if you'd like to add your own styling, you're free to do so! We also additionally need to add the CSS file handling route - otherwise, our HTML won't be able to find it. We can do this like so: ```rust // note this assumes your file is at "templates/styles.css" async fn styles() -> impl AxumIntoResponse { Response::builder() .status(StatusCode::OK) .header("Content-Type", "text/css") .body(include_str!("../templates/styles.css").to_owned()) .unwrap() } ``` Then when we add the route to our `Router`, we need to specify the route as `/styles.css`. ### Hooking everything up Now it's time to hook everything up! All you need to do is to create the `AppState`, spawn the monitoring request loop as a `tokio` task and then create your `Router`: ```rust // main.rs #[shuttle_runtime::main] async fn main(#[shuttle_shared_db::Postgres] db: PgPool) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&db).await.unwrap(); let state = AppState::new(db.clone()); tokio::spawn(async move { check_websites(db).await; }); let router = Router::new() .route("/", get(get_websites)) .route("/websites", post(create_website)) .route( "/websites/:alias", get(get_website_by_id).delete(delete_website), ) .route("/styles.css", get(styles)) .with_state(state); Ok(router.into()) } ``` ## Deploying Now it's time to deploy! Type in `shuttle deploy` (add `--allow-dirty` if on a dirty Git branch) and watch the magic happen. When your program has deployed, it'll give you the URL of your deployment where you can try it out as well as deployment ID and other details like your database connection (password is hidden until you use the `--show-secrets` flag). ## Finishing up Thanks for reading! I hope you have learned a little bit more about Rust by writing an uptime monitoring service. --- # Deploying Rust Web Applications - Complete Guide Source: https://www.shuttle.dev/blog/2024/02/07/deploy-rust-web Date: 7 February 2024 Author: josh Tags: rust, deployment, guide Learn how to deploy a Rust web app with different hosting options. Compare VPS, serverless, and managed platforms to find the best way to deploy your Rust application. Recently, Rust has started to become more and more popular as a choice for writing web services and Rust web apps with. Although Rust deployments are typically not given first-class support, there are still a variety of platforms that you can use to deploy a Rust web app. In this article, we'll be going over what your options are and the (dis)advantages of each of them - as well as the best way to deploy Rust for your use case. ## How do you deploy Rust? When you compile Rust, it gets compiled to an executable. You can then run the binary from anywhere - including your own self-hosted server! The compiled nature of Rust programs means that they are normally best ran in containers or alternatively a VPS. You can also run them as serverless functions through supported platforms like AWS Lambda. Each of these deployment methods have their own tradeoffs - which we'll be looking at below. Interested in writing a Rust API? We also have a guide to writing and deploying a Rust API with the Axum framework, which you can find [here.](https://www.shuttle.dev/blog/2024/01/31/write-a-rest-api-rust) ## Different types of hosting ### VPS VPS (Virtual Private Servers) deployments allow you to deploy to a machine that you have full control over. By SSHing into it, you can add or remove any software on the VPS that you want (or don't want!) to use. The basic process involves mostly setting up Nginx or Apache (or a similar proxy), then grabbing the files you need from your Git repository on GitHub or GitLab and compiling the program itself. You can then set the application up as a systemd service and it will automatically start up whenever the machine starts. Unfortunately, there are a couple of issues that come along with this: you need to handle all issues yourself (unless the VPS itself has an outage), and typically it's less scalable than other deployment options. VPS machines typically have a given hard maximum for resource usage and you pay for a set amount - so if you're not making full usage of the given resources, you may be overpaying. This is especially relevant to Rust, as Rust web applications are typically quite low memory footprint. Most applications are around 50-150mb of usage depending on what your application does. Here's a list of advantages and disadvantages for using a VPS: | Advantage | Disadvantage | | ------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Total control over all aspects of the machine | If anything goes wrong you need to fix it yourself | | You know what you're getting | Less scalable than serverless - machines typically have a hard limit | | Once you learn how to do it, it's not too hard to do it again | You need to set everything up (Nginx, HTTPS, etc...) | | You can run whatever you want (NixOS, Ubuntu, Red Hat Enterprise Linux, etc...) | More expensive than other forms of deployment | | | Not reproducible (Infra as Code setup required to reproduce) | ### Serverless Serverless deployments nowadays are quite popular and a great way to host functions that see usage but aren't run all the time. Typically when developers talk about "serverless", it is normally referring to serverless functions - Rust code that gets run on servers when a given endpoint is hit and otherwise isn't run. Serverless is typically used to solve the "scale-to-zero" problem. Sometimes, you may have an endpoint that isn't used often - but your Rust code is still consuming memory because it's in a deployed application. By using serverless, we can move the endpoint to a separate function that doesn't consume memory while not serving HTTP traffic. Platforms like Cloudflare and AWS both have their own version of "serverless functions" (Cloudflare workers and AWS Lambda, respectively) that simply get run when a HTTP request gets made to a given endpoint. This allows companies to save money by only running the code when required to do so, and is reflected in their pricing - AWS lambda gives you up to 1 million serverless function invocations per month for free! However, there are some caveats. You often need to adapt your code to the platform so that it can serve your serverless functions. When it comes to Rust, this can typically mean you need to write multiple binaries. You will also be unable to use regular Rust backend frameworks - though given that one serverless function equates to an endpoint, it's not too much of a loss. Additionally, cold starts can cause your functions to initially function slower while the machine is "warming up". This can cause a huge problem with applications that require low latency. There is also the potential issue of over-engineering your functions - by creating several serverless functions when you only need one. Coupled with cold starts, this can be quite a performance (and wallet) drain while also hurting maintainability of said functions. This can be mitigated with good engineering practices. Here's a list of advantages and disadvantages for deploying via serverless: | Advantage | Disadvantage | | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Much cheaper than hosting a VPS (you only pay for what you use!) | Cold starts | | You don't need to worry about any of the hosting infrastructure | Vendor lock-in due to needing to adapt your Rust code to fit the platform | | Very economically friendly | Unpredictable billing - can become expensive very quickly without a price cap if you get a traffic spike | | Often has integrations to other parts of the platform ecosystem that you can leverage | Extremely difficult to debug | | | Over-engineering | ### Managed serverless While VPS has a maximum hard limit on resources and serverless forces you to adapt your Rust code to fit the platform, managed serverless allows you to pay only for what you use while being able to still allow whatever you want to run. This is done by putting your Rust application into a Docker container image, which then gets built into a final image that gets added to a Kubernetes cluster (for example) or a similar orchestrator. This has the huge advantage of being able to deploy whatever will fit in a container image - which allows you to ship software quickly and efficiently without delay. A lot of managed serverless platforms also have database integrations as well as other kinds of infrastructure which saves time in needing to find other platforms or tools that you can use with your Rust code. On the other hand, there are certain disadvantages inherited from both VPS and Serverless. The web host itself typically uses AWS, GCP or one of the larger cloud computing platforms and service outages can cause a domino effect of outages. Some companies have found ways around this, but otherwise you're still at the mercy of whatever provider is being used in the hood. However, this also comes with an advantage in that these platforms can also leverage their provider's infrastructure. See this short list of advantages of disadvantages for managed serverless below: | Advantage | Disadvantage | | --------------------------------------------------- | ------------------------------------------ | | You can deploy whatever fits in a Docker container | Unpredictable pricing | | Only pay for what you need | Platform is dependent on a larger platform | | Often has convenience integrations (like databases) | More difficult to debug than a VPS | | More reproducible than a VPS | | ### Shuttle So, where does Shuttle fit into this? Shuttle uses AWS under the hood and aims to reduce the amount of work you need to do with cloud deployments by using our own runtime and dockerizing your Rust application for you with dependency caching when deploying to Shuttle servers. We use Infrastructure from Code so that you can declare your infrastructure in-code instead of through configuration files. When you run the application, the runtime will know what to provision for you based on the annotations. This brings a couple of advantages: - No docker knowledge required - No configuration files - Just run `shuttle deploy` and you're done Of course, we _are_ talking about our own product here - we're biased. Our product is also somewhat early-stage when it comes to provisioning resources and various use cases. However, if you're looking to quickly deploy a Rust web app (for example, an MVP or POC), we believe Shuttle is a great fit for you! ## Deploying your Rust app to Shuttle Let me show you how quick it is to get a Rust web app deployed on Shuttle. We'll use Axum, the most popular Rust web framework. First, install the Shuttle CLI: **Linux and macOS:** ```bash curl -sSfL https://www.shuttle.dev/install | bash ``` **Windows (PowerShell):** ```powershell iwr https://www.shuttle.dev/install-win | iex ``` **Alternatively, using Cargo:** ```bash cargo install cargo-shuttle ``` Create a new Rust project using the Axum template: ```bash shuttle init --template axum ``` This generates a basic Axum web service with Shuttle annotations already set up, along with all necessary dependencies. The Rust project structure looks like this: ```rust use axum::{routing::get, Router}; async fn hello_world() -> &'static str { "Hello, world!" } #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/", get(hello_world)); Ok(router.into()) } ``` Notice the `#[shuttle_runtime::main]` macro - this tells Shuttle how to run your application. The return type `shuttle_axum::ShuttleAxum` indicates we're deploying an Axum web service. To test locally: ```bash shuttle run ``` Your application will be available at `http://localhost:8000`: When you're ready to deploy: ```bash shuttle deploy ``` Going to the [Shuttle console](https://console.shuttle.dev), you'll see your app building: That's it. Shuttle handles building and deploying your application. You'll get a URL where it's live - typically something like `.shuttle.app`. Visit the public URL and you'll see your app running in production: Your Rust web app is now deployed and accessible to the world - all done in minutes without writing a single configuration file. Want to add a database? Just add the annotation to your main function: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_axum::ShuttleAxum { // Your database is provisioned and ready to use let router = Router::new().route("/", get(hello_world)); Ok(router.into()) } ``` Shuttle provisions the database automatically when you deploy. No configuration files, no separate database setup, no environment variables to manage manually - it's all declared in your Rust code. Learn more about [provisioning databases with Shuttle](https://docs.shuttle.dev/resources/shuttle-shared-db). ### Automating deployments with GitHub Once you've deployed manually, you'll likely want to automate the process. Shuttle lets you connect your GitHub repository directly from the console for automatic deployments on push. Navigate to your project settings in the Shuttle Console, connect your GitHub repository, and enable automatic deployments. Select which branch to track, and you're done. Now whenever you push to GitHub, Shuttle automatically pulls your latest Rust code, builds it, and deploys the updated application. Learn more about [GitHub integration with Shuttle](https://docs.shuttle.dev/integrations/github). ## Finishing up Thanks for reading! I hope you enjoyed this guide on how to deploy a Rust web application. Rust applications are easier to deploy than ever - from simple APIs to full web apps. With so many deployment choices available, it can be difficult to figure out what the best fit is for your specific use case. Feel free to join our Discord server if you have any questions or need help deploying your Rust application. --- # Writing a REST API in Rust Source: https://www.shuttle.dev/blog/2024/01/31/write-a-rest-api-rust Date: 31 January 2024 Author: josh Tags: rust, axum, guide This article talks about how you can write a Rust REST API using Axum, SQLx and Postgres. In this article, we're going to talk about writing a REST API in Rust! Following on from our first [Shuttle Bytes](https://www.shuttle.dev/blog/2024/01/22/introducing-shuttlebytes) stream where we live-streamed a tutorial on this, we've created a text write-up to allow you to follow along in text. Interested in checking out what the final code should look like? You can find it [here.](https://github.com/joshua-mo-143/shuttle-axum-example/tree/main) ## Getting started ### Project initialisation If you haven't already, make sure you have Rust and Cargo installed! You can find install instructions [here.](https://www.rust-lang.org/tools/install) We'll be initialising our project with `cargo-shuttle`, Shuttle's CLI. You can install it by running the following: ```bash cargo install cargo-shuttle ``` Installation via `cargo-binstall` is also supported, along with our other install scripts which you can find [here.](https://docs.shuttle.dev/getting-started/installation) First of all we'll want to initialise our project with `shuttle init` (required `cargo-shuttle` to be installed). For the project name we'll be using `shuttle-example-axum`. Then we'll want to set up our dependencies with the following shell snippet: ```rust cargo add sqlx -F postgres,runtime-tokio-rustls cargo add shuttle-shared-db -F sqlx,postgres cargo add serde -F derive ``` ### Adding a Database & Migrations in Rust To add a database to our project, all we need to do is to add the annotation to our main function - which allows us to automatically grab a SQLx Postgres pool: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool, ) -> shuttle_axum::ShuttleAxum { // .. your code here } ``` During deployment, Shuttle will automatically provision a database to our project without any input required from us! Locally, it will use Docker to spin up the Postgres container - you can also use Podman and similar programs that allow Docker usage through them. You can find the installation for Docker [here.](https://www.docker.com/) Interested in just getting the connection string? You can disable the `sqlx` feature of `shuttle-shared-db` and instead change the type to `String`, then connect to your PgPool. You can find more information about connecting to a PgPool [here.](https://docs.rs/sqlx/latest/sqlx/type.PgPool.html#method.connect) Since we're using only a single migrations file, we can use `include_str!()` to include the whole file's text content as a string. We can then execute the query with the database pool. For the migrations, we can make a `migrations.sql` file in the project root: ```sql -- migrations.sql CREATE TABLE IF NOT EXISTS users ( id serial primary key, name varchar not null, age int not null ); ``` Then when you're finished and ready to run your migrations, you can add this snippet to your code: ```rust // this trait is required use sqlx::Executor; db.execute(include_str!("../migrations.sql")).await.unwrap(); ``` Note that here, we will want to open a `Shuttle.toml` file in our project root and add the migrations file so that when we deploy, it will automatically include the migrations file: ```toml assets = ["migrations.sql"] ``` We can add the database to our application by wrapping it in a struct and adding it to the application as shared state. The only requirement to add shared state is that the state must implement the `Clone` trait - which we can do here: ```rust #[derive(Clone)] pub struct AppState { db: PgPool, } ``` Note that if you're using a struct that is unable to use Clone (for example because of generic trait bounds), you can wrap the type in a `std::sync::Arc`. Then we need to add it to our API: ```rust let state = AppState { db }; let router = Router::new().route("/", get(hello_world)).with_state(state); ``` ### Writing routes When writing your routes, Axum will accept anything that implements the `IntoResponse` trait (which means the type can be turned into a HTTP response). Many primitives already have `IntoResponse` implemented so you can return things like `i32` or a `String` (or a `'static &str`) without being required to implement the trait. By using Axum's `extractors`, we can extract information from the HTTP request and declare them as handler parameters: for example, adding a `State` extractor means we want to include the state that we added to our `axum::Router`. Then we want to implement `sqlx::FromRow` using a derive macro for our struct - doing this allows it to automatically be converted from a query: ```rust #[derive(sqlx::FromRow)] struct User { id: i32, name: String, age: i32, } async fn retrieve_all_records( State(state): State ) -> Result { let res = match sqlx::query_as::<_, User>( "SELECT * FROM USERS" ) .fetch_all(&state.db).await { Ok(res) => res, Err(e) => { return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())); } }; Ok(Json(res)) } ``` After this, now that we've written our initial route for fetching all records, we can also write our other routes. For retrieving record by ID, we can use the `Path` extractor which extracts a dynamic URL slug (this will be shown later on): ```rust async fn retrieve_record_by_id( State(state): State, Path(id): Path ) -> Result { let res = match sqlx::query_as::<_, User>( "SELECT * FROM USERS WHERE id = $1" ) .bind(id) .fetch_one(&state.db).await { Ok(res) => res, Err(e) => { return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())); } }; Ok(Json(res)) } ``` Note that for the SQL query, we also bind a parameter to the query itself. By using `bind` variables exclusively when we need to add data from the JSON body, SQL injection can be easily prevented! Likewise, below we can also create a `UserSubmission` struct to represent a JSON request body for a user submission. We need to implement `serde::Deserialize` to be able to successfully extract it from the request body. We can also add a handler function for deleting records by ID in this snippet below: ```rust use serde::Deserialize; #[derive(Deserialize)] pub struct UserSubmission { name: String, age: i32 } async fn create_record( State(state): State, Json(json): Json ) -> Result { if let Err(e) = sqlx::query("INSERT INTO USERS (name, age) VALUES ($1, $2)") .bind(json.name) .bind(json.age) .execute(&state.db) .await { return Err( (StatusCode::INTERNAL_SERVER_ERROR, format!("Error while inserting a record: {e}")) ); } Ok(StatusCode::OK) } async fn delete_record_by_id( State(state): State, Path(id): Path ) -> Result { if let Err(e) = sqlx::query_as::<_, User>("DELETE FROM USERS WHERE ID = $1") .bind(id) .fetch_all(&state.db) .await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Error while deleting a record: {e}")) ); } Ok(StatusCode::OK) } ``` If you're looking to implement an update function, there are a couple of ways you could do it. Here, we use an `UpdateRecord` struct that holds option placeholder versions of the fields we want the user to be able to update. Then we write an SQL query that only changes the field if the bound value isn't null: ```rust #[derive(Deserialize)] pub struct UpdateRecord { name: Option, age: Option } async fn update_record_by_id( State(state): State, Path(id): Path, Json(json): Json ) -> Result { if let Err(e) = sqlx::query("UPDATE USERS (name, age) SET name = (case when $1 is not null then $1 else name end), age = (case when $2 is not null then $2 else age end) WHERE id = $3") .bind(json.name) .bind(json.age) .bind(id) .execute(&state.db) .await { return Err( (StatusCode::INTERNAL_SERVER_ERROR, format!("Error while inserting a record: {e}")) ); } Ok(StatusCode::OK) } ``` Now that we've written all of our routes, we can append them to the router like so. Note that the `:id` marks a dynamic route which will be used by the `Path` extractor: ```rust let router = Router::new() .route("/", get(hello_world)) .route("/users", get(retrieve_all_records) .post(create_record) ) .route("/users/:id", get(retrieve_record_by_id) .put(update_record_by_id) .delete(delete_record_by_id) ) .with_state(state); ``` Need to double check what the deployment code looks like? You can find it in [this file](https://github.com/joshua-mo-143/shuttle-axum-example/blob/main/src/main.rs) on GitHub! ## Deployment Now that we've finished, the only thing left to do is deploying the Rust application. You can run `shuttle deploy` (with the `--allow-dirty` flag if working on a dirty Git branch) and deploy with one line! ## Extending this example Looking to extend the project from this article? Here's a couple of ways you can extend it: - Add your own Error enum type and implement `axum::response::IntoResponse` for it! This allows you to propagate errors by implementing `From` for your error type. Error handling is a huge part of Rust and propagating errors can help you clean up your application nicely. - Add tests! You can use `testcontainers` to spin up Docker containers for Postgres and other infrastructure which makes testing your database extremely easy. You can find more about this [here.](https://github.com/testcontainers/testcontainers-rs) ## Finishing Up Thanks for reading! Hopefully this article has helped you deploy your first API in Rust. With the backend framework ecosystem already being in a relatively mature state, it's becoming easier than ever to write powerful and performant but low-memory footprint web services in Rust. Further reading: - Learn more about Axum [here.](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - Learn more about using SQL with SQLx [here.](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust) --- # Introducing ShuttleLabs: Cutting-Edge Rust Talks Source: https://www.shuttle.dev/blog/2024/01/30/introducing-shuttlelabs Date: 30 January 2024 A new series revolving around discussing cutting-edge advancements in Rust from thought leaders in the field. ### Discussing cutting-edge advancements in the Rust ecosystem If you're looking to stay ahead of the curve on all things Rust, connect with fellow Rustaceans and expand your skills, we'd love to have you join us at **ShuttleLabs**. Each month we will be hosting a thought leader from the space, diving into the hottest topics and creating an opportunity for discussion that can't be found anywhere else. ### Why we decided to start ShuttleLabs Rust is growing faster than its resources and it's difficult to stay up-to-date on all the advancements in the space. We hope that ShuttleLabs will solve this problem and create an environment for both seasoned and new Rust devs to continually learn, share their perspective, and hear from great contributors in the ecosystem. In our first session we will be exploring **Async In Rust, with Stefan Baumgartner!** Async in Rust is a big topic as Rust deals with async at a low level: using futures and executors, as well as being able to programatically create your own async runtime! The details of the talk are below: Topic: **Async in Rust** Speaker: **Stefan Baumgartner** (@ddprrt on X) Date: **February 6th, 15:00 (UTC)** Location: [https://discord.gg/shuttle](https://discord.gg/shuttle) ### [Sign up now](https://shuttlerust.typeform.com/shuttlelabs) If you want to be kept in the loop, [sign up here](https://shuttlerust.typeform.com/shuttlelabs) to secure your spot and receive an email ahead of our next session. See you there! --- # Writing Cronjobs in Rust Source: https://www.shuttle.dev/blog/2024/01/24/writing-cronjobs-rust Date: 23 January 2024 Author: josh Tags: cronjob, rust, tutorial, guide This article talks about how you can write cron jobs as a web service on Shuttle using the apalis cron job framework. In this article we're going to talk about how you can write your own cron jobs as a web service using Shuttle! Cron jobs (or "scheduled tasks") are useful for many things. They allow you to automatically do things like: - Automate data back-ups. - Adding daily reminders (for example to customers who are signed up to a service you own but haven't started using it yet). - Creating/writing reports. If you're interested in the final code and want to deploy quickly, you can find the repo [here.](https://github.com/shuttle-hq/shuttle-examples/tree/main/shuttle-cron) You can deploy it with two simple steps: 1. Run `shuttle init --from shuttle-hq/shuttle-examples --subfolder shuttle-cron` and follow the prompt (requires `cargo-shuttle` installed) 2. Run `shuttle deploy`. That's it! ## Getting Started If you don't have `cargo-shuttle` installed, you can install it by running the command below (requires Rust + Cargo installed): ```bash cargo install cargo-shuttle ``` To get started, you'll want to initialise a project with `cargo-shuttle`: ```bash shuttle init ``` You'll want to make sure to follow the prompt and choose `None` when the framework options come up. This will spawn a custom service that you can use with Shuttle. For this article, we will be using the project name `shuttle-example-cron`. Once done, we'll want to install the following dependencies: - `serde` - Allows us to de/serialize tasks - `chrono` - Allows us to use timestamps - `apalis` - The task queue framework we'll be using - `sqlx` - Allows you to interact with your provisioned database - `shuttle-shared-db` - Provisions database for you from Shuttle (locally via Docker, via the runtime in deployment) - `tower` - Used with apalis so that the cron job can be hosted. We can install all of these with the following shell snippet: ```bash cargo add apalis -F cron,postgres,extensions,retry cargo add chrono -F serde,clock cargo add serde -F derive cargo add shuttle-shared-db -F postgres cargo add sqlx -F runtime-tokio-native-tls,postgres cargo add tower ``` Now that everything is installed, it's time to get coding! ## Writing our first cron job ### Adding a database Before we do anything else, let's provision a database using the Shuttle runtime. You'll want to add the `shuttle-shared-db` annotation to your main function like so and set up our Postgres connection pool: ```rust use sqlx::{PgPool, postgres::PgPoolOptions}; pub struct MyService { db: PgPool, } #[shuttle_runtime::main] async fn shuttle_main( #[shuttle_shared_db::Postgres] conn_string: String, ) -> Result { let db = PgPoolOptions::new() .min_connections(5) .max_connections(5) .connect(&conn_string) .await .unwrap(); Ok(MyService { db }) } ``` As you can see, pretty simple! You would otherwise need to run a Docker command to spin this up. In production, you would also need to manually instantiate and manage your Postgres instance or rely on an IaC (infrastructure as code) tool like Terraform. Then we need to implement a few new things: - A struct that implements `apalis::prelude::Job` - A struct that can be added as a shared data extension to our cron service with a function that will do the actual work we want - A function that gets called by `apalis` when work is required to be done (according to the cronjob schedule) Let's start with implementing our job struct. Note that when using `apalis` jobs in a cronjob context, they must also implement `From>` (essentially this means they can't hold any other fields): ```rust use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use apalis::prelude::Job; #[derive(Default, Debug, Clone, Serialize, Deserialize)] struct Reminder(DateTime); impl From> for Reminder { fn from(t: DateTime) -> Self { Reminder(t) } } // set up an identifier for apalis impl Job for Reminder { const NAME: &'static str = "reminder::DailyReminder"; } ``` Now we want to implement the function that will do the actual work when called by `apalis`. We can set this up so that it just says "Hello world from `say_hello_world()`!": ```rust use apalis::prelude::JobContext; async fn say_hello_world(job: Reminder, ctx: JobContext) { println!("Hello world from `say_hello_world()`!"); } ``` However, if you want to add extra variables (for example, a database connection pool) you need to add an extension layer in your service to be able to access them. Let's have a look at what this struct might look like: ```rust #[derive(Clone)] struct CronjobData { message: String } impl CronjobData { fn execute(&self, item: Reminder) { println!("{} from CronjobData::execute()!", &self.message); } } ``` You can then feed this struct back into the `say_hello_world` function, which uses the `execute()` function: ```rust async fn say_hello_world(job: Reminder, ctx: JobContext) { println!("Hello world from send_reminder()!"); // this lets you use variables stored in the CronjobData struct let svc = ctx.data_opt::().unwrap(); // this executes CronjobData::execute() svc.execute(job); } ``` The variables for the `CronjobData` struct come from instantiating the struct and then adding it as a shared data extension to the Tower service that we use. This allows `apalis` to be able to use the data by using `.data_opt()` from the `JobContext` struct. ### Hooking it all up Now we're going to hook the structs we made previously into our main app! To start with, we need to set up the `PostgresStorage` type so that we can use Postgres for durable job queues. Without durable job queues, our jobs would disappear if our web service has any outages! We can convert this directly from the `PgPool` type stored in our main struct and run the migrations for it like so: ```rust #[shuttle_runtime::async_trait] impl shuttle_runtime::Service for MyService { async fn bind( self, _addr: std::net::SocketAddr ) -> Result<(), shuttle_runtime::Error> { // set up Postgres-backed storage let storage = PostgresStorage::new(self.db); // set up storage storage.setup().await.expect("Unable to run migrations :("); Ok(()) } } ``` The `shuttle_runtime::Service` trait is what allows your custom service to be packaged into a project that the Shuttle runtime can use. It also provides a HTTP address that you can optionally bind a HTTP-bound service to (for example, a web server). Now we can create a `tower` service that holds our job that we want to run. We can use the `DefaultRetryPolicy` provided to us by `apalis` to be able to add automatic re-tries, as well as layering the shared data as an `Extension`: ```rust use tower::ServiceBuilder; use apalis::layers::{DefaultRetryLayer, Extension, RetryLayer}; use apalis::prelude::job_fn; // .. your previous code let cron_service_ext = CronjobData { message: "Hello world".to_string(), }; // create a servicebuilder for the cronjob let service = ServiceBuilder::new() .layer(RetryLayer::new(DefaultRetryPolicy)) .layer(Extension(cron_service_ext)) .service(job_fn(say_hello_world)); ``` Now that we've built the service, we need to build the worker and then finally create an `apalis::Monitor` process that will carry the work out: ```rust use apalis::prelude::timer::TokioTimer; use apalis::cron::{Schedule, CronStream}; use apalis::prelude::{WorkerBuilder, Monitor}; // .. your previous code let schedule = Schedule::from_str("* * * * * *").expect("Couldn't start the scheduler!"); // create a worker that uses the service created from the cronjob let worker = WorkerBuilder::new("morning-cereal") .with_storage(storage.clone()) .stream( CronStream::new(schedule) .timer(TokioTimer) .to_stream() ) .build(service); // start your worker up Monitor::new().register(worker).run().await.expect("Unable to start worker"); ``` Here is the full code for our main application: ```rust #[shuttle_runtime::async_trait] impl shuttle_runtime::Service for MyService { async fn bind( self, _addr: std::net::SocketAddr ) -> Result<(), shuttle_runtime::Error> { let storage = PostgresStorage::new(self.db.clone()); // set up storage storage.setup().await.expect("Unable to run migrations :("); let cron_service_ext = CronjobData { message: "Hello world".to_string() }; // create a servicebuilder for the cronjob let service = ServiceBuilder::new() .layer(RetryLayer::new(DefaultRetryPolicy)) .layer(Extension(cron_service_ext)) .service(job_fn(say_hello_world)); let schedule = Schedule::from_str("* * * * * *") .expect("Couldn't start the scheduler!"); // create a worker that uses the service created from the cronjob let worker = WorkerBuilder::new("morning-cereal") .with_storage(storage.clone()) .stream(CronStream::new(schedule).timer(TokioTimer).to_stream()) .build(service); // start your worker up Monitor::new() .register(worker) .run() .await .expect("Unable to start worker"); Ok(()) } } ``` ## Deploying Now that we've written everything, all you need to do is `shuttle deploy` (with the `--allow-dirty` flag if working on a dirty Git branch). When the deployment is finished, you'll get the deployment information as well as the deployment database URL string. ## Extending Now that we've finished our new cron service, here's a few ideas you can use to extend it: - Add more cron jobs! You can also abstract creating the scheduler, worker, etc into a new struct (or enum) so that you don't need to manually instantiate everything as you may want to use the same defaults over many if not all of your jobs. - Augment the service to use Axum (or another web framework!) instead of purely `tower`. You can find out more about this from [this GitHub example.](https://github.com/geofmureithi/apalis/blob/master/examples/axum/src/main.rs) ## Finishing Up Thanks for reading! I hope this cron job template helps you. Further reading: - Find out more about the Axum framework [here.](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - Find out more about using a database with SQLx [here.](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust) --- # Using Serde in Rust Source: https://www.shuttle.dev/blog/2024/01/23/using-serde-rust Date: 23 January 2024 Author: josh Tags: rust, serde, guide This article talks about serde, a Rust serialization library and how you can use it in applications. In this article we'll be talking about Serde, how you can use it in your Rust application as well as some more advanced tips and tricks. ## What is serde? The `serde` Rust crate is used to efficiently serialize and deserialize data in many formats. It does this by providing two traits you can use, aptly named `Deserialize` and `Serialize`. Being one of the most well-known crates in the ecosystem, it currently supports (de)serialization to over 20 types. To get started, you'll want to install the crate into your Rust application: ```bash cargo add serde ``` ## Using serde ### Deserializing and Serializing data The simple way to serialize and deserialize data is by adding the serde `derive` feature. This adds a macro that you can use to implement `Deserialize` and `Serialize` automatically - you can do this with the `--features` flag (`-F` for short): ```bash cargo add serde -F derive ``` Then we can add a macro to any struct or enum that we want to implement `Deserialize` or `Serialize` for: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] struct MyStruct { message: String, // ... the rest of your fields } ``` This allows us to use any crate with `serde` support to convert between said formats. As an example, let's use `serde-json` to convert to and from JSON format: ```rust use serde_json::json; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] struct MyStruct { message: String, } fn to_and_from_json() { let json = json!({"message": "Hello world!"}); let my_struct: MyStruct = serde_json::from_str(&json).unwrap(); assert_eq!(my_struct, MyStruct { message: "Hello world!".to_string()); assert!(serde_json::to_string(my_struct).is_ok()); } ``` If you're interested in using `serde-json` for your Rust application, we have an article talking about JSON parsing libraries which you can check out [here.](https://www.shuttle.dev/blog/2024/01/18/parsing-json-rust) We can also deserialize and serialize to/from many sources including from a file stream I/O, a JSON byte array and more! ### Implementing Deserialize and Serialize manually In order to better understand how `serde` works under the hood, we can also implement `Deserialize` and `Serialize` manually. This is quite complicated, but for now we will stick with a simple implementation. Here is a simple implementation for serializing an `i32` primitive type: ```rust use serde::{Serializer, Serialize}; impl Serialize for i32 { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_i32(*self) } } ``` To be able to convert the type, `serde` internally requires us to use a type that implements `Serializer`. To implement `Serialize` for a type that isn't directly a primitive, we can extend this by serializing into a primitive, then converting into whatever type we want from the primitive. If we want custom serialization for structs, we can also do the same with use of the `SerializeStruct` trait: ```rust use serde::ser::{Serialize, Serializer, SerializeStruct}; struct Color { r: u8, g: u8, b: u8, } impl Serialize for Color { fn serialize(&self, serializer: S) -> Result where S: Serializer, { // 3 is the number of fields in the struct. let mut state = serializer.serialize_struct("Color", 3)?; state.serialize_field("r", &self.r)?; state.serialize_field("g", &self.g)?; state.serialize_field("b", &self.b)?; state.end() } } ``` Note that to serialize a field, the field type also needs to implement `Serialize`. If you have a custom type that doesn't implement `Serialize`, you will either need to implement `Serialize` or use a `Serialize` derive macro (if the struct/enum type holds types that all implement `Serialize`). The `Deserialize` trait is a little bit different and is a fair bit more complicated to implement. To be able to deserialize to a type, the type itself needs to implement `Sized` which means that there are a number of types which can't use this trait (for example `&str`) because they are unsized types. To deserialize a type, you also need to use a type that implements the `Visitor` trait. The `Visitor` trait uses the Visitor design pattern in Rust. This means that it encapsulates an algorithm that operates over a collection of same-sized objects. It allows you to write multiple different algorithms for operating over the data, without needing to change any original functionality. You can find out more about this [here.](https://rust-unofficial.github.io/patterns/patterns/behavioural/visitor.html) Below is an example for a `MessageVisitor` type that attempts to deserialize multiple types to String: ```rust use std::fmt; use serde::de::{self, Visitor}; struct MessageVisitor; impl<'de> Visitor<'de> for MessageVisitor { type Value = String; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("A message that can either be deserialized from an i32 or String") } fn visit_string(self, value: String) -> Result where E: de::Error, { Ok(value) } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(value.to_owned()) } fn visit_i32(self, value: i32) -> Result where E: de::Error, { Ok(value.to_string()) } } ``` As you can see, the implementation is quite large! However, it also allows us to make the implementation much simpler. By implementing the `Visitor` trait, we can pass the type that implements it to our `Deserialize` method and then deserialize JSON into our struct: ```rust use serde::{Deserialize, Deserializer}; impl<'de> Deserialize<'de> for MyStruct { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { // note: don't use unwrap in production! let message = deserializer.deserialize_string(MessageVisitor).unwrap(); Ok(Self { message }) } } ``` There is also documentation on deserializing structs which you can find [here.](https://serde.rs/deserialize-struct.html) However, generally speaking it is recommended that you use the `derive` feature macros as the manual implementation (as seen on the page itself) is quite large. The implementation involves mostly using a visitor that can visit a map or sequence then iterate through the elements to deserialize it. ### Using serde attributes When it comes to serde, the crate also has a number of useful attribute macros that we can use on our types to allow things like field renaming when deserializing a field or serializing to a struct. One of the best examples of this would be when you're interacting with an API written in a language that may have a key that is a reserved keyword in Rust. You can add a `#[serde(rename)]` attribute macro like so: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] pub struct MyStruct { #[serde(rename = "type")] kind: String } ``` This allows you to get around the issue! You can also rename all of your fields to another casing by using the `rename_all` attribute: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MyStruct { my_message: String } ``` Now when you serialize this struct, `my_message` should automatically turn into `myMessage`! Perfect for working with APIs written in other languages or with different conventions. If you'd prefer to not wrap fields in `Option`, you can also implement default values by using `#[serde(default)]`. This simply allows fields to be filled in with default values instead of automatically erroring out. You can also use `#[serde(default = "path")]` to be able to point to functions for providing the automatic default. For example, this struct and function: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] pub struct MyStruct { #[serde(path = "my_function")] my_message: String, } fn my_function() -> String { "Hello world!".to_string() } ``` `serde` also offers other useful attributes, like being able to deny unknown fields using `#[serde(deny_unknown_fields)]` on top of the struct. This allows you to make sure that the struct is exactly as-is when serializing and deserializing. ### Deserializing and Serializing enums Let's examine this enum type: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] enum MyEnum { Data { id: String, data: Value }, SomeOtherData { id: i32, name: String } } ``` Note that when converting to and from this enum, it can take two options: - A String field named `id` and a JSON value with the key `data` (this can be a map, a value or anything that the `Json` value can hold) - An `i32` field named `id` and a `String` field named `name` You can then match the enum variant for further processing. When the first enum variant is written in JSON, you can see that it should correspond with this: ```json { "Data": { "id": "your_id_here", "data": { .. } } } ``` This type of data is "externally tagged" - meaning the data is characterized by the identifier being on the outside of the JSON object. We can add inline tagging so that the identifier is on the inside of the crate - let's have a look at what this would look like: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] #[serde(tag = "type")] enum MyEnum { Data { id: String, data: Value }, SomeOtherData { id: i32, name: String } } ``` Now the JSON representation looks like this: ```json { "type": "Data", "id": "your_id_here", "data": { .. } } ``` Interested in reading more? The `serde` documentation has a page on tagging which you can find [here.](https://serde.rs/enum-representations.html) ## Crates that work well with Serde ### serde_with `serde_with` is a crate that provides custom de/serialization helpers to use with `serde`'s `with` annotation. Normally, you can define a module for the (de)serializer to use that follows a custom module for custom (de)serialization: ```rust #[derive(Deserialize, Serialize)] pub struct MyStruct { #[serde(with = "my_module")] my_message: String } ``` When using `serde_with`, it works by replacing the `with` annotation with a new one called `serde_as`. With this new attribute macro you can do quite a few things: - De/serializing a type using `Display` and `FromStr` traits. - Support arrays larger than 32 elements. - Skip serializing empty Option types. - Deserialize a comma separated list into a `Vec`. To use `serde_with`, you need to add it to your Cargo.toml either manually or by using the following command: ```bash cargo add serde_with ``` Then you need to add `serde_as` to the type you want to use it for, like so: ```rust use serde_with::{serde_as, DisplayFromStr}; #[serde_as] #[derive(Deserialize, Serialize)] struct MyStruct { // Serialize with Display, deserialize with FromStr #[serde_as(as = "DisplayFromStr")] my_number: u8, } ``` This struct lets you convert to/from a string but have the type itself in your Rust struct be `u8`! Pretty useful, right? This crate also comes with a [guide](https://docs.rs/serde_with/3.5.0/serde_with/guide/index.html) that you can use to fully capitalise on `serde_with`. Overall, a strong companion crate for `serde`. ### serde_bytes `serde_bytes` is a crate that allows for optimised handling of `&[u8]` and `Vec` types - while `serde` is capable of dealing these types by itself, some formats can be de/serialized more efficiently. It's quite simple to use - you just add it to your Cargo.toml and then add it via the `#[serde(with = "serde_bytes")]` annotation like so: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] struct MyStruct { #[serde(with = "serde_bytes")] byte_buf: Vec, } ``` Overall, an easy to use and simple crate that improves performance without much knowledge required. ## Finishing up I hope you enjoyed reading about Serde! It's a pretty powerful Rust crate and forms the backbone of most Rust applications. Read more: - Read more about how you can get started with the Rocket web framework [here.](https://www.shuttle.dev/blog/2023/12/13/using-rocket-rust) - Read more about using JSON parsers [here.](https://www.shuttle.dev/blog/2024/01/18/parsing-json-rust) --- # Introducing ShuttleBytes — live bite-sized Rust tutorials Source: https://www.shuttle.dev/blog/2024/01/22/introducing-shuttlebytes Date: 22 January 2024 A new series designed to teach the basics of Rust web development with a hands on approach. ## Introducing ShuttleBytes: bite-sized Rust tutorials We've decided it was time to up our game and give back to the ecosystem by launching a mini series that demystifies Rust web development with an interactive, hands-on experience. Over the past 2 years, we've been doing our best to provide quality educational content to existing Rust developers and those just getting started, and this year, we want to take it to the next level. **What is ShuttleBytes?** 1. **30 minute, live streamed tutorials** followed by a **Q&A** 2. Held **monthly** on the **[Shuttle Discord](https://discord.gg/shuttle)** server 3. **Hosted** by **Joshua Mo** (the mastermind behind our blog) **Tell me more** Our first ShuttleBytes edition will be taking place on the **25th of January** where we will be covering "How to Write a REST API" in Rust. So make sure to swing by, say hi and learn something new. P.S. This is a good opportunity to get your Rust-hesitant friends to give Rust a go! [Sign up now to secure your spot!](https://shuttlerust.typeform.com/shuttlebytes) If you're looking to improve your web development skills, meet other developers and learn how to use tools like Shuttle to streamline your workflow, sign up now and secure you seat. We can't wait to have you with us. --- # Parsing JSON in Rust Source: https://www.shuttle.dev/blog/2024/01/18/parsing-json-rust Date: 18 January 2024 Author: josh Tags: rust, json, guide, comparison This article talks about parsing JSON in Rust and compares JSON parsing libraries. In this article we're going to talk about how to use JSON parsing libraries in Rust, as well as a comparison of the most popular libraries and how they perform. ## JSON Parsing Basics ### Parsing JSON manually To get started with working with JSON in Rust, you'll want to install a library that lets you manipulate JSON easily. One of the popular crates currently available to use is `serde-json`. You can install it by running the following: ```rust cargo add serde-json ``` Once done, you can create JSON manually like this: ```rust use serde_json::{Result, Value}; fn untyped_example() -> Result<()> { // Some JSON input data as a &str. Maybe this comes from the user. let data = r#" { "name": "John Doe", "age": 43, "phones": [ "+44 1234567", "+44 2345678" ] }"#; // Parse the string of data into serde_json::Value. let v: Value = serde_json::from_str(data)?; // Access parts of the data by indexing with square brackets. println!("Please call {} at the number {}", v["name"], v["phones"][0]); Ok(()) } ``` However, we can do much better than this. For example, we can serialize JSON to and from structs, which has many applications. We can use it in JSON templating, web services, CLI arguments and more. Let's have a look at this in the next section. ### Parsing JSON with Serde Serde is a crate that helps you serialize and deserialize data to and from various formats, with one popular use of this being for JSON. If you write web services in Rust, Serde is your friend as you'll be dealing quite often with JSON data that you may need to either send or receive. Serde provides two main traits to help you with this: `Serialize` and `Deserialize` . For convenience, a derive macro implementation has been added to help with this. See below for how you can carry this out: ```rust use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct MyStruct { message: String } fn convert_json_to_struct() { // create a raw JSON string from the json! macro and turn it into a MyStruct struct let raw_json_string = json!({"message": "Hello world!"}); let my_struct: MyStruct = serde_json::from_str(raw_json_string).unwrap(); } ``` You can also create nested JSON by adding a struct that implements `Serialize` and `Deserialize` as a field of another struct that also implements `Serialize` and `Deserialize`: ```rust use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct Post { nested_json: PostMetadata, title: String, body: String } #[derive(Serialize, Deserialize)] pub struct PostMetadata { timestamp_created: DateTime, timestamp_last_updated: Datetime, categories: Vec, } ``` One use case for this would be nested JSON in a web service. For example, when you are receiving a POST request to your API that has a JSON body, you would normally pass the relevant `Json` type in as a handler function parameter. See below: ```rust use axum::Json; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct Post { nested_json: PostMetadata, title: String, body: String } #[derive(Serialize, Deserialize)] pub struct PostMetadata { timestamp_created: DateTime, timestamp_last_updated: Datetime, categories: Vec, } async fn receive_some_json( // this extractor consumes a JSON body and converts it into the struct type given Json(json): Json ) -> Json { println!("{:?}", json); Json(json) } ``` In addition to the previous code snippet that shows how you can use `serde_json` to convert to a struct from a JSON string, you can also convert to a struct from its byte representation: ```rust let json_as_bytes = b" { \"message\": \"Hello world!\", }"; let my_struct: MyStruct = serde_json::from_slice(json_as_bytes).unwrap(); ``` This is particularly useful if you want to store a struct somewhere as a byte array and then turn it back into a struct later on! Similarly, you can also read JSON and turn it into a struct from a IO stream of JSON using the `.from_reader()` method. Below is an example taken from the `serde_json` docs for how you could use it with a TCP stream: ```rust use serde::Deserialize; use std::error::Error; use std::net::{TcpListener, TcpStream}; #[derive(Deserialize, Debug)] struct User { fingerprint: String, location: String, } fn read_user_from_stream(tcp_stream: TcpStream) -> Result> { let mut to_be_deserialized = serde_json::Deserializer::from_reader(tcp_stream); let user = User::deserialize(&mut to_be_deserialized)?; Ok(user) } fn main() { let listener = TcpListener::bind("127.0.0.1:4000").unwrap(); for stream in listener.incoming() { println!("{:#?}", read_user_from_stream(stream.unwrap())); } } ``` Doing it this way allows you to deserialize from the stream directly instead of adding buffering in memory. If you're receiving a lot of JSON-based data, this can help you quite a bit! ## Comparing Rust JSON crates Although `serde-json` may be the most popular crate, it is by no means the fastest. A few other crates have popped up in the meantime to improve general JSON parsing performance. In exchange for performance, however, there are certain caveats regarding CPU SIMD extension requirements. There is also increased use of unsafe code, though generally speaking best efforts have been upheld to make sure the code is safe to use. All of these crates for the most part have the same API. Unless stated otherwise, you can safely go between these libraries and expect roughly the same interfaces for working with JSON in each library. ### serde-json `serde-json` is the easiest to use of the Rust JSON libraries. It requires no extra dependencies to use and is often recommended alongside `serde` when you need access to idiomatic manipulation of raw JSON values. `serde-json` also has support for `no_std` by allowing you to turn off the default `std` feature and enabling `alloc` instead. In terms of performance, `serde-json` itself is not slow by any means. However, it is slower than some of the other JSON libraries on this list. This is primarily due to being optimised for non-parallelized CPU usage. Particularly if you are able to access a modern x86 CPU, you may want to read on to find out more about some of the better-performing options. However, this crate is also the most well used and supported within the Rust community, so if you are having issues with it then it is easy to find assistance! ### simd-json [`simd-json`](https://github.com/simd-lite/simd-json) is a Rust port of the `simdjson` C++ JSON parser, with `serde` compatibility built in. As the name states, this library uses SIMD - short for Single Instruction Multiple Data. This is a technique used to be able to process multiple data points with parallel processing, making it significantly faster! As a caveat however, it requires that your system is x86 capable and during runtime it will select the best SIMD feature set for performance. If no feature sets are available there is also an unoptimised Rust implementation, but in the documentation it mentions that it should not be relied on. It is mentioned in the documentation that `simd-json` can be used at full capacity on native target compilation. You can do this by enabling the following compiler option in rustc when running your program, like so: ```rust rustc -C target-cpu=native ``` However, if you're like most people using Cargo you probably want to use `cargo run`. As in the example, you can create a config at `.cargo/config` and then add the following: ```rust [build] rustflags = ["-C", "target-cpu=native"] ``` Generally speaking, although this library is quite fast, it should be noted that there is quite a lot of **unsafe** code in this crate due to it being a port of a C++ crate. This is not to say that you shouldn't use it, but rather to use it with caution (as the crate says). In spite of this, there's a section on safety that details how best practices (like unit testing) are upheld to make sure the crate is as safe to use as possible. It should also be mentioned that for best performance, it's typically best to enable the `jemalloc` or `mimalloc` features to be able to make the most of the library. Generally, the API for `simd-json` is the same as `serde-json`, so if you want to switch at any point then generally you should not have any problems doing so. ### sonic-rs [`sonic-rs`](https://github.com/cloudwego/sonic-rs) is a Rust implementation of JSON manipulation with SIMD functionality. This library also has a counterpart library in C++ and Go! Although it used to require the Rust nightly toolchain, it supports stable Rust. Similarly to `simd-json`, it also requires x86 CPU architecture to function at full capacity. Like `simd-json`, to use `sonic-rs` you need to enable the following compiler option in rustc when running your program: ```rust rustc -C target-cpu=native ``` You can create a config at `.cargo/config` and then add the following to enable it while using `cargo run`: ```rust [build] rustflags = ["-C", "target-cpu=native"] ``` This allows you to build for SIMD without needing to do anything else! Like `simd-json`, there is a fair amount of `unsafe` code being used. However, if you search for unsafe code within the library you will find probably even more `unsafe` code than in the previous library. There is also not much documentation as to how unsafe guarantees are upheld, so although this library may be even faster than `simd-json` you will want to double-check that there is no undefined behavior! `sonic-rs` additionally has some extra methods for lazy evaluation and additional speed. For example, if you want a JSON string literal, you can use the `LazyValue` type when deserializing to convert it to a JSON string value that still has the forward slashes. There are also quite a few `unchecked` methods you can use if you are either not afraid of unsafe behavior or are sure it won't error out. Although `sonic-rs` is a pretty fast library, it is also a more recent crate and therefore there are some methods like `from_reader` (to allow reading from an IO stream) missing from the crate. This has been raised as a GitHub issue already, so hopefully it will get implemented sooner rather than later. ### Benchmarks You can find the benchmarks for `simd-json` and `serde-json` [here.](https://github.com/serde-rs/json-benchmark) There is a fairly significant improvement for `simd-json` versus `serde-json`. You can find the benchmarks for `sonic-rs` [here](https://github.com/cloudwego/sonic-rs#benchmark), which also compares it against `simd-json` and `serde-json`. As you can see, the final results are not in the same format as the `simd-json` and `serde-json` benchmarks so it is somewhat more difficult to understand in terms of data processed per second. However, `sonic-rs` is significantly (and sometimes hugely!) faster than both `simd-json` and `serde-json` under most scenarios. ## Finishing Up Thanks for reading! I hope this article has helped you gain an understanding of how to effectively use Rust JSON parsing libraries. Interested in more? - Check out our guide for Axum, Rust's most popular framework [here.](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - Check out our guide for using raw SQL in Rust with [SQLx.](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust) --- # A Guide to Rust ORMs in 2025 Source: https://www.shuttle.dev/blog/2024/01/16/best-orm-rust Date: 16 January 2024 Author: josh Tags: rust, sql, orm, comparison This article talks about Rust ORMs, what they are and which ORM is best for your use case. In this article, we're going to talk about Rust ORMs and compare the most popular Rust ORMs that you can use today in your applications. We'll also explore whether you should use an ORM at all, and when raw SQL might be a better choice. ## What is an ORM? A Relational Object Mapper (ORM for short) is a piece of software that aims to solve the issue of using SQL directly by letting you map objects in your code to SQL. For example, you may have an SQL query that looks like this: ```sql SELECT FROM CAKES WHERE NAME = 'Test Cake'; ``` This may be written as this: ```rust let name = "Test Cake"; let cake: cake::Model = User::find() .filter(cake::Column::Name.contains(name)) .all(db_connection) .await?; ``` Although initially there is more boilerplate setup than using a raw SQL library might use, in the long run it can save a lot of developer headaches when getting SQL queries to work - it also makes onboarding developers who are new to a codebase much easier. Additionally, you get the benefits of any IDE plugins you wish to use - for example, LSP (Language Server Protocol) plugins and Intellisense. ## SeaORM ### What is SeaORM? SeaORM is a fully async-friendly Rust ORM that aims to "help you build web services in Rust with the familiarity of dynamic languages". This library builds on SQLx and abstracts the raw SQL away to provide a clean interface that allows you to use structs as models, using derive macros and traits to allow you to build the experience that you want. It also comes with a CLI for generating migrations, entities, and models. SeaORM also implements a system called `ActiveModel` through traits to be able to extend the behavior of models that an application might use. Additionally, you can add traits for extending behavior before or after saving a record, and the `ActiveModel` itself. This is quite helpful for us as it allows us to slim down the application code while abstracting it away to other areas. A new framework called [Loco](https://loco.rs/) aims to reproduce the "Ruby on Rails" experience in Rust by including heavy use of SeaORM to slim down application code by allowing you to instead use traits to implement the behavior that you want - you can explore this in our [guide to using Loco with Rust](https://www.shuttle.dev/blog/2023/12/28/using-loco-rust-rails). SeaORM is plug-and-play and intuitive for basic use cases, with fast compilation times compared to Diesel. It auto-generates structs from your schema and works well with frameworks like Axum. SeaORM has quite a lot of [helpful documentation on the SeaQL website](https://www.sea-ql.org/SeaORM/docs/index/). There's a page for mostly everything you can do with SeaORM. Some parts like `ActiveModel` that are quite useful to know about are mainly tucked away into parts of other pages, so it could be inferred there's an assumption that you're going to read every page or use the search bar. If you plan to use SeaORM regularly it would be a good idea to do so already, but this can make casual browsing somewhat more awkward. If you have a lot of different models or tables that you need to use, SeaORM is very helpful. If you have a lot of different things you need to keep track of and have a Rust LSP plugin or intellisense installed, it's easy to ensure that all of the SQL database interactions "just work" without needing to debug anything! This solves a particularly large issue for teams with members who may need to interact with the database but are not skilled in SQL. One thing that you might find to be a hindrance is knowing where to import your dependencies from. Particularly if you're using multiple models in one file, it can be annoying to rename everything! It can also be somewhat complicated to implement your own `ActiveModel` behavior. If you're a less experienced developer, this can lead to some headaches. The set-up time may also be a turn-off particularly if you have a lot of tables to set up due to how much method chaining there is. Unlike Diesel, SeaORM doesn't provide compile-time type checking - you'll need to rely on runtime integration tests to catch database-related bugs. However, SeaORM does offer flexibility with escape hatches for raw SQL when you need it, and it's particularly good for dynamic queries. Additionally, there are a couple of initial bumps that a newer developer may come across while using it - particularly, the need for a CLI and looking at what the migrations do exactly. Additionally, although you can migrate SQL files directly to SeaORM migrations, the generated migration files themselves are extremely long. This is a migration that adds one table with one column: ```rust // src/migrator/m20220602_000001_create_bakery_table.rs (create new file) use sea_orm_migration::prelude::*; pub struct Migration; impl MigrationName for Migration { fn name(&self) -> &str { "m_20220602_000001_create_bakery_table" } } #[async_trait::async_trait] impl MigrationTrait for Migration { // Define how to apply this migration: Create the Bakery table. async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .create_table( Table::create() .table(Bakery::Table) .col( ColumnDef::new(Bakery::Id) .integer() .not_null() .auto_increment() .primary_key(), ) .col(ColumnDef::new(Bakery::Name).string().not_null()) .col(ColumnDef::new(Bakery::ProfitMargin).double().not_null()) .to_owned(), ) .await } // Define how to rollback this migration: Drop the Bakery table. async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(Bakery::Table).to_owned()) .await } } #[derive(Iden)] pub enum Bakery { Table, Id, Name, ProfitMargin, } ``` As you can see, it's pretty long. Additionally, the `Iden` derive macro is not clearly explained to the user in the documentation. Despite this, it is crucial to be able to implement the definitions for the migration itself. In terms of performance, it is slower than other ORM crates (namely, Diesel) - you can find [detailed performance metrics in the Diesel repository](https://github.com/diesel-rs/metrics/). While SeaORM is a crate that can offer a lot of functionality, you may have to sacrifice some performance in exchange for it. Diesel also produces smaller binaries and has better overall performance characteristics. ### Using SeaORM with Shuttle By default, Shuttle provides a SQLx connection from our `shared_db` crate which you can turn into a SeaORM connection: ```rust #[shuttle_runtime::main] async fn axum( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_axum::ShuttleAxum { let conn = SqlxPostgresConnector::from_sqlx_postgres_pool(pool); // pg conn ... let app = Router::new() .route("/", get(some_route)) .with_state(Arc::new(conn)); Ok(app.into()) } ``` In production, the macro will automatically allow the Shuttle servers to provision a Postgres instance to you with no setup required! ## Diesel ### What is Diesel? Diesel is the "other big choice" that you might consider when wanting to use an ORM in Rust. It can be more accurately described as a data mapper and query builder. However, because it offers many features that an ORM normally might (compile-time checking, migrations, mapping structs to database objects) it is considered functionally the same as an ORM. Diesel is used at scale in production - notably powering crates.io via diesel-async. Compared to SQLx, the table setup is much cleaner: ```rust diesel::table! { users { id -> Integer, name -> VarChar, favorite_color -> Nullable, } } ``` Instead of mapping directly to Rust types, Diesel maps SQL types as Rust unit structs. However, when you're writing a struct that you may want to use for querying the database, note that the documentation says you shouldn't directly use these types in your structs. Instead of being required to use models or entities directly, you can use Diesel's methods to do simple inserts, updates, or selects instead: ```rust let new_user = (id.eq(1), name.eq("Sean")); let rows_inserted = diesel::insert_into(users) .values(&new_user) .execute(connection); ``` The combination of both of these makes for a much more simple interface to work with than SeaORM. There is no specific interface for extending your models, but you can also add a manual implementation. One of Diesel's main strengths is that it enforces compile-time safety by checking the queries from the `table!` macros. This is a huge advantage for developers who want to make sure that their queries work and it means you won't get runtime errors trying to run SQL queries. It is also particularly relevant if you're running a lot of large queries where you have a lot of things going on. Diesel targets flexibility, database-specific extensions, and type safety - it explicitly doesn't try to hide database differences for the sake of portability. If you're looking to use a web service with Diesel, it should be noted that Diesel is primarily synchronous and uses native drivers (the primary reason behind native async incompatibility). When using Diesel, you're using a highly-optimised implementation of the transport protocol of the database library. However, if you want to use a pure Rust stack, Diesel may not be for you. With regards to enabling async, there has been [ongoing discussion in the Diesel GitHub issues](https://github.com/diesel-rs/diesel/issues/2084). If you'd like to use Diesel in an async context idiomatically you can always use `diesel-async` or `diesel-deadpool` (or one of the many other crates that do this). Diesel has very extensive documentation that goes beyond the crate itself and has sections on composing applications with Diesel and best practices, extending Diesel with whatever functionality you'd like as well as how to configure the CLI. Compared to SeaORM, the [docs.rs](http://docs.rs/) documentation has quite a lot on there! There is explicit documentation on writing queries, using the library traits, and more. In comparison, however, SeaORM has much more documentation on its own docs page which isn't based on [docs.rs](http://docs.rs/). Neither particularly loses in this category, although it can be slightly more difficult to find documentation about certain topics in SeaORM like ActiveModel. Due to the way that Diesel is built, it makes very heavy use of generics. Using generics can help write crates because it can make your structs much more flexible while maintaining good performance. However, it can also result in extremely unhelpful errors when writing your application - though recent compiler improvements have made these error messages better. Other libraries (Axum, for example) have gotten around this by adding a macros flag that also allows you to add a `debug_handler` macro that lets you add a macro to any function that doesn't use generics to avoid the wall-of-errors issue. Like Axum, Diesel also has a macro to be able to automatically check for errors, which you can use like so: ```rust #[derive(Selectable)] pub struct SomeStruct { #[diesel(check_for_backend(diesel::pg::Pg))] some_field: String } ``` This automatically allows Diesel to type-check your struct without you needing to do anything. Diesel also has [documentation for understanding compile-time error messages](https://docs.diesel.rs/2.1.x/diesel/index.html#how-to-read-diesels-compile-time-error-messages) dedicated to helping you tackle the various trait-related errors. ### Using Diesel with Shuttle At the moment Diesel isn't supported out of the box, but a [community plugin for using Diesel with Shuttle](https://github.com/aumetra/shuttle-diesel-async) has been created to allow you to use Diesel (via `diesel-async`) with Shuttle natively. To use it, you need to run the following command: ```rust cargo add shuttle-diesel-async --git cargo add diesel-async ``` Then you can add it to your code like so: ```rust use diesel_async::{ pooled_connection::deadpool::Pool, AsyncPgConnection, }; #[shuttle_runtime::main] async fn axum( #[shuttle_diesel_async::Postgres] pg: Pool ) -> shuttle_axum::ShuttleAxum { // .. your code } ``` ## What Should You Use? This table illustrates the main differences between SeaORM, Diesel, and SQLx for those who just want a comparison: | Library | SeaORM | Diesel | SQLx | | ------------------- | --------------------------------------------------------- | ---------------------------------------------- | ------------------------------ | | Type | Full ORM | Query builder / Data mapper | Raw SQL with macros | | Migrations | Yes | Yes | Yes (via sqlx-cli) | | Query building | Yes | Yes | No (raw SQL) | | Models | Yes (auto-generated) | Yes (manual via derive) | Manual struct mapping | | Lazy loading | Yes | No | No | | Compile time checks | No (runtime only) | Yes | Yes (via query! macros) | | Raw SQL support | Yes (escape hatches) | Yes | Yes (primary interface) | | Extendable? | Not particularly although you can extend the ActiveModels | Yes - you can extend Diesel as well as the CLI | N/A | | Async friendly? | Yes (native) | Plugins required | Yes (native) | | Performance | Slower than Diesel | Fast, optimized | Fast, no abstraction overhead | | Learning curve | Moderate (ORM patterns) | Steep (generics, type system) | Easy (if you know SQL) | | Transparency | Low (abstracted queries) | Medium (query builder) | High (write actual SQL) | | Best for | Complex models, team needs ORM patterns | Type safety, compile-time guarantees | Direct control, simple queries | Below, we'll also go through some of the other major changes that differentiate SeaORM and Diesel from each other. SeaORM is a more complete ORM experience compared to Diesel. However, it also requires more setup and boilerplate writing. Depending on how you feel about writing boilerplate, this can be a turnoff. In exchange for this, however, it allows you to slim down the application code by using the crate instead of having to implement things yourself. Compared to SeaORM, Diesel has a larger community, with more GitHub stars. Diesel's main communities are on Gitter and GitHub Discussions. However, this may be somewhat less accessible for some users depending on if you use Gitter. SeaORM uses Discord in comparison which is more popular generally (and therefore easier to access), but there aren't as many people. It's worth noting that Diesel has a steeper learning curve, particularly if you're not familiar with complex generic types and language theory concepts. Compile times can also increase significantly when using Diesel, though it offers better runtime performance in exchange. Because Diesel is a smaller library and is primarily intended to be used as a query builder and data mapper, the library is a bit more barebones and leaves more to the user. However, you can also extend Diesel itself to include whatever behaviour you'd like. Some extensions have been added as community crates - which while great, is not particularly helpful if you are working within an environment that requires vetting of crates before usage. On the other hand, SeaORM doesn't allow any extension at all. Ultimately, what you should use depends on your use case. If you want an ORM that can take care of a lot of different responsibilities in your application, you should use SeaORM. If you want to use a smaller and more extensible crate with better performance, Diesel is likely to be better. However, before committing to either, you might want to consider whether you need an ORM at all. ## SQLx SQLx is an async, pure-Rust SQL library that takes a different approach from traditional ORMs - it lets you write raw SQL while providing compile-time checking and type safety. Rather than abstracting SQL away, SQLx gives you direct control over your queries while catching errors at compile time through its `query!` and `query_as!` macros. SQLx supports multiple databases (Postgres, MySQL, MariaDB, SQLite) and provides connection pooling out of the box. The library is designed to work with async Rust and integrates smoothly with web frameworks like Axum and Actix. The main advantage of SQLx is transparency. You write actual SQL queries, so you know exactly what's being executed against your database. The `query!()` macro verifies your SQL at compile time by connecting to a development database and checking that columns exist, types match, and the query is valid. This catches bugs like typos, wrong column types, or missing tables before your code ever runs. For straightforward queries, SQLx is minimal and direct. For complex queries with joins and subqueries, you're working in SQL rather than learning an ORM's query builder syntax. The tradeoff is that you need to know SQL, but you avoid the abstraction layer that can make debugging difficult with traditional ORMs. SQLx also provides runtime query execution through `query()` for dynamic queries where compile-time checking isn't possible. The library includes migration support via the `sqlx-cli` tool, making it easy to version and manage your database schema. ### Using SQLx with Shuttle Shuttle provides first-class support for SQLx through the [`shuttle-shared-db` crate](https://docs.shuttle.dev/resources/shuttle-shared-db). You can get a connection pool provisioned automatically: ```rust #[shuttle_runtime::main] async fn axum( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!() .run(&pool) .await .expect("Failed to run migrations"); let router = Router::new() .route("/", get(hello)) .with_state(pool); Ok(router.into()) } ``` In development, Shuttle can spin up a local Postgres instance in Docker automatically. In production, it provisions a managed database on Shuttle automatically. The `sqlx::migrate!()` macro runs your migrations from the `migrations/` directory at startup. [Learn more about Shuttle databases](https://docs.shuttle.dev/resources/shuttle-shared-db). ## Why Not Use an ORM? There's a significant sentiment in the Rust community that questions whether ORMs are the right solution for database interactions. The core argument is that ORMs add an additional layer of abstraction that can introduce complexity rather than reduce it. When you use raw SQL (particularly with libraries like SQLx that provide compile-time checking), you get transparency and direct control over your database queries. This makes debugging performance issues much easier because you can see exactly what queries are being executed. With an ORM, problematic queries can be hidden behind abstraction layers, making it harder to identify bottlenecks or inefficiencies. SQLx's `query!` and `query_as!` macros provide compile-time checking that catches subtle bugs like wrong column types, missing constraints, and dead code - issues that ORMs without compile-time checking might miss. The feedback loop is fast (1-2 seconds) compared to running a full test suite, which encourages refactoring and makes developers more willing to modify database schemas. For simple CRUD operations, ORMs can be convenient. They handle basic inserts, updates, and deletes with minimal boilerplate. But once you need complex queries involving multiple joins, subqueries, or database-specific features, you often end up fighting the ORM to express what would be straightforward in SQL. At that point, you're learning both SQL and the ORM's query builder syntax, effectively dealing with two problems instead of one. Raw SQL also offers better performance characteristics. There's no translation layer between your code and the database, and you have complete control over query optimization. For applications where performance matters, this can make a noticeable difference. The myth of database portability is worth addressing as well. While ORMs theoretically let you switch databases easily, in practice this rarely works smoothly. Even basic operations like `INSERT RETURNING` work differently across databases. If you ever need to switch from MySQL to Postgres or MongoDB to Postgres, it's going to be painful regardless of whether you use an ORM. Diesel explicitly acknowledges this by not prioritizing database portability - instead, it exposes database-specific features and lets you use them. That said, raw SQL isn't always the answer. If you're working with a team where not everyone is comfortable with SQL, an ORM can help level the playing field. If you're building a prototype and need to move quickly, an ORM's scaffolding can speed up development. If your queries are straightforward and your schema is stable, the convenience of an ORM might outweigh its drawbacks. The decision comes down to your specific context. If you value transparency, performance, and direct control, raw SQL with a library like SQLx is worth considering. If you value convenience and are willing to trade some performance and debuggability for it, an ORM might be the right choice. Just be aware of the tradeoffs you're making. ## Finishing Up Thanks for reading! I hope you have gained a better understanding of what Rust ORM you'd like to use for your application. Want to get started building a database-backed Rust API? Try our quickstart template: ```bash shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/postgres ``` Further reading: - Want to use raw SQL instead? Check out our [guide to using SQLx in Rust](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust). - Interested in finding the best web framework for you? Check out our [Rust web framework comparison](https://www.shuttle.dev/blog/2023/08/23/rust-web-framework-comparison). --- # How We're Bypassing AWS Complexity Source: https://www.shuttle.dev/blog/2024/01/11/bypassing-aws-complexity Date: 11 January 2024 This article talks about how Shuttle bypasses AWS complexity. ## Our mission.. ..is to empower Rust developers by providing tooling that bypasses the need to interact with complicated AWS infrastructure or infrastructure files when the requirement is to move fast. When you're self-hosting, using a VPS, or on the cloud, you have control over everything. It can also be very complex: anything related to AWS, Terraform files, Dockerfiles... the list goes on. If you're also playing the part as a Software Engineer, this can use up quite a lot of time, especially in cases where moving fast is a higher priority than having control over everything. What you end up with is: - Spending time context-switching which interrupts your flow. - Ending up with responsibilities that you may not specialize in, potentially spending more time debugging your work. - Needing to figure out Cloud consoles; they solve a lot of problems, but are also very complex to use. ![Old man yelling at cloud](/images/blog/bypass-aws-complexity-article/old-man-yelling-at-cloud.jpeg) Other solutions aim to solve this problem by dockerizing your applications for you. However, on those platforms Rust is typically treated as a second-class citizen, requiring you to write your own Dockerfile to deploy on the platform. You may also hit errors that you normally wouldn't with other languages. Our aim is to enhance the Rust development experience. We've built our open-source platform with Rust, and we strive to use Rust for our internal tools wherever feasible. Our goal is to provide Rust developers with a smooth and efficient deployment process comparable to what JavaScript developers enjoy with platforms like [Vercel](https://vercel.com), emphasizing ease-of-use with minimal setup. ## What are we doing currently? ### 1. Providing tooling that simplifies deployments We provide tooling that simplifies deployment by allowing users to add our Shuttle runtime macro to their applications. This means all you need to do is run `shuttle deploy` and your web service gets sent to the Shuttle servers, containerized, and started. No further steps are required! Check the video below for a short example: ### 2. Making resource provisioning as easy as possible You shouldn't need to set up YAML files just to get your application going. When using our runtime, you can simply pass in annotation macros that [provision resources](https://docs.shuttle.dev/resources/overview) for you. Whether you need a database, key-value store, or some metadata about your project, we can provide that! By doing this, it allows a seamless experience between getting the infra stuff that you want and then going straight back to writing good code. ### What we're doing in 2024 There are several updates we're looking forward to announcing this year. Most notably, our upcoming builder service which will allow many more use cases and bring additional platform stability. We are also planning on adding more integrations like Redis, S3 object storage, and more so keep an eye out for our 2024 product roadmap which will have more information! Feedback is always welcome at hello@shuttle.rs if there's anything you'd like us to know! --- # Getting Started with Tracing in Rust Source: https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust Date: 9 January 2024 Author: josh Tags: rust, logging, tracing, tutorial, guide This article talks about tracing in Rust and how you can use it to log activity in your Rust applications. In this article, we're going to talk about how you can get started with the Rust `tracing` crate. ## What is Tracing? The `tracing` crate and the family of crates that fall under it are designed for completely async-compatible logging. Many if not all of the alternative solutions, like `log`, `log4rs` etc. are not completely proven to be async-friendly. Tracing solves this problem by introducing the concept of logging over a generic period rather than a specific given time period. Additionally, the tracing crates provide a powerful system for logging in your application. It is compatible with many other crates like the OpenTelemetry SDK, allowing you to also send your logs for further analysis. ## Getting Started with Tracing You can get started with tracing by installing the crate into your project: ```bash cargo add tracing ``` If your program compiles to a binary (it's not a library), you will need to install a logging subscriber. `tracing-subscriber` is perfect for this - you can add it by running the following: ```bash cargo add tracing-subscriber ``` ## Basic Setup with Tracing At a basic level, `tracing` uses the concept of spans to record the flow of program execution. Tracing uses the concept of spans which represent a generic period. The idea is that when a program performs any actions it enters a span, and when it's finished it exits the span. In comparison, other logging crates represent logging as a specific given period in time. These spans (and events that happen in them) get aggregated by a subscriber, which produces logs that the user sees as the end product. See the diagram below: ![A high-level view of how the Rust tracing crate works](/images/blog/tracing-article/how-tracing-works.png) To get started, let's initialize a `tracing-subscriber` with some default settings: ```rust use tracing_subscriber::fmt::fmt; use tracing_subscriber::filter::EnvFilter; tracing_subscriber::registry() .with(fmt::layer()) .with(EnvFilter::from_default_env()) .init(); ``` Note that `from_default_env()` will use the value of whatever the `RUST_LOG` environment variable has been set to. You can also create your own `EnvFilter` like so: ```rust EnvFilter::builder() .with_default_directive(LevelFilter::ERROR.into()) .from_env_lossy() ``` There are also some cases where you might want to mute all tracing from certain modules. `EnvFilter` lets you do this by adding directives - check out the code snippet below that limits the `sqlx::migrations` logging to only errors: ```rust use tracing_subscriber::filter::EnvFilter; let mut filter = EnvFilter::try_from_default_env()? .add_directive("sqlx::migrations=error".parse()?); ``` Interested in seeing how much you can do with filters for your subscriber? Check out [this documentation page,](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#) ## Working With Spans in Tracing Once we're done setting up our tracing subscriber, we can start with logging some events. In terms of getting set up quickly, we can use `tracing` macros instead of manually orchestrating the spans ourselves. It's not unlike using the logging macros from the `log` crate and is quite simple. ```rust fn do_something() { tracing::debug!("This function is doing something!"); } ``` 5 levels of logging can be emitted: `info`, `warn`, `debug`, `error`, and `trace`. The similarly-named `tracing` macros work by setting up a span, recording an event, and then exiting the span. This is quite useful if you want to set something up quickly. However, if you're looking to dive a bit deeper you probably want to handle spans yourself: ```rust use tracing::{event, span, Level}; // create a span with the tracing level and name of the span let span = span!(Level::INFO, "my_span"); let _guard = span.enter(); // records an event within "my_span". A logging level is required here in the macro. event!(Level::DEBUG, "something happened inside my_span"); ``` Check out the table below for a cheat sheet of what attributes can be accepted by the `span` macro: | Parameter | Optional? | Input type | |-----------|------|------| | Target | Yes | `target = "string literal"` | | Parent span | Yes | `parent = "parent span id"` | | Level | No | Any `tracing::Level` enum variant | | Span name | No | `"string literal"` | See the following code snippet for a created span using the macro that uses `Level::DEBUG` and a span description. ```rust let span = span!(Level::DEBUG, "my_debug_span"); ``` Like before with the various logging macros, we can use macros that generate spans with a pre-generated logging level according to the name of the macro: ```rust let span = debug_span!("my_debug_span"); ``` Note, however, that the documentation says that spans may produce incorrect traces if the guard is held across an await point. We can remedy this by using `.in_scope()` instead of entering the span itself. See below: ```rust async fn my_async_function() { // constructs a span at the "info" log level let span = info_span!("my_async_function"); let some_value = span.in_scope(|| { // run some synchronous code inside the span... }); // This is okay! The span has already been exited before we reach // the await point. some_other_async_function(some_value).await; // ... } ``` Spans can also have child-parent relationships. This means that you can also link spans to each other, as noted on [this documentation page.](https://docs.rs/tracing/latest/tracing/macro.event.html) Below is a short example of how it would work: ```rust let span = span!(Level::INFO, "my span"); let span2 = span!(parent: &span, Level::TRACE, "span in a span"); ``` This means you can have larger overall spans and have specific spans within those spans. An example of this might be having a span for general database events and a smaller span for record creation events. When you close the parent span, all of the child spans will also close. ![A high-level view of how parent-child span relationships work in the tracing Rust crate](/images/blog/tracing-article/parent-child-spans.png) Additionally, you can also declare that a span follows on from another span: ```rust let span1 = span!(Level::INFO, "span_1"); let span2 = span!(Level::DEBUG, "span_2"); span2.follows_from(span1); ``` This has the advantage of a particular span being able to follow from any number of prior spans, as well as each span being executed in its own space. You can use this to model causal relationships. In practice, this could mean a Tokio process that spawns several child processes, for example. You could have a final span that follows on from several other spans (one for each child process), and then await each process before returning and recording the final result. ![A high-level view of how follow-on spans work in the tracing Rust crate](/images/blog/tracing-article/follow-on-spans.png) ## Instrumentation in Tracing Of course, having to manually add spans for every function is a bit tiring. We can instead use the `#[instrument]` macro from `tracing` to do this automatically for us. To do this, you will want to make sure the `attributes` feature of `tracing` is installed first: ```bash cargo add tracing -F attributes ``` Like the `span!` macros previously, we can attach extra attributes to it: ```rust #[instrument(level = "debug", target = "this_crate::some_span", name = "my_instrumented_span")] async fn do_something_async() { // do some work } ``` The instrument macro as above currently does the following: - It creates a span when invoking the function at the debug level of logging. - The name of the span is "my_instrumented_span". - The target normally follows the `my_crate::my_module` format, but has been overridden to be `this_crate::some_span`. You can also add additional fields to be recorded that don't exist in the struct. The fields take Strings, integers, and boolean literals. Check out the snippet below for an example of an `instrument` macro that also uses some `axum` web service methods to get the URI and request of a method. ```rust #[instrument(fields(http.uri = req.uri(), http.method = req.method()))] pub fn handle_request(req: http::Request) -> http::Response { // ... handle the request ... } ``` There are quite a lot of attributes that the instrument macro can take. You can find more about this on the documentation page [here.](https://docs.rs/tracing-attributes/latest/tracing_attributes/attr.instrument.html) ## Storing Logs with Tracing We can use `tracing-appender` to be able to store the logs that are produced by our tracing subscriber. The `tracing-appender` docs front page documentation provides this code snippet for providing a non-blocking writer. ```rust let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); tracing_subscriber::fmt() .with_writer(non_blocking) .init(); ``` This is for a writer that writes directly to `stdout`. What we want is a non-blocking writer that sends the logs to a file, which is also helpfully provided by the front page documentation. ```rust let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log"); let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt() .with_writer(non_blocking) .init(); ``` If you're interested in customizing your file log appender, you can manually invoke the `RollingFileAppender` builder method and then attach the methods you want to use on it: ```rust use tracing_appender::rolling::{Rotation, RollingFileAppender}; let file_appender = RollingFileAppender::builder() .rotation(Rotation::DAILY) // rotate log files once per day .filename_prefix("mywebservice.logging") // log files will have names like "mywebservice.logging.2024-01-09" .build("/var/log/mywebservice") // write log files to the '/var/log/mywebservice' directory .expect("failed to initialize rolling file appender"); let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt() .with_writer(non_blocking) .init(); ``` ## Tracing with Other Crates Despite tracing itself being very capable, it still requires extending to be used with other platforms - for example, OpenTelemetry or Honeycomb.io. Here are some crates you can use to beef up your tracing capabilities: ### tracing-opentelemetry Sometimes you need your logging to connect to an outside source. That's where `tracing-opentelemetry` comes in. By converting your tracing subscriber to use `opentelemetry`, you can then use one of the many crates that are supported by OpenTelemetry. This is quite useful for sending your logging to any telemetry-based app you'd like for further analysis! To get started, you'll need to install the following packages: ```bash cargo add opentelemetry cargo add opentelemetry_sdk cargo add opentelemetry_stdout ``` Now you can create your opentelemetry tracing layer and apply it to a subscriber: ```rust use opentelemetry::trace::TracerProvider as _; use opentelemetry_sdk::trace::TracerProvider; use opentelemetry_stdout as stdout; use tracing_subscriber::layer::SubscriberExt; fn main() { // Create a new OpenTelemetry trace pipeline that prints to stdout let provider = TracerProvider::builder() .with_simple_exporter(stdout::SpanExporter::default()) .build(); let tracer = provider.tracer("readme_example"); // Create a tracing layer with the configured tracer let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); tracing_subscriber::registry() .with(fmt::layer()) .with(telemetry) .init(); } ``` Note that `tracing-opentelemetry` will also allow you to use `opentelemetry` and `opentelemetry-sdk` compatible crates! You can pipe your traces to Datadog, for example, using the `opentelemetry_datadog` crate. ### tracing-flame Tracing can also be used to create a flamegraph! This is particularly useful if you want to find hot paths in your code or handler functions that use a particularly large amount of memory. To get started with `tracing-flame`, we just need to install it into our project: ```bash cargo add tracing-flame ``` Now you just need to initialize it: ```rust use std::{fs::File, io::BufWriter}; use tracing_flame::FlameLayer; use tracing_subscriber::{registry::Registry, prelude::*, fmt}; fn main() { let fmt_layer = fmt::Layer::default(); let (flame_layer, _guard) = FlameLayer::with_file("./tracing.folded").unwrap(); tracing_subscriber::registry() .with(fmt::layer()) .with(flame_layer) .init(); // ... the rest of your code } ``` Once you've set your tracing subscriber up and you've started receiving traces into your `tracing.folded` file, you can convert it to a flamegraph by installing the `inferno` package: ```bash cargo install inferno ``` Then you can use `inferno` to convert the contents of your file to a flamegraph or flamechart: ```bash # flamegraph cat tracing.folded | inferno-flamegraph > tracing-flamegraph.svg # flamechart cat tracing.folded | inferno-flamegraph --flamechart > tracing-flamechart.svg ``` ### spandoc Interested in spawning spans from doc comments? You can with `spandoc`! By adding triple forward slashes and a `SPANDOC:` marker, you can create spans without needing to explicitly make them! You can get started by installing spandoc into your project: ```bash cargo add spandoc ``` Now you can add it to your app. Note that the function must use the `spandoc` macro and any events must have the Spandoc comment above it: ```bash use spandoc::spandoc; use tracing::info; #[spandoc] fn main() { tracing_subscriber::fmt::init(); let local = 4; /// SPANDOC: Emit a tracing info event {?local} info!("event 1"); } ``` When run, this produces the following output: ```bash INFO main::comment{local=4 text=Emit a tracing info event}: scoped: event 1 ``` Note that there are a couple of limitations when using this crate, notably that the errors have a fixed name/target. You would ideally want to use this crate alongside other tracing crates to potentially provide more context. ## Finishing Up Thanks for reading this article! I hope you got something out of this. Getting started with the tracing crates can be intimidating, but they're very powerful when used correctly! See more below: - Check out this general guide for [logging in Rust.](https://www.shuttle.dev/blog/2023/09/20/logging-in-rust) - Check out our [getting started guide for Loco,](https://www.shuttle.dev/blog/2023/12/28/using-loco-rust-rails) the Rails on Rust framework. --- # What is Rust and Why Should You Use It? Source: https://www.shuttle.dev/blog/2024/01/04/what-is-rust Date: 4 January 2024 Author: josh Tags: rust, tutorial, guide This article provides a deep dive into the Rust programming language and benefits, cons as well as some companies using Rust in production. In today's world, Rust is becoming more and more popular. Yet, there are still a lot of people (and companies!) who misunderstand what Rust's value proposition is, or even what it is. In this article, we'll be talking about what Rust is and why it's a good programming language to enhance your skillset. ## What is Rust? In a nutshell, Rust is a multi-paradigm, general purpose language. Due to this, it's currently being used and experimented with in many domains. From the Rust website: > A language empowering everyone to build reliable and efficient software. The borrow checker and ownership model allows it to keep memory usage low. The type system, combined with Rust's traits, allows certain safety garuantees that would not normally be possible in other languages (like memory safety). Rust's package manager, Cargo, is also a formatter, linter and test runner. Rust has a small standard library that provides building blocks for your own tools. This removes the bureaucracy of getting things added to the standard library. ## A short summary of Rust's history up to now In 2006, Rust started out as a small side-project created by Graydon Hoare, a software developer for Mozilla at the time. In 2009 Mozilla, officially sponsored the project, reaching a stable release in May 2015. Since then many companies like Microsoft, Amazon, and Cloudflare have adopted Rust. In December 2022 the Linux kernel also started using Rust. The language has also enjoyed a notable amount of popularity on social media with many Rust developers using the crab emoji in their names. The growth of the primarily open source Rust ecosystem has become more and more rapid as time has gone on. The community is powerful, with many contributors now using Rust in their jobs. ## Reasons to Use Rust Below are some of the reasons that Rust will be able to help you become a better programmer - and not just because "Rust is fast and has fearless concurrency". While that's still true, we'd like to dig into some of the deeper details. ### Rust saves you money without trying Due to the low memory footage, it is almost assured that you are going to save money by using Rust. An average Java Spring or Python Django app can use many gigabytes of memory in large applications. This is particularly true if you have not yet optimised your application, or you're getting random memory leaks somewhere. With Rust, you are more than likely already ahead in memory consumption without any fancy tricks - but they are there if you need them! Don't just take our word for it. One company saved 87% on compute costs by switching from Ruby to Rust. You can find more about how they did it [here.](https://worldwithouteng.com/articles/i-saved-87-percent-on-compute-costs-by-switching-languages/) Even if you are not planning to use Rust directly in production, you can still use it in your applications. You can do this by using Rust modules then then use a Foreign Function Interface (FFI). While this needs some effort on your part (particularly if you're new to using FFI), it can also save money by allowing you to process much quicker. This also allows much more gradual adoption of Rust, which may be more to your liking. You can also of course use `wasm-bindgen` to convert Rust to WASM, which is perfect for any JavaScript-based applications. ### Error handling in Rust is awesome Rust is a language that forces you to deal with errors upfront. It's known by most Rust developers that you should avoid `.unwrap()` in production where possible. Yet, it's good to know that you can always come back and improve the error handling when you're ready. Take the following statement for instance: ```rust thing.use_function_that_can_fail().unwrap(); ``` You could convert this to use pattern-matching: ```rust let result = thing.use_function_that_can_fail(); match result { Ok(result) => result, Err(error) => println!("{error}"); } ``` This can be used to match a single error. Meanwhile, you could also use a question mark to propagate the error: ```rust thing.use_function_that_can_fail()?; ``` This attempts to turn the error into the error type returned by the function. meaning you can avoid unwraps and pattern matching! There are many packages to help you also improve your error handling like `eyre` and `anyhow`. You can find more about error handling [here.](https://www.shuttle.dev/blog/2022/06/30/error-handling) ### Rust syntax is ugly Yes, the syntax is ugly. There, I said it. However, getting used to new types of syntax (whether they're ugly or not!) is typically a good thing because it helps you gain a new perspective. It is a little bit similar to getting a new perspective after learning a new spoken language because of where words can originate from. Cultural ideas can inform the way that the spoken language is formed. In particular, pattern matching has led to things like `let-else` and `if-let`. These are two examples of syntax you wouldn't see in other mainstream programming languages. Let's have a quick look at these two in action: ```rust // using let-else let Ok(some_result) = function_that_can_fail() else { return Err("The function failed!"); } // using if-let if let Ok(result) = function_that_can_fail() { println!("The function succeeded!"); } ``` As you can see, fairly straightforward. It also avoids us having to directly use pattern matching and instead allows us to "match" against the arm because Rust is an expression-orientated language. This is enabled by the fact that Rust is a highly expressive language. By exposing ourselves to new ideas, we can learn new ways to do things that either make our code more readable or perform better. Many developers have even taken ideas from Rust (or functional programming in general) and implemented them in other languages: for example, the Result enum type. Rust can also end up becoming a gateway to other more functional programming ("FP") languages like OCaml. Although not recent, Rust's first compiler iterations were actually written in OCaml. Despite the two languages not officially being affiliated with each other, there are quite a few similarities between the two languages, notably Rust's use of sum types ("enums") and general algebraic data types. ## Where is Rust being used? Of course, this article isn't complete without a list of companies who are using Rust. Here are a few examples. ### Cloudflare It's not a huge secret that Cloudflare is using Rust. One Google search for "cloudflare rust" returns Oxy, their new proxy framework. They also wrote their own internal proxy called [Pingora](https://blog.cloudflare.com/how-we-built-pingora-the-proxy-that-connects-cloudflare-to-the-internet/) in Rust. A look on their [blog](https://blog.cloudflare.com/tag/rust) also returns many search results for Rust-related articles. The earliest article talking about Cloudflare development with Rust was [back in 2019.](https://blog.cloudflare.com/how-we-made-firewall-rules/) ### 1Password 1Password is not exactly shouting to the world that they use Rust. However, one look at their [GitHub organization page](https://github.com/1Password) shows that they are very much using it. They've also contributed to the Rust ecosystem by publishing crates. One of them is [passkey-rs,](https://github.com/1Password/passkey-rs) a collection of libraries that implement the [Webauthn Level 3](https://www.w3.org/TR/webauthn-3/) and [CTAP2](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html) standards. ### Daimler (Mercedes-Benz) Around 4 years ago, some internal source code from the Mercedez-Benz group was [leaked.](https://www.zdnet.com/article/mercedes-benz-onboard-logic-unit-olu-source-code-leaks-online/) Despite most of the code being C++, some Rust was notably included. Additionally, the organisation's first ever open-source contribution was [in Rust.](https://github.com/mercedes-benz/vehicle-information-service) It can be roughly inferred that they've probably continued to use it. This is a great sign for Rust in the automotive industry - even if it's not the majority of the code. ### Shuttle Our platform is also written primarily in Rust! We provision our runtime and infrastructure resources via Rust crates that use macros. Our platform also allows you to write your own resource macros. While we currently primarily support HTTP services, we are currently looking into upgrading our internal systems to allow for features like raw TCP, specifying your Rust toolchain and more. A more exhaustive list of companies using Rust can be found [here.](https://github.com/omarabid/rust-companies) ## Reasons to not use Rust Despite all of Rust's advantages, there are still some glaring edges if you're planning to adopt Rust. Here's a small list of things that can cause you issues while using Rust. ### The learning curve Once you actually learn Rust, it's pretty smooth sailing. However, up until then be prepared for some very uncomfortable bumps. In particular, the borrow checker (especially without proper care of scoping) can be a big one. Here are some quick tips you can use to improve your initial Rust learning experience: - Use references where you can. Functions can also take references as parameters. - When it comes to iterators, `.iter()` iterates over a vector of element references. If you want to iterate over owned elements, you want to use `.into_iter()`. - When it comes to error handling, `anyhow` is your friend. You can also use `thiserror` to easily extend the behaviour of your own error types. ### Small ecosystem Many areas of Rust are fairly robust, particularly when it comes to async and WebAssembly. However because Rust hasn't yet managed to achieve general mainstream adoption, some companies do not actively support Rust. Additionally, many crates are at the mercy of open-source labor. This isn't to say that they are poorly maintained. Due to obligations outside of open-source work though, sometimes crates can take a long time to be updated. This is something you will need to think about when adopting Rust; if it doesn't exist yet, you will probably need to write it yourself. ### Deploying Rust Deploying Rust web services is not particularly easy compared to other languages. This is primarily due to a lack of general Rust adoption. Normally, you would need to deploy your Rust program via Dockerfile, set up your own Nginx proxy and add SSL certs (among other things). It's a lot, especially if it's your first time doing it. Shuttle is aiming to solve this problem by allowing one-command deploys and letting you declare your infrastructure in your code. ## Finishing Up Thanks for reading! I hope this has helped you learn about whether or not Rust is the right choice for you. Interested in more? - Learn more about the best Rust web framework for you [here.](https://www.shuttle.dev/blog/2023/08/23/rust-web-framework-comparison) - Learn more about enums in Rust [here.](https://www.shuttle.dev/blog/2023/11/23/enums-in-rust) --- # Getting Started with Loco in Rust: Part 1 Source: https://www.shuttle.dev/blog/2023/12/28/using-loco-rust-rails Date: 28 December 2023 Author: josh Tags: rust, loco, tutorial, guide This article goes into a deep dive on getting started with Loco in Rust and how you can utilise its capabilities to speed up your productivity. In this article we're going to talk about how you can get started with Loco - a new Rust web framework that builds on Axum and takes inspiration from Ruby on Rails. We will cover getting started using controllers, migrations, middleware and static files. Following on from [our previous article](https://www.shuttle.dev/blog/2023/12/20/loco-rust-rails), we're going to get more indepth and experiment with creating a CRUD controller as well as middleware. ## Getting Started To get started, you will need to make sure to have Loco's CLI installed by using the following: ```bash cargo install loco-cli ``` Loco also uses `sea-orm-cli` to carry out database migrations. You can install it using the following: ```bash cargo install sea-orm-cli ``` Now we've installed all of the required packages, we can get initialise our project. We can get started by using `loco new` and then selecting the SaaS application which will give us an app with full functionality. The given name we will be using for the app will be `example_app`. Don't forget to `cd` into the project folder! When you're writing your API, you will probably want to spin up a local Docker database for database testing. In that case, you will wnat to use this docker command: ```bash $ docker run -d -p 5432:5432 -e POSTGRES_USER=loco -e POSTGRES_DB=example_app_development -e POSTGRES_PASSWORD="loco" postgres:15.3-alpine ``` ## Routing in Loco The first step that we need to take will be generating a "scaffold". This generates a controller, model and migration all at the same time. We can also add pre-generated field names and types beforehand, which you can find more about [here.](https://loco.rs/docs/the-app/models/#migrations) We can create our own scaffold below: ```bash cargo loco generate scaffold item name:string! description:string quantity:int! ``` This will generate a controller, model and migration for a table named `items` with: - A non-nullable name field - A nullable description field - A non-nullable quantity field The entities will also be generated so that you should not need to generate them yourself. Once the scaffold is done, you may notice that your Loco controller and other relevant parts have been added in `app.rs`, so there is no need to add it manually. Now we can go to our new controller file which should be located under `src/controllers/item.rs`. When opened, we should be greeted with something that looks like this: ```rust #![allow(clippy::unused_async)] use loco_rs::prelude::*; pub async fn echo(req_body: String) -> String { req_body } pub async fn hello(State(_ctx): State) -> Result { format::string("Hello world!") } pub fn routes() -> Routes { Routes::new() .prefix("item") .add("/", get(hello)) .add("/echo", post(echo)) ``` Now we can get to work on the routes for this! If you check the source code for what `AppContext` contains [here](https://github.com/loco-rs/loco/blob/68cb7598127893253478c4eddae0762e208aab6e/src/app.rs#L31), you should get this: ```rust #[derive(Clone)] #[allow(clippy::module_name_repetitions)] pub struct AppContext { /// The environment in which the application is running. pub environment: Environment, #[cfg(feature = "with-db")] /// A database connection used by the application. pub db: DatabaseConnection, /// An optional connection pool for Redis, for worker tasks pub redis: Option>, /// Configuration settings for the application pub config: Config, /// An optional email sender component that can be used to send email. pub mailer: Option, } ``` This means we only need to use `ctx.db` to access the database connection. Let's have a look at what a simple request for getting all of the `item` records from the database would look like: ```rust #![allow(clippy::unused_async)] use loco_rs::prelude::*; use crate::models::_entities::items::Entity as Item; use crate::models::_entities::items::Model as ItemModel; pub async fn hello(State(ctx): State) -> Result>> { let items = Item::find().all(&ctx.db).await?; format::json(items) } ``` Note that we need to import our models and entities from the `entities` folder. We can extend this to create a full CRUD controller: ```rust use crate::models::_entities::items::ActiveModel; pub async fn view_item_by_id(Path: Path, State(ctx): State) -> Result> { let item: Option = Item::find_by_id(id).one(db).await?; let item: item::Model = item.unwrap(); format::json(item) } pub async fn create_item( State(ctx): State, Json(item): Json ) -> Result { let item: ActiveModel = item.into(); item.insert(&ctx.db).await?; format::text("Created") } #[derive(Deserialize)] struct ItemQty { qty: i32 }; pub async fn update_item_quantity( State(ctx): State, Path(id): Path, Json(json): Json ) -> Result { let item: Option = Item::find_by_id(id).one(&ctx.db).await?; let mut item: item::ActiveModel = item.unwrap().into(); item.quantity = Set(json.qty); let updateditem = item.update(db).await?; format::text("Updated") } pub async fn delete_item( Path: Path, State(ctx): State ) -> Result { let item: Option = Item::find_by_id(id).one(db).await?; let item: item::Model = item.unwrap(); let res: DeleteResult = item.delete(db).await?; format::text("Deleted") } ``` Once you're done writing all of your routes, you need to make sure you attach them to your router in the `routes()` function for the controller file: ```rust pub fn routes() -> Routes { Routes::new() .prefix("items") .add("/", get(get_all_items).post(create_item)) .add("/:id", get(get_item_by_id).put(update_item_qty).delete(delete_item)) } ``` Congrats! You just created your first full CRUD router. To build onto this, let's add some validation for when you need to save an item. Loco itself re-exports the `validator` crate which allows you to validate that a struct meets certain requirements. Loco ties this in with `sea_orm` to be able to validate a struct before saving it to the database. A validator struct might look like this: ```rust #[derive(Debug, Validate, Deserialize)] pub struct ModelValidator { #[validate(range(min = 0, message = "Item must have at least a quantity of 0."))] pub quantity: i32, } ``` Now that this is done, we just need to implement `From` for the validator struct, and implement the `ActiveModelBehavior` trait for the ActiveModel. Note that because we have to always convert a `Model` to an `ActiveModel` before saving it, the `before_save` function will always kick in. ```rust impl From<&ActiveModel> for ModelValidator { fn from(value: &ActiveModel) -> Self { Self { quantity: *value.quantity.as_ref(), } } } #[async_trait::async_trait] impl ActiveModelBehavior for super::_entities::items::ActiveModel { async fn before_save(self, db: &C, insert: bool) -> Result where C: ConnectionTrait, { { self.validate()?; Ok(self) } } } ``` You can also write an `impl` for the `Model` itself to extend its behavior! Note that you will need to pass in the database connection as a function parameter. Check out this function for finding users by emails (can be found in the pre-generated `users::Model` model): ```rust // src/models/users.rs pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult let user = users::Entity::find() .filter(users::Column::Email.eq(email)) .one(db) .await?; user.ok_or_else(|| ModelError::EntityNotFound) } ``` ## Middleware in Loco Middleware in Loco can be implemented in a few ways: 1. Implementing `axum::FromRequestParts` (or `FromRequest`) for a given struct or enum 2. Implmenting the optional `after_routes()` method in the Hooks trait (in `app.rs`) Implementing `FromRequest` is probably the easiest way to go about being able to implement middleware for select routes, while `after_routes()` is likely much better for globally implementing a middleware (for example, a timeout). ### Using FromRequestParts Although `FromRequestParts` (and `FromRequest` respectively) look tricky to implement, you can make it substantially easier on yourself by remembering that the state itself just needs to implement `Send + Sync` - which means you can use `AppContext` with it! Check out the code snippet below for an overall implementation of how you would write something that implements `FromRequestParts`. ```rust #[derive(Deserialize)] pub struct MyMiddlewareState(String); #[async_trait::async_trait] impl FromRequestParts for MyMiddlewareState { type Rejection = ApiError; async fn from_request_parts(parts: &mut Parts, _state: &AppContext) -> Result { let string = "Hello world!".to_string(); if string != *"Hello world!" { return Err(ApiError::Unknown); } Ok(MyMiddlewareState(string)) } } enum ApiError { Unknown } impl IntoResponse for ApiError { // ... implement IntoResponse for ApiError for it to return a HTTP response } ``` Of course, in a real-world application this will be much more extensive than assigning "Hello world!" to a variable and then returning the struct. ### Using Global Middleware As mentioned before, you can also use the `after_routes()` function in the `Hooks` trait. The function itself looks like this: ```rust async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result { Ok(router) } ``` Because the Axum router gets passed in as a parameter, you can attach any kind of Axum middleware or layer you want. This also means you can add things like a `tower-http` service layer if you'd like! Let's have a look at adding a timeout layer, which will stop any slow loris attacks. To do this, we'll need to add `tower-http` with the `timeout` feature: ```bash cargo add tower-http -F timeout ``` Then we just need to add the layer: ```rust async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result { let router = router.layer(TimeoutLayer::new(Duration::from_secs(10))); Ok(router) } ``` Now any requests that last longer than 10 seconds will automatically be aborted with a `408 Request Timeout` response. Pretty nifty! We can also implement our own Tower service, which you can find more about [here.](https://docs.rs/tower/latest/tower/trait.Service.html) ## Serving a Frontend in Loco Serving a frontend with Loco is as easy as going into the `frontend` folder, then running `npm i` to install the dependencies. Then you run `npm run build` to build your application. When you use `cargo loco start` and go to localhost:8000 you should see the main screen for the Loco.rs homepage but blank. Note that the main frontend approach uses React. You can switch this out for any other framework you like. The only thing you need to do is to make sure the frontend you are serving matches the config file under the `static` section (the `folder` key, with the default being `/frontend/dist`). If you're a Svelte or Vue user, or want to use Leptos or Dioxus you can freely switch around! If you are not experienced in any of the aforementioned frameworks, you can also use raw HTML/CSS/JS. This may be particularly more favourable if you either don't have a lot of HTML/CSS you need to serve. ## Deploying Loco Loco have provided their own commands for generating a deployment. You can run it by using `cargo loco generate deployment`. It will then generate a Dockerfile or Shuttle deployment depending on what you select. If you're deploying with Shuttle, don't forget you can add your frontend assets by going to your Shuttle.toml file and then adding `frontend/dist` to your assets key: ```toml name = "" assets = ["frontend/dist/*"] ``` Then you can run the following to deploy your application (don't forget to install `cargo-shuttle`): ```bash shuttle deploy ``` We are planning to release a native database integration with Loco! Stay tuned for part 2 where we will go into further detail about how you can build out your dream web application. ## Finishing Up Thanks for reading! We hope this Rust Loco guide has helped you. If you're looking to get started with Rust web development, now is a better time than ever to do so. Interested in more? - Read about our getting started with Axum article [here](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - Learn about how you can implement OAuth for your application [here](https://www.shuttle.dev/blog/2023/08/30/using-oauth-with-axum) --- # Introducing Loco: The Rails of Rust Source: https://www.shuttle.dev/blog/2023/12/20/loco-rust-rails Date: 20 December 2023 Author: josh Tags: rust, loco, tutorial, guide This article talks about how you can deploy Loco.rs to Shuttle, as well as an in-depth review of what the framework offers. Although Ruby on Rails is not as popular as it used to be, in its prime, it was a force to be reckoned with. Many successful businesses were built from it - Airbnb and Shopify being two of many big names coming out of this, although more recently Shopify has started experimenting with other languages and late last year announced that they would be [officially supporting Rust.](https://shopify.engineering/shopify-rust-systems-programming) This has led to many frameworks attempting to emulate the Rails philosophy - Loco.rs being no different. In this case, however, it aims to solve a long-standing issue within the Rust web backend framework in terms of there being no truly batteries-included Rust framework. Let's talk about it. ## Why was Ruby on Rails popular? A quick refresher Ruby on Rails was popular because it is a framework that does all the heavy lifting for you and abstracts away a lot of the heavy lifting - which means there is a very short gap between thinking of the business logic for an idea and time to full productivity. This is a great thing for a few reasons, especially in web development: you can ship faster without needing to do any of the boilerplate, you can rely on the framework to do all of the difficult low-level things for you and you don't need to be necessarily fluent in Ruby to use it (although it helps massively if you are!). This is something that a lot of web developers resonate with, as evidenced by the huge number of developers who use Laravel, a PHP framework that is very similar to Ruby on Rails. It achieves these things by gating everything behind a command-line interface: you use the command line to start the web service itself, you use it for migrations, and job processing as well as creating new controllers, models, and more. For example, you can generate a database model by using `rails generate model test` which will generate a model. You can then create a route controller by using `rails generate controller test`, which will generate a controller called `TestController` - you can do the same for migrations by using `rake generate migration`. This is somewhat at odds when it comes to Rust which is why Loco.rs is interesting: Rust is a language that allows you to get into the meat of the matter when it comes to low-level details, which means that it tends to attract programmers who don't mind doing the extra work because they would rather things be either implemented to their standard or because they want to understand how everything works so that when something breaks, they know how to fix it. In addition, Loco is not itself a standalone framework - currently it uses `axum` under the hood, alongside `sidekiq-rs` for job processing and `sea-orm` for migrations. ## Getting Started with Loco To get started with the Rust Loco crate, you need to use their CLI which you can install by using the following: ```bash cargo install loco-cli ``` You can start a new project by using `loco new` - it'll ask you what the name of your app is and then what kind of app you want. For this article, we'll be talking about the full Rust SaaS starter application. ## Routing in Loco Although Loco uses `axum` under the hood, it abstracts some things away into config files that you can find in the `config` folder. The Axum service that gets run by the application implements the `Hooks` trait from `loco_cli`, which requires several functions to use - going to `src/app.rs` shows that we have functions for registering routes, getting the app name, connecting workers and registering tasks, truncate tables and seed data into the database. We can also add extra functions to the router that get hooked into the CLI as well - `after_routes()` which is for adding things like middleware, and `before_run()` which allows you to carry out operations before your application itself starts. Note that any commands we use through the project CLI to generate things will automatically be appended to the `app.rs` file - no need to do it yourself! To add a controller, we need to run `cargo loco generate controller test` from the project root which generates a controller called `test` and simultaneously adds a new file in the `controllers` folder. Then we can create any routes we need to and append them to the router in the same file, and it'll automatically be added to the application - no further work required! Your new controller should look like something like this: ```rust #![allow(clippy::unused_async)] use loco_rs::prelude::*; pub async fn echo(req_body: String) -> String { req_body } pub async fn hello(State(_ctx): State) -> Result { // do something with context (database, etc) format::text("hello") } pub fn routes() -> Routes { Routes::new() .prefix("test") .add("/", get(hello)) .add("/echo", post(echo)) } ``` Now you can add any routes you want to this file and it will be put under this controller when it's added to the `routes()` function in the file. Route-wise, this means it'll be all under the same route. You can access the database connection from the provided `State` - unlike in Axum normally, you don't need to create this yourself. The great thing about Loco's routing is that everything you know from using Axum can be applied here - so if you know how to write your own extractors, write middleware, and other things this can all be used in Loco since it essentially builds on top of Axum. Once you've finished adding all the controllers and routes you want, you can use `cargo loco routes` to display all of the routes your application currently has. ## Models in Loco Models in Loco represent the database models used by `sea_orm`. To get started, you'll want to run the following: ```bash cargo loco generate model ``` This will then generate a model that you can use in your application. You can also initialise with extra fields to generate a full model: ```bash cargo loco generate model movies title:string rating:int ``` Note that if you want to initialise with extra fields, you will want to check the [reference docs](https://loco.rs/docs/the-app/models/) so you can find what fields you need to use. Once you're done adding all the models you need to, you can simply run the following two commands to get back the migrations and entities required: ```bash cargo loco db migrate cargo loco db entities ``` When you generate a blank model, when you go to the model file you will probably find something that looks like this: ```rust use sea_orm::entity::prelude::*; use super::_entities::notes::ActiveModel; impl ActiveModelBehavior for ActiveModel { // extend activemodel below (keep comment for generators) } ``` When we use this model in our controller, typically speaking we won't reference the struct that holds the model itself - instead we reference the `ActiveModel` or `Entity`/`Model` - a blank model file looks like this: ```rust use sea_orm::entity::prelude::{ActiveModelBehavior}; use super::_entities::notes::ActiveModel; impl ActiveModelBehavior for ActiveModel { // extend activemodel below (keep comment for generators) } ``` We can extend the behaviour of our ActiveModel by adding a `before_save()` method as mentioned before, like so: ```rust use sea_orm::entity::prelude::{ActiveModelBehavior}; use super::_entities::notes::ActiveModel; #[async_trait::async_trait] impl ActiveModelBehavior for ActiveModel { // extend activemodel below (keep comment for generators) async fn before_save(self, _db: &C, insert: bool) -> Result where C: ConnectionTrait, { println!("This is happening before we save something!"); Ok(self) } } ``` The `ActiveModelBehaviour` trait implementation (from `sea_orm`) allows us to define behaviour for an `ActiveModel` - more specifically, we can add methods for before and after saving a model, as well as before and after deleting a model. We can also extend the behaviour of our model by adding extra methods to it: ```rust impl super::_entities::users::Model { // .. your own methods } ``` Now we can use it in a handler function by loading the item from the database - then we can do whatever we need to with the data: ```rust async fn load_item(ctx: &AppContext, id: i32) -> Result { let item = Entity::find_by_id(id).one(&ctx.db).await?; item.ok_or_else(|| Error::NotFound) } pub async fn update( Path(id): Path, State(ctx): State, Json(params): Json, ) -> Result> { // use sea_orm to load an item based on the id let item = load_item(&ctx, id).await?; // turn the item into an ActiveModel that we can then use let mut item = item.into_active_model(); // update the parameters of the current item with the new properties params.update(&mut item); // feed the new item back into the database let item = item.update(&ctx.db).await?; // return the updated item format::json(item) } ``` However - that isn't all that Loco.rs has to offer. We can also use the `loco_rs` Validator struct to be able to verify a new model before needing to do anything with it! A use case for this, for example, might be if we needed to check if an email is a valid email. You can check this out below: ```rust [derive(Debug, Validate, Deserialize)] pub struct ModelValidator { #[validate(length(min = 2, message = "Name must be at least 2 characters long."))] pub name: String, #[validate(custom = "validation::is_valid_email")] pub email: String, } impl From<&ActiveModel> for ModelValidator { fn from(value: &ActiveModel) -> Self { Self { name: value.name.as_ref().to_string(), email: value.email.as_ref().to_string(), } } } ``` ## Job Processing in Loco Like with everything else in Loco, you can also generate workers and tasks via the CLI. Running `cargo loco generate task` or `cargo loco generate worker` will let you generate a task or a worker at will. Under the hood, Loco uses `sidekiq-rs` to do job processing - which is a Rust re-implementation of its Ruby counterpart, `sidekiq.rb`. Once the worker is generated, you want to go to the `workers` folder and check out the file you made - it will have a struct for the worker itself, a struct that holds the arguments that the worker will take, an implementation of the `AppWorker` trait for the worker and then an async trait implementation that lets the worker do something. You can then run it like so: ```rust ReportWorkerWorker::perform ( &boot.app_context, ReportWorkerWorkerArgs {} ) .await .unwrap(); ``` As you can see, we don't need to initialise the struct to use it - we can call the method from the struct directly and it will work assuming the arguments are valid - as you can see here, no arguments are required since the args struct does not have any fields. ## Deploying Loco Currently, Loco.rs allows you to generate a deployment by using the following comamnd: ```bash cargo loco generate deployment ``` This lets you choose between Docker and Shuttle. When choosing Docker it will generate a Dockerfile that you can use to deploy anywhere, but when you pick Shuttle it will automatically generate everything you need for a Shuttle deployment - no further work required! You can then use the Shuttle CLI to start a new project and deploy it: ```bash // note that if you want to avoid using the --name flag // you should use the name key in Shuttle.toml shuttle deploy --name ``` ## Finishing Up Thanks for reading! Loco is a great framework that shows a lot of promise, and is growing very quickly. Building a Rest API in Rust has never been made easier! Interested in more? Check out the full tour of Loco [here.](https://loco.rs/docs/getting-started/tour/) Check out their discussions [here.](https://github.com/loco-rs/loco/discussions) --- # Getting Started with Actix Web in Rust Source: https://www.shuttle.dev/blog/2023/12/15/using-actix-rust Date: 15 December 2023 Author: josh Tags: rust, actix-web, actix, tutorial, guide This article talks about how you can use Actix Web to write a web application, covering routing, middleware, static files and databases. To this day, [Actix Web](https://actix.rs/) remains an extremely formidable competitor in the Rust web backend ecosystem. It's no wonder that it's lasted so long; despite any impact that previous events may have had on it, it's still going strong and is one of the most recommended web frameworks in Rust. Originally based on the actor framework of the same name (`actix`), it has since moved away, and `actix` is now only really used for websocket endpoints. This article will primarily be concerning v4.4. ## Getting Started with Actix Web To get started, you want to use `cargo init example-api` to generate your project, `cd` into the folder, and then use the following to add the `actix-web` crate to your project: ```rust cargo add actix-web ``` Now you're pretty much ready to start! If you'd like to copy the boilerplate for getting straight to writing your app, here it is: ```rust use actix_web::{web, App, HttpServer, Responder}; #[get("/")] async fn index() -> impl Responder { "Hello world!" } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service( // prefixes all resources and routes attached to it... web::scope("/") // ...so this handles requests for `GET /app/index.html` .route("/", web::get().to(index)), ) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` ## Routing in Actix Web When using [Actix Web](https://actix.rs/), mostly any function that returns the `actix_web::Responder` trait can be used as a route. See below for a basic Hello World example: ```rust #[get("/")] async fn index() -> impl Responder { "Hello world!" } ``` This handler function can then be fed into an `actix_web::App` which then gets passed in as a parameter to a `HttpServer`: ```rust #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service( // prefixes all resources and routes attached to it... web::scope("/") // ...so this handles requests for the base route .route("/index.html", web::get().to(index)), ) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` Now whenever you go to `/index.html`, it should return "Hello world!". However, you may find this approach a little bit lacking if you want to create multiple mini-router types and then merge them all into the app at the end. In this case, you will want the `ServiceConfig` type, which you can also write like this: ```rust use actix_web::{web, App, HttpResponse}; // this function could be located in different module fn config(cfg: &mut web::ServiceConfig) { cfg.service(web::resource("/test") .route(web::get().to(|| HttpResponse::Ok())) .route(web::head().to(|| HttpResponse::MethodNotAllowed())) ); } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().configure(config) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` Extractors in Actix Web are exactly that: type-safe request implementations that, when passed into a handler function, will attempt to extract the relevant data from the request for the handler function. For example, the `actix_web::web::Json` extractor will attempt to extract JSON from the request body. To successfully deserialize JSON successfully however, you need to use the `serde` crate - ideally with the `derive` function that adds automatic Deserialize and Serialize derive macros for your structs. You can install serde by executing the following command: ```rust cargo add serde -F derive ``` Now you can use it as a derive macro like this: ```rust use actix_web::web; use serde::Deserialize; #[derive(Deserialize)] struct Info { username: String, } // deserialize `Info` from request's body #[post("/submit")] async fn submit(info: web::Json) -> String { format!("Welcome {}!", info.username) } ``` Actix Web also has support for paths, queries and forms. You'll also need to use `serde` here as well - although with paths, you will additionally want to use the Actix Web routing macro to declare where exactly the path parameters are. We can find examples of all 3 of these in action below: ```rust #[derive(Deserialize)] struct Info { username: String, } // extract path info using serde #[get("/users/{username}")] // <- define path parameters async fn index(info: web::Path) -> String { format!("Welcome {}!", info.username) } // data is passed in here through query parameters in the URL // for example, google.com/?hello=world #[get("/")] async fn index(info: web::Query) -> String { format!("Welcome {}!", info.username) } // data is passed into the Form extractor through a HTML Form element #[post("/")] async fn index(form: web::Form) -> actix_web::Result { Ok(format!("Welcome {}!", form.username)) } ``` Interested in writing your own extractor? You can do that! Writing your own extractor simply requires that you implement the `FromRequest` trait. Check out this code for a [HTTP header extractor](https://docs.rs/actix-web/4/actix_web/web/struct.Header.html) that shows you exactly how it's done: ```rust use actix_web::dev::Payload; use actix_web::{FromRequest, http::header::Header as ParseHeader, HttpRequest, error::ParseError }; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Header(pub T); impl FromRequest for Header where T: ParseHeader, { type Error = ParseError; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { match T::parse(req) { Ok(header) => ok(Header(header)), Err(e) => err(e), } } } ``` Note that the `T: ParseHeader` trait bound is specific to this trait implementation because in order for a header to be a valid header, it needs to be able to be parsed successfully as a header, with the error implementing `actix_web::error::Error`. Although we also have `extract` as a provided method, `from_request` is the only required method to be implemented here, which returns `Self::Future`. That is to say, we need to return a result that's ready to be awaited - you can find more about the Ready struct [here](https://doc.rust-lang.org/std/future/struct.Ready.html). Other extractors, like the JSON extractor, also allow you to change their configuration - you can find out more about it in [this docs page](https://docs.rs/actix-web/4/actix_web/trait.FromRequest.html). Generally speaking, responses only need to implement the `actix_web::Responder` trait to be able to respond. Although there is a [broad range of implementations already](https://docs.rs/actix-web/4/actix_web/trait.Responder.html#implementations-1) when it comes to actual response types so you should not generally need to implement your own types, there can be specific use cases where this is helpful; for example, being able to document all the types of responses that your application may have. ## Adding a Database Normally when setting up a database, you might need to set up your database connection: ```rust use sqlx::PgPoolOptions; #[actix_web::main] async fn main() -> std::io::Result<()> { let dbconnection = PgPoolOptions::new() .max_connections(5) .connect(r#""#).await; //... rest of your code } ``` You would then need to provision your own Postgres instance, whether installed locally on your computer, provisioned through Docker or something else. However, with Shuttle we can eliminate this as the runtime provisions the database for you: ```rust use actix_web::{get, web::ServiceConfig}; use shuttle_actix_web::ShuttleActixWeb; #[shuttle_runtime::main] async fn actixweb( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> ShuttleActixWeb { let state = AppState { pool }; // .. the rest of your code } ``` Locally this is done through Docker, but in deployment there is an overarching process that does this for you! No extra work required. We also have an AWS RDS database offering that requires zero AWS knowledge to set up - visit [here](https://www.shuttle.dev/pricing) to find out more. ## App State in Actix Web Routing is great and all (as well as adding databases being pretty easy!) but when you need to store variables, you may be wanting to look for something that lets you store and use them across your application. This is where shared mutable state comes in: you declare it while building your service across your whole application, then you can use it as an extractor in your handler functions. For example, you might need to share a database pool, a counter or a shared hashmap of websocket subscribers. You can declare and use state like this: ```rust use sqlx::PgPool; #[derive(Clone)] struct AppState { db: PgPool } #[get("/")] async fn index(data: web::Data) -> String { let res = sqlx::query("SELECT 'Hello World!'").fetch_all(&data.db).await.unwrap(); format!("{res}") } ``` You can then implement it like this: ```rust #[actix_web::main] async fn main() -> std::io::Result<()> { let db = connect_to_db(); let state = web::Data::new(AppState { db }); HttpServer::new(move || { // move app state into the closure App::new() .app_data(state.clone()) // <- register the created data .route("/", web::get().to(index)) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` ## Middleware in Actix Web Within [Actix Web](https://actix.rs/), middleware is used as a medium for being able to add general functionality to a (set of) route(s) by taking the request before the handler function runs, carrying out some operations, running the actual handler function itself and then the middleware does additional processing (if required). By default, Actix Web has several default middlewares that we can use, including logging, path normalisation, access external services and modifying application state (through the `ServiceRequest` type). See below for an example of how to implement a default Logger middleware: ```rust use actix_web::{middleware::Logger, App}; #[actix_web::main] async fn main() -> std::io::Result<()> { // access logs are printed with the INFO level so ensure it is enabled by default env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); let app = App::new() .wrap(Logger::default()); // ... rest of your code } ``` Additionally, you can also write your own middleware in Actix Web! For many use cases, we can use the handy `middleware::from_fn` helper from the sister crate `actix-web-lab` (which will be promoted to `actix-web` itself in an upcoming release). For example, printing a messages at different parts of the request handling flow like this: ```rust use actix_web::{body::MessageBody, dev::{ServiceRequest, ServiceResponse}}; use actix_web_lab::middleware::{from_fn, Next}; async fn print_before_and_after_handler( req: ServiceRequest, next: Next, ) -> Result, Error> { println!("Hi from start. You requested: {}", req.path()); let res = next.call(req).await?; println!("Hi from response"); Ok(res) } let app = App::new() .wrap(from_fn(print_before_and_after_handler)) .route( "/", web::get().to(|| async { "Hello from handler!" }), ); ``` To be able to write more complex middleware, we actually need to implement two traits - `Service` which is for implementing the actual middleware itself as well as `Transform` which is the required for the builder for the actual Service that handles requests (in terms of when we're building our service, we will actually use the builder, not the middleware! The outer service will automatically call the middleware upon detecting a HTTP request). For an actual middleware implementation, let's have a look at writing a middleware that simply prints messages. We can create the builder for the middleware by implementing the `Transform` trait: ```rust use std::{future::{ready, Ready, Future}, pin::Pin}; use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, web, Error, }; pub struct SayHi; // `S` - type of the next service // `B` - type of response's body impl Transform for SayHi where S: Service, Error = Error>, S::Future: 'static, B: 'static, { // setting up the types for the middleware to work type Response = ServiceResponse; type Error = Error; type InitError = (); type Transform = SayHiMiddleware; type Future = Ready>; // this immediately returns the middleware fn new_transform(&self, service: S) -> Self::Future { ready(Ok(SayHiMiddleware { service })) } } ``` Now we can write the middleware itself! Internally the middleware must implement a generic type - which then gets declared in the `Service` trait. Note that we manually re-implement a type from `futures_util` called `LocalBoxFuture` - that is to say, a future that doesn't require the `Send` trait and is safe to use because it implements `Unpin` on dereferencing, which automatically cancels any previous thread-safety guarantees. ```rust pub struct SayHiMiddleware { /// The next service to call service: S, } // This future doesn't have the requirement of being `Send`. // See: futures_util::future::LocalBoxFuture type LocalBoxFuture = Pin + 'static>>; // `S`: type of the wrapped service // `B`: type of the body - try to be generic over the body where possible impl Service for SayHiMiddleware where S: Service, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture>; // This service is ready when its next service is ready forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { println!("Hi from start. You requested: {}", req.path()); // A more complex middleware, could return an error or an early response here. // we do not immediately await this, which means nothing happens // this future gets moved into a Box let fut = self.service.call(req); Box::pin(async move { // this future gets awaited now let res = fut.await?; // we can now do any work we need to after the request println!("Hi from response"); Ok(res) }) } } ``` Now that we've written our middleware, we can now add it to our App: ```rust #[actix_web::main] async fn main() -> std::io::Result<()> { let app = App::new() .wrap(SayHi); // ... rest of your code } ``` ## Static Files in Actix Web Plain, no-frills static file serving in [Actix Web](https://actix.rs/) is done through the `actix_files` crate - to add it, you simply need to add it through Cargo like so: ```bash cargo add actix-files ``` Setting up a route for static file serving would look like this: ```rust use actix_files::NamedFile; use actix_web::{HttpRequest, Result}; use std::path::PathBuf; #[get("/")] async fn index(req: HttpRequest) -> Result { let path: PathBuf = req.match_info().query("filename").parse().unwrap(); Ok(NamedFile::open(path)?) } ``` This route allows us to serve any file that can be found and matches the filename - for example, if we have a base route that serves this route, if we then run our app and go to `/index.html`, the route will try to look for a file named `index.html` in the project root. Note that you should _not_ under any circumstances try and use a path tail with `.*` to return a `NamedFile` - this has serious security implications and will open your web service up to path traversal! This is documented in the Actix Web docs [here](https://actix.rs/docs/static-files), and you can find more about path traversal attacks [here](https://owasp.org/www-community/attacks/Path_Traversal). As a guard against this, you can attempt to validate the path file is correct or is not attempting to traverse outside of the intended folder by using `std::fs::canoncalize`). However, this is a bit clumsy when you need to serve multiple files - particularly if you need to serve a folder of HTML files, for instance. To be able to serve a folder of files from your web service, the best way to do this would be to use `actix_files::Files` and attach it to your `App`: ```rust use actix_files as fs; use actix_web::{App, HttpServer}; #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service( fs::Files::new("/static", ".") .use_last_modified(true), ) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` Note that you can also augment the `Files` struct with several options like showing the file directory itself at the base route (for the file service) and allowing hidden files, which you can find more about [here.](https://docs.rs/actix-files/latest/actix_files/struct.Files.html#method.show_files_listing). Additionally, we can also use the power of HTML templating with [`askama`](https://github.com/djc/askama) to supercharge our HTML file serving! We can get started like so: ```rust cargo add askama askama-actix-web -F askama/with-actix-web ``` This adds [`askama`](https://github.com/djc/askama) itself as well as the `Responder` trait implementation for the `askama::Template` type. [`askama`](https://github.com/djc/askama) expects your files to be in a subfolder of the project root called `templates` by default, so let's create the folder and then create an `index.html` file with the following HTML code in: ```html Hello, {{name}}! ``` To use Askama in our app, we need to declare a struct that uses the `Template` derive macro and use askama's `template` macro to point the struct to the file that we want it to use: ```rust #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate<'a> { name: &'a str } #[get("/")] async fn index_route() -> impl Responder { IndexTemplate { name: "Shuttle" } } ``` Then we can add it as a regular handler function in our Actix Web service and we're good to go! When you go to a path that returns `index_route`, you should see "Hello, Shuttle!" as the HTML response. Interested in learning more about Askama? We have a Shuttle Launchpad newsletter issue that talks about this! You can find it [here](https://www.shuttle.dev/launchpad/issues/2023-10-17-issue-10-Serving-HTML). ## Deploying Deployment with Rust backend programs, in general, can be less than ideal due to having to use Dockerfiles, although if you are experienced with Docker already this may not be such an issue for you - particularly if you are using `cargo-chef`. However, if you're using Shuttle you can just use `shuttle deploy` and you're done already. No setup is required. ## Finishing Up Thanks for reading! Actix Web is a strong framework that you can use to boost your Rust portfolio and is a great framework to do a deep dive into Rust with if you're looking to build your first Rust API. Interested in more? - Check out this [comprehensive primer for Rust error handling](https://www.shuttle.dev/blog/2022/06/30/error-handling). - Compare how Actix Web measures up against other frameworks in [our framework comparison article](https://www.shuttle.dev/blog/2023/08/23/rust-web-framework-comparison). --- # Getting Started with Rocket in Rust Source: https://www.shuttle.dev/blog/2023/12/13/using-rocket-rust Date: 13 December 2023 Author: josh Tags: rust, rocket, tutorial, guide This article talks about how you can use Rocket to write a web application, covering routing, middleware, static files and databases. Although Rust is often perceived as being an intimidating language to learn, there have been many advancements in terms of making Rust accessible to everybody - particularly in the Rust backend web framework space. With Rocket v0.5 being released, now is a better time than ever to try out the iconic Rust web framework that was previously somewhat in limbo because of organisational issues but is now running at full throttle, with Rocket now being managed by the [Rocket Web Foundation](https://rwf2.org/). We'll be talking primarily about how you can get started with Rocket 0.5, but we will include the major notes from the 0.4 to 0.5 migration guide if you need to upgrade. ## Migrating to Rocket 0.5 The main things you need to know: - `rocket_contrib` is deprecated - you need to enable features in Rocket itself (and use `rocket_dyn_templates` and `rocket_sync_db_pools`/`rocket_db_pools` as required) - Rocket is now primarily async and now re-exports `tokio` if you need anything Tokio-related - You need to use `#[rocket::async_trait]` for trait implementations now - Query parameters now use `FromForm`! - Server Sent Events and Websockets are now officially supported. The above listed points are but a highlight - there have been an extremely significant number of changes in the upgrade to Rocket 0.5. If you're looking to migrate, you can check out the docs [here.](https://rocket.rs/v0.5/guide/upgrading/) ## Getting Started You can get started by creating a new web service with `cargo init example-rocket-api`, then cd'ing into the newly generated folder and then using adding `rocket`: ```bash cargo add rocket ``` Rocket 0.5 internally uses Tokio (as noted in the release changes). However, you don't need to add it to your project specifically to be able to write a Rust Rocket API and it is also re-exported through Rocket so you can use `rocket::tokio` if you need anything from the `tokio` crate (although if you need a specific feature that can't be found through Rocket's version, you may need to add the `tokio` crate to your web service separately). ## Routing ### Requests When it comes to routing in Rocket, you need to use macros for your routes. Like many other features in Rocket, the usage of macros has propagated out to most other frameworks and would otherwise require the use of trait bounds like Axum. Check out the following code below: ```rust #[get("/")] fn index() -> &'static str { "Hello, world!" } ``` We can attach a route to a router like this by using `rocket::build()` and then `.mount()`: ```rust let rocket = rocket::build().mount("/hello", routes![index]); ``` This essentially means that when you load up your web service and go to the `/hello` route in the browser, it should also print "Hello world!" - note that it is **based on the route where it is mounted**. You can also add additional routes to the `routes!` macro - so if you have multiple sub-routes that you want to host under main route, you can do that. Deserializing JSON in Rocket, similarly to other web frameworks in Rust, involves using `serde` to make the data compatible with (de)serialization. You can add the serde functionality to your web service by adding it as a crate with the `derive` feature: ```bash cargo add serde -F derive ``` Then adding it as an argument to the function: ```rust use rocket::serde::json::Json; use serde::Deserialize; #[derive(Deserialize)] #[serde(crate = "rocket::serde")] struct Task<'r> { description: &'r str, complete: bool } #[post("/todo", data = "")] fn new(task: Json>) { /* .. */ } ``` You can find out more about using JSON data with Rocket [here.](https://rocket.rs/v0.5/guide/requests/#json) Are you using forms? You can also use those! Rocket has a [huge](https://rocket.rs/v0.5/guide/requests/#forms) section detailing everything you can do with forms, but we'll cover the highlights and essentials you need to become good at using forms in Rocket. You can get started with forms by using the `FromForm` derive macro, then adding it as an argument to a handler function: ```rust use rocket::form::Form; #[derive(FromForm)] struct Task<'r> { complete: bool, r#type: &'r str, } #[post("/todo", data = "")] fn new(task: Form>) { /* .. */ } ``` It should be noted that by default, **missing, duplicate or extra fields will be allowed by default** - missing fields simply get filled with defaults and duplicates/extras are ignored. To stop this behavior, you can use the `Strict` type: ```rust use rocket::form::Strict; #[derive(FromForm)] struct Task<'r> { complete: Strict, r#type: &'r str, } ``` You can also just add `Strict` to the parameters when writing your handler function: ```rust #[post("/todo", data = "")] fn new(task: Form>>) { /* .. */ } ``` Rocket is miles ahead of other web frameworks when it comes to forms, particularly because multipart forms are handled similarly to regular forms and do not require any extra work. For comparison: in Axum for example, you need to enable the `multipart` feature and iterate through every single multipart field manually in the function handler, which can be quite ugly. You can also nest your form structs: ```rust #[derive(FromForm)] struct MyForm<'r> { owner: Person<'r>, pet: Pet<'r>, } #[derive(FromForm)] struct Person<'r> { name: &'r str } #[derive(FromForm)] struct Pet<'r> { name: &'r str, #[field(validate = eq(true))] good_pet: bool, } ``` If you're looking to separate your form structs and then combine them together and separately for certain forms, this would be an ideal way to do it. You can also serve paths by simply changing what you put into the handler function macro: ```rust #[get("/hello/")] fn hello(name: &str) -> String { format!("Hello, {}!", name) } ``` ### Rocket Request Guards Request guards in Rocket are types that represent specific validation policies, which can be passed into a handler. They are one of the strongest tools in Rocket as they allow you to split your validation into several functions instead of having one large function which handles all of the validation . See the following code below: ```rust use rocket::response::Redirect; #[get("/login")] fn login() -> Template { /* .. */ } #[get("/admin")] fn admin_panel(admin: AdminUser) -> &'static str { "Hello, administrator. This is the admin panel!" } #[get("/admin", rank = 2)] fn admin_panel_user(user: User) -> &'static str { "Sorry, you must be an administrator to access this page." } #[get("/admin", rank = 3)] fn admin_panel_redirect() -> Redirect { Redirect::to(uri!(login)) } ``` As you can see, we have an "Admin User" as a request guard for the admin route. If the AdminUser request guard cannot be satisfied, Rocket will then attempt to route the user to "/admin" as a user and return a string about not being able to access the page unless the user is an admin; if both of those fail, the user will then instead be redirected to the login page. To be able to implement your own request guards, your type must implement the `FromRequest` trait. Let's have a look at how to implement the `FromRequest` trait. Like in other frameworks, you need to would need to implement the trait like so: ```rust use rocket::request::{self, Request, FromRequest}; pub struct MyError; pub struct MyType; #[rocket::async_trait] impl<'a> FromRequest<'a> for MyType { type Error = MyError; async fn from_request(req: &'a Request<'_>) -> request::Outcome { Outcome::Success(MyType) } } ``` It should be noted that if you need authentication or other processes that only need to be applied on certain routes through middleware, request guards are highly advised - middleware ("fairings") in Rocket are typically supposed to be used as global middleware. ### Error Handling Instead of implementing a trait for your errors, error handling is done a bit differently in Rocket. Instead of traits, you handle errors by using an error handler which is called a "catcher" - the equivalent of this in other frameworks like Axum might be a fallback service, or a route that the HTTP client gets automatically redirected to if it can't find anything or a user isn't authenticated (for example). You can use a handler function for creating a catcher, like so: ```rust #[catch(default)] fn default_catcher(status: Status, request: &Request) -> String { format!("ERROR: {} - {}", status.code, status.reason) } #[launch] fn rocket() -> _ { rocket::build().register("/", catchers![default_catcher]) } ``` If you need a more specific catcher (for example, catching a 404 Not Found error), you can instead use the specific code: ```rust #[catch(404)] fn foo_not_found() -> &'static str { "Foo 404" } ``` Although it is quite easy to use macros for error handling functions in Rocket, you need to also make sure that you attach the handlers to your router! ## Adding a Database Normally when setting up a database in Rust, you might need to set up your own database connection. To get started with doing this in Rocket, you will need to add the `rocket-db-pools` crate with the `sqlx_postgres` feature: ```bash cargo add rocket-db-pools -F sqlx_postgres ``` Then you need to initialise your database connection and add it to a struct which you can then simply attach and use `DB::init()` - see below: ```rust use rocket_db_pools::{sqlx, Database}; #[derive(Database)] #[database("sqlx")] struct DB(sqlx::PgPool); #[launch] fn rocket() -> _ { rocket::build().attach(DB::init()) } ``` You would then need to provision your own Postgres instance, whether installed locally on your computer, provisioned through Docker or something else, add it to your Rocket.toml file (this is covered in a later section) and then it works. Doing it this way means you can add it as a Request guard and it will also work: ```rust #[get("/")] async fn read(mut db: Connection, id: i64) -> Option { sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id) .fetch_one(&mut **db).await .and_then(|r| Ok(r.try_get(0)?)) .ok() } ``` However, with Shuttle we can retrieve the connection pool from a main function annotation, add it to a state struct and then add it to what we're managing (you would then need to access it through `&State`): ```rust #[shuttle_runtime::main] async fn rocket( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_rocket::ShuttleRocket { let state = AppState { pool }; pool.execute(include_str!("../schema.sql")) .await .map_err(CustomError::new)?; let rocket = rocket::build() .mount("/todo", routes![retrieve, add]) .manage(state); Ok(rocket.into()) } ``` Locally the database is provisioned through Docker, but in deployment there is an overarching process that does this for you! No extra work required. We also have an AWS RDS database offering that requires zero AWS knowledge to set up - visit [here](https://www.shuttle.dev/pricing) to find out more. ## App State Rocket, like other Rust web frameworks, allows you to share variables between routes in your web application by holding it in a state struct in memory. Then in your handler functions, you can call the data as required (for example, if you need a database pool, you can attach it to your state struct and then use it as below). To get started, you can add application state like so: ```rust use sqlx::PgPool; struct AppState { db: PgPool, } async fn main() { let db = connect_to_db(); rocket::build() .manage(AppState { db }}); } ``` As you can see, unlike in Axum where the application state struct requires it to implement Clone, there are no trait bounds besides `Send + Sync`. Now you can use whatever's in your state struct like this: ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] struct Thing { message: String, } #[get("/count/")] fn get_data(state: &State, id: i32) -> Vec { let result = sqlx::query_as::<_, Thing>("SELECT * FROM TABLE WHERE id = 1") .bind(id) .fetch_all(&state.db) .await .unwrap(); result } ``` Note that the state gets passed in by reference! Additionally, any state which is not originally added to the `.manage()` function while building the web service will automatically be denied at compile-time. This is quite helpful for avoiding accidental errors. State in Rocket is an example of a Request guard which we talked about earlier - which means that using it in other request guards gets a little bit tricky! To remedy this, we can retrieve the guard itself from the `FromRequest` trait implementation for a struct by using `request.guard::<&State>()`, as you can see below: ```rust use rocket::State; use rocket::request::{self, Request, FromRequest}; use rocket::outcome::IntoOutcome; use rocket::http::Status; struct Item<'r>(&'r str); struct AppState { db: PgPool, } #[rocket::async_trait] impl<'r> FromRequest<'r> for Item<'r> { type Error = (); async fn from_request(request: &'r Request<'_>) -> request::Outcome { // Using `State` as a request guard. Use `inner()` to get the inner value. let outcome = request.guard::<&State>().await .map(|my_config| Item(&my_config.user_val)); // Or alternatively, using `Rocket::state()`: let outcome = request.rocket().state::() .map(|my_config| Item(&my_config.user_val)) .or_forward(Status::InternalServerError); outcome } } ``` ## Fairings (Middleware) Middleware in Rocket, or "fairings" as the crate itself calls them, are a way to add processes that take place before the request handler itself with the most common use cases being validation, authentication/authorization or rewriting request information before passing it onto another request. However, because fairings in Rocket affect the whole application rather than a set of routes, it is highly advised that you instead implement authentication and similar things that only need to be implemented over a certain number of routes as a Request guard rather than a fairing - this differs from most other Rust REST API frameworks where you can use middleware to achieve the same effect.. To write a fairing, you need to declare a struct that implements the `Fairing` trait. In terms of trait bounds, the type itself is required to be `Send + Sync + 'static` - meaning that essentially it just needs to be thread-safe and have only static references if any exist: ```rust use std::io::Cursor; use std::sync::atomic::{AtomicUsize, Ordering}; use rocket::{Request, Data, Response}; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::{Method, ContentType, Status}; struct Counter { get: AtomicUsize, post: AtomicUsize, } #[rocket::async_trait] impl Fairing for Counter { // This is a request and response fairing named "GET/POST Counter". fn info(&self) -> Info { Info { name: "GET/POST Counter", kind: Kind::Request | Kind::Response } } // Increment the counter for `GET` and `POST` requests. async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) { match request.method() { Method::Get => self.get.fetch_add(1, Ordering::Relaxed), Method::Post => self.post.fetch_add(1, Ordering::Relaxed), _ => return }; } async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) { // Don't change a successful user's response, ever. if response.status() != Status::NotFound { return } // Rewrite the response to return the current counts. if request.method() == Method::Get && request.uri().path() == "/counts" { let get_count = self.get.load(Ordering::Relaxed); let post_count = self.post.load(Ordering::Relaxed); let body = format!("Get: {}\nPost: {}", get_count, post_count); response.set_status(Status::Ok); response.set_header(ContentType::Plain); response.set_sized_body(body.len(), Cursor::new(body)); } } } ``` However, as you can see there is quite a lot of boilerplate code involved in this! There will almost certainly be times when we don't want to bother with all of this. In cases like this, we can also use Rocket's `AdHoc` type, which creates a fairing from a function or closure. You can attach an `AdHoc` type like this: ```rust rocket::ignite() .attach(AdHoc::on_launch("Launch Printer", |_| { println!("Rocket is about to launch! Exciting! Here we go..."); })) ``` ## Static Files When it comes to serving static files in Rocket, there's multiple ways you can do it - each having their own pros and cons. To start with, we can serve a single file with the `NamedFile` struct: ```rust use std::path::{Path, PathBuf}; use rocket::fs::NamedFile; #[get("/")] async fn files(file: PathBuf) -> Option { NamedFile::open(Path::new("static/").join(file)).await.ok() } ``` This is great for serving a single file at a time, but of course this doesn't really work in larger numbers, especially when you need to serve a folder of files (for example a folder of images, or a folder of text files - anything similar to this). For this we can use the `FileServer` type and serve that instead by mounting it at the router level instead of trying to write a function for it: ```rust rocket.mount("/public", FileServer::from("static/")) ``` The third way to do static file serving is to do HTML templating. Unlike other web frameworks, Rocket already has built-in support for templating via `rocket_dyn_templates` - to use it, you only need to use the following: ```bash cargo add rocket-dyn-templates ``` Doing this allows you to use `Template` as a return type: ```rust use rocket_dyn_templates::Template; #[get("/")] fn index() -> Template { Template::render("index", context! { foo: 123, }) } #[launch] fn rocket() -> _ { rocket::build() .mount("/", routes![/* .. */]) .attach(Template::fairing()) } ``` The `rocket_dyn_template` library supports both Handlebars templating as well as Tera, with the files needing to be put in a configurable template directory (default is "templates") inside the project root. Note that for Tera you might normally need to build the list of templates in the Tera instance - in this case, you don't need to and it will automatically be done for you. Files ending in `.tera` will use Tera templating, while files ending in `.hbs` will use Handlebars templating. If you're looking to try out Tera, you can find the docs [here](https://keats.github.io/tera/docs/) - meanwhile if you're interested in Handlebars, you can get started [here.](https://handlebarsjs.com/guide/) ## Configuration Unlike most other frameworks, Rocket comes with config files based on the [figment crate](https://docs.rs/figment/0.10.12/figment/) that allow you to set the config from a few files rather than having the configuration spread out over your application, where you're able to split your configuration into development, staging and production. From this file you can set things like address and port, keep_alive timer, request timeouts, secret keys as well as request body limiting! An example of this file would be like this: ```toml # Rocket.toml ## defaults for _all_ profiles [default] address = "0.0.0.0" limits = { form = "64 kB", json = "1 MiB" } [default.tls] key = "path/to/key.pem" # Path or bytes to DER-encoded ASN.1 PKCS#1/#8 or SEC1 key. certs = "path/to/certs.pem" # Path or bytes to DER-encoded X.509 TLS cert chain. ## set only when compiled in debug mode, i.e, `cargo build` [debug] port = 8000 ## only the `json` key from `default` will be overridden; `form` will remain limits = { json = "10MiB" } ## set only when the `nyc` profile is selected [nyc] port = 9001 ## set only when compiled in release mode, i.e, `cargo build --release` [release] port = 9999 ip_header = false secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk=" ``` ## Deployment Deployment with Rust backend programs in general can be less than ideal due to having to use Dockerfiles, although if you are experienced with Docker already this may not be such an issue for you - particularly if you are using cargo-chef. However, if you're using Shuttle you can just use `shuttle deploy` and you're done already. No setup is required. ## Finishing Up Thanks for reading! Although Rocket has previously fell out of favor among people who wanted to use cutting-edge Rust frameworks, the 0.5 upgrade brings a lot of new changes - hopefully this has helped you to create a competent Rust web API using Rocket! Interested in more? - We have a guide to getting started with Axum if you'd like to compare the two frameworks [here.](https://www.shuttle.dev/blog/2023/12/06/using-axum-rust) - We also have a web framework direct comparison article [here!](https://www.shuttle.dev/blog/2023/08/23/rust-web-framework-comparison) --- # Writing a CLI Tool in Rust with Clap Source: https://www.shuttle.dev/blog/2023/12/08/clap-rust Date: 8 December 2023 Author: josh Tags: rust, clap, tutorial, guide Learn about how you can write a CLI tool with Clap by following this article. We also cover libraries that go well with Clap and example repositories. When it comes to learning Rust, often the first real program you'll make might be a command-line interface (CLI) application. Although command-line tools are quite common, they are also a good way to practice learning the basics of a language, and this is no less true in Rust. Crates like [`clap`](https://github.com/clap-rs/clap) make it super easy to write your own CLI tool in Rust by making it as easy as possible using structs and macros via the Derive feature, as well as offering a more low-level option with the Builder API. In this article, we'll be looking at how you can get started with the [`clap`](https://github.com/clap-rs/clap) Rust crate and write a versatile Rust CLI, crates that synergise well with [`clap`](https://github.com/clap-rs/clap) as well as real-world use cases. ## Getting Started First of all, let's initialise our project by using `cargo init example-cli`. We can then add [`clap`](https://github.com/clap-rs/clap) to our program by running the following command: ```rust cargo add clap -F derive ``` We'll primarily be going through using the `derive` feature as it is generally much simpler to use to get what you want. ## Baby's First Clap To get started, we'll want to use the following code in our `src/main.rs`: ```rust use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { name: String } fn main() { let args = Args::parse(); println!("Hello, {}!", args.name); } ``` As you can see, we are using a struct that uses the [`clap::Parser`](https://docs.rs/clap/latest/clap/trait.Parser.html) derive macro, which automatically generates all of the functions that we need to be able to use the struct as a parser. Initially when we load the program up and use `Args::parse()`, it will take the arguments from whatever is in `std::env::os` - our environment arguments that we have given it. Our program takes one argument at the moment, which is `name`. If you try running `cargo run test`, it should print out "Hello, test!" - but if you try to run it without any extra values, it will print the help menu. Clap has a help feature included in the default features that automatically displays the help menu when no valid commands are entered, which saves us a lot of time! Let's try adding a flag to it so that if you don't add anything, it will print a default value: ```rust #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { #[arg(short, long, default_value = "shuttle")] name: String } ``` Now if you try using `cargo run` without anything else, it should print "Hello, shuttle!". There are lots of different things you can add to augment your CLI, which you can find more about [here.](https://docs.rs/clap/latest/clap/_derive/index.html#command-attributes) ## Writing Clap Subcommands Now for your first (sub)command! With using the `derive` feature in `clap`, all we need to do is declare some structs that use `clap`'s macros: ```rust use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { #[command(subcommand)] cmd: Commands } #[derive(Subcommand, Debug, Clone)] enum Commands { Get, Set } fn main() { let args = Args::parse(); println!("{:?}", args); } ``` Currently, this program will take two different commands - which are "get" and "set". If we run `cargo run get` now, we should receive the debug printout of what was parsed from when we ran the program. The same would be the same if you run `cargo run set` - if you attempt to run the program without outputting any commands, or if you try to input an invalid command, clap should return the help menu as mentioned before. ## Adding Clap Command Flags We can also use tuple-like struct syntax and named-field struct syntax for enum variants within our enum; this is because unlike in other OOP languages, Rust enums are actually **sum types**. You can read more about how powerful Rust enums are in another article we wrote [here.](https://www.shuttle.dev/blog/2023/11/23/enums-in-rust) You can have optional arguments by simply wrapping the types in `Option`, but if you want to add a flag to a command you can use `bool`, since [`clap`](https://github.com/clap-rs/clap) recognises that flags are either there or not there. Let's have a look at what this might look like: ```rust use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { #[command(subcommand)] cmd: Commands } #[derive(Subcommand, Debug, Clone)] enum Commands { Get(String), Set { key: String, value: String, is_true: bool } } ``` As you can see above, our `get` command now takes a non-optional argument of a String, and our `set` command now takes a key value and a string value - necessarily speaking though when we're running our program, we won't have to provide the keys themselves: we just need to provide the values that we want to use! Let's have a look at what this would look like for our main function: ```rust fn main() { let args = Args::parse(); match args.cmd { Commands::Get(value) => get_something(value), Commands::Set{key, value, is_true} => set_something(key, value, is_true) } } ``` Now if we were to run this again by using for example `cargo run get foo`, it should work. For the `set` command, the `key` and `value` arguments are mandatory but for the `--is-true` flag, it will only be set to true if you add the flag - otherwise, it will be set to false. ## Clap CLI Looping Most of the time you might only want to execute a command once, but there may be times where you want to create a CLI where the user may want to keep the process running in case they want to run extra commands. For example, a REPL (also known as a "language shell" or read-eval-print-loop) will want to be able to store variables and execute statements. For this purpose, the [`clap::Parser`](https://docs.rs/clap/latest/clap/trait.Parser.html) trait also has the `try_parse_from` function where it will try to parse CLI commands from a variable that implements `Into` - for this case, we can use a vector as they can be iterated on. Let's have a look at the Rust code below: ```rust fn main() { loop { let mut buf = String::from(crate_name!()); std::io::stdin().read_line(&mut buf).expect("Couldn't parse stdin"); let line = buf.trim(); let args = shlex::split(line).ok_or("error: Invalid quoting").unwrap(); println!("{:?}" , args); match Args::try_parse_from(args.iter()).map_err(|e| e.to_string()) { Ok(cli) => { match cli.cmd { Commands::Get(value) => get_something(value), Commands::Set{key, value, is_true} => set_something(key, value, is_true) } } Err(_) => println!("That's not a valid command!"); }; } } ``` As you can see above, we first initialise a loop that starts with a String from the crate name itself and not a completely new string; without this, whenever you try to use a command, [`clap`](https://github.com/clap-rs/clap) will error out because using it this way requires you to input the crate name. Then we expect some kind of input from the user, and once the user passes in a command we try to parse our `Args` type from the input. If it's successful we can move on with matching the command, if not then it uses an error. There is a somewhat small issue with this in that you're trying to parse the arguments this way and you write an invalid command, it doesn't automatically show you the help menu - which means we will need to re-implement our own. You can do this by writing a function that prints out all of the commands: ```rust fn show_commands() { println!(r#"COMMANDS: get - Gets the value of a given key and displays it. If no key given, retrieves all values and displays them. set - Sets the value of a given key. Flags: --is-true "#); } ``` Then you can add this to the error handling instead of only writing that the command is invalid and leaving a potential user confused! You can also, of course, add a help command that displays this instead of displaying it every time an incorrect command is entered and then refer the user to it. The full CLI program would look like this: ```rust #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { #[command(subcommand)] cmd: Commands } #[derive(Subcommand, Debug, Clone)] enum Commands { Get(String), Set { key: String, value: String, is_true: bool }, Help } fn main() { loop { let mut buf = String::from(crate_name!()); std::io::stdin().read_line(&mut buf).expect("Couldn't parse stdin"); let line = buf.trim(); let args = shlex::split(line).ok_or("error: Invalid quoting").unwrap(); println!("{:?}", args); match Args::try_parse_from(args.iter()).map_err(|e| e.to_string()) { Ok(cli) => { match cli.cmd { Commands::Get(value) => get_something(value), Commands::Set{key, value, is_true} => set_something(key, value, is_true), Commands::Help => show_commands(), } } Err(_) => println!("That's not a valid command - use the help command if you are stuck."); }; } } ``` ## Extending Clap Although [`clap`](https://github.com/clap-rs/clap) is a great tool by itself, using it by itself can be a bit barebones. Thankfully there are quite a lot of crates within the terminal/command-line crate ecosystem - you can add crates for making prompts, colouring your terminal, and much more. Here is a list of a few of our favourites that may prove quite helpful to you for writing a command-line tool in Rust: ### Crossterm [`crossterm`](https://docs.rs/crossterm/latest/crossterm/) is a library crate that aims to be a pure Rust terminal manipulation library. It comes with all kinds of cool stuff like being able to change background and text colors, manipulating the terminal itself and the cursor as well as capturing keyboard and other events. Crossterm is also the backbone of many, many popular other crates! ### comfy-table [`comfy-table`](https://github.com/nukesor/comfy-table) is a crate designed to facilitate tables that look pretty in the terminal. You can get started in as little as this: ```rust use comfy_table::Table; fn main() { let mut table = Table::new(); table .set_header(vec!["Header1", "Header2", "Header3"]) .add_row(vec![ "This is a text", "This is another text", "This is the third text", ]) .add_row(vec![ "This is another text", "Now\nadd some\nmulti line stuff", "This is awesome", ]); println!("{table}"); } ``` When you add the above code to a program and run it, the output would look like this: ```bash +----------------------+----------------------+------------------------+ | Header1 | Header2 | Header3 | +======================================================================+ | This is a text | This is another text | This is the third text | |----------------------+----------------------+------------------------| | This is another text | Now | This is awesome | | | add some | | | | multi line stuff | | +----------------------+----------------------+------------------------+ ``` Pretty easy, right? Crates don't have to do everything: they just have to be great at one thing in particular, and `comfy-table` is designed to be just that. ### inquire [`inquire`](https://github.com/mikaelmello/inquire) is a crate designed for building interactive prompts on the terminal. It supports single-select, multi-select, calendar picking, and more: ```rust let name = Text::new("What is your name?").prompt(); match name { Ok(name) => println!("Hello {}", name), Err(_) => println!("An error happened when asking for your name, try again later."), } ``` If you don't want a looping CLI but your program needs more than a couple of inputs, you may want to try this out! ## Clap in Action If you're stuck with getting a high-level view of how clap can be used in production, here are a couple of repositories where you can look for inspiration! ### cargo-shuttle [`cargo-shuttle`](https://github.com/shuttle-hq/shuttle/tree/main/cargo-shuttle) is Shuttle's own CLI for interacting with the Shuttle platform. Within the `src` folder, you will be able to get a better sense of how you can organise your folders/files for a larger CLI project for a live service. There is also use of async here with `tokio`, so if you're interested in learning how to get started with using clap with async services (for example setting up an async client for a database service), this would be a perfect opportunity to learn to do so! ### git-cliff [`git-cliff`](https://github.com/orhun/git-cliff) is a terminal tool that can generate changelog from the Git history by using conventional commits, as well as by using regex-powered parsers and you can even change the changelog template itself by using a configuration file. This tool is a great example of text parsing on the terminal and also uses `clap_mangen` which generates man pages. Useful for anyone who is serious about looking into making a production-ready terminal tool! ## Finishing Up Thanks for reading! Writing a CLI tool in Rust can be a great first step into learning the language - but that doesn't mean you can't also make great production-grade tools in the command line. Hopefully, this article has given you some insight into how you can improve any of the CLI tools you've made, or perhaps help you write a new Rust [`clap`](https://github.com/clap-rs/clap) application. Interested in more? - Check out how you can use raw SQL in Rust with SQLx [here.](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust) - If you'd like to know more about how macros work, [this article should help you out.](https://www.shuttle.dev/blog/2022/12/23/procedural-macros) --- # The Ultimate Guide to Axum: From Hello World to Production in Rust (2025) Source: https://www.shuttle.dev/blog/2023/12/06/using-axum-rust Date: 6 December 2023 Author: josh Tags: rust, axum, tutorial, guide A deep-dive on Axum, a Rust web backend framework. We look at using Axum to write a competent web service with middleware, routing, state, testing, and more. Updated for Axum 0.8. With so many backend web frameworks in the Rust web ecosystem, it's difficult to know what to choose. Although much further in the past you might have seen Rocket shoot to the top of the leadeboard for popularity, nowadays it's typically Axum and Actix Web battling it out with Axum slowly coming on top. In this article, we are going to do a deep dive into Axum, a web application framework for making Rust REST APIs backed by the Tokio team that's simple to use and has hyper-compatibility with Tower, a robust library of reusable, modular components for building network applications. What makes Axum stand out in the Rust programming landscape is its macro free api design, predictable error handling model, and own middleware system built on Tower. Whether you're building a single route handler or a complex API, Axum's design minimizes boilerplate while giving you full control. - **Routing & Handlers**: Define routes with `axum::Router` and write async handler functions. - **State Management**: Share state (like a database pool) safely using `axum::extract::State` with `std::sync::Arc`. - **Middleware**: Leverage the entire `tower` and `tower-http` ecosystem for powerful, reusable middleware. - **Testing**: Test your handlers directly and efficiently without a running server using `tower::ServiceExt`. - **Deployment**: Deploy easily with tools like Shuttle, abstracting away Docker and complex infrastructure. In this article we'll take a comprehensive look at how to use Axum to write a web service. This article has been updated for Axum 0.8 and Tokio 1.0, reflecting the latest best practices. ## Getting Started with Axum: Building REST APIs in Rust Axum is designed specifically for building REST APIs in the Rust programming ecosystem. Let's start with the fundamentals of routing and handlers. ## Routing in Axum and Handler Functions Axum follows the style of REST-style APIs like Express where you can create async function handlers and attach them to axum's `axum::Router` type. The path parameter syntax is intuitive and the Rust compiler helps catch errors at compile time. An example of a route might look like this: ```rust async fn hello_world() -> &'static str { "Hello world!" } ``` Then we can add it to our Router like so: ```rust use axum::{Router, routing::get}; fn init_router() -> Router { Router::new() .route("/", get(hello_world)) } ``` For a handler function to be valid, it needs to either be an `axum::response::Response` type or implement `axum::response::IntoResponse`. This is already implemented for most primitive types and all of Axum's own types - for example, if we wanted to send a json response back to a user, we can do that quite easily using Axum's JSON type by using it as a return type, with the `axum::Json` type wrapping whatever we want to send back. As you can see above, we can also return a String (slice) by itself with minimal boilerplate. We can also use `impl IntoResponse` directly which at first glance immediately solves having to figure out what type we need to return; however, using it directly also means making sure all the return types are the same type! This means we can run into errors unnecessarily. We can instead implement `IntoResponse` for an enum or a struct that we can then use as the return type. See below: ```rust use axum::{response::{Response, IntoResponse}, Json, http::StatusCode}; use serde::Serialize; // here we show a type that implements Serialize + Send #[derive(Serialize)] struct Message { message: String } enum ApiResponse { OK, Created, JsonData(Vec), } impl IntoResponse for ApiResponse { fn into_response(self) -> Response { match self { Self::OK => (StatusCode::OK).into_response(), Self::Created => (StatusCode::CREATED).into_response(), Self::JsonData(data) => (StatusCode::OK, Json(data)).into_response() } } } ``` This pattern allows you to declaratively parse requests and return appropriate status code responses based on your application logic. Then you would implement the enum in your handler function like this: ```rust async fn my_function() -> ApiResponse { // ... rest of your code } ``` Of course, we can also use a Result type for returns! Although the error type will also technically accept anything that can be turned into a HTTP response, we can also implement an error response type that can illustrate several different ways a HTTP request can fail within our application just like we did with our successful HTTP request enum. This gives you a predictable error handling model across your entire application. See below: ```rust enum ApiError { BadRequest, Forbidden, Unauthorised, InternalServerError } // ... your IntoResponse implementation goes here async fn my_function() -> Result { // ... your code } ``` This allows us to differentiate between errors and successful requests when writing our Axum routing, providing robust error handling throughout your web application framework. ## Error Handling in Axum Handlers Proper error handling is crucial for building reliable Rust web applications. As we've seen, Axum handlers can return Result types with custom error responses that map to appropriate HTTP status codes. ## Structuring Your Application As your application grows, you'll want to split your routes into multiple files. Axum's `Router` makes this easy with the `merge` method. You can create separate routers for different parts of your application and then merge them into one main router. For example, you could have a `user_routes.rs` file: ```rust // in user_routes.rs use axum::{Router, routing::get}; async fn get_users() { /* ... */ } async fn get_user() { /* ... */ } pub fn users_router() -> Router { Router::new() .route("/users", get(get_users)) .route("/users/:id", get(get_user)) // path parameters are extracted automatically } ``` And then merge it into your main router: ```rust // in main.rs mod user_routes; fn init_router() -> Router { Router::new() .route("/", get(hello_world)) .merge(user_routes::users_router()) //... with_state, layers, etc. } ``` This approach helps keep your `main.rs` or `lib.rs` clean and organizes your application by feature. ## Adding a Database in Axum Normally when setting up a database, you might need to set up your database connection: ```rust use axum::{Router, routing::get, extract::State}; use sqlx::{PgPool, PgPoolOptions}; use std::sync::Arc; // AppState now uses Arc to hold the connection pool struct AppState { db: PgPool, } #[tokio::main] // Using tokio main as the asynchronous runtime async fn main() { let db_connection_str = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://user:password@localhost/database".to_string()); let pool = PgPoolOptions::new() .max_connections(5) .connect(&db_connection_str).await .expect("can't connect to database"); let app_state = Arc::new(AppState { db: pool }); let app = Router::new() .route("/", get(hello_world)) .with_state(app_state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } async fn hello_world() -> &'static str { "Hello, world!" } ``` You would then need to provision your own Postgres instance, whether installed locally on your computer, provisioned through Docker, or something else. However, with Shuttle we can eliminate this as the runtime provisions the database for you: ```rust #[shuttle_runtime::main] async fn axum( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_axum::ShuttleAxum { let state = Arc::new(AppState { db: pool }); // .. the rest of your code } ``` Locally this is done through Docker, but in deployment there is an overarching process that does this for you! No extra work is required. We also have an AWS RDS database offering that requires zero AWS knowledge to set up - visit [here](https://www.shuttle.dev/pricing) to find out more. ## App State in Axum Now you might be wondering, "how do I store my database pool and other state-wide variables? I don't want to initialise my connection pool every time I want to do something!" - which is a perfectly valid question and is easily answered! You may have noticed that before we used `axum::Extension` to store it - this is perfectly fine for some use cases, but comes with the disadvantage of not being entirely typesafe. In most Rust web frameworks, Axum included, we use what is called "app state" - a struct dedicated to holding all of your variables that you want to share across your routes on the app. The best practice for sharing state in Axum is to wrap it in an `Arc` (Atomic Reference Counter). This allows multiple parts of your application to safely access the state concurrently. ```rust use sqlx::PgPool; use std::sync::Arc; struct AppState { pool: PgPool, } #[shuttle_runtime::main] async fn axum( #[shuttle_shared_db::Postgres] pool: PgPool, ) -> shuttle_axum::ShuttleAxum { let state = Arc::new(AppState { pool }); // .. the rest of your code } ``` To use this, we will insert it into our router and add the state into our functions by passing it as an parameter: ```rust use axum::{Router, routing::get, extract::State}; use std::sync::Arc; // This would be your AppState from before struct AppState { /* ... */ } fn init_router(state: Arc) -> Router { Router::new() .route("/", get(hello_world)) .route("/do_something", get(do_something)) .with_state(state) } // note that adding the app state is not mandatory - only if you want to use it async fn hello_world() -> &'static str { "Hello world!" } async fn do_something( State(state): State> ) -> Result { // .. your code } ``` You can also `#[derive(Clone)]` on your state struct. Axum's `with_state` will automatically wrap it in an `Arc` for you. However, being explicit with `Arc` often makes the code clearer about how state is being shared, which is why we recommend it. You can also derive sub-state from an application state! This is great for when we need some variables from the main state but want to limit access control on what a given route has access to. See below: ```rust // the application state #[derive(Clone)] struct AppState { // that holds some api specific state api_state: ApiState, } // the api specific state #[derive(Clone)] struct ApiState {} // support converting an `AppState` in an `ApiState` impl FromRef for ApiState { fn from_ref(app_state: &AppState) -> ApiState { app_state.api_state.clone() } } ``` ## Extractors in Axum: Path Parameters and Query Parameters Extractors are exactly that: they extract things from the incoming request, and work by allowing you to let them be passed as parameters into the handler function. Currently, this already has native support for a wide range of things like getting separate headers, path parameters, query params, forms and JSON, as well as there being community support for things like MsgPack, JWT extractors, and more! You can also create your own extractors, which we will get to in a bit. ### Working with Path Parameters and Path Parameter Syntax Axum makes it easy to extract path parameters from your routes. The path parameter syntax uses a colon (`:`) prefix to denote dynamic segments in your route paths. ### Extracting JSON and Query Params As an example, we can use the `axum::Json` type to consume the HTTP request by extracting a JSON request body from the HTTP request. See below for how this can be done: ```rust use axum::Json; use serde_json::Value; async fn my_function( Json(json): Json ) -> Result { // ... your code } ``` However, this is probably not very ergonomic in the fact that we're using `serde_json::Value` which is unshaped and could contain anything! Let's try this again with a Rust struct that implements `serde::Deserialize` - which is required to be able to turn the raw data into the struct itself: ```rust use axum::Json; use serde::Deserialize; #[derive(Deserialize)] pub struct Submission { message: String } async fn my_function( Json(json): Json ) -> Result { println!("{}", json.message); // ... your code } ``` Note that any fields that are not in the struct **will be ignored** - depending on your use case, this can be a good thing; for example, if you're receiving a webhook but only want to look at certain fields from the webhook request. Forms and URL query parameters can be handled the same way by adding the appropriate type to your handler function. Axum provides a query extractor for parsing query strings - so for example, a form extractor might look like this: ```rust async fn my_function( Form(form): Form ) -> Result { println!("{}", json.message); // ... your code } ``` On the HTML side when you're sending a HTTP request to your API, you will also of course want to make sure you are sending the correct content type. Headers can also be handled the same way except that headers don't consume the request body - which means you can use as many as you want! We can use the `TypedHeader` type to do this. For Axum 0.6 you will need to enable the `headers` feature, but in 0.7 this has been moved to the `axum-extra` crate which you will need to add the `typed-header` feature, like so: ```bash cargo add axum-extra -F typed-header ``` Using typed headers can be as simple as adding it as a parameter to a handler function: ```rust use headers::ContentType; use axum::{TypedHeader, headers::Origin}; // use this if on axum 0.6 use axum_extra::{TypedHeader, headers::Origin}; // use this if on axum 0.7 async fn my_function( TypedHeader(origin): TypedHeader ) -> Result { println!("{}", origin.hostname); // ... your code } ``` You can find the docs for the `TypedHeader` extractor/response [here.](https://docs.rs/axum-extra/latest/axum_extra/struct.TypedHeader.html) In addition to `TypedHeaders`, `axum-extra` also offers many other helpful types we can use. For example, it has a `CookieJar` extractor which helps with managing cookies and has additional features built into the cookie jar like having cryptographic security if you need it (although it should be noted that there are different cookie jar features depending on which one you need), and a `protobuf` extractor for working with gRPC. You can find the documentation for the library [here.](https://docs.rs/axum-extra/latest/axum_extra/index.html) ## Custom Extractors in Axum Now that we know a bit more about extractors, you probably want to know how we can create our own extractors - for example, let's say that you need to create an extractor that parses based on whether the request body is either Json or a Form. Let's set up our structs and the handler function: ```rust #[derive(Debug, Serialize, Deserialize)] struct Payload { foo: String, } async fn handler(JsonOrForm(payload): JsonOrForm) { dbg!(payload); } struct JsonOrForm(T); ``` Now we can implement `FromRequest` for our `JsonOrForm` struct! ```rust #[async_trait] impl FromRequest for JsonOrForm where B: Send + 'static, S: Send + Sync, Json: FromRequest<(), B>, Form: FromRequest<(), B>, T: 'static, { type Rejection = Response; async fn from_request(req: Request, _state: &S) -> Result { let content_type_header = req.headers().get(CONTENT_TYPE); let content_type = content_type_header.and_then(|value| value.to_str().ok()); if let Some(content_type) = content_type { if content_type.starts_with("application/json") { let Json(payload) = req.extract().await.map_err(IntoResponse::into_response)?; return Ok(Self(payload)); } if content_type.starts_with("application/x-www-form-urlencoded") { let Form(payload) = req.extract().await.map_err(IntoResponse::into_response)?; return Ok(Self(payload)); } } Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()) } } ``` In Axum 0.7, this was modified slightly. `axum::body::Body` is now no longer a re-export of `hyper::body::Body` and is instead its own type - meaning that it is no longer generic and the `Request` type will always use `axum::body::Body`. What this translates to essentially is that we just remove the `B` generic - see below: ```rust #[async_trait] impl FromRequest for JsonOrForm where S: Send + Sync, Json: FromRequest<()>, Form: FromRequest<()>, T: 'static, { type Rejection = Response; async fn from_request(req: Request, _state: &S) -> Result { let content_type_header = req.headers().get(CONTENT_TYPE); let content_type = content_type_header.and_then(|value| value.to_str().ok()); if let Some(content_type) = content_type { if content_type.starts_with("application/json") { let Json(payload) = req.extract().await.map_err(IntoResponse::into_response)?; return Ok(Self(payload)); } if content_type.starts_with("application/x-www-form-urlencoded") { let Form(payload) = req.extract().await.map_err(IntoResponse::into_response)?; return Ok(Self(payload)); } } Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()) } } ``` ## Middleware in Axum As mentioned before, one of Axum's great wins over other frameworks is that it is hyper-compatible with the `tower` crates, which means that we can effectively use any Tower middleware that we want for our Rust API! This own middleware system gives you incredible flexibility and reusability. For example, we can add a Tower middleware to compress responses: ```rust use tower_http::compression::CompressionLayer; use axum::{routing::get, Router}; fn init_router() -> Router { Router::new().route("/", get(hello_world)).layer(CompressionLayer::new) } ``` There are a number of crates consisting of Tower middleware that are available to use without us even having to write any middleware ourselves! If you're already using Tower middleware in any of your applications, this is a great way to re-use your middleware system without having to write yet more code as the compatibility ensures no issues. The Rust language's type system also helps prevent common issues like memory leaks in middleware chains. We can also create our own middleware by writing a function. The function requires a `` generic bound over the `Request` and `Next` types, as Axum's body type is generic in 0.6. See below for an example: ```rust use axum::{http::Request, middleware::Next}; async fn check_hello_world( req: Request, next: Next ) -> Result { // requires the http crate to get the header name if req.headers().get(CONTENT_TYPE).unwrap() != "application/json" { return Err(StatusCode::BAD_REQUEST); } Ok(next.run(req).await) } ``` In Axum 0.7 and later, you'd remove the `` constraint, as Axum's `axum::body::Body` type is no longer generic. This makes the API cleaner while maintaining the same macro free api approach: ```rust use axum::{http::Request, middleware::Next}; async fn check_hello_world( req: Request, next: Next ) -> Result { // requires the http crate to get the header name if req.headers().get(CONTENT_TYPE).unwrap() != "application/json" { return Err(StatusCode::BAD_REQUEST); } Ok(next.run(req).await) } ``` To implement the new middleware we created in our application, we want to use axum's `axum::middleware::from_fn` function, which allows us to use a function as a handler. In practice it would look like this: ```rust use axum::middleware::self; fn init_router() -> Router { Router::new().route("/", get(hello_world)).layer(middleware::from_fn(check_hello_world)) } ``` If you need to add app state to your middleware, you can add it to your handler function then use `middleware::from_fn_with_state`: ```rust fn init_router() -> Router { let state = setup_state(); // app state initialisation goes here Router::new() .route("/", get(hello_world)) .layer(middleware::from_fn_with_state(state.clone(), check_hello_world)) .with_state(state) } ``` ## Serving Static Files in Axum Let's say you want to serve some static files using Axum - or that you have an application made using a frontend JavaScript framework like React, and you want to combine it with your Rust Axum backend to make one large application instead of having to host your frontend and backend separately. How would you do that? Axum does not by itself have capabilities to be able to do this; however, what it does have is super-strong compatibility with `tower-http`, which offers utility for serving your own static files whether you're running a SPA, statically-generated files from a framework like Next.js or simply just raw HTML, CSS and JavaScript. If you're using static-generated files, you can easily slip this into your router (assuming your static files are in a `dist` folder at the root of your project): ```rust use tower_http::services::ServeDir; fn init_router() -> Router { Router::new() .nest_service("/", ServeDir::new("dist")) } ``` If you're using a SPA like React, Vue or something similar, you can build the assets into the relevant folder and then use the following: ```rust use tower_http::services::{ServeDir, ServeFile}; fn init_router() -> Router { Router::new().nest_service( "/", ServeDir::new("dist") .not_found_service(ServeFile::new("dist/index.html")), ) } ``` You can also use HTML templating with crates like [`askama`](https://github.com/djc/askama), [`tera`](https://github.com/Keats/tera) and [`maud`](https://maud.lambda.xyz/)! This can be combined with the power of lightweight JavaScript libraries like [`htmx`](https://htmx.org) to speed up time to production. You can read more about this on our other article about using HTMX with Rust which you can find [here.](https://www.shuttle.dev/blog/2023/10/25/htmx-with-rust). We also collaborated with [Stefan Baumgartner](https://fettblog.eu) on an article for [serving HTML with Askama!](https://www.shuttle.dev/launchpad/issues/2023-10-17-issue-10-Serving-HTML) ## Testing Your Handlers A major advantage of Axum's design is that its components (`Router`, handlers) are `tower::Service`s. This means you can test them without running an actual HTTP server. The `tower::ServiceExt` trait provides a `oneshot` method that sends a single route request to your service, making testing straightforward in the Rust programming language. Here's how you can test a handler: ```rust use axum::{ body::Body, http::{Request, StatusCode}, routing::get, Router, }; use http_body_util::BodyExt; // for `to_bytes` use tower::ServiceExt; // for `oneshot` // a router for testing fn app() -> Router { Router::new().route("/", get(|| async { "Hello, World!" })) } #[tokio::test] // Using async fn main pattern in tests with tokio async fn test_hello_world() { let app = app(); // `Router` implements `tower::Service>` so we can // call it like any tower service, no need to run an HTTP server. let response = app .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); assert_eq!(&body[..], b"Hello, World!"); } ``` This method is fast, reliable, and lets you test your application's logic directly. You can construct any `http::Request` to test different scenarios, including headers, request bodies, and more. For this to work, you'll need `http-body-util` with the `full` feature in your `[dev-dependencies]`. ## Beyond REST: WebSockets and OpenAPI While Axum is excellent for REST APIs, its capabilities don't stop there. ### WebSockets Axum has first-class support for WebSockets. You can add a WebSocket handler using the `axum::extract::ws::WebSocketUpgrade` extractor. This extractor will handle the WebSocket handshake and upgrade the connection, giving you a `WebSocket` stream to send and receive messages. ```rust use axum::{ extract::ws::{WebSocket, WebSocketUpgrade}, response::IntoResponse, }; async fn websocket_handler(ws: WebSocketUpgrade) -> impl IntoResponse { ws.on_upgrade(handle_socket) } async fn handle_socket(mut socket: WebSocket) { while let Some(msg) = socket.recv().await { let msg = if let Ok(msg) = msg { msg } else { // client disconnected return; }; if socket.send(msg).await.is_err() { // client disconnected return; } } } ``` ### OpenAPI For building documented and maintainable APIs, OpenAPI (formerly Swagger) is the standard. While Axum doesn't have built-in OpenAPI generation, the `utoipa` crate provides excellent integration. It allows you to generate an OpenAPI specification from your Axum handlers and data types using procedural macros. ## How to Deploy Axum: Production Rust Web Applications Deployment with Rust backend programs in general can be less than ideal due to having to use Dockerfiles, although if you are experienced with Docker already this may not be such an issue for you - particularly if you are using `cargo-chef`. However, if you're using Shuttle you can just use `shuttle deploy` and you're done already. No setup is required. Building production-ready Rust web applications with Axum is straightforward, and with modern deployment platforms, you can focus on writing code rather than managing infrastructure. ## Frequently Asked Questions --- # Why Type Safety is Important Source: https://www.shuttle.dev/blog/2023/11/29/type-safety Date: 29 November 2023 Author: josh Tags: rust, type-safety, tutorial, guide This article takes a deep dive into type safety, language features that enable type safety, and why type safety is a good idea. Type safety: two words that, when put together, have the potential to cause plenty of heated debate. While many software engineers advocate for type safety no matter the situation, other people of note like [DHH](https://world.hey.com/dhh/programming-types-and-mindsets-5b8490bc) would consider themselves to be dynamic typing enjoyers and are much more skeptical. Although there are a lot of strong opinions on what is "better", it is not quite a clear-cut deal. Let's talk about it. ## Using Dynamically-Typed Languages One example that is often used as a target of a reason why type safety is important is JavaScript. Despite it being a highly recommended language for beginner developers due to how easy it is to learn, the simplicity of the language combined with the use of type coercion can lead to language quirks. It's no secret that while true is equal to 1 and false is equal to 0 in a lot of languages, JavaScript takes this further. The following classic JavaScript snippet can be used to illustrate this: ```javascript "b" + "a" + +"a" + "a"; // -> 'baNaNa' ``` As you can see, this leads to 'baNaNa' - but why? In the middle of the expression it has `+ +"a"` which gets evaluated to `+(+'a')` which ends up ultimately becoming `"NaN"`. This is pretty funny in isolation, but when you're trying to build production-grade codebases based around a language that has type coercion, it can be difficult to ensure things are the same type. This has caused a lot of people to initially use JavaScript early on in their software development careers, and then transfer later on to something else where there is more of a solid typing system. This has also caused the rise of TypeScript and libraries like JSDoc, which aim to make typing much easier (although this doesn't stop the fact that it still compiles to JavaScript). In addition to the core types, the TypeScript types system itself is quite expressive. You can use Interfaces to define the shape of an object or its structure - for example, let's say we have an interface called Message: ```typescript interface Message { message: string; user_id: number; created_at: Date; } /* now we can instantiate the interface by declaring a variable and the type */ let message: Message = { message: "Hello world!", user_id: 1, created_at: Date.now(), }; ``` You can also of course add optional parameters by adding question marks to the variable names, like so: ```typescript interface Message { message: string; user_id: number; created_at: Date; updated_at?: Date; } ``` In addition to having interfaces, you can also use enum types in TypeScript! Enums are a way of having conceptual containers that hold all the variants of a concept. For example, you can have a Directions enum that can hold all the various directions that something can be facing in: ```typescript enum Direction { Up, Down, Left, Right, } ``` However, due to it not being a JavaScript type-level feature, it is often heavily recommended against using enums in TypeScript as compilation can typically cause problems; for example, `const enum` and `enum` are two different things. In this case, you would need to either learn how to use enums properly or not use them at all. In addition to this, TypeScript also has other issues of varying severity; many libraries have either non-existent or poor support for TypeScript. Many non-trivial codebases will also require a more complex setup and it takes time to configure everything. It should also be noted that if you're working in a team where you're the only person who knows TypeScript, this can also make it exponentially more difficult as you'll need to potentially upscale your team to be able to use TypeScript. However, if you can get past this, it's much easier to refactor things in your codebase because you can be assured that when it compiles there are no errors. It should be said that type inference makes this much easier - so you can just declare a variable and TypeScript will guess what the variable type is. No declarations are required! Of course, when you're working by yourself on a small codebase (like a product POC for example), it doesn't matter much; just fix the error and move on. However, it's worth considering that by using typing, you can also eliminate the mental overhead of having to think about whether something is the correct type or not. There are also efforts to recreate other type concepts in other languages in TypeScript - for example, there is a [Typescript library for adding functional programming types](https://github.com/gcanti/fp-ts). Another example of dynamic typing would be Python - although it is strongly typed, so if you get a typing error, it'll tell you. Being dynamically yet strongly typed allows Python to be more ergonomic because you don't need to think about what type something is - which is great for new developers, and has led Python to also be another highly recommended language for beginners that can be used for a wide variety of things. The method that Python uses when you add types to things is called type hinting - like so: ```python def greeting(name: str) -> str: return 'Hello ' + name ``` However, because it's not statically typed it loses some of the benefits of being statically typed; there's no way to check types automatically so fewer errors are caught before runtime (unless you use static type analysis tools like `mypy`), and if you're migrating a large codebase where there is little type hinting it will take a considerable effort to do so. Needless to say, there are far fewer quirks in Python than JavaScript. ## Using Statically Typed Languages Statically typed languages are exactly that: languages where the type of variables and similar things must be known by the compiler. With the advent of typed inference, the developer experience when using statically typed languages has been greatly improved - primarily because you don't need to declare the type explicitly; you can just declare the variable and instantiate it, and then the compiler will infer what type the variable is by looking at what the value is. In the C family of languages, you need to declare that you want the compiler to infer the type by using a keyword. In C# you would use the `var` keyword: ```csharp var text = "Hello world!"; ``` With C++ types, you would instead use `auto`: ```cpp auto x = 4; ``` It's not particularly ergonomic, but it's there! Of course, in C++ you can also use `void\* ' for a variable to signify a universal pointer - however, most C++ devs will tell you that this is almost certainly a huge footgun that's going to end badly whichever way you use it. Other languages like Rust have type inference by default and simply let you declare what the variable is without a specific keyword: ```rust let name = "Shuttle"; ``` The advantages of strong static typing are numerous; you can catch errors during compilation rather than during runtime, documentation for libraries will always have proper typing support and your team will always be on the same page when talking about types. It's not hard to see why people like using it. On the other hand, you need to compile every time you want to run a new version; incremental builds and caching help (via ccache or sccache), but all the same, if you're running a large codebase it can take time to do so. Additionally, in some languages type signatures can also play a role in informing the compiler of what type of functionality a function or other thing may require. This is particularly notable in Rust with trait bounds - for example, a function may require that an argument be of a type that implements the `Send` trait - both of these being required for a variable to be sent to another thread. You would represent this like so: ```rust async fn(thing: T) {} ``` This further enhances the "static typing" functionality for Rust types as it ensures that your variables are not only the correct type - but that they have the correct functionality. This is also relevant in languages like Haskell that have a `Traits` class that you can use: ```haskell -- "a" here is a generic variable where it just needs to be an int when instantiated class FloatTraits a where mantissaDigits :: a -> Int -- as you can see here, we implement FloatTraits for Float, with mantissaDigits being 24 instance FloatTraits Float where mantissaDigits _ = 24 ``` It should be noted that languages like Java have their own equivalent in the form of Interfaces (not to be confused with TypeScript interfaces!). ## Can type safety ever be a bad thing? While it is difficult to justify why exactly it could be a bad thing, typing can complicate things. Especially in languages that have language features that define specific functional behaviour of a type, it can be very easy to create a long type signature even during regular use. For example, a common design pattern for async Rust is to use `Arc>` to wrap a generic type in a type that locks access while in use on a thread, but then can be copied to other threads. This is fine at first but then can escalate quite quickly when you need to use things like `UnboundedSender` for things like hashmaps of websocket client lists (for example). This is particularly relevant in async Rust as when working with complex functions or writing Rust async libraries, you need to implement functions that may require generics - and the generics will likely require several trait bounds, or more. Suffice it to say, it can get complicated pretty quickly and people who are discovering this for the first time with Rust will soon find themselves with brain freeze. However, there is a good reason for this. Let's take the `String` type in Rust for example; trying to copy a `String` type in Rust will simply tell you through the compiler that `String does not implement Copy` (or some variation of this message). Why doesn't it implement it? The [docs.rs page for the Copy trait](https://doc.rust-lang.org/std/marker/trait.Copy.html) has the following: > Types whose values can be duplicated simply by copying bits. Strings in Rust are actually smart pointers and not the data itself - if you try to copy a String, you will only be copying the pointer which will lead to a double-free error down the line and a memory leak. Although traits themselves are language features and not type, this is a good example of how language features in combination with typing can be used to ensure that errors are caught upfront instead of during runtime (and potentially, in production!). There's also the annoyance of many languages not having type inference, meaning you have to explicitly declare every variable's type. This makes type safety considerably more awkward to use and can turn people off from it, which has led to newer programming languages adopting type inference. ## Finishing Up Type safety, while being a very good thing to have, does come with some annoyances - some much larger than others, depending on what programming language you're using. Although there are a lot of fans of both sides of the spectrum when it comes to typing, with the advent of certain features of functional programming [making its way into other languages](https://typeable.io/blog/2021-11-15-fp-features) it's almost certain that there is more that can be learned from functional programming about how typing can be made easier. --- # Why Enums in Rust feel so much better Source: https://www.shuttle.dev/blog/2023/11/23/enums-in-rust Date: 23 November 2023 Author: josh Tags: rust, enums, guide, opinion This article talks about what enums in Rust are, how they compare to other languages that use enums and what makes Rust enums better. A commonly said piece of feedback from someone who's learning Rust as a second language tends to be that enums are far better supported in Rust than any other language. A cursory glance at Google for "enums in Rust" returns a result in the "People also searched for" that asks "why are enums in Rust so good". On initial inspection, this seems to be a good question; in isolation, enums are simply a conceptual container of values that represent potential value - for example: directions, or seasons. However, Rust runs with this and supercharges enums in ways that are simply not there in other languages. In this article we'll talk about what makes Rust enums significantly better than in other languages, as well as some use cases for them. ## A quick recap about Enums First, a quick recap of what Rust enums actually are for the uninitiated: (or those who need a reminder!) Enums are types that are able to represent a defined number of variants. Consider the following enum: ```rust enum Directions { Up, Down, Left, Right } ``` This represents some directions. The advantage of using an enum over just strings is that when we're pattern matching, we can simply match against the different variants instead of having to account for variations in strings. ## Enums in Other Languages For some context, let's have a look at what enums look like in other languages. In TypeScript, a cursory Google search for Typescript enums will return a number of results that either tell you the following: - Don't use enums in TypeScript because they are bad - There is only one correct way to use enums - There are a number of wrong ways to use enums that are not immediately obvious because enums aren't a thing when compiled to JavaScript What this tells us that although they are a feature in TypeScript, they do not seem to be very popular - typically because of user error, or language quirks that make using enums awkward. In Java and other languages, it should be noted that enums are significantly more sane because they don't have an underlying language that they compile to that doesn't support enums - however, the nature of having to use them in classes or using things like method overriding to do anything (in terms of extending or implementing functionality for them) means that enums as a whole don't really receive first class support. Other languages like Go do not necessarily have enums, but you can represent enums by using something like this (in Go): ```go const ( A base = iota C T G ) ``` However, the lack of an official enum keyword means that it seems that it is somewhat frustrating to use. In Rust, enums receive first class support through struct-like types being valid as an enum - so you can have an enum that holds a struct-like structure where there are named values within the enum variant, or a tuple struct where you can just refer to the variables by number, or you can just have the enum variant itself. Although you can't (by default) declare an initial value without extra crates to do so unless you instantiate it, it is relatively easy to turn an enum variant into another type by implementing a method that matches against the enum variants then returning whatever you'd like. Enums also see pretty heavy usage within the Rust type system by virtue of the `Result` and `Option` types, two types that form the basis of the error handling system in Rust. You can also supercharge enums by implementing traits for them, which we will see more of below. ## Implementing Methods for Enums Enums in Rust receive the ability to implement methods specifically for the enum, no class required. Let's have a look at the following method: ```rust enum Number { Odd(i64), Even(i64) } ``` This enum represents a Number as well as whether it's odd or even. We can implement a method for it that automatically instantiates the enum variant based on whether the number can be divided by 2, like so: ```rust impl Number { fn from_i64(num: i64) => Self { match num % 2 == 0 { true => Number::Even(num), false => Number::Odd(num) } } } ``` This eliminates a lot of boilerplate code and makes it much easier to use the method by using `Number::from_i64(number)`. In other languages you could of course write a separate method that returns the enum, but being able to namespace it under the enum itself makes the code much cleaner. Just ike structs, you can also use derive macros on enums; derive macros are a huge part of the Rust ecosystem and simplify boilerplate code generation by auto-generating the code for you at compile-time. ## Enums as Error Types Check out the following enum: ```rust #[derive(Debug)] enum MyError { SQLError(sqlx::Error), RedisError(redis::RedisError), Forbidden, BadRequest, Unauthorized } ``` This enum represents several different ways that a web app might fail: for example, a SQL query might result in an error because the syntax is incorrect, your Redis server might have an error connecting to it and users may also either try to access pages they shouldn't have access to or fill out a form wrong. The Error trait requires our enum type to implement both `Debug` and `Display` - we already used a derive macro for the Debug trait so we don't have to manually implement it, but we do need to implement `Display`. We can do this by matching each enum variant in the function below: ```rust impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyError::SQLError(e) => write!(f, format!("Something went wrong while using an SQL query: {e}")), MyError::RedisError(e) => write!(f, format!("Something went wrong while using Redis: {e}")), MyError::Forbidden => write!(f, "User tried to access a page but was forbidden!"), MyError::BadRequest => write!(f, "User tried to submit a HTTP request but it returned 400!"), MyError::Unauthorized => write!(f, "User tried to access a page but wasn't authorised!"), } } } ``` Implementing this also gives us `.to_string()` for free and will return the above when done so according to the enum variant that it is - useful for us! The `Error` trait type looks like this: ```rust pub trait Error: Debug + Display { fn description(&self) -> &str { /* ... */ } fn cause(&self) -> Option<&Error> { /* ... */ } fn source(&self) -> Option<&(Error + 'static)> { /* ... */ } } ``` However, all of these functions are optional and already have a default implementation - so you can simply implement `Error` for your type like this: ```rust impl Error for MyError {} ``` Technically, this will give you the implementation - although of course, if you would like to include more customised behaviour (including usage of held variables by a particular enum variant, for example), you will probably want to do just that. When you're using a web framework like Axum or Actix, typically speaking you won't have to implement `Error` yourself - you'll implement whatever type the framework uses that also implement `Error`. For example, in Axum the `IntoResponse` trait implements `Error` as well as also being a successful return type, so technically you can have `Result` as a function return signature. Let's have a look at how you'd implement it. ```rust impl IntoResponse for MyError { fn into_response(&self) -> Response { match self { MyError::SQLError(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error while using SQL: {e}")).into_response(), MyError::RedisError(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error while using Redis: {e}")).into_response(), MyError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden!".to_string()).into_response(), MyError::BadRequest => (StatusCode::FORBIDDEN, "Bad request. Did you fill something out wrong?".to_string()).into_response(), MyError::Unauthorized => (StatusCode::FORBIDDEN, "Unauthorised!".to_string()).into_response(), } } } ``` Enums can be extremely effective as error types: by setting an error type as an enum, you only ever need to match against each arm of the enum and you don't need to use a non-exhaustive patten marker (`_`) - although you may want to, if you only want to match against certain enum variants. To do this, you simply just replace the enum variants you don't want to match against with a single `_` then return something for it. ## Enums as Newtypes ("Wrapper Types") We can also wrap a type in an enum that may also have several variants that contain types from a single crate, or multiple crates. The benefit of this versus just exposing another bit of said crates' API is that you can introduce new functionality for your own program while maintaining backwards compatibility by not needing to interact with the original type itself - you can also use it to create an abstraction over the original type. For example, the `poise` crate builds on top of the `serenity` crate by exposing new types as abstractions to provide a more high-level function instead of using low-level functions. As another example: using our previous knowledge of the `Display` trait, we can actually overwrite what the type displays when we use `.to_string()`! Consider a struct that holds a password and the time at which the struct was created: ```rust struct Password { password: String, created_at: DateTime } ``` We can wrap an enum over this: ```rust enum PasswordEnum { Secured(Password), Unsecured(Password) } ``` Now we can do two things: - We can display the password as a load of stars (based on what the length is) - We can return whether the password is secure or not (according to some criteria) See below for what this might look like: ```rust impl fmt::Display for PasswordEnum { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PasswordEnum::Secured(password) => { password = password.chars().map(|_| "*".to_owned()).collect::(); write!(f, password); }, PasswordEnum::Unsecured(password) => { password = password.chars().map(|_| "*".to_owned()).collect::(); write!(f, password); }, } } } impl PasswordEnum { fn is_secure(&self) -> bool { match self { PasswordEnum::Secured(_) => true, PasswordEnum::Unsecured(_) => false } } } ``` As you can see, it's quite easy to use the new-type pattern to your advantage with enums! You can also do this with structs. ## Finishing up Thank you for reading and I hope you learned something about how to use enums in Rust! Enums are extremely powerful and form part of a strong backbone for Rust development. Interested in learning more about Rust? Here's some ideas: - Find out more about macros [here.](https://www.shuttle.dev/blog/2022/12/23/procedural-macros) - Find out more about [using design patterns in Rust.](https://www.shuttle.dev/blog/2022/07/28/patterns-with-rust-types) --- # Building and Deploying A Static Site Generator Source: https://www.shuttle.dev/blog/2023/11/15/ssg-in-rust Date: 15 November 2023 Author: josh Tags: rust, static-site-generator, frontend, guide This article documents how someone built and deployed a static site generator using Rust in an hour, converting Markdown to HTML as well as adding OG tag support and CSS. We'll be making a website in the form of a Rust static site generator that we'll be able to add our own pages to. If you want to skip straight to the project, deploy your own instance and try it out, you can follow the instructions below: 1. Run `shuttle init --from joshua-mo-143/shuttle-ssg` and follow the prompt 2. `cd` to the folder 3. Run `shuttle deploy --allow-dirty` and your static site generator is live! Don't forget to make sure that `cargo-shuttle` is installed and that you're logged in! You can also visit the repo [here.](https://www.github.com/joshua-mo-143/shuttle-ssg)
Rust has always been seen from the outside as a language that takes quite a long time to write something in. While that might be true for more things involving more advanced type machinery (implementing your own job queues, implementing async traits manually, lifetimes), for a lot of things that don't require things that amount to mental space rocketry you can knock out a project surprisingly quickly. I want to challenge the assumption that Rust is a slow language to program in by writing a usable, extendable web service in an hour that also provides immediate value to anyone who would also like to use it. ## 59:59 - Getting Started It's time to lock in. I went on Youtube and put on a breakcore playlist, then used `shuttle init --template axum` to quickly spin up what I wanted. I used the project name `ssg` and put it in my projects folder, then got to work. What helps me move quickly is that with Shuttle I don't need a Dockerfile; I just use the runtime, provision the things I need and I'm ready to get cracking. To finish my preparations, I created a `Shuttle.toml` file and added the following: ```toml // Shuttle.toml assets = ["templates/*", "templates/**/*"] ``` This lets Shuttle know where we keep all of our files so that when we deploy, it should just include the files automatically. Anyway, onto the next part! ## 59:39 - Adding basic functionality I knew initially from previous experience that you can use [`pulldown-cmark`](https://github.com/raphlinus/pulldown-cmark) to turn Markdown to HTML, so I quickly added it using `cargo add pulldown-cmark`. I remember that they had an example on their GitHub repo for parsing Markdown, so I grabbed it and quickly threw it in a handler function: ```rust // src/main.rs async fn parser(State(state): State, Path(page): Path) -> impl IntoResponse { let mut file = format!("templates/{page}.md"); let markdown_input = match tokio::fs::read(file).await { Ok(res) => res, Err(_) => return Html("Couldn't find this page - does it exist?".to_string()), }; let string_output = std::str::from_utf8(&markdown_input).unwrap(); let mut html_output = String::new(); let parser = pulldown_cmark::Parser::new(&string_output); pulldown_cmark::html::push_html(&mut html_output, parser); Html(html_output) ``` I made the handler generic by using the Path extractor, then quickly made the `templates` folder and put in a `hello_world.md` file: ```md ## Hello world! ``` I then used `cargo clippy` to make sure it all works. There's no errors. After that I used `shuttle run` to get it working and then made the mistake of going to the base route at `/` instead of at `/hello_world` where the web service could read the Markdown file. Never mind! I'll fix that later - it looks like going to `/hello_world` works. Unfortunately, this part took the better part of 10 minutes due to waiting for Rust to compile; granted however, I'm on a laptop. We're not exactly expecting NASA-level performance out of this one. ## 47:12 - Adding CSS styling Now that we've got basic Rust markdown parsing functionality, we can move to the next part: styling our pages. I didn't want to spend a lot of time on this - if you're not careful, styling can eat up quite a lot of your time! I primarily wanted to just make sure the page didn't look horrible, so I added a small amount of CSS to center-align the text: ```css /* templates/styles.css */ html { display: flex; text-align: center; flex-direction: column; align-items: center; justify-content: center; } body { width: 80%; } ``` As you can see, it's not quite [Awwwards](https://www.awwwards.com)-worthy CSS. However, it'll do for making our pages look at least somewhat aligned. Then I added a handler function to send the CSS text as a response. I struggled with it for a few minutes by trying to set the response type to [`axum::response::Response`](https://docs.rs/axum/latest/axum/response/type.Response.html) and using `cargo clippy` before remembering that I needed to set it as `impl IntoResponse`: ```rust // src/main.rs async fn styles() -> impl IntoResponse { Response::builder() .status(StatusCode::OK) .header("Content-Type", "text/css") .body(include_str!("../templates/styles.css").to_owned()) .unwrap() } ``` Then I added it to the HTML output before pushing the HTML to the empty string buffer: ```rust html_output.push_str(r#""#); ``` After starting up local development using `shuttle run` and quickly checking the `/hello_world` route, I could see that the text was in fact now centre-aligned on the page. Great. Time to move onto the next part. With Shuttle's easy-to-use local development environment, you can also specify a port with the `-p` flag or test on a local network using `--external` to expose your web app to the local network. ## 36:28 - Adding OpenGraph tags Now it's time to add [OpenGraph](https://ogp.me/) tags so I can get better SEO using the static site generator! This was a part I struggled with quite a lot. I remember when I last logged into Bearblog for my own blog articles that there was a bit at the top indented by three dashes where you could just add properties to an article and it would add the properties in the fenced off text block to [OpenGraph](https://ogp.me) meta tags, but I didn't know what it was called. I did some extensive Googling, and realised it was called "frontmatter" - as in the first bit of a book. That makes sense. I had a quick look for `pulldown-cmark` compatible libraries to see what would turn up. Unfortunately, there was nothing and I lost quite a bit of time trying to figure out what I was supposed to do. I did some more extensive searching and concluded that I needed to parse the text manually - which was when I came across the library [`yaml-front-matter`](https://github.com/EstebanBorai/yaml-front-matter). This had to be it. I used `cargo add yaml-front-matter` and then quickly checked the GitHub repo. Thankfully, there was a great example showing an example that had both the front-matter block as well as some other text below it - meaning I can use it! I got to work and wrote a function that would comprise of the HTML page head, which would get inserted before the markdown text itself, as well as moving the stylesheet link tag into this function and adding some extra helpful HTML boilerplate: ```rust // src/main.rs #[derive(Deserialize)] struct PageMetadata { title: String, description: String, } fn get_page_header(input: &str) -> String { let document: Document = YamlFrontMatter::parse::(input).unwrap(); let PageMetadata { title, description } = document.metadata; let mut html_output = String::new(); html_output.push_str(""); html_output.push_str( r#" "#); html_output.push_str(&format!( "{title} " )); html_output.push_str(""); html_output } ``` I only added two tags for now as a matter of convenience. I didn't want to spend too much more time on it and I'd already used quite a lot of time just trying to figure out what library to use. Then I added the following to my `hello_world.md` file: ```md --- title: "Hello world!" description: "Hello world!" --- ## Hello world! ``` Then I spun up local run, went to the route and... oh no. It looks like there was just a huge line at the top, with the title and description showing up as h2 tags. That's definitely not what I wanted to happen. I quickly made a new regex pattern to detect the first instance of three hyphens, followed by any characters (including newlines) and then another three hyphens on the handler function. It's not bulletproof, but it'll work for now: ```rust // src/main.rs async fn parser(State(state): State, Path(page): Path) -> impl IntoResponse { let mut file = format!("templates/{page}.md"); let markdown_input = match tokio::fs::read(file).await { Ok(res) => res, Err(_) => return Html("Couldn't find this page - does it exist?".to_string()), }; let string_output = std::str::from_utf8(&markdown_input).unwrap(); let mut html_output = get_page_header(string_output); let regex = regex::Regex::new(r"---((.|\n)*?)---").unwrap(); let res = regex.replace(string_output, ""); let parser = pulldown_cmark::Parser::new(&res); pulldown_cmark::html::push_html(&mut html_output, parser); Html(html_output) } ``` I quickly span up local run again, then went to `/hello_world` and checked everything worked. I only saw a large "Hello world!" on the page. I breathed a sigh of relief - the worst was somewhat over, for now. ## 16:20 - Pages in directories Now that the hard part was over, I could turn my attention to other things: for one, I'd like to be able to serve pages that are in a directory. I quickly cloned the handler function for one route and then augmented it very quickly to be able to accept multiple path arguments: ```rust // src/main.rs async fn parser_dir( Path((dir, page)): Path<(String, String)>, ) -> impl IntoResponse { let file = format!("templates/{dir}/{page}.md"); let markdown_input = match tokio::fs::read(file).await { Ok(res) => res, Err(_) => return Html("Couldn't find this page - does it exist?".to_string()), }; let string_output = std::str::from_utf8(&markdown_input).unwrap(); let mut html_output = get_page_header(string_output); let regex = regex::Regex::new(r"---((.|\n)*?)---").unwrap(); let res = regex.replace(string_output, ""); let parser = pulldown_cmark::Parser::new(&res); pulldown_cmark::html::push_html(&mut html_output, parser); Html(html_output) } ``` Nothing much needed to be changed really - at this point it was just adding extra arguments and then adding them to the filepath for the file to be parsed. I added the route to the Axum router. I remembered I also wanted to make sure that if a directory had an index, that the parser would still be able to reach it - which meant I had to quickly change the error handling for the file reading pattern matching in the original parser handler function: ```rust // src/main.rs async fn parser(State(state): State, Path(page): Path) -> impl IntoResponse { // .. rest of code let markdown_input = match tokio::fs::read(file).await { Ok(res) => res, Err(_) => { file = format!("templates/{page}/index.md"); match tokio::fs::read(file).await { Ok(res) => res, Err(_) => return Html("Couldn't find this page - does it exist?".to_string()), } } }; // .. rest of code } ``` Not much to change besides just trying to read the index file. I also remembered I needed to actually serve `index.md` at the base route - so I quickly replaced the base route function with a similar function to the other handlers, except it's just trying to read `templates/index.md`: ```rust // src/main.rs async fn hello_world() -> impl IntoResponse { let markdown_input = match tokio::fs::read("templates/index.md").await { Ok(res) => res, Err(_) => return Html("Couldn't find this page - does it exist?".to_string()), }; let string_output = std::str::from_utf8(&markdown_input).unwrap(); let mut html_output = get_page_header(string_output, &state.domain); let regex = regex::Regex::new(r"---((.|\n)*?)---").unwrap(); let res = regex.replace(string_output, ""); let parser = pulldown_cmark::Parser::new(&res); pulldown_cmark::html::push_html(&mut html_output, parser); Html(html_output) } ``` ## 10:05 - Navigation, more OG tags At this point, we're going pretty smoothly and things are moving quickly - except for one thing: Navigation. I didn't consider that a user might actually want to consider moving between pages, despite it being such a normal part of using the Internet. At this point, I panicked a little bit and had a quick look at how to iterate through files in a directory with Tokio. I remembered that it returned a stream - so you could use `while let Some...` to retrieve a stream of files and then append them all to a vector. Great! I added the directory parsing to the main function, then added it to an `AppState` struct which was appended to my router: ```rust // src/main.rs #[shuttle_runtime::main] async fn main( #[shuttle_metadata::ShuttleMetadata] metadata: Metadata, ) -> shuttle_axum::ShuttleAxum { let domain = if cfg!(debug_assertions) { "http://localhost:8000".to_string() } else { format!("https://{}.shuttleapp.rs", metadata.project_name) }; let mut files = tokio::fs::read_dir("templates").await.unwrap(); let mut filenames: Vec = Vec::new(); while let Some(file) = files.next_entry().await.unwrap() { let meme = file.file_name().into_string().unwrap(); if meme.ends_with(".md") { filenames.push(meme.replace(".md", "")); } } let state = AppState { domain , filenames}; let router = Router::new() .route("/", get(hello_world)) .route("/:page", get(parser)) .route("/:dir/:page", get(parser_dir)) .route("/styles.css", get(styles)) .with_state(state); Ok(router.into()) } ``` I decided for now to only add pages in the home page - the rest can come later. Now that the filename vector is in the app state, we can use it in our handler functions. An small addition was made to the HTML head function to add access back to the main page: ```rust // src/main.rs fn get_page_header(input: &str, domain: &str) -> String { // .. rest of code html_output.push_str("
"); html_output.push_str("Home")); html_output.push_str("
"); html_output } ``` Then I added the article links to my index route: ```rust async fn hello_world(State(state): State) -> impl IntoResponse { let markdown_input = match tokio::fs::read("templates/index.md").await { Ok(res) => res, Err(_) => return Html("Couldn't find this page - does it exist?".to_string()), }; let string_output = std::str::from_utf8(&markdown_input).unwrap(); let mut html_output = get_page_header(string_output, &state.domain); let regex = regex::Regex::new(r"---((.|\n)*?)---").unwrap(); let res = regex.replace(string_output, ""); let parser = pulldown_cmark::Parser::new(&res); pulldown_cmark::html::push_html(&mut html_output, parser); html_output.push_str("
"); for link in &state.filenames { html_output.push_str(&format!("{link}")); } html_output.push_str("
"); Html(html_output) } ``` I spun up the local server and checked to see if it works or not - it does! I had previously created an index file and the `hello_world` route appear on the navbar. However, the links have no space between each other - which is bad. I made a quick, small addition to the styling file so the site links aren't running headlong into each other: ```css /* templates/styles.css */ #nav { display: flex; justify-content: center; gap: 2em; } #content { display: flex; flex-direction: column; align-items: center; gap: 1em; } ``` Next, I wanted to add the [OpenGraph](https://ogp.me) tag for URL (`og:url`). Thankfully, Shuttle has a helper crate for this thing exactly called [`shuttle-metadata`](https://docs.shuttle.dev/resources/shuttle-metadata) that gives you all the information about your project! I `cargo add`ed it, then added it into my main function and added a variable that forms the domain string based on whether we're in debug or release mode: ```rust // src/main.rs #[shuttle_runtime::main] async fn main( #[shuttle_metadata::ShuttleMetadata] metadata: Metadata, ) -> shuttle_axum::ShuttleAxum { let domain = if cfg!(debug_assertions) { "http://localhost:8000".to_string() } else { format!("https://{}.shuttleapp.rs", metadata.project_name) }; // .. rest of code } ``` Shuttle will always run in release mode during deployment, so we can safely assume that when we deploy, it'll always refer to the correct URL. I added the domain as a variable in the `AppState` struct - we'll pass it in the HTML head function, like so: ```rust let mut html_output = get_page_header(string_output, &format!("{}/{dir}/{page}", state.domain), state.filenames); ``` Then we can simply add another line in our function to add another [OpenGraph](https://ogp.me) tag: ```rust html_output.push_str(format!("); ``` ## 00:40 - Finishing Up At this point, I'm basically done and there isn't much time left so I gave the app a quick `cargo clippy && cargo fmt`. I took a sip of my tea and realised it had gone cold. So much for being able to develop things quickly, I guess. I hammered out `shuttle deploy --allow-dirty` to deploy my program from a dirty Git branch straight to the Shuttle servers. It started compiling and I leaned back and breathed a sigh of relief. ## 00:00 - Retrospective So, I'd finally done it! I wrote a fully working Rust SSG in an hour. It wasn't quite the behemoth I thought it would be to get working, and for my personal blog it would _actually_ be something I'd consider using. However, although we accomplished quite a bit in the alloted time, we could have extended it by doing things that wouldn't have taken too much more time than we have spent (although perhaps adding all of them would maybe double the time!) that could have provided some value: - Adding OG image tags which would have made the app fully compatible with OpenGraph and make it much better for SEO - Make the navbar a bit more flexible - Potentially adding the files in directories to the navigation - Adding a sitemap - Adding a hits counter Despite the CSS being extremely minimal, we still managed to make it look relatively good. It shouldn't go without saying, of course, that real-world software engineering is much more than just creating an application in a vacuum. You have to deal with business logic, technical constraints, and many other things: coding is merely one piece of the puzzle. But for those of us who value being able to get code done quickly, being able to learn how to adapt to and use new things quickly is a valuable tool. Thanks for reading! I hope this has served as a helpful tutorial on how to make a static site generator in Rust if you're also looking to make something very quickly for your own website. Interested in Shuttle? Make sure to [give us a star on GitHub!](https://www.github.com/shuttle-hq/shuttle) --- # Rust for JavaScript Developers: An Overview of Testing Source: https://www.shuttle.dev/blog/2023/11/08/testing-in-rust Date: 8 November 2023 Author: josh Tags: rust, testing, guide This article explores how you can test a web application in Rust and compares it to the way you would do it in JavaScript - covering unit tests, integration tests and API testing as well as mocking. Whether you're a new web developer or more experienced senior developer, testing is a key component of being able to make sure your web applications are bug-free and work as intended. Although at first you might only want to do manual testing (for example if you're quickly hacking something together), in more robust work pipelines where your application may have some paid users that depend on the service, testing can be a great tool to assist with automation and saving time (especially with regards to more complicated workflows). This article will describe and compare how testing is done in JavaScript, and then follow with how to test in Rust. Jest will be primarily used for comparison. If you plan on following the Rust parts, you may wish to install Rust [here.](https://www.rust-lang.org/tools/install) ## Test Setup In JavaScript, testing is typically something that you either do manually (by just opening the webpage and testing that the functionality you implemented works) or through a specific JavaScript testing framework - two of the most popular ones being Mocha and Jest, with Jest in particular originally being a tool created for testing React but being able to be run other frameworks, such as Express or whatever you need and Mocha being a more general testing framework for Node.js. As you may know (or not!) already, you will generally need to put your tests in their own separate files for tests in JavaScript to run, then use `mocha` or `jest` depending on which library you're using - which then runs all of the tests. Best practice dictates that you put your test file next to wherever the actual file is (switching out the ".js" at the end for ".test.js"). Specific frameworks and libraries like React may also have their own testing facilities - for example, you can use React Testing Library if you want to test React. Your file/folder setup might look like this: ![A picture of file/folder setup for JS testing](/images/blog/testing-article-screenshots/javascript.png) In comparison when it comes to using Rust, tests are an inbuilt part of the language tooling via Cargo, the language's build system and package manager. You don't need any external crates (libraries) - you just use `cargo test` and if there's any tests, it will run them. You can either place tests within the same files as your code, or you can create your own tests folder and run the tests from there. Either way, `cargo test` will recognise and run it. You can also specify whether the tests are unit tests or integration tests. A test folder for Rust might look like this: ![A picture of file/folder setup for Rust testing](/images/blog/testing-article-screenshots/rust.png) Although the language tooling itself has good testing capabiliities, we can take this one step further by using packages like `cargo-nextest`, a test runner CLI for Rust projects that builds on top of the already-existing test capabilities to greatly improve the user experience when it comes to identifying slow and leaky tests (meaning tests which are either slow, or create things during the tests that are not cleaned up after), test speed improvements and Continuous Integration pipeline compatibility. You can get started with it by using the following: ```bash cargo install cargo-nextest ``` When it's installed, you can use `cargo nextest run` and it will run all tests within a workspace. Interested? Find more about cargo-nextest [here.](https://nexte.st/) Although we will primarily be focusing on the Rust standard testing capabilities for ease of use, there are also quite a few helper libraries for testing; for example, `test-case` which helps you build up test cases for tests by providing macros that you can stack on top of a test function and `lets_expect` which provides a macro in the form of `lets_expect!()` to help you test faster and more ergonomically. Some Rust crates may also have their own testing library, in addition to this. Backend web frameworks in particular have their own Rust testing crates that let you do things like calling specific handler functions by themselves, so that you don't have to mock the entire API - we'll discuss this later on in the article. ## Unit Testing With regards to unit testing, there is not a huge amount of difference between JavaScript and Rust. You import what files you need, and then run them. For example, if you have a file called `sum.js` with the following JS unit test: ```javascript // sum.js // This function we're testing adds two numbers together function sum(a, b) { return a + b; } module.exports = sum; ``` Then you would create a file called `sum.test.js`, which contains our actual test: ```javascript // sum.test.js const sum = require("./sum"); test("adds 1 + 2 to equal 3", () => { expect(sum(1, 2)).toBe(3); }); ``` As you can see, we've imported the function from our other file then added a simple test that expects the function with some given parameters to be equal to another given value. Then once we're done with whatever tests we want to add, we then run `jest` and the test should pass. Want to group tests together? You can do that pretty easily! Just use `describe` to create a group of tests, then insert the tests you want to make like so: ```javascript describe("matching cities to foods", () => { // Applies only to tests in this describe block beforeEach(() => { return initializeFoodDatabase(); }); test("Vienna <3 veal", () => { expect(isValidCityFoodPair("Vienna", "Wiener Schnitzel")).toBe(true); }); test("San Juan <3 plantains", () => { expect(isValidCityFoodPair("San Juan", "Mofongo")).toBe(true); }); }); ``` As mentioned before, you can group both your regular code and Rust unit testing together. Typically, this is used for things that are not declared as public, but if you don't want to put your tests in a tests folder, you can also add your tests this way. You can put your files and code together like this: ```rust // lib.rs // for reference: the "usize" type means any non-negative integer pub fn add(left: usize, right: usize) -> usize { // because Rust implicitly returns, no explicit "return" phrase is required left + right } // this is a required annotation to set up tests #[cfg(test)] mod tests { // this means "import everything" use super::*; // this macro indicates this function is for a test #[test] fn it_works() { let result = add(1, 2); // assert_eq! is a macro we can use to ensure one value is exactly equal to another value assert_eq!(result, 3); } } ``` In comparison to Jest, Rust doesn't have test grouping enabled by default in the standard library although you can enable similar tests for behaviour-driven development if you use [rspec](https://github.com/rust-rspec/rspec), which is a framework that supports this kind of testing. In terms of organising your integration tests, normally if you have a lot of different integration tests you can split them into separate files if you want to separate them by user behaviour, what part of a program it's testing or any other criteria. You can find more about this [here.](https://doc.rust-lang.org/book/ch11-03-test-organization.html#submodules-in-integration-tests) ## Setup and Teardown Sometimes, you might need to do some work before or after every test: with testing in JavaScript via Jest, this is very simple to do. You need to create functions that carry out what you need to do, then either attach them to a `beforeEach` or `afterEach` statement, depending on what you want to do. In JS testing with Jest, this is quite simple: ```javascript async function initialiseCityDatabase { // ... your code for setting up the test database here } async function clearCityDatabase { // ... your code for resetting the test database here } // this runs before each test beforeEach(() => { initializeCityDatabase(); }); // this runs after each test afterEach(() => { clearCityDatabase(); }); ``` In Rust, although there is no such thing provided by the default testing functionality, you can still run a setup and teardown function so that you can delete any artefacts created by the tests (for example, SQL records that may have been left over in the test database). Below is an example of what this might look like: ```rust // the PgPool type is a Postgres connection pool type provided by SQLx, a Rust SQL crate async fn setup_database() -> sqlx::PgPool { // ... your code to set the database up } async fn teardown_database(db: sqlx::PgPool) { // ... your code to reset the database } #[test] async fn do_stuff() { let database = setup_database().await; // ... do some stuff and assertions teardown_database(database).await; } ``` You can also use libraries like [test-context](https://docs.rs/test-context/latest/test_context/) to be able to write your setup and teardown, which makes it much easier as you can use a macro on top of a test function and you can also implement test contexts for multiple structs. A small snippet from the library's documentation follows below: ```rust use test_context::{test_context, TestContext}; // declare a struct struct MyContext { value: String } // implement the "TestContext" trait for said struct by implementing the functions the trait provides impl TestContext for MyContext { fn setup() -> MyContext { MyContext { value: "Hello, world!".to_string() } } fn teardown(self) { // Perform any teardown you wish. } } // add the test_context macro here #[test_context(MyContext)] #[test] fn test_works(ctx: &mut MyContext) { assert_eq!(ctx.value, "Hello, world!"); } ``` ## Mocking Don't worry, there will be no insults will be flying around here! Mocking is a practice commonly used with unit testing where you create a mock of an object (as much as is required) for the purpose of using it in a test. The goal of mocking is to isolate a unit of code away from things that would normally use or create said code. Testing in isolation helps to determine whether or not a certain function by itself is correct or not. Do note for this section that "fakes" and "mocks" are not the same thing - fakes are functional versions of mocks that attempt to replicate the real thing that you want to use. You don't always need mocks, but you might need fakes! With JavaScript testing, you can create a mock function to be used in test code, or you can write a manual mock that overrides a module dependency (although typically, you'll be doing more of the first one unless you have a specific reason as to why you need to override the module dependency). ```javascript // forEach.js // this function takes an array and function, // then applies the callback function to each item in the array export function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } } // forEach.test.js const forEach = require("./forEach"); const mockCallback = jest.fn((x) => 42 + x); test("forEach mock function", () => { forEach([0, 1], mockCallback); // The mock function was called twice expect(mockCallback.mock.calls).toHaveLength(2); // The first argument of the first call to the function was 0 expect(mockCallback.mock.calls[0][0]).toBe(0); // The first argument of the second call to the function was 1 expect(mockCallback.mock.calls[1][0]).toBe(1); // The return value of the first call to the function was 42 expect(mockCallback.mock.results[0].value).toBe(42); }); ``` As you can see above, by using mocking you can get extra information about a function for testing purposes - for example, how many times a function was called or how many times a mock was created. Now let's talk about mocking modules. Jest is able to mock dependencies and modules for testing - but what does this mean in practice? Let's say you need to use a real API or have an API you're building that is supposed to have some data that your web app is supposed to call normally. You can mock a dependency like `axios` to be able to model fake data from the API and then test your code using the fake data mock! Let's have a look at what this would look like below: ```javascript // users.js import axios from "axios"; class Users { static all() { return axios.get("/users.json").then((resp) => resp.data); } } export default Users; // users.test.js import axios from "axios"; import Users from "./users"; jest.mock("axios"); test("should fetch users", () => { const users = [{ name: "Bob" }]; const resp = { data: users }; axios.get.mockResolvedValue(resp); // or you could use the following depending on your use case: // axios.get.mockImplementation(() => Promise.resolve(resp)) return Users.all().then((data) => expect(data).toEqual(users)); }); ``` This is not all of course - you can also mock a subset of a module (a "partial") and then have the rest of the module keep its regular implementation. In Rust, mocking doesn't require any special libraries can be done quite easily through creating custom implementations of any traits you want in a test to get the same behaviour as mocking. Traits in Rust are groups of methods defined for a particular type, which you can implement for a struct or enum. You can read more about this [here.](https://doc.rust-lang.org/book/ch10-02-traits.html). Additionally, there are also several libraries that aim to provide assistance with mocking but one of the most popular ones we'll be looking at is `mockall`. If you want to try `mockall` for yourself and have Rust installed, you'll want to spin up a new Rust project, navigate to the project folder and run the following: ```bash cargo add mockall ``` Let's have a look at how you can create a mock that implements multiple traits: ```rust use mockall::*; use mockall::predicate::*; trait MyTrait { fn foo(&self) -> u32; fn bar(&self, x: u32) -> u32; } trait MySecondTrait { fn baz(&self) -> u32; fn boo(&self, x: u32) -> u32; } // mock macro from mockall mock! { pub MyStruct {} // implementing impl MyTrait for MyStruct { fn foo(&self) -> u32 { 1u32 } fn bar(&self, x: u32) -> u32 { x + 1 } } impl MySecondTrait for MyStruct { fn baz(&self) -> i32 { 1i32 } fn boo(&self, x: i32) -> i32 { x + 1 } } } let mut mock = MockMyStruct::new(); ``` As you can see above, we've now implemented two traits on a single struct that we can now use for any kind of testing we want to do! That's not all however. We can also modify the behaviour of our mock object's behaviour, through using the methods on our `mock` variable to return a specific value: ```rust let mut mock = MockMyStruct::new(); // for reference: the return value is 44, but adding u32 specifies that it should be a u32 type mock.expect_foo() .return_const(44u32); ``` Now whenever we use `mock.foo()`, the answer will always return 44 and it will always return a type of u32 (unsigned 32-bit integer). This is great for us as it means we can mock any kind of dependency we want and can be used similarly to the example we used before for the `axios` mock where we mocked up some data and then created some tests around the provided data. ## Integration Testing Integration testing (otherwise known as end-to-end testing) is a more general kind of testing to test how functions integrate together by using them in the same test. Initially, you might do this manually (for example, testing out registering to a website and then logging in) but eventually at some point you're going to want to automate testing the overall functionality of your application, especially as you start getting more and more functions. In JavaScript, how you'll approach this depends on what libraries you're using. Are you using React? You'll need Jest along with React Testing Library. Are you using Express? There's a number of ways you can do it - but for the purposes of this example, let's assume you already have an Express.js application that you want to test using Jest: ```javascript const express = require("express"); const app = express(); const port = 3000; app.get("/", (req, res) => { res.send("Hello World!"); }); app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); ``` You would ideally want to install `supertest` to make it easier to test, by using `npm i --save-dev supertest`. Your `package.json` file would want to have this added in: ```javascript "scripts": { // ... your other scripts "test": "jest", // ... your other scripts }, "jest": { "testEnvironment": "node", "coveragePathIgnorePatterns": [ "/node_modules/" ] }, ``` Then you serve a single test (or however many you want) from your test file: ```javascript // server.test.js const request = require("supertest"); const app = require("../server"); describe("Testing routes", () => { it("should return Hello World", async () => { const res = await request(app).get("/").send(); expect(res.statusCode).toEqual(200); }); }); ``` As you can see, using `supertest` with `jest` boosts its capabilities considerably by allowing it to expect things from the response - for example, the status code or what the body contains. This makes it much, much easier to test your backend and makes the testing experience much better. With regards to Rust integration tests, although there's no default helper library you can still write integration tests by just writing tests that utilise many parts or functions from your program together. For example, you might have some code to setup an SQL database instance, then an integration test that adds and manipulates some data that's contained in your database and finally the teardown code to reset your test database. ```rust // tests/common/mod.rs pub fn setup() { // some setup code, like creating required files/directories, starting // servers, etc. } ``` ```rust // tests/integration_test.rs // importing common module. mod common; #[test] fn test_add() { // using common code. common::setup(); assert_eq!(adder::add(3, 2), 5); } ``` Although how you test a server generally depends on what web framework you're using, some web frameworks are built from the ground up to be made easier to test. For example, in `axum` which has extremely strong compatibility with `tower` (a set of utilities for robust networking clients), you can simply just initialise your `axum::Router` then send a oneshot request to it. We can see an example of this below. Let's say we have a `main.rs` file, which has a basic Axum router (see codeblock comments for explanations): ```rust // main.rs // import functions from dependencies to bring them into scope use axum::Router; use axum::routing::get; use axum::http::{Request, StatusCode}; use std::net::SocketAddr; // a handler function that simply returns "Hello world!" async fn hello_world() -> &'static str { "Hello world!" } // this function returns a Router type that uses the hello_world handler with GET method fn init_router() -> Router { Router::new().route("/users", get(hello_world)) } #[tokio::main] async fn main() { let router = init_router(); // parse the socket address from a tuple that contains an IPv4 array plus port let addr = SocketAddr::from(([0,0,0,0], 8000)); // serve the router at the socket address axum::Server::bind(&addr).serve(router.into_make_service()).await.unwrap(); } ``` We could then set up the tests like this - in this case we could probably just set it up in the same file as the `main.rs` file: ```rust #[cfg(test)] mod tests { use super::*; // we use the Tokio test macro here - using the regular one will not work // as this test requires async functionality to work // you will need the "macros" feature enabled for Tokio to use this #[tokio::test] async fn it_works() { let app = init_router(); let response = app // send a request to the root endpoint with an empty body .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) .await .unwrap(); // assert the response has 200 OK status assert_eq!(response.status(), StatusCode::OK); let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); // assert the response body assert_eq!(&body[..], b"Hello, World!"); } } ``` You can also assert things like cookies, iterating through cookies, headers, checking the response status and so on and so forth. You can also choose to save or not save cookies, clearing headers - everything you need to be able to reliably test your web service. You can check out more about the `TestResponse` methods [here.](https://docs.rs/axum-test/latest/axum_test/struct.TestResponse.html#) We also can carry out black-box API testing if our program or web application is reliant on usage of an external API and you don't want to trigger API calls. [Wiremock](https://github.com/LukeMathWalker/wiremock-rs), a crate originally created by Luca Palmieri who wrote [Zero to Production in Rust](https://www.zero2prod.com/index.html?country=the%20UK&discount_code=VAT20&country_code=GB), is a great way to do this. It's quite simple to use and works by creating a lightweight HTTP server that you can then use for any kind of testing that you want to use; for example, you can mock an API that returns some data, then use that data in a real function that you want to test. Let's see how this works. We will start a `MockServer`, mount a route to it then use `surf` (a HTTP request library) to send a HTTP request to our mock server, which we will then use `assert_eq!` on to make sure that the status code is 200 OK. See the example below: ```rust // main.rs // import items for use, bringing them into scope use wiremock::{MockServer, Mock, ResponseTemplate}; use wiremock::matchers::{method, path}; // the tokio main macro is what allows async main functions in Rust // see the Tokio library documentation for more information #[tokio::main] async fn main() { // Start a background HTTP server on a random local port let mock_server = MockServer::start().await; // Arrange the behaviour of the MockServer adding a Mock: // when it receives a GET request on '/hello' it will respond with a 200. Mock::given(method("GET")) .and(path("/hello")) .respond_with(ResponseTemplate::new(200)) // Mounting the mock on the mock server - it's now effective! .mount(&mock_server) .await; // If we probe the MockServer using any HTTP client it behaves as expected. let status = surf::get(format!("{}/hello", &mock_server.uri())) .await // "unwrap" means get the value or abort and terminate the process .unwrap() .status(); assert_eq!(status.as_u16(), 200); // If the request doesn't match any `Mock` mounted on our `MockServer` a 404 is returned. let status = surf::get(format!("{}/missing", &mock_server.uri())) .await .unwrap() .status(); assert_eq!(status.as_u16(), 404); } ``` As you can see, this is pretty easy! ## Finishing Up Thanks for reading this article! I hope you have gained a better understanding of testing in Rust vs JS and maybe a little bit of knowledge about setting up a basic web router in Rust using Axum. Looking for more info? Check out some of our other articles: - Check out our docs page for the fundamentals on writing an Axum web service [here.](https://docs.shuttle.dev/tutorials/rest-http-service-with-axum) - New to Rust? Check out [Shuttle Launchpad](https://www.shuttle.dev/launchpad), our free newsletter for learning Rust! If this article helped you, feel free to [give us a star on GitHub!](https://www.github.com/shuttle-hq/shuttle) --- # htmx, Rust & Shuttle: A New Rapid Prototyping Stack Source: https://www.shuttle.dev/blog/2023/10/25/htmx-with-rust Date: 1 November 2023 Author: josh Tags: rust, htmx, guide This article details how htmx with Rust and Shuttle can speed up your workflow and let you focus on the code with the assistance of Axum and Askama. When it comes to Rust, although it's lauded as a language that is memory-safe, blazing fast and efficient, it's also known for having a compiler that will complain at you for everything (hence terms like "compiler-driven development" becoming a thing) and complex trait bounds that can getting it just right take time. In this article, we'll talk about tools that you can use to speed up your workflow: htmx with a templating engine and the web framework Axum (and of course, Shuttle!). htmx is a JavaScript library designed to help you ship faster by allowing you to call endpoints from HTML elements instead of being required to do it manually which when combined with a HTML templating engine makes prototyping extremely quick - and we don't need to set anything up to do it, only being required as a minimum to use the CDN script (although we can also use it as an npm package). Shuttle allows you to move quickly by declaratively provisioning infrastructure like databases, key-value stores and more as main function parameters using the Shuttle runtime, letting you prototype new projects extremely quickly when used with htmx. ## Using Shuttle Shuttle is a service designed to make deployment as easy as possible, by provisioning a runtime that lets you add macros (or "annotations") as function arguments to your entrypoint function. The runtime will then do static code analysis to figure out what needs provisioning and will then spin up the relevant infrastructure required - for example, if you need a Postgres instance, you can just declare it in your `fn main` arguments, use `cargo shuttle run` to run locally and then it'll spin up a container for you using Docker without any further input on your part! By using Shuttle, we can turn this: ```rust #[tokio::main] async fn main() { let sqlx_connection = PgPoolOptions::new().connect("your-addr-here").await.unwrap(); let router = Router::new().route("/", get(hello_world)).layer(Extension(Arc::new(sqlx_connection))); let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); axum::Server::bind(&addr).serve(router.into_make_service()).await.unwrap() } ``` to this: ```rust #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] sqlx_connection: PgPool ) -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/", get(hello_world)).layer(Extension(Arc::new(sqlx_connection))); Ok(router.into()) } ``` Once you're done writing code, all you need to do is use `cargo shuttle deploy` (with the `--allow-dirty` flag if on a Git branch with uncommitted changes) and when it's done deploying, you should get a link to see your website! If you need to check your database connection string again, you can also use `cargo shuttle resource list` to quickly check it. ## Using HTMX To start off with, we want a `base.html` file that includes the head - which we'll add htmx to through the CDN. ```html Index {% block head %}{% endblock %}
{% block content %}

Placeholder content

{% endblock %}
``` We are also using Askama, which is a Rust HTML templating crate, with htmx. If you've ever used Python before, you might notice the syntax is quite similar to Jinja2 templates. Jinja2 is a web templating engine that describes itself as a "fast, expressive and extensible web templating engine" that's been around for quite a while and is a well known format given how many copies there are of libraries, inside and outside of Rust, that emulate Jinja2 syntax. Interested in learning more about Askama? Our new recent Shuttle Launchpad issue talks about it [here.](https://www.shuttle.dev/launchpad/issues/2023-10-17-issue-10-Serving-HTML) Now let's make our `index.html` file: ```html {% extends "base.html" %} {% block content %}

Shuttle Todos

Loading...
{% endblock %} ``` As you can see, we are extending the `base.html` file and then declaring a block called `content` - this is where we put our HTML that we want to add. As a simple example, we've added a form to add a new todo, as well as a placeholder div with an ID of "list". For the next part, we'll want to have some of our HTML already written, so let's do that now: ```html
{% for todo in todos %} {% include "todo.html" %} {% endfor %}
ID Description Delete
``` ```html {{ todo.id }} {{ todo.description }} ``` As you can see, we've included a button with the `todo` component that makes a DELETE request to the `/todos/:id` route, but it targets the whole row and just deletes the row after the API call is done, which saves time having to manually delete the component from the DOM. ## Making API calls htmx allows you to make an API call without explicitly writing JavaScript for it, by allowing you use HTML attributes instead. When you make an API call with htmx, the library requires you to return HTML as a response - which is great for us because we can combine it with Askama templating so that we don't have to go through the hassle of trying to create a whole new element through pure JavaScript and then appending it to whatever element we choose. Let's take the form from above as an example: ```html
``` The button makes a POST request to `/todos`, triggered by clicking the button, targets the HTML element with an id of "todos-content" and places the resulting HTML as the last element within the target element. As you can see, this speeds up development speed quite a lot! Not having to set up an opinionated framework and being able to quickly write things with a HTML templating engine makes things a lot quicker. ## Streams and Server Sent Events with htmx Being able to quickly mock up a CRUD app with htmx is great. However, Server Sent Events (SSE) and Websockets are also important functions for web applications and web services to work. Thankfully, htmx natively supports both. We can look at a basic mockup of receiving SSE with htmx by creating a new channel in our main function, then appending it as an Extension to our main function, as well as creating the necessary structs we need for the messages we're going to send through the channel: ```rust // src/main.rs #[derive(Clone, Serialize, Debug)] enum MutationKind { Create, Delete, } #[derive(Clone, Serialize, Debug)] pub struct TodoUpdate { mutation_kind: MutationKind, id: i32, } #[shuttle_runtime::main] async fn main(#[shuttle_shared_db::Postgres] db: PgPool) -> shuttle_axum::ShuttleAxum { sqlx::migrate!() .run(&db) .await .expect("Looks like something went wrong with migrations :("); let (tx, rx) = channel::(10); let state = AppState { db }; let router = Router::new() .route("/", get(home)) .route("/stream", get(stream)) .route("/todos", get(fetch_todos).post(create_todo)) .route("/todos/:id", delete(delete_todo)) // new handler - we will make this later .route("/todos/stream", get(handle_stream)) .with_state(state) .layer(Extension(tx)) .layer(Extension(rx)); Ok(router.into()) } ``` Now we want to send a message from our `create_todo` and `delete_todo` handlers through our channel - we can add them by including our extension in the function signature, then after a successful SQL transaction we simply send a message to the channel: ```rust // src/main.rs type TodosStream = Sender; async fn create_todo( State(state): State, Extension(tx): Extension, Form(form): Form, ) -> impl IntoResponse { let todo = sqlx::query_as::<_, Todo>( "INSERT INTO TODOS (description) VALUES ($1) RETURNING id, description", ) .bind(form.description) .fetch_one(&state.db) .await .unwrap(); if let Err(e) = tx.send(TodoUpdate { mutation_kind: MutationKind::Create, id: todo.id }) { eprintln!("Tried to send log of record with ID {} created but something went wrong: {e}", todo.id); } TodoNewTemplate { todo } } async fn delete_todo( State(state): State, Path(id): Path, Extension(tx): Extension, ) -> impl IntoResponse { sqlx::query("DELETE FROM TODOS WHERE ID = $1") .bind(id) .execute(&state.db) .await .unwrap(); if let Err(e) = tx.send(TodoUpdate { mutation_kind: MutationKind::Delete, id }) { eprintln!("Tried to send log of record with ID {id} created but something went wrong: {e}"); } StatusCode::OK } ``` Now we need to implement the stream handler, which we can do by creating a `BroadcastStream` and then mapping our stream to Axum SSE events: ```rust use std::convert::Infallible; use tokio_stream::{Stream, StreamExt as _}; use std::time::Duration; use tokio::sync::broadcast::{channel, Sender, Receiver}; use axum::{ http::StatusCode, response::{sse::Event, IntoResponse, Sse, Response}, Extension }; use serde_json::json; pub async fn handle_stream( Extension(tx): Extension, Extension(rx): Extension> ) -> Sse>> { let stream = BroadcastStream::new(rx); // map the stream to axum Events which get sent through the SSE stream Sse::new( stream .map(|msg| { let msg = msg.unwrap(); // wrap the message in HTML because htmx expects a HTML fragment response let json = format!("
{}
", json!(msg)); Event::default().data(json) }) .map(Ok), ) .keep_alive( axum::response::sse::KeepAlive::new() .interval(Duration::from_secs(600)) .text("keep-alive-text"), ) } ``` Then in our HTML, we will want to add an element that looks like this: ```html
``` When you have the `hx-sse` HTML element and then add or delete an item from the main page, you will then see a log of what ID the record had and what the action was. The item will get appended to the inner div without any other input from our side! ## Finishing Up Thanks for reading! I hope this guide to using htmx with Rust has helped you get a better insight into why it's currently rising in popularity at the moment. htmx is a great library that can be taken to new heights by using it in conjunction with HTML templating in Rust. Did this article help you? Feel free to [give us a star on GitHub!](https://www.github.com/shuttle-hq/shuttle) You can find the article code [here.](https://github.com/shuttle-hq/shuttle-examples/tree/main/axum/htmx-crud) --- # Using GraphQL in Rust Source: https://www.shuttle.dev/blog/2023/10/16/graphql-in-rust Date: 16 October 2023 Author: josh Tags: rust, graphql, sql, guide This article details how to build a GraphQL server in Rust. We will explore using queries, mutations as well as subscriptions to make a fully working endpoint. ## Introduction When it comes to writing an API, sometimes you might have several data sources and want to coalesce them into one easy-to-query API on the frontend. This is where GraphQL comes in: an query language made for APIs and declarative data fetching (you only query what you want). Here are some advantages that GraphQL can bring to your Rust web application: - Test your queries out in real-time via the GraphQL playground - Makes it much easier for your frontend to query your backend - You can use any data source In this example, we will use GraphQL through the `async-graphql` Rust crate as an Axum endpoint with an SQL data source and we'll be creating an API that can create, update, and delete a table of records about dogs, as well as subscribing to any updates. Stuck or want to know what the final code looks like? [You can find the repository here.](https://github.com/joshua-mo-143/shuttle-axum-gql-ex) ## Getting Started You'll want to initiate a new Shuttle project (requires `cargo-shuttle`): ```bash shuttle init ``` For this article we'll be using the project name "graphql-example". When the CLI asks you what framework you want, pick Axum. Next, you'll want to make a migrations schema file like so in the root of your project: ```sql schema.sql CREATE TABLE IF NOT EXISTS dogs ( id serial primary key, name TEXT NOT NULL, age INT NOT NULL, ); ``` Now you'll want to install the required dependencies. We can do this with a one-line command: ```bash cargo add async-graphql async-graphql-axum async-stream axum futures-channel futures-core \ futures-util once-cell shuttle-axum shuttle-runtime shuttle-shared-db slab \ sqlx tokio tokio-stream --features \ shuttle-shared-db/postgres,sqlx/postgres,\ sqlx/runtime-tokio-native-tls,tokio/sync,tokio-stream/sync ``` ## Setting up GraphQL At the very minimum, we'll want to create an endpoint that serves the GraphQL playground so we can quickly try queries out, and then a basic "Hello world!" query in GraphQL. Let's have a look at how this would look in code: ```rust src/queries.rs use sqlx::PgPool; use async_graphql::{context::Context, Object}; pub struct Query; #[Object] impl Query { async fn howdy(&self) -> &'static str { "partner" } } async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() .endpoint("/") .subscription_endpoint("/ws") .finish(), ) } #[shuttle_runtime::main] pub async fn axum( #[shuttle_shared_db::Postgres] db: PgPool ) -> Router { pool.execute(include_str!("../schema.sql")) .await .context("Failed to initialize DB")?; let schema = Schema::build(EmptyQuery, EmptyMutation, EmptySubscription) .data(db) .finish(); // start the http server let router = Router::new() .route( "/", get(graphiql).post_service(GraphQL::new(schema.clone())), ); Ok(router.into()) } ``` If we use `shuttle run` to load up our program and go to `http://localhost:8000`, we should see the GraphQL playground. Clicking the Queries on the left hand side will show us all the queries we can run - there should be one called "howdy" (which corresponds to the function we wrote under the Query implementation). You can verify it works by running it. Now we're ready to get started on queries, mutations and subscriptions! ## Queries Although a simple "hello world" query shows how basic data fetching works, we probably want to figure out how to do more complicated queries: for example, returning some records from our SQL data source. Let's change our `impl Query` to include a method for getting a list of Dogs: ```rust src/queries.rs // the records we want to return - the struct currently reflects the schema of the table // however if you you don't want to return everything, you can change it accordingly #[derive(sqlx::FromRow, Clone, Debug)] pub struct Dog { pub id: i32, name: String, age: i32, } #[Object] impl Query { async fn howdy(&self) -> &'static str { "partner" } async fn dogs(&self, ctx: &Context<'_>) -> Result>, String> { // unwrap the database value that we passed as data into the GraphQL builder // if there's an error, just return the error let db = match ctx.data::() { Ok(db) => db, Err(err) => return Err(err.message.to_string()), }; // write an sql query to grab all the fields we need // change the SQL query accordingly if you don't need it let res = match sqlx::query_as::<_, Dog>("SELECT * FROM dogs") .fetch_all(db) .await { Ok(res) => res, Err(err) => return Err(err.to_string()), }; Ok(Some(res)) } } ``` As you can see, we've written a function that returns the vector of structs. Because we're using `query_as`, it automatically binds the query results to the structs so we don't have to worry about mapping it out - you can find more about this [here](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust). What about if we want to query only specific rows based on filter criteria? We will want to make sure to add a description on each of the parameters so that when anyone visits our GraphQL playground, they'll be able to understand what each of the parameters actually does - then in our SQL query, we will want to filter conditionally based on what parameters have been filled in: ```rust src/queries.rs async fn dogs(&self, ctx: &Context<'_>, #[graphql(desc = "Filter by specific ID")] id: Option, #[graphql(desc = "Filter by specific name")] name: Option, #[graphql(desc = "Filter by exact age")] age: Option ) -> Result>, String> { // unwrap the database value that we passed as data into the GraphQL builder // if there's an error, just return the error let db = match ctx.data::() { Ok(db) => db, Err(err) => return Err(err.message.to_string()), }; // note that we use a CASE for SQL - this is like a "switch case" or pattern match let res = match sqlx::query_as::<_, Dog>("SELECT * FROM dogs WHERE (CASE when $1 is not null then (id = $1) else (id = id) end) AND (CASE WHEN $2 is not null then (name = $2) else (name = name) end) AND (CASE when $3 is not null then (age = $3) else (age = age) end) ") .bind(id) .bind(name) .bind(age) .fetch_all(db) .await { Ok(res) => res, Err(err) => return Err(err.to_string()), }; Ok(Some(res)) } ``` Ideally, we want to be able to use GraphQL to extract data out of it by only calling specific fields. Now that we've retrieved our records, we can write an `impl` for our Dog struct, like so (make sure to attach the `#[Object]` macro so it gets picked up by GraphQL!): ```rust src/queries.rs #[Object] impl Dog { async fn id(&self) -> i32 { self.id } async fn name(&self) -> String { self.name.clone() } async fn age(&self) -> i32 { self.age } } ``` If you run `shuttle run` and go to `http://localhost:8000`, you'll be able to see that if you click on Queries on the left-hand side, it'll let you use `dogs` as a query. ## Mutations Now for the next part: mutations! Mutations in GraphQL are methods for changing our data through GraphQL. To use a mutation, we need to create a unit struct (for this article we'll call it `Mutation`) and create an `impl` for it with the `#[Object]` macro, just like with the GraphQL queries. ```rust src/mutations.rs pub struct Mutation; #[Object] impl Mutation { async fn create_dog(&self, ctx: &Context<'_>, name: String, age: i32) -> Result { let db = match ctx.data::() { Ok(db) => db, Err(err) => return Err(err.message.to_string()), }; let res = match sqlx::query_as::<_, Dog>( "INSERT INTO dogs (NAME, AGE) VALUES ($1, $2) RETURNING id, name, age", ) .bind(name) .bind(age) .fetch_one(db) .await { Ok(res) => res, Err(err) => return Err(err.to_string()), }; Ok(res.id) } } ``` As you can see, it's practically the same as if we just did it normally in SQL - we grab the SQL connection and insert the record, then return the ID. We'll also want to be able to only update certain parameters - for example, if a dog's name needs to be updated but not their age. We learned about how we can use optional parameters in `async-graphql`, and we can write the function like so: ```rust src/mutations.rs #[Object] impl Mutation { // ... your other functions async fn update_dog(&self, ctx: &Context<'_>, #[graphql(desc = "New name value to update to")] name: Option, #[graphql(desc = "New age value to update to")] age: Option, #[graphql(desc = "(REQUIRED) The ID of the record to update")] id: i32) -> Result { let db = match ctx.data::() { Ok(db) => db, Err(err) => return Err(err.message.to_string()), }; let res = match sqlx::query_as::<_, Dog>( "UPDATE dogs SET NAME = (CASE when $1 IS NOT NULL THEN $1 ELSE name END), AGE = (CASE when $2 IS NOT NULL THEN $2 ELSE age END) WHERE id = $3 RETURNING id, name, age", ) .bind(name) .bind(age) .bind(id) .fetch_one(db) .await { Ok(res) => res, Err(err) => return Err(err.to_string()), }; Ok(res.id) } } ``` Similarly, we can do it exactly the same way for delete functions. ## Subscriptions Subscriptions in GraphQL are a way of subscribing to changes - for example, when a new record gets created or updated, you might want a way for your users to know about or get real time updates. Subscriptions in this respect are similar to PostgreSQL Listen/Notify functions which you can use to listen and notify updates through channels in Postgres - which if you don't know about yet, which you can read more about [here](https://www.shuttle.dev/blog/2023/10/04/sql-in-rust#postgresql-listennotify). In `async-graphql`, subscriptions are types that implement `futures_util::Stream` and always return an `impl Stream`; that is to say, the type we're returning needs to implement `Stream` so that the compiler knows that the type can return a stream of data. The most common way to do this is through types that wrap channel Senders/Receivers, and we will show how to do this below. We can get started by defining some types: ```rust src/broker.rs use std::{ any::{Any, TypeId}, collections::HashMap, sync::Mutex, }; use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; use once_cell::sync::Lazy; use slab::Slab; // a HashMap wrapped in the Arc> pattern (which makes it thread safe) // a Lazy once_cell here is used to allow initialisation only on first access // once_cell allows us to get a shared reference to inner without requiring a Mutex guard or Ref static SUBSCRIBERS: Lazy>>> = Lazy::new(Default::default); // slab is used here for allocation purposes // we want to make the type generic so we can send anything we want across struct Senders(Slab>); // this will be the type that gets sent back to the HTTP client on successful subscription struct BrokerStream(usize, UnboundedReceiver); // PhantomData is a type that allows us to act as if the broker type can own the type // without PhantomData we can't add the generic pub struct SimpleBroker(PhantomData); ``` The `BrokerStream` struct doesn't get added to the Subscribers hashmap itself, but is returned to the users. When users subscribe to the GraphQL subscription, we create a channel with a Sender/Receiver and then insert the `Sender` into the subscribers list while returning the receiver to the HTTP client. Next, we'll want to set up the methods needed for our stuff to work. Let's start with retrieving the list of senders from the HashMap: ```rust fn with_senders(f: F) -> R where T: Sync + Send + Clone + 'static, F: FnOnce(&mut Senders) -> R, { // get access to the subscribers hashmap let mut map = SUBSCRIBERS.lock().unwrap(); // using .or_insert_with() ensures we can insert a value if .entry() returns nothing // ie, if there's nobody connected to the GraphQL subscription let senders = map .entry(TypeId::of::>()) .or_insert_with(|| Box::new(Senders::(Default::default()))); // do some work on the message senders, which are downcasted to Senders f(senders.downcast_mut::>().unwrap()) } ``` There's quite a few generics here, but don't be intimidated: these are simply required in order for the function to be usable with more than one time. Let's break it down: - The `T` type must implement Sync, Send, Clone and 'static. This means that the type must be able to be marked as being able to safely share and synchronise across threads. - The `F` type is a function that must implement a closure, where the item inside a closure is a `Senders` (which we created earlier). Next, we need to implement `Drop` and `futures_util::Stream` for our type - for `Drop` we want a custom implementation because we need it to work a specific way. `futures_util::Stream` is required by `async-graphql` for the type to work. ```rust // because we want to remove our BrokerStream from the hashmap we need to implement // our own Drop function impl Drop for BrokerStream { fn drop(&mut self) { with_senders::(|senders| senders.0.remove(self.0)); } } // implement `futures_util::Stream` for our BrokerStream impl Stream for BrokerStream { type Item = T; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.1.poll_next_unpin(cx) } } ``` Note that the T type, as above, requires `Sync + Send + Clone + 'static` - this is also required for us to use more than one type with the `SimpleBroker`. Otherwise, we will end up being able to only stream one type - which is good in some cases, but let's assume we want to stream more than one type eventually and will therefore, need to make it generic. Once we're done with the above, we need to write the implementation for the broker itself. See below: ```rust impl SimpleBroker { /// Publish a message that all subscription streams can receive. pub fn publish(msg: T) { // note that we use the with_senders function before to get the list of senders to send messages through // FnOnce dictates that we need to use a closure for this in particular with_senders::(|senders| { for (_, sender) in senders.0.iter_mut() { sender.start_send(msg.clone()).ok(); } }); } /// Subscribe to the message of the specified type and returns a `Stream`. pub fn subscribe() -> impl Stream { // note that we use the with_senders function before to get the list of senders to send messages through // FnOnce dictates that we need to use a closure for this in particular with_senders::(|senders| { let (tx, rx) = mpsc::unbounded(); let id = senders.0.insert(tx); BrokerStream(id, rx) }) } } ``` Our code for writing the broker is done, so we can get started with the GraphQL subscription as below: ```rust pub struct Subscription; #[derive(Enum, Eq, PartialEq, Copy, Clone)] pub enum MutationType { Created, Updated, Deleted, } #[derive(Clone)] // the type we will be sending/receiving through the BrokerStream we set up earlier pub struct DogChanged { pub mutation_type: MutationType, pub id: i32, } #[Object] impl DogChanged { async fn mutation_type(&self) -> MutationType { self.mutation_type } async fn id(&self) -> i32 { self.id } } ``` We can now add the subscription method itself: ```rust #[Subscription] impl Subscription { async fn dogs_changed( &self, mutation_type: Option, ) -> impl Stream { SimpleBroker::::subscribe().filter(move |evt| { // if the mutation_type input param is not none, // filter out all where the event mutation type is not the same let res = if let Some(mutation_type) = mutation_type { evt.mutation_type == mutation_type } else { true }; async move { res } }) } } ``` However, this won't work on its own and we still need to push the messages to the broker. We can publish our mutation updates to the `SimpleBroker` by using the `SimpleBroker::publish` method after a successful SQL update, like so: ```rust SimpleBroker::publish(DogChanged { mutation_type: MutationType::Created, id: res.id, }); ``` We've finished all of the parts we need to make our GraphQL server fully functional, so it's time to hook it all up! ## Connecting it all up See below for what your main file should look like: ```rust src/main.rs async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() .endpoint("/") .subscription_endpoint("/ws") .finish(), ) } pub fn init_router(db: PgPool) -> Router { let schema = Schema::build(Query, Mutation, Subscription) .data(db) .finish(); // start the http server Router::new() .route( "/", get(graphiql).post_service(GraphQL::new(schema.clone())), ) .route_service("/ws", GraphQLSubscription::new(schema)) } #[shuttle_runtime::main] async fn shuttle_main( #[shuttle_shared_db::Postgres] db: PgPool ) -> shuttle_axum::ShuttleAxum { pool.execute(include_str!("../schema.sql")) .await .context("Failed to initialize DB")?;` let router = init_router(db); Ok(router.into()) } ``` If you use `shuttle run` and go to `http://localhost:8000`, you'll be able to access all of your queries, mutations and the subscription we created. ## Deployment Once you're done, feel free to deploy by using `shuttle deploy` (with `--allow-dirty` if on a dirty Git branch). Your app will then be deployed to Shuttle servers along with a provisioned database - nothing more is needed! When finished, you'll be able to view your connection string (if you lose it for whatever reason, you can use `cargo-shuttle resource list` to get the connection string again). If you're looking for something a bit more isolated, we also offer a completely isolated AWS RDS database as a paid add-on. Find out more about our pricing [here.](https://www.shuttle.dev/pricing) ## Finishing Up I hope you enjoyed reading this article about using GraphQL in Rust! It can be a powerful resource for data fetching if you're in a team, but it's important to cover all angles so that we can make the most of it. --- # Raw SQL in Rust with SQLx Source: https://www.shuttle.dev/blog/2023/10/04/sql-in-rust Date: 4 October 2023 Author: josh Tags: rust, sql, sqlx, tutorial This article shows you how you can use SQL in Rust with SQLx - you will find a rundown of all the advantages SQLx offers you, the best ways to use it and how to use SQLx with Shuttle. When it comes to using SQL, the Rust ecosystem has us spoiled for choice: thankfully, there are already a few that have come out on top which we can use. SQLx is a purely async, runtime-agnostic Rust SQL crate that allows you to use compile-time type checked queries without a DSL. As [one of the most popular ways to use SQL in Rust,](https://github.com/search?q=sql+rust+language%3ARust&type=repositories&s=stars&o=desc&l=Rust) it offers the following advantages: - It's compatible with all your favourite flavours of SQL (MySQL, SQLite, Postgres) - Compile-time checked queries ensure type and query validity - Support for extra features like Postgres listen/notify - Many different ways to build and use queries - You can also make your own query builder using SQLx! Let's look at SQLx in action! ## Getting Started To get started you'll need to add `sqlx` to your Rust program: ```bash cargo add sqlx ``` You'll also want to install `sqlx-cli`, the official SQLx CLI which helps you manage your migrations more easily amongst other things. You can install it by running the commands below: ```bash cargo install sqlx-cli ``` ## Migrations First step: migrations. If you wanted to, you could just manually create the tables yourself - but that would be a lot of time and effort... and you'd need to remember what you did! Thankfully, we can write `.sql` files to represent our migrations and then migrate them over to whatever database we're using, either through `sqlx-cli` or by using `sqlx::execute` command. A simple SQL schema might look like this: ```sql -- this only creates a table if it doesn't exist, avoiding the issue of tables being wiped CREATE TABLE IF NOT EXISTS foo ( id SERIAL PRIMARY KEY, message TEXT ); ``` As long as it's valid SQL, whichever method you decide to use will succeed and will create a `_sqlx_migrations` table in your database, with a list of the migrations that have been applied. An in-app migrate command might look like this: ```rust pool.execute(include_str!("../schema.sql")) .await .context("Failed to initialize DB")?; ``` As a personal recommendation, I use `sqlx-cli` and use `sqlx migrate -r add `. This command essentially adds a new migration, but the `-r` flag allows you to revert your migrations at any time, should things go wrong. It's a handy way to be able to revert things, should anything go wrong after deploying a new migration to production. ## Queries By default, you can use raw SQL queries by quickly running a query then executing it with your connection pool: ```rust let query = sqlx::query("SELECT * FROM TABLE") .execute(&pool) .await .unwrap(); ``` By default, SQLx promotes using bound parameters which are very important for preventing SQL injection - you can do so simply by adding them to your query (find more about this [here](https://docs.rs/sqlx/latest/sqlx/query/struct.Query.html#method.bind)): ```rust sqlx::query("INSERT INTO TABLE (foo) VALUES ($1)") .bind("bar".to_string()) .execute(&pool) .await .unwrap(); ``` Now let's say you're writing a query that returns something. When you fetch the rows from that query, you will more than likely have to grab each value individually - at a small scale this is fine, but when you're using `fetch_all`, you'll have to make an iterator to get what you need from each row. Conveniently, SQLx knows this and has thankfully provided a macro for us to be able to extract a vector of structs from a vector of SQL rows - you can use `query_as` to bind the return results to a struct that uses `#[derive(Sqlx::FromRow)]`. You'd use it like so: ```rust #[derive(sqlx::FromRow)] struct Foo { id: i32, message: String } async fn foo(pool: PgPool) -> Vec { let res = sqlx::query_as::<_, Foo>("SELECT * FROM FOO") .fetch_all(&pool).await.unwrap(); Ok(res) } ``` Looking for something a bit more complex? You can also use the [QueryBuilder](https://docs.rs/sqlx/latest/sqlx/struct.QueryBuilder.html) type to construct queries. While it's great for programatically adding dynamic phrases to queries, you should be careful while using this as it has methods for adding values that are not bound parameters - you would ideally want to use `push_bind` if you are not sure about whether or not what you're using is secure. A usage example: ```rust const BIND_LIMIT: usize = 65535; // This would normally produce values forever! let records = (0..).map(|i| Foo { id: i, message: format!("This is note {i}"), }); let mut query_builder: QueryBuilder = QueryBuilder::new( // Note the trailing space; most calls to `QueryBuilder` don't automatically insert // spaces as that might interfere with identifiers or quoted strings where exact // values may matter. "SELECT * FROM users WHERE (id, username, email, password) in" ); // Note that `.into_iter()` wasn't needed here since `users` is already an iterator. query_builder.push_tuples(records.take(BIND_LIMIT / 2), |mut bound, foo| { // If you wanted to bind these by-reference instead of by-value, // you'd need an iterator that yields references that live as long as `query_builder`, // e.g. collect it to a `Vec` first. bound.push_bind(foo.id) .push_bind(foo.username); }); let mut query = query_builder.build(); let res = query.fetch_all(&pool).await.unwrap(); ``` Now if you try to run this, you'll be able to get a vector of `Foo` structs! Bear in mind however, that this method does have its caveats as you'll see below: you won't benefit from SQLx compile-time checking macros, and this method of query generation can be somewhat unsafe if you aren't careful. However, when you need to dynamically generate queries using SQL in Rust it's quite powerful. One last type of query we can also use is a scalar query, which returns the result as a tuple. If we don't specifically know how many fields there are (for example) when we're executing a `SELECT * FROM TABLE` query, we can use `query_scalar` to be able to refer to the columns simply by what order they appear in rather than a given name. See the example below: ```rust let query = sqlx::query_scalar("SELECT * FROM FOO LIMIT 1").fetch_one(&pool).await.unwrap(); println!("{:?}", query.0); ``` ## Macros Now onto one of SQLx's strengths as a crate: compile time query checking. If you're using raw SQL, having some kind of garuantee that your SQL is valid is almost never a bad thing: unless you're a database admin, if you're running a query with several joins on it you'll definitely want to make sure it's actually valid before it gets ran. It should be said here that you will need `sqlx-cli` installed for this to be able to take advantage of this feature: if not, you'll have to use the previous methods. A simple query using the `query!` macro might look like this: ```rust // note that bound parameters are added to the query macro let query = query!("SELECT * FROM FOO WHERE ID = $1", 1).fetch_one(&pool).await.unwrap(); ``` Likewise, an equivalent query using the `Foo` struct we made earlier can be used to bind our results directly to create a vector of Structs: ```rust #[derive(sqlx::FromRow)] struct Foo { id: i32, message: String } let query = query_as!(Foo, "SELECT * FROM FOO").fetch_all(&pool).await.unwrap(); ``` When you use the `query!` or a `query_as!` macro, you'll need to use `cargo sqlx prepare` which will generate JSON files for your queries. When you compile your program, it'll automatically check it during compile time: if anything is wrong, it'll automatically check it for you. There is one particular gotcha that may trip you up while using the compile-time checking macros, specifically with Postgres: if you're using `as _` to rename your SQL fields, the type will be automatically wrapped in an Option if you don't explicitly set it as a non-nullable value. SQLx has an answer for this in being able to use raw strings to declare values explicitly as a non-nullable column. For example, take the following statement below: ```rust let query = query_as!(Foo, "SELECT id, message as message from foo").fetch_all(&pool).await.unwrap(); ``` If we still had the Message type as a String, this query would actually fail to compile because `message` is now an `Option` and not a `String` type. However, by converting the query above to a raw string above, we can force the field to be non-nullable again: ```rust // note that message is now "message!" let query = query_as!(Foo, r#"SELECT id, message as "message!" from foo"#).fetch_all(&pool).await.unwrap(); ``` You can read more about this [here](https://docs.rs/sqlx/latest/sqlx/macro.query.html#type-overrides-output-columns). Similarly of course, `query_scalar` also has a macro associated with it and can be used similarly to the `query!` macro, while returning tuples. Something else that we can also do that's really awesome is storing a SQL query within a file and running a macro to run the contents of the SQL file, while still binding our paramaters. See below: ```sql query.sql SELECT * FROM FOO WHERE id = $1; ``` ```rust let query = query_file!("query.sql", 1i32).fetch_one(&pool).await.unwrap(); ``` This particular macro also, of course, supports struct binding and scalar queries with `query_file_as!` and `query_file_scalar!`. Something of note is that if you _only_ want to compile-time check the syntax and not whether or not the database inputs and outputs are correct for a query macro, you can add `unchecked` at the end of a macro. For example: `query!` would become `query_unchecked!`. This is useful in cases where you don't actually have a database set up yet or there's no convenient method for retrieving the database URL (or in other such circumstances where you don't want to give SQLx direct access to your database). ## PostgreSQL Listen/Notify With as many as features as Postgres has, it's a good thing that SQLx supports them - while SQLx is primarily about writing raw SQL, there is no reason why we should have to write everything in it. SQLx supports channels, `LISTEN` and more importantly `pg_notify`, which is a great way for us to be able to handle notifications from Postgres when records are updated. Let's have a look at the example below on how we can set up an event listener: ```rust // set up pool beforehand let mut listener = PgListener::connect_with(&pool).await.unwrap(); listener.listen("testNotify").await.unwrap(); // set up a loop to receive notifications tokio::spawn(async move || { while let Some(notification) = listener.try_recv().await.unwrap() { println!("{notification:?}"); } }); loop { sqlx::query("SELECT pg_notify('testNotify', 'Hello world!')").execute(&pool).await; } ``` As you can see here, we've spawned a Tokio task to be able to asynchronously loop and receive notifications then print them out - meanwhile, within the main execution thread we've also set up a loop to continuously send a query that sends "Hello world!" down the channel which gets received by our `PgListener`. For a more advanced implementation in a web service that implements a stream of database updates as an endpoint, you would want to use the `.into_stream()` method as frameworks will typically accept a stream of data that is then wrapped in the frameworks' relevant type. For example, in Axum you'd use the `axum::response::Sse` type (note this assumes you already have a web service set up): ```rust use axum::{Extension, response::{Sse, sse::Event}}; use tokio_stream::StreamExt as _ ; use futures_util::stream::{self, Stream}; use std::convert::Infallbile; async fn return_stream(Extension(listener): Extension) -> Sse>> { let stream = listener.into_stream(); Sse::new(stream .map(|msg| { let msg = msg.uwnrap(); let json = json!(msg).to_string(); Event::default().data(json) }).map(Ok), ).keep_alive(KeepAlive::default()) } ``` When we're setting up our web service, we can create notifications through one of two ways: - Using SQL - Using `pg_notify` on specific events Using `pg_notify` itself is pretty easy, although you could do this without SQL by just using Tokio channels instead. Let's take it up a notch and use SQL to set up our channels so that we don't have to manually generate it in-code. ```rust CREATE TABLE IF NOT EXISTS test_table ( id SERIAL PRIMARY KEY, message TEXT NOT NULL ); CREATE TRIGGER "testNotify" AFTER INSERT ON test_table FOR EACH ROW EXECUTE PROCEDURE testNotify(); CREATE OR REPLACE FUNCTION testNotify() RETURNS TRIGGER AS $$ DECLARE BEGIN PERFORM pg_notify('testNotify', ROW_TO_JSON(NEW)::text); RETURN NEW; END; $$ LANGUAGE plpgsql; ``` Now if we add this to an SQL migration file then run the app we're using and go to the endpoint we're using for our stream, now we'll be able to receive a stream of notifications! ## Using SQLx with Shuttle Shuttle currently offers SQLx as a default connection via our annotation macros that save you time by letting you provision your infrastructure straight from code. All you need to do is to declare the macro in-code, like so: ```rust main.rs use sqlx::PgPool; #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool // gets declared here ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&db).await.map_err(|e| format!("Oh no! Migrations failed :( {e}"); ... the rest of your code } ``` Our free database offering is via a shared database server (with users having separate databases for each application). However, we are now offering 100% isolated AWS RDS databases as a paid add-on which you can find more about [here](https://www.shuttle.dev/pricing) which supports MySQL, Postgres and MariaDB. ## Finishing Up Thanks for reading this article! I hope you've gained a good understanding of how to use SQL in Rust, as well as how much utility SQLx provides when it comes to making the power of raw, compile-time checked SQL queries work for you when using Rust SQL. Did this article help you? Feel free to [give us a star on GitHub!](https://www.github.com/shuttle-hq/shuttle) --- # Rust Vs Go: A Hands-On Comparison Source: https://www.shuttle.dev/blog/2023/09/27/rust-vs-go-comparison Date: 27 September 2023 Author: matthias Tags: rust, go, comparison, guide Rust versus Go is a controversial topic that pops up from time. In this post, we will compare both languages in the context of web development by writing a small web service which shows weather data in both languages. Oh no, not another 'Is Rust better than Go?' article. Seriously, haven't we all had our fill of these comparisons by now? But before you sigh in exasperation, hear us out! Many comparisons between Go and Rust emphasize their differences in syntax and the initial learning curve. However, ultimately, what matters is the ease of use for non-trivial projects. Since we are a platform-as-a-service provider, we think that we can contribute the most by _showing_ you how to build a small web service in both languages. We will use the same task and popular libraries for both languages to compare the solutions side-by-side so that you can make up your own mind and get a _feel_ for what it's like to work in each ecosystem. So, before you dismiss this as 'just another comparison', give it a read. There might be some details that other comparisons have missed before. ## That old "Rust versus Go" debate Rust vs Go is a topic that keeps popping up and there has been a lot written about it already. That is in part because developers are looking for information to help them decide which language to use for their next web project, and both languages get frequently mentioned in that context. We looked around, but there really is not much in-depth content on the topic out there, so developers are left to figure it out on their own and run the risk of dismissing an option too early due to misguided reasons. Both communities often face misconceptions and biases. Some view Rust primarily as a systems programming language, questioning its suitability for web development. Meanwhile, others label Go as overly simplistic, doubting its capacity to handle intricate web applications. However, these are merely superficial judgments. In reality, both languages are fine to be used for writing fast and reliable web services. However, their approaches are quite different, and it is hard to find a good comparison that tries to be fair to both. This post is our attempt to give you an overview of the differences between Go and Rust with a focus on web development by building a non-trivial real-world application in both languages. We will go beyond the syntax and take a closer look at how the langauges handle typical web tasks like routing, middleware, templating, db access and more. By the end of this post, you should have a good idea of which language is the right one for you. Although we are aware of our own biases and preferences, we we will try to be as objective as possible and highlight the strengths and weaknesses of _both_ languages. ## Building a small web service We will cover the following topics: - Routing - Templating - Database access - Deployment We will leave out topics like client-side rendering or migrations, and focus on the server-side only. ### The task Picking a task that is representative for web development is not easy: On one hand, we want to keep it simple enough so that we can focus on the language features and libraries. On the other hand, we want to make sure that the task is not _too_ simple so that we can show how to use the language features and libraries in a realistic setting. We decided to build a _weather forecast service_. The user should be able to enter a city name and get the current weather forecast for that city. The service should also show a list of recently searched cities. As we extend the service, we will add the following features: - A simple UI to display the weather forecast - A database to store recently searched cities ## The Weather API For the weather forecast, we will use the [Open-Meteo API](https://open-meteo.com/), because it is open source, easy to use, and offers a generous [free tier for non-commercial](https://open-meteo.com/en/pricing) use of up to 10,000 requests per day. We will use these two API endpoints: - The [GeoCoding API](https://open-meteo.com/en/docs/geocoding-api) to get the coordinates of a city. - The [Weather Forecast API](https://open-meteo.com/en/docs) to get the weather forecast for the given coordinates. There are libraries for both Go ([omgo](https://github.com/HectorMalot/omgo)) and Rust ([openmeteo](https://github.com/angelodlfrtr/open-meteo-rs)), which we would use in a production service. However, for the sake of comparison, we want to see what it takes to make a "raw" HTTP request in both languages and convert the response to an idiomatic data structure. ### A Go web service #### Choosing a web framework Being originally created to simplify building web services, Go has a number of great web-related packages. If the standard library doesn't cover your needs, there are a number of popular third-party web frameworks like [Gin](https://gin-gonic.com), [Echo](https://echo.labstack.com/), or [Chi](https://go-chi.io/#/) to choose from. Which one to pick is a matter of personal preference. Some experienced Go developers prefer to use the standard library and add a routing library like Chi on top of it. Others prefer a more batteries-included approach and use a full-featured framework like Gin or Echo. Both options are fine, but for the purpose of this comparison, we will choose [Gin](https://gin-gonic.com) because it is [one of the most popular frameworks](https://github.com/mingrammer/go-web-framework-stars) and it supports all the features we need for our weather service. #### Making HTTP requests Let's start with a simple function that makes an HTTP request to the Open Meteo API and returns the response body as a string: ```go func getLatLong(city string) (*LatLong, error) { endpoint := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=en&format=json", url.QueryEscape(city)) resp, err := http.Get(endpoint) if err != nil { return nil, fmt.Errorf("error making request to Geo API: %w", err) } defer resp.Body.Close() var response GeoResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { return nil, fmt.Errorf("error decoding response: %w", err) } if len(response.Results) < 1 { return nil, errors.New("no results found") } return &response.Results[0], nil } ``` The function takes a city name as an argument and returns the coordinates of the city as a `LatLong` struct. Note how we handle errors after each step: We check if the HTTP request was successful, if the response body could be decoded, and if the response contains any results. If any of these steps fails, we return an error and abort the function. So far, we just needed to use the standard library, which is great. The `defer` statement ensures that the response body is closed after the function returns. This is a common pattern in Go to avoid resource leaks. The compiler does not warn us in case we forget, so we need to be careful here. Error handling takes up a big part of the code. It is straightforward, but it can be tedious to write, and it can make the code harder to read. On the plus side, the error handling is easy to follow, and it is clear what happens in case of an error. Since the API returns a JSON object with a list of results, we need to define a struct that matches that response: ```go type GeoResponse struct { // A list of results; we only need the first one Results []LatLong `json:"results"` } type LatLong struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` } ``` The `json` tags tell the JSON decoder how to map the JSON fields to the struct fields. Extra fields in the JSON response are ignored by default. Let's define another function that takes our `LatLong` struct and returns the weather forecast for that location: ```go func getWeather(latLong LatLong) (string, error) { endpoint := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%.6f&longitude=%.6f&hourly=temperature_2m", latLong.Latitude, latLong.Longitude) resp, err := http.Get(endpoint) if err != nil { return "", fmt.Errorf("error making request to Weather API: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("error reading response body: %w", err) } return string(body), nil } ``` For a start, let's call these two functions in order and print the result: ```go func main() { latlong, err := getLatLong("London") // you know it will rain if err != nil { log.Fatalf("Failed to get latitude and longitude: %s", err) } fmt.Printf("Latitude: %f, Longitude: %f\n", latlong.Latitude, latlong.Longitude) weather, err := getWeather(*latlong) if err != nil { log.Fatalf("Failed to get weather: %s", err) } fmt.Printf("Weather: %s\n", weather) } ``` This will print the following output: ```bash Latitude: 51.508530, Longitude: -0.125740 Weather: {"latitude":51.5,"longitude":-0.120000124, ... } ``` Nice! We got the weather forecast for London. Let's make this available as a web service. #### Routing Routing is one of the most basic tasks of a web framework. First, let's add gin to our project. ```bash go mod init github.com/user/goforecast go get -u github.com/gin-gonic/gin ``` Then, let's replace our `main()` function with a server and a route that takes a city name as a parameter and returns the weather forecast for that city. Gin supports path parameters and query parameters. ```go // Path parameter r.GET("/weather/:city", func(c *gin.Context) { city := c.Param("city") // ... }) // Query parameter r.GET("/weather", func(c *gin.Context) { city := c.Query("city") // ... }) ``` Which one you want to use depends on your use case. In our case, we want to submit the city name from a form in the end, so we will use a query parameter. ```go func main() { r := gin.Default() r.GET("/weather", func(c *gin.Context) { city := c.Query("city") latlong, err := getLatLong(city) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } weather, err := getWeather(*latlong) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"weather": weather}) }) r.Run() } ``` In a separate terminal, we can start the server with `go run .` and make a request to it: ```bash curl "localhost:8080/weather?city=Hamburg" ``` And we get our weather forecast: ```json {"weather":"{\"latitude\":53.550000,\"longitude\":10.000000, ... } ``` I like the log output and it's quite fast, too! ```bash [GIN] 2023/09/09 - 19:27:20 | 200 | 190.75625ms | 127.0.0.1 | GET "/weather?city=Hamburg" [GIN] 2023/09/09 - 19:28:22 | 200 | 46.597791ms | 127.0.0.1 | GET "/weather?city=Hamburg" ``` #### Templates We got our endpoint, but raw JSON is not very useful to a normal user. In a real-world application, we would probably serve the JSON response on an API endpoint (say `/api/v1/weather/:city`) and add a separate endpoint that returns the HTML page. For the sake of simplicity, we will just return the HTML page directly. Let's add a simple HTML page that displays the weather forecast for a given city as a table. We will use the `html/template` package from the standard library to render the HTML page. First, let's add some structs for our view: ```go type WeatherData struct type WeatherResponse struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` Timezone string `json:"timezone"` Hourly struct { Time []string `json:"time"` Temperature2m []float64 `json:"temperature_2m"` } `json:"hourly"` } type WeatherDisplay struct { City string Forecasts []Forecast } type Forecast struct { Date string Temperature string } ``` This is just a direct mapping of the relevant fields in the JSON response to a struct. There are tools like [transform](https://transform.tools/json-to-go), which make conversion from JSON to Go structs easier. Take a look! Next we define a function, which converts the raw JSON response from the weather API into our new `WeatherDisplay` struct: ```go func extractWeatherData(city string, rawWeather string) (WeatherDisplay, error) { var weatherResponse WeatherResponse if err := json.Unmarshal([]byte(rawWeather), &weatherResponse); err != nil { return WeatherDisplay{}, fmt.Errorf("error decoding weather response: %w", err) } var forecasts []Forecast for i, t := range weatherResponse.Hourly.Time { date, err := time.Parse(time.RFC3339, t) if err != nil { return WeatherDisplay{}, err } forecast := Forecast{ Date: date.Format("Mon 15:04"), Temperature: fmt.Sprintf("%.1f°C", weatherResponse.Hourly.Temperature2m[i]), } forecasts = append(forecasts, forecast) } return WeatherDisplay{ City: city, Forecasts: forecasts, }, nil } ``` Date handling is done with the built-in `time` package. To learn more about date handling in Go, check out [this article](https://gobyexample.com/time). We extend our route handler to render the HTML page: ```go r.GET("/weather", func(c *gin.Context) { city := c.Query("city") latlong, err := getLatLong(city) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } weather, err := getWeather(*latlong) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } //////// NEW CODE STARTS HERE //////// weatherDisplay, err := extractWeatherData(city, weather) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.HTML(http.StatusOK, "weather.html", weatherDisplay) ////////////////////////////////////// }) ``` Let's deal with the template next. Create a template directory called `views` and tell Gin about it: ```go r := gin.Default() r.LoadHTMLGlob("views/*") ``` Finally, we can create a template file `weather.html` in the `views` directory: ```html Weather Forecast

Weather for {{ .City }}

{{ range .Forecasts }} {{ end }}
Date Temperature
{{ .Date }} {{ .Temperature }}
``` With that, we have a working web service that returns the weather forecast for a given city as an HTML page! Oh! Perhaps we also want to create an index page with an input field, which allows us to enter a city name and displays the weather forecast for that city. Let's add a new route handler for the index page: ```go r.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) }) ``` And a new template file `index.html`: ```html Weather Forecast

Weather Forecast

``` Now we can start our web service and open http://localhost:8080 in our browser: ![index page](/images/blog/rust-vs-go-index.png) The weather forecast for London looks like this. It's not pretty, but... functional! (And it works without JavaScript and in terminal browsers!) ![forecast page](/images/blog/rust-vs-go-forecast.png) As an exercise, you can add some styling to the HTML page, but since we care more about the backend, we will leave it at that. #### Database access Our service fetches the latitude and longitude for a given city from an external API on every single request. That's probably fine in the beginning, but eventually we might want to cache the results in a database to avoid unnecessary API calls. To do so, let's add a database to our web service. We will use [PostgreSQL](https://www.postgresql.org/) as our database and [sqlx](https://github.com/jmoiron/sqlx) as the database driver. First, we create a file named `init.sql`, which will be used to initialize our database: ```sql CREATE TABLE IF NOT EXISTS cities ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, lat NUMERIC NOT NULL, long NUMERIC NOT NULL ); CREATE INDEX IF NOT EXISTS cities_name_idx ON cities (name); ``` We store the latitude and longitude for a given city. The `SERIAL` type is a PostgreSQL auto-incrementing integer. To make things fast, we will also add an index on the `name` column. It's probably easiest to use Docker or any of the cloud providers. At the end of the day, you just need _a database URL_, which you can pass to your web service as an environment variable. We won't go into the details of setting up a database here, but a simple way to get a PostgreSQL database running with Docker locally is: ``` docker run -p 5432:5432 -e POSTGRES_USER=forecast -e POSTGRES_PASSWORD=forecast -e POSTGRES_DB=forecast -v `pwd`/init.sql:/docker-entrypoint-initdb.d/index.sql -d postgres export DATABASE_URL="postgres://forecast:forecast@localhost:5432/forecast?sslmode=disable" ``` However once we have our database, we need to add the [sqlx](https://github.com/jmoiron/sqlx) dependency to our `go.mod` file: ```go go get github.com/jmoiron/sqlx ``` We can now use the `sqlx` package to connect to our database by using the connection string from the `DATABASE_URL` environment variable: ```go _ = sqlx.MustConnect("postgres", os.Getenv("DATABASE_URL")) ``` And with that, we have a database connection! Let's add a function to insert a city into our database. We will use our `LatLong` struct from earlier. ```go func insertCity(db *sqlx.DB, name string, latLong LatLong) error { _, err := db.Exec("INSERT INTO cities (name, lat, long) VALUES ($1, $2, $3)", name, latLong.Latitude, latLong.Longitude) return err } ``` Let's rename our old `getLatLong` function to `fetchLatLong` and add a new `getLatLong` function, which uses the database instead of the external API: ```go func getLatLong(db *sqlx.DB, name string) (*LatLong, error) { var latLong *LatLong err := db.Get(&latLong, "SELECT lat, long FROM cities WHERE name = $1", name) if err == nil { return latLong, nil } latLong, err = fetchLatLong(name) if err != nil { return nil, err } err = insertCity(db, name, *latLong) if err != nil { return nil, err } return latLong, nil } ``` Here we directly pass the `db` connection to our `getLatLong` function. In a real application, we should decouple the database access from the API logic, to make testing possible. We would probably also use an in-memory-cache to avoid unnecessary database calls. This is just to compare database access in Go and Rust. We need to update our handler: ```go r.GET("/weather", func(c *gin.Context) { city := c.Query("city") // Pass in the db latlong, err := getLatLong(db, city) // ... }) ``` With that, we have a working web service that stores the latitude and longitude for a given city in a database and fetches it from there on subsequent requests. #### Middleware The last bit is to add some middleware to our web service. We already got some nice logging for free from Gin. Let's add a basic-auth middleware and protect our `/stats` endpoint, which we will use to print the last search queries. ```go r.GET("/stats", gin.BasicAuth(gin.Accounts{ "forecast": "forecast", }), func(c *gin.Context) { // rest of the handler } ) ``` That's it! Pro-tip: you can also [group routes together](https://jonathanmh.com/go-gin-http-basic-auth/) to apply authentication to multiple routes at once. Here's the logic to fetch the last search queries from the database: ```go func getLastCities(db *sqlx.DB) ([]string, error) { var cities []string err := db.Select(&cities, "SELECT name FROM cities ORDER BY id DESC LIMIT 10") if err != nil { return nil, err } return cities, nil } ``` Now let's wire up our `/stats` endpoint to print the last search queries: ```go r.GET("/stats", gin.BasicAuth(gin.Accounts{ "forecast": "forecast", }), func(c *gin.Context) { cities, err := getLastCities(db) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.HTML(http.StatusOK, "stats.html", cities) }) ``` Our `stats.html` template is simple enough: ```html Latest Queries

Latest Lat/Long Lookups

{{ range . }} {{ end }}
Cities
{{ . }}
``` And with that, we have a working web service! Congratulations! We have achieved the following: - A web service that fetches the latitude and longitude for a given city from an external API - Stores the latitude and longitude in a database - Fetches the latitude and longitude from the database on subsequent requests - Prints the last search queries on the `/stats` endpoint - Basic-auth to protect the `/stats` endpoint - Uses middleware to log requests - Templates to render HTML That's quite a lot of functionality for a few lines of code! Let's see how Rust stacks up! ### A Rust web service Historically, Rust didn't have a good story for web services. There were a few frameworks, but they were quite low-level. Only with the emergence of async/await, did the Rust web ecosystem really take off. Suddenly, it was possible to write highly performant web services without a garbage collector and with fearless concurrency. We will see how Rust compares to Go in terms of ergonomics, performance and safety. But first, we need to choose a web framework. #### Which web framework? If you're looking to get a better overview of Rust web frameworks as well as their strengths and weaknesses, we recently did a [Rust web framework deep-dive](https://www.shuttle.dev/blog/2023/08/23/rust-web-framework-comparison). For the purpose of this article, we consider two web frameworks: [Actix](https://actix.rs/) and [Axum](https://github.com/tokio-rs/axum). Actix is a very popular web framework in the Rust community. It is based on the actor model and uses async/await under the hood. In benchmark, [it regularly shows up as one of the fastest web frameworks in the world](https://www.techempower.com/benchmarks/#section=data-r21&test=composite). Axum on the other hand is a new web framework that is based on [tower](https://github.com/tower-rs/tower), a library for building async services. It is quickly gaining popularity. It is also based on async/await. Both frameworks are very similar in terms of ergonomics and performance. They both support middleware and routing. Each of them would be a good choice for our web service, but we will go with Axum, because it ties in nicely with the rest of the ecosystem and has gotten a lot of attention recently. #### Routing Let's start the project with a `cargo new forecast` and add the following dependencies to our `Cargo.toml`. (We will need a few more, but we will add them later.) ```toml [dependencies] # web framework axum = "0.6.20" # async HTTP client reqwest = { version = "0.11.20", features = ["json"] } # serialization/deserialization for JSON serde = "1.0.188" # database access sqlx = "0.7.1" # async runtime tokio = { version = "1.32.0", features = ["full"] } ``` Let's create a little skeleton for our web service, which doesn't do much. ```rust use std::net::SocketAddr; use axum::{routing::get, Router}; // basic handler that responds with a static string async fn index() -> &'static str { "Index" } async fn weather() -> &'static str { "Weather" } async fn stats() -> &'static str { "Stats" } #[tokio::main] async fn main() { let app = Router::new() .route("/", get(index)) .route("/weather", get(weather)) .route("/stats", get(stats)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } ``` The `main` function is pretty straightforward. We create a router and bind it to a socket address. The `index`, `weather` and `stats` functions are our handlers. They are async functions that return a string. We will replace them with actual logic later. Let's run the web service with `cargo run` and see what happens. ```bash $ curl localhost:3000 Index $ curl localhost:3000/weather Weather $ curl localhost:3000/stats Stats ``` Okay, that works. Let's add some actual logic to our handlers. #### Axum macros Before we move on, I'd like to mention that axum has some rough edges. E.g. it will yell at you if you forgot to make your handler function async. So if you run into `Handler<_, _> is not implemented` errors, add the [axum-macros](https://docs.rs/axum-macros/latest/axum_macros/) crate and annotate your handler with `#[axum_macros::debug_handler]`. This will give you much better error messages. #### Fetching the latitude and longitude Let's write a function that fetches the latitude and longitude for a given city from an external API. Here are the structs representing the response from the API: ```rust use serde::Deserialize; pub struct GeoResponse { pub results: Vec, } #[derive(Deserialize, Debug, Clone)] pub struct LatLong { pub latitude: f64, pub longitude: f64, } ``` In comparison to Go, we don't use tags to specify the field names. Instead, we use the `#[derive(Deserialize)]` attribute from [serde](https://serde.rs/) to automatically derive the `Deserialize` trait for our structs. These derive macros are very powerful and allow us to do a lot of things with very little code, including handling parsing errors for our types. It is a very common pattern in Rust. Let's use the new types to fetch the latitude and longitude for a given city: ```rust async fn fetch_lat_long(city: &str) -> Result> { let endpoint = format!( "https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json", city ); let response = reqwest::get(&endpoint).await?.json::().await?; response .results .get(0) .cloned() .ok_or("No results found".into()) } ``` The code is a bit less verbose than the Go version. We don't have to write `if err != nil` constructs, because we can use the [`?` operator](https://doc.rust-lang.org/rust-by-example/std/result/question_mark.html) to propagate errors. This is also mandatory, as each step returns a [`Result`](https://doc.rust-lang.org/std/result/) type. If we don't handle the error, we won't get access to the value. That last part might look a bit unfamiliar: ```rust response .results .get(0) .cloned() .ok_or("No results found".into()) ``` A few things are happening here: - `response.results.get(0)` returns an `Option<&LatLong>`. It is an `Option` because the `get` function might return `None` if the vector is empty. - `cloned()` clones the value inside the `Option` and converts the `Option<&LatLong>` into an `Option`. This is necessary, because we want to return a `LatLong` and not a reference. Otherwise, we would have to add a lifetime specifier to the function signature and it makes the code less readable. - `ok_or("No results found".into())` converts the `Option` into a `Result>`. If the `Option` is `None`, it will return the error message. The `into()` function converts the string into a `Box`. An alternative way to write this would be: ```rust match response.results.get(0) { Some(lat_long) => Ok(lat_long.clone()), None => Err("No results found".into()), } ``` It is a matter of taste which version you prefer. Rust is an expression-based language, which means that we don't have to use `return` to return a value from a function. Instead, the last value of a function is returned. We can now update our `weather` function to use `fetch_lat_long`. Our first attempt might look like this: ```rust async fn weather(city: String) -> String { println!("city: {}", city); let lat_long = fetch_lat_long(&city).await.unwrap(); format!("{}: {}, {}", city, lat_long.latitude, lat_long.longitude) } ``` First we print the city to the console, then we fetch the latitude and longitude and unwrap (i.e. "unpack") the result. If the result is an error, the program will panic. This is not ideal, but we will fix it later. We then use the latitude and longitude to create a string and return it. Let's run the program and see what happens: ```bash curl -v "localhost:3000/weather?city=Berlin" * Trying 127.0.0.1:3000... * Connected to localhost (127.0.0.1) port 3000 (#0) > GET /weather?city=Berlin HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/8.1.2 > Accept: */* > * Empty reply from server * Closing connection 0 curl: (52) Empty reply from server ``` Furthermore, we get this output: ```bash city: ``` The `city` parameter is empty. What happened? The problem is that we are using the `String` type for the `city` parameter. This type is not a valid [extractor](https://docs.rs/axum/latest/axum/extract/index.html). We can use the `Query` extractor instead: ```rust async fn weather(Query(params): Query>) -> String { let city = params.get("city").unwrap(); let lat_long = fetch_lat_long(&city).await.unwrap(); format!("{}: {}, {}", *city, lat_long.latitude, lat_long.longitude) } ``` This will work, but it is not very idiomatic. We have to `unwrap` the `Option` to get the city. We also need to pass `*city` to the `format!` macro to get the value instead of the reference. (It's called "dereferencing" in Rust lingo.) We could create a struct that represents the query parameters: ```rust #[derive(Deserialize)] pub struct WeatherQuery { pub city: String, } ``` We can then use this struct as an extractor and avoid the `unwrap`: ```rust async fn weather(Query(params): Query) -> String { let lat_long = fetch_lat_long(¶ms.city).await.unwrap(); format!("{}: {}, {}", params.city, lat_long.latitude, lat_long.longitude) } ``` Cleaner! It's a little more involved than the Go version, but it's also more type-safe. You can imagine that we can add constraints to the struct to add validation. For example, we could require that the city is at least 3 characters long. Now about the `unwrap` in the `weather` function. Ideally, we would return an error if the city is not found. We can do this by changing our return type. In axum, anything that implements [`IntoResponse`](https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html) can be returned from handlers, however it is advisable to return a concrete type, as there are [some caveats with returning `impl IntoResponse`] (https://docs.rs/axum/latest/axum/response/index.html) In our case, we can return a `Result` type: ```rust async fn weather(Query(params): Query) -> Result { match fetch_lat_long(¶ms.city).await { Ok(lat_long) => Ok(format!( "{}: {}, {}", params.city, lat_long.latitude, lat_long.longitude )), Err(_) => Err(StatusCode::NOT_FOUND), } } ``` This will return a `404` status code if the city is not found. We use `match` to match on the result of `fetch_lat_long`. If it is `Ok`, we return the weather as a `String`. If it is `Err`, we return a `StatusCode::NOT_FOUND`. We could also use the `map_err` function to convert the error into a `StatusCode`: ```rust async fn weather(Query(params): Query) -> Result { let lat_long = fetch_lat_long(¶ms.city) .await .map_err(|_| StatusCode::NOT_FOUND)?; Ok(format!( "{}: {}, {}", params.city, lat_long.latitude, lat_long.longitude )) } ``` This variant has the advantage that we the control flow is more linear: we handle the error right away and can then continue with the happy path. On the other hand, it takes a while to get used to these combinator patterns until they become second nature. In Rust, there are usually multiple ways to do things. It's a matter of taste which version you prefer. In general, keep it simple and don't overthink it. In any case, let's test our program: ```bash curl "localhost:3000/weather?city=Berlin" Berlin: 52.52437, 13.41053 ``` and ```bash curl -I "localhost:3000/weather?city=abcdedfg" HTTP/1.1 404 Not Found ``` Let's write our second function, which will return the weather for a given latitude and longitude: ```rust async fn fetch_weather(lat_long: LatLong) -> Result> { let endpoint = format!( "https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m", lat_long.latitude, lat_long.longitude ); let response = reqwest::get(&endpoint).await?.text().await?; Ok(response) } ``` Here we make the API request and return the raw response body as a `String`. We can extend our handler to make the two calls in succession: ```rust async fn weather(Query(params): Query) -> Result { let lat_long = fetch_lat_long(¶ms.city) .await .map_err(|_| StatusCode::NOT_FOUND)?; let weather = fetch_weather(lat_long) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(weather) } ``` This would work, but it would return the raw response body from the Open Meteo API. Let's parse the response and return the data similar to the Go version. As a reminder, here's the Go definition: ```go type WeatherResponse struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` Timezone string `json:"timezone"` Hourly struct { Time []string `json:"time"` Temperature2m []float64 `json:"temperature_2m"` } `json:"hourly"` } ``` And here is the Rust version: ```rust #[derive(Deserialize, Debug)] pub struct WeatherResponse { pub latitude: f64, pub longitude: f64, pub timezone: String, pub hourly: Hourly, } #[derive(Deserialize, Debug)] pub struct Hourly { pub time: Vec, pub temperature_2m: Vec, } ``` While we're at it, let's also define the other structs we need: ```rust #[derive(Deserialize, Debug)] pub struct WeatherDisplay { pub city: String, pub forecasts: Vec, } #[derive(Deserialize, Debug)] pub struct Forecast { pub date: String, pub temperature: String, } ``` We can now parse the response body into our structs: ```rust async fn fetch_weather(lat_long: LatLong) -> Result> { let endpoint = format!( "https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m", lat_long.latitude, lat_long.longitude ); let response = reqwest::get(&endpoint).await?.json::().await?; Ok(response) } ``` Let's adjust the handler. The easiest way to make it compile is to return a `String`: ```rust async fn weather(Query(params): Query) -> Result { let lat_long = fetch_lat_long(¶ms.city) .await .map_err(|_| StatusCode::NOT_FOUND)?; let weather = fetch_weather(lat_long) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let display = WeatherDisplay { city: params.city, forecasts: weather .hourly .time .iter() .zip(weather.hourly.temperature_2m.iter()) .map(|(date, temperature)| Forecast { date: date.to_string(), temperature: temperature.to_string(), }) .collect(), }; Ok(format!("{:?}", display)) } ``` Note how we mix the parsing logic with the handler logic. Let's clean this up a bit by moving the parsing logic into a constructor function: ````rust impl WeatherDisplay { /// Create a new `WeatherDisplay` from a `WeatherResponse`. fn new(city: String, response: WeatherResponse) -> Self { let display = WeatherDisplay { city, forecasts: response .hourly .time .iter() .zip(response.hourly.temperature_2m.iter()) .map(|(date, temperature)| Forecast { date: date.to_string(), temperature: temperature.to_string(), }) .collect(), }; display } }``` That's a start. Our handler now looks like this: ```rust async fn weather(Query(params): Query) -> Result { let lat_long = fetch_lat_long(¶ms.city) .await .map_err(|_| StatusCode::NOT_FOUND)?; let weather = fetch_weather(lat_long) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let display = WeatherDisplay::new(params.city, weather); Ok(format!("{:?}", display)) } ```` That's already a little bit better. What's distracting is the `map_err` boilerplate. We can remove that by introducing a custom error type. For instance, we can follow [the example in the `axum` repository](https://github.com/tokio-rs/axum/blob/main/examples/anyhow-error-response/src/main.rs) and use [anyhow](https://github.com/dtolnay/anyhow), a popular crate for error handling: ```bash cargo add anyhow ``` Let's copy the code from the example into our project: ```rust // Make our own error that wraps `anyhow::Error`. struct AppError(anyhow::Error); // Tell axum how to convert `AppError` into a response. impl IntoResponse for AppError { fn into_response(self) -> Response { ( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", self.0), ) .into_response() } } // This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into // `Result<_, AppError>`. That way you don't need to do that manually. impl From for AppError where E: Into, { fn from(err: E) -> Self { Self(err.into()) } } ``` You don't have to fully understand this code. Suffice to say that will set up the error handling for the application so that we don't have to deal with it in the handler. We have to adjust the `fetch_lang_long` and `fetch_weather` functions to return a `Result` with an `anyhow::Error`: ```rust async fn fetch_lat_long(city: &str) -> Result { let endpoint = format!( "https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json", city ); let response = reqwest::get(&endpoint).await?.json::().await?; response.results.get(0).cloned().context("No results found") } ``` and ```rust async fn fetch_weather(lat_long: LatLong) -> Result { // code stays the same } ``` At the price of adding a dependency and adding the additional boilerplate for error handling, we managed to simplify our handler quite a bit: ```rust async fn weather(Query(params): Query) -> Result { let lat_long = fetch_lat_long(¶ms.city).await?; let weather = fetch_weather(lat_long).await?; let display = WeatherDisplay::new(params.city, weather); Ok(format!("{:?}", display)) } ``` #### Templates `axum` doesn't come with a templating engine. We have to pick one ourselves. I usually use either [tera](https://github.com/Keats/tera) or [askama](https://github.com/djc/askama/tree/main) with a slight preference for `askama` because it supports compile-time syntax checks. With that, you cannot accidentally introduce typos in a template. Every variable you use in a template has to be defined in the code. ```bash # Enable axum support cargo add askama --features=with-axum # I also needed to add this to make it compile cargo add askama_axum ``` Let's create a `templates` directory and add a `weather.html` template, similar to the Go table template we created earlier: ```html Weather

Weather for {{ city }}

{% for forecast in forecasts %} {% endfor %}
Date Temperature
{{ forecast.date }} {{ forecast.temperature }}
``` Let's convert our `WeatherDisplay` struct into a `Template`: ```rust #[derive(Template, Deserialize, Debug)] #[template(path = "weather.html")] struct WeatherDisplay { city: String, forecasts: Vec, } ``` and our handler becomes: ```rust async fn weather(Query(params): Query) -> Result { let lat_long = fetch_lat_long(¶ms.city).await?; let weather = fetch_weather(lat_long).await?; Ok(WeatherDisplay::new(params.city, weather)) } ``` It was a bit of work to get here, but we now have a nice separation of concerns without too much boilerplate. If you open the browser at `http://localhost:3000/weather?city=Berlin`, you should see the weather table. Adding our input mask is easy. We can use the exact same HTML we used for the Go version: ```html
Weather Forecast

Weather Forecast

``` and here is the handler: ```rust #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate; async fn index() -> IndexTemplate { IndexTemplate } ``` Let's move on to storing the latitudes and longitudes in a database. #### Database access We will use [sqlx](https://github.com/launchbadge/sqlx) for database access. It's a very popular crate that supports multiple databases. In our case, we will use Postgres, just like in the Go version. Add this to your `Cargo.toml`: ```toml sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "macros", "any", "postgres", ] } ``` We need to add a `DATABASE_URL` environment variable to our `.env` file: ```bash export DATABASE_URL="postgres://forecast:forecast@localhost:5432/forecast?sslmode=disable" ``` If you don't have Postgres running still, you can start it with the same Docker snippet from our Go section. With that, let's adjust our code to use the database. First, the `main` function: ```rust #[tokio::main] async fn main() -> anyhow::Result<()> { let db_connection_str = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?; let pool = sqlx::PgPool::connect(&db_connection_str) .await .context("can't connect to database")?; let app = Router::new() .route("/", get(index)) .route("/weather", get(weather)) .route("/stats", get(stats)) .with_state(pool); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .await?; Ok(()) } ``` Here's what changed: - We added a `DATABASE_URL` environment variable and read it in `main`. - We create a database connection pool with `sqlx::PgPool::connect`. - Then we pass the pool to `with_state` to make it available to all handlers. In each route, we can (but don't have to) access the database pool like this: ```rust async fn weather( Query(params): Query, State(pool): State, ) -> Result { let lat_long = fetch_lat_long(¶ms.city).await?; let weather = fetch_weather(lat_long).await?; Ok(WeatherDisplay::new(params.city, weather)) } ``` To learn more about `State`, check out the [documentation](https://docs.rs/axum/latest/axum/extract/struct.State.html). To make our data fetchable from the database, we need to add a `FromRow` trait to our structs: ```rust #[derive(sqlx::FromRow, Deserialize, Debug, Clone)] pub struct LatLong { pub latitude: f64, pub longitude: f64, } ``` Let's add a function to fetch the latitudes and longitudes from the database: ```rust async fn get_lat_long(pool: &PgPool, name: &str) -> Result { let lat_long = sqlx::query_as::<_, LatLong>( "SELECT lat AS latitude, long AS longitude FROM cities WHERE name = $1", ) .bind(name) .fetch_optional(pool) .await?; if let Some(lat_long) = lat_long { return Ok(lat_long); } let lat_long = fetch_lat_long(name).await?; sqlx::query("INSERT INTO cities (name, lat, long) VALUES ($1, $2, $3)") .bind(name) .bind(lat_long.latitude) .bind(lat_long.longitude) .execute(pool) .await?; Ok(lat_long) } ``` and finally, let's update our `weather` route to use the new function: ```rust async fn weather( Query(params): Query, State(pool): State, ) -> Result { let lat_long = fetch_lat_long(¶ms.city).await?; let weather = fetch_weather(lat_long).await?; Ok(WeatherDisplay::new(params.city, weather)) } ``` And that's it! We now have a working web app with a database backend. The behavior is identical to before, but now we cache the latitudes and longitudes. #### Middleware The last feature that we're missing from our Go version is the `/stats` endpoint. Remember that it shows the recent queries and is behind basic auth. Let's start with basic auth. It took me a while to figure out how to do this. There are numerous authentication libraries for axum, but very little information on how to do basic auth. I ended up writing a custom middleware, that would - check if the request has an `Authorization` header - if it does, check if the header contains a valid username and password - if it does, return an "unauthorized" response and a `WWW-Authenticate` header, which instructs the browser to show a login dialog. Here's the code: ```rust /// A user that is authorized to access the stats endpoint. /// /// No fields are required, we just need to know that the user is authorized. In /// a production application you would probably want to have some kind of user /// ID or similar here. struct User; #[async_trait] impl FromRequestParts for User where S: Send + Sync, { type Rejection = axum::http::Response; async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { let auth_header = parts .headers .get("Authorization") .and_then(|header| header.to_str().ok()); if let Some(auth_header) = auth_header { if auth_header.starts_with("Basic ") { let credentials = auth_header.trim_start_matches("Basic "); let decoded = base64::decode(credentials).unwrap_or_default(); let credential_str = from_utf8(&decoded).unwrap_or(""); // Our username and password are hardcoded here. // In a real app, you'd want to read them from the environment. if credential_str == "forecast:forecast" { return Ok(User); } } } let reject_response = axum::http::Response::builder() .status(StatusCode::UNAUTHORIZED) .header( "WWW-Authenticate", "Basic realm=\"Please enter your credentials\"", ) .body(axum::body::Body::from("Unauthorized")) .unwrap(); Err(reject_response) } } ``` [FromRequestParts](https://docs.rs/axum/latest/axum/extract/trait.FromRequestParts.html) is a trait that allows us to extract data from the request. There's also [FromRequest](https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html), which consumes the entire request body and can thus only be run once for handlers. In our case, we just need to read the `Authorization` header, so `FromRequestParts` is enough. The beauty is, that we can simple add the `User` type to any handler and it will extract the user from the request: ```rust async fn stats(user: User) -> &'static str { "We're authorized!" } ``` Now about the actual logic for the `/stats` endpoint. ```rust #[derive(Template)] #[template(path = "stats.html")] struct StatsTemplate { pub cities: Vec, } async fn get_last_cities(pool: &PgPool) -> Result, AppError> { let cities = sqlx::query_as::<_, City>("SELECT name FROM cities ORDER BY id DESC LIMIT 10") .fetch_all(pool) .await?; Ok(cities) } async fn stats(_user: User, State(pool): State) -> Result { let cities = get_last_cities(&pool).await?; Ok(StatsTemplate { cities }) } ``` ## Deployment Lastly, let's talk about deployment. Since both languages compile to a statically linked binary, they can be hosted on any Virtual Machine (VM) or Virtual Private Server (VPS). That is amazing because it means that you can run your application natively on bare metal if you like. Another option is to use containers, which run your application in an isolated environment. They are very popular because they are easy to use and can be deployed virtually anywhere. For Golang, you can use any cloud provider that supports running static binaries or containers. One of the more popular options is [Google Cloud Run](https://cloud.google.com/run). You can of course also use containers to ship Rust, but there are other options, too. One of them is [Shuttle](https://www.shuttle.dev/), of course, and the way it works is different to other services: You don't need to build a Docker image and push it to a registry. Instead, you just push your code to a Git repository and Shuttle will build and run the binary for you. Thanks to Rust's procedural macros, you can enhance your code with additional functionality quickly. All it takes to get started is [`#[shuttle_runtime::main]`](https://docs.shuttle.dev/examples/axum) on your main function: ```rust #[shuttle_runtime::main] async fn main() -> Result<(), Box> { // Rest of your code goes here } ``` To get started, [install the Shuttle CLI](https://docs.shuttle.dev/getting-started/installation) and dependencies. You can utilize [cargo binstall](https://github.com/cargo-bins/cargo-binstall), a Cargo plugin designed to install binaries from crates.io. First, ensure you have the plugin installed. After that, you'll be able to install the Shuttle CLI: ```bash cargo binstall cargo-shuttle cargo add shuttle-axum shuttle-runtime ``` Let's modify our `main` function to use Shuttle. Note how we no longer need the port binding, as Shuttle will take care of that for us! We just hand it the router and it will take care of the rest. ```rust #[shuttle_runtime::main] async fn main() -> shuttle_axum::ShuttleAxum { let db_connection_str = std::env::var("DATABASE_URL").context("DATABASE_URL must be set")?; let pool = sqlx::PgPool::connect(&db_connection_str) .await .context("can't connect to database")?; let router = Router::new() .route("/", get(index)) .route("/weather", get(weather)) .route("/stats", get(stats)) .with_state(pool); Ok(router.into()) } ``` Next, let's set up our production postgres database. There's a macro for that, too. ```bash cargo add shuttle-shared-db --features=postgres ``` and ```rust #[shuttle_runtime::main] async fn main(#[shuttle_shared_db::Postgres] pool: PgPool) -> shuttle_axum::ShuttleAxum { pool.execute(include_str!("../schema.sql")) .await .context("Failed to initialize DB")?; let router = Router::new() .route("/", get(index)) .route("/weather", get(weather)) .route("/stats", get(stats)) .with_state(pool); Ok(router.into()) } ``` See that part about the schema? That's how we initialize our database with our existing table definitions. [Migrations are also supported through sqlx](https://docs.rs/sqlx/latest/sqlx/macro.migrate.html) and [sqlx-cli](https://github.com/launchbadge/sqlx/tree/main/sqlx-cli). We got rid of a lot of boilerplate code and can now deploy our app with ease. ```bash # Run as often as you like shuttle deploy ``` When it's done, it will print the URL to the service. It should work just like before, but now it's running on a server in the cloud. 🚀 ## A Comparison Between Go And Rust Let's see how the two versions stacked up against each other. ### The Go version The Go version is very simple and straightforward. We only needed to add two dependencies: `Gin` (the web framework) and `sqlx` (the database driver). Apart from that, everything was provided by the standard library: the templating engine, the JSON parser, the datetime handling, etc. Even though I'm personally not a big fan of Go's templating engine and error handling mechanisms, I felt productive throughout the entire development process. We could have used an external templating library, but we didn't need to as the built-in one was just fine for our use case. If you're looking to leverage the power of Go for your projects, you might want to [hire a Golang developer](https://www.toptal.com/golang). ### The Rust version The Rust code is a little more involved. We needed to add a lot of dependencies to get the same functionality as in Go. For example, we needed to add a templating engine, a JSON parser, a datetime library, a database driver, and a web framework. This is by design. Rust's standard library is very minimal and only provides the most basic building blocks. The idea is that you can pick and choose the dependencies that you need for your project. It helps the ecosystem to evolve faster and allows for more experimentation while the core of the language stays stable. Even though it took longer to get started, I enjoyed the process of working my way up to higher levels of abstraction. At no point did I feel like I was stuck with a suboptimal solution. With the proper abstractions in place, such as the `?` operator and the `FromRequest` trait, the code felt easy to read without any boilerplate or unnecessarily verbose error handling. ### Summary - Go: - Easy to learn, fast, good for web services - Batteries included. We did a lot with just the standard library. For example, we didn't need to add a templating engine or a separate auth library. - Our only external dependencies were `Gin` and `sqlx` - Rust: - Fast, safe, evolving ecosystem for web services - No batteries included. We had to add a lot of dependencies to get the same functionality as in Go and write our own small middleware. - The final handler code was free from distracting error handling, because we used our own error type and the `?` operator. This makes for very readable code, at the cost of having to write additional adapter logic. The handlers are succinct, and there's a natural separation of concerns. That begs the question... ## Is Rust better than Go, or will Rust replace Go? Personally, I'm a big fan of Rust and I think it's a great language for web services. But there are still a few rough edges and missing pieces in the ecosystem. Especially for newcomers, the error messages when using axum can at times be quite cryptic. For example, a [common one is this error message](https://github.com/tokio-rs/axum/discussions/2239), which occurs on routes that do not implement the handler trait because of type mismatches: ```rust error[E0277]: the trait bound `(): Handler<_, _>` is not satisfied --> src\router.rs:22:50 | 22 | router = router.route("/", get(handler)); | --- ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for `()` | | | required by a bound introduced by this call | note: required by a bound in `axum::routing::get` ``` For this case, I recommend the axum `debug_handler`, which simplifies the error messages quite a bit. Read more about it in their [documentation](https://docs.rs/axum-macros/latest/axum_macros/attr.debug_handler.html). In comparison to Go, the authorization part was also more involved. In Go, we could just use a middleware and be done with it. In Rust, we had to write our own middleware and error type. This is not necessarily a bad thing, but it requires some research in the axum docs to find the right solution. Granted, basic auth is not a common use case for real-world applications, and there are plenty of advanced auth libraries to choose from. The mentioned issues are not deal breakers and mostly papercuts related to specific crates. Core Rust has reached a point of stability and maturity that makes it suitable for production use. The ecosystem is still evolving, but it's [already in a good place](https://www.arewewebyet.org/). On the other hand, I personally find the final Go code a little bit too verbose. The error handling is very explicit, but it also distracts from the actual business logic. In general, I found myself reaching for higher-level abstractions in Go (like the aforementioned `FromRequest` trait in the Rust version). The final Rust code feels more succinct. It felt like the Rust compiler was quietly guiding me towards a better design throughout the entire process. There's certainly a higher upfront cost to using Rust, but the ergonomics are great once you get over the initial scaffolding phase. I don't think one language is better than the other. It's a matter of taste and personal preference. The philosophies of the two languages are quite different, but they both allow you to build fast and reliable web services. ## Should I use Rust or Go in 2023? If you're just starting out with a new project, and you and your team could freely pick a language to use, you might be wondering which one to choose. It depends on the timeframe for the project and your team's experience. If you're looking to get started quickly, Go might be the better choice. It offers a batteries-included development environment and is great for web apps. However, don't underestimate the long-term benefits of Rust. Its rich type system paired with its awesome error handling mechanisms and compile-time checks can help you build apps which are not only fast but also robust and extensible. With regards to developer velocity, Shuttle can substantially lower the operational burden from running Rust code in production. As we've seen, you don't need to write a Dockerfile to get started and your code builds natively in the cloud, which allows for very fast deployment- and iteration cycles. So if you're looking for a long-term solution, and you're willing to invest in learning Rust, I'd say it's a great choice. I invite you to compare both solutions and decide for yourself which one you like better. In any case, it was fun to build the same project in two different languages and look at the differences in idioms and ecosystem. Even though the end result is the same, the way we got there was quite different. --- # Logging in Rust (2025) Source: https://www.shuttle.dev/blog/2023/09/20/logging-in-rust Date: 20 September 2023 Author: josh Tags: rust, logging, guide This article talks about the most popular logging crates in Rust and what the best one for your use case is, including both simple and more complex crates. ## Introduction With so many different libraries at our disposal for outputting logs in Rust, it's difficult to know which one to choose. When `println!`, `dbg!` and `eprintln!` don't cut it, having a way to structure your logs is extremely important, especially in production-grade applications. This article will help you gain insight on what the best log crate for your use case is when it comes to Rust logging. ## How does logging work in Rust? In short: loggers in Rust depend on a library to act as a "logging facade" - a crate which provides the logging API that the logger can work with. So for example, if we have a crate like `log` that provides a logging implementation for us that we can use with a logger, we then will also need to add a crate that actually carries out the logging - for example, `simple-logger` being one of many crates that can use `log`. Some logging facades may only be able to be used by their own special logger - for example, `tracing` either requires you to use the `tracing-subscriber` crate, or otherwise implement your own custom type that implements `tracing::Subscriber`. Without further ado, let's start the Rust logging crate comparison! ## Understanding Structured vs Unstructured Logging Before we dive into specific crates, it's worth understanding the difference between structured and unstructured logging - this will help you choose the right tool for your needs. **Unstructured logging** is what you're probably used to - free-form text messages like `"User logged in"` or `"Error: connection failed"`. While easy to write, these logs are harder for machines to parse and query at scale. **Structured logging** treats log entries as data with key-value pairs, like `{"event": "user_login", "user_id": 123, "timestamp": "2024-01-15"}`. This makes logs machine-readable and much easier to search, filter, and analyze in production systems. For small projects or development, unstructured logging is often perfectly fine. But as your application grows and you need to query logs to debug production issues or analyze patterns, structured logging becomes invaluable. Most Rust logging crates play well together. The `log` crate serves as a universal standard that other interfaces can bridge to - for example, you can use `tracing` (which supports structured logging) with any `log`-compatible consumer through the `tracing-log` crate. This flexibility means you're not locked into one ecosystem. ## log [`log`](https://github.com/rust-lang/log) is a crate that calls itself a "lightweight logging facade". The crate defines a logging facade as a library that "provides a single logging API that abstracts over the actual logging implementation" - essentially, this means that we'll need to run another library that provides the actual logging and then use this crate to provide the logging messages. Log is also maintained by the Rust core team and is probably the first crate you'll see on [the Rust Cookbook](https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/log.html#log-a-debug-message-to-the-console), so there's that. As taken from the GitHub repository, here's a simple example on how it can be used: ```rust use log; pub fn shave_the_yak(yak: &mut Yak) { log::trace!("Commencing yak shaving"); loop { match find_a_razor() { Ok(razor) => { log::info!("Razor located: {}", razor); yak.shave(razor); break; } Err(err) => { log::warn!("Unable to locate a razor: {}, retrying", err); } } } } ``` It should be noted that `log` is also compatible with a **lot** of logger crates - their GitHub repository alone lists over 20 and is a non-exhaustive list! If you're looking for a versatile logger, this is definitely for you. However, it's also not as powerful as some other crates so there's that to bear in mind. For most common use cases, it's the easiest to use crate: you simply set the message level, then send your message! A quick summary: - Maintained by the official Rust team - Works with nearly all logger crates - Not as powerful as some other log facade crates ## env-logger [env-logger](https://github.com/rust-cli/env_logger) is a simple Rust logger that's easy to use and is quite convenient for any small project where you want to implement logging but don't want something heavy-duty that will more than likely require a considerable amount of boilerplate. It's owned by the Rust CLI Working Group (WG), meaning it'll see long-term support which is great for us. It can be set up in a one-line statement: ```rust let logger = Logger::from_default_env(); ``` Then you'd simply run your program from cargo like so, with the `RUST_LOG` environment variable in front of the command: ```bash # This command will run your program and only print out error messages from logs RUST_LOG=ERROR cargo run ``` You can also hard-code your minimum log level in your application like so: ```rust use env_logger::{Logger, Env}; let env = Env::new() // filters out any messages that aren't at "info" log level or above .filter_or("MY_LOG", "info") // always use styles when printing .write_style_or("MY_LOG_STYLE", "always"); let logger = Logger::from_env(env); ``` Have an overly verbose crate that loves spitting out logs? You can also set the log level for a specific dependency (this is in conjunction with the `log` crate): ```rust use env_logger::Builder; use log::LevelFilter; let mut builder = Builder::new(); builder.filter_module("path::to::module", LevelFilter::Info); .unwrap(); ``` For all of its convenience however, `env-logger` does suffer from a couple of things that you might be looking for in a production-grade application: namely, that there is little documented functionality on writing your own pipe for logs which can make it quite tricky to implement, and [it's also unclear whether this crate is thread-safe.](https://github.com/rust-cli/env_logger/issues/269) **When to use env-logger:** This is perfect for quick prototypes, development environments, or small CLI tools where you want the convenience of the `RUST_LOG` environment variable without any setup complexity. A quick summary: - Owned by the Rust CLI Working Group - Simple to use and feels good to use - Perfect for development and quick prototyping - Lack of documentation on more complex functionality like log appending/piping - Some unclear issues on whether the crate is 100% thread-safe ## fern [fern](https://github.com/daboross/fern) is a simple, runtime-configurable logging library that works with the `log` crate. If you're coming from JavaScript (Winston) or Python (logging module), fern will feel familiar - it uses a builder pattern that's intuitive and straightforward. ## log4rs [log4rs](https://github.com/estk/log4rs) is a logging crate modeled after Java's log4j - a logging package that's probably one of the most deployed pieces of open source software. This crate requires a bit more setup than the others and configuration can be done with either a YAML file or programmatically. `log4rs` is compatible with `log`, which is great for us as it means we don't have to adopt a new paradigm just to use `log4rs`. If you wanted to create a config file to load in from, you'd set your YAML file up like this: ```yaml # set a refresh rate refresh_rate: 30 seconds # appenders appenders: # this appender will append to the console stdout: kind: console # this appender will append to a log file requests: kind: file path: "log/requests.log" # this is a simple string encoder - this will be explained below encoder: pattern: "{d} - {m}{n}" # the appender that prints to stdout will only print if the log level of the message is warn or above root: level: warn appenders: - stdout # set minimum logging level - log messages below the mnimum won't be recorded loggers: app::backend::db: level: info app::requests: level: info appenders: - requests additive: false ``` The encoder can either use JSON encoding, or pattern encoding. Here we've decided to use pattern encoding, which follows similarly to the original log4j pattern but with Rust string formatting - you can check out more about how to format your encoder pattern [here.](https://www.tutorialspoint.com/log4j/log4j_patternlayout.htm) Then you can just initialise it when you're setting your program up, like so: ```rust log4rs::init_file("log4rs.yml", Default::default()).unwrap(); ``` You can also programatically create your configuration: ```rust use log::LevelFilter; use log4rs::append::console::ConsoleAppender; use log4rs::append::file::FileAppender; use log4rs::encode::pattern::PatternEncoder; use log4rs::config::{Appender, Config, Logger, Root}; fn main() { // set up ConsoleAppender to allow appending logs to the console (stdout) let stdout = ConsoleAppender::builder().build(); // set up FileAppender to allow appending logs to a log file let requests = FileAppender::builder() .encoder(Box::new(PatternEncoder::new("{d} - {m}{n}"))) .build("log/requests.log") .unwrap(); let config = Config::builder() .appender(Appender::builder().build("stdout", Box::new(stdout))) .appender(Appender::builder().build("requests", Box::new(requests))) .logger(Logger::builder().build("app::backend::db", LevelFilter::Info)) .logger(Logger::builder() .appender("requests") .additive(false) .build("app::requests", LevelFilter::Info)) .build(Root::builder().appender("stdout").build(LevelFilter::Warn)) .unwrap(); let handle = log4rs::init_config(config).unwrap(); // use handle to change logger configuration at runtime } ``` You can also automatically archive your logs with `log4rs`, which is great! This is a feature that must otherwise be manually implemented by yourself when it comes to most of if not all other logger crates, so having this feature built into the logger itself is a huge convenience. We can get started with setting it up by adding the following to a YAML configuration file (under "appenders"): ```yaml rolling_appender: kind: rolling_file path: log/foo.log append: true encoder: kind: pattern pattern: "{d} - {m}{n}" policy: kind: compound trigger: kind: size limit: 10 mb # upon reaching the max log size, the file simply gets deleted on successful roll roller: kind: delete ``` Now we have a policy that fills up the main log file, then appends it to an archived log file once the main log file reaches 10 megabytes' worth of logs, then deletes the currently active log file ready to receive more logs. As you can see, `log4rs` is an extremely versatile crate that works with the previously mentioned `log` crate to provide powerful functionality with regards to logging in Rust, and is espespecially great if you're coming from a language like Java where you already understand the mental model and just want to find out how to do logging in Rust. However, in exchange for this you have to learn how to set the logger up and the setup itself is quite complicated compared to other logging crates, so bear that in mind. Summary: - Large all-in-one crate that can do it all - Requires extensive boilerplate or a config file - Easy to set up your own file appending for a log egress service - Works with `log` ## simplelog [simplelog](https://github.com/Drakulix/simplelog.rs) lives up to its name - it's one of the simplest logging implementations you'll find that works with the `log` crate. If you want basic logging with minimal configuration, this is your option. ## slog [slog](https://github.com/slog-rs/slog) is an older structured logging framework that predates `tracing`. While it offers structured logging capabilities and has its own ecosystem of drain implementations, it's less widely adopted today compared to `tracing`. ## tracing [tracing](https://github.com/tokio-rs/tracing) is a crate that calls itself "a framework for instrumenting Rust programs to collect structured, event-based diagnostic information", requiring its logger counterpart `tracing-subscriber` to be used or a custom type that implements the `tracing::Subscriber` function. Developed by the Tokio team, it's fully built up from the ground for async which is perfect for web applications with Rust logs. `tracing` uses the concept of "spans" which are used to record the flow of execution through a program. Events can happen inside or outside of a span and can also be used similarly to unstructured logging (ie, just recording the event any which way) but can also represent a point of time within a span. See below: ```rust use tracing::Level; // records an event outside of any span context: tracing::event!(Level::DEBUG, "something happened"); // create the span while entering it let span = tracing::span!(Level::INFO"my_span").entered(); // records an event within "my_span". tracing::event!(Level::DEBUG, "something happened inside my_span"); ``` Spans can form a tree structure, and the entire subtree is represented by its children - therefore, a parent span will always last as long as its longest-lived child span if not longer. Because all of this can be a bit excessive, `tracing` has also included the regular macros that would be in other log facade libraries for logging - namely, `info!`, `error!`, `debug!`, `warn!` and `trace!`. There's also a span version of each of these macros - but if you're coming from `log` and want to try `tracing` out without getting lost in the complexity of trying to make sure everything is in a span, tracing's got your back. ```rust use tracing; tracing::debug!("Looks just like the log crate!"); tracing::info_span!("a more convenient version of creating spans!"); ``` [tracing-subscriber](https://github.com/tokio-rs/tracing/tree/master/tracing-subscriber) is the logger crate designed to work with `tracing` by letting you define a logger that implements the `Subscriber` trait from `tracing`. You can start a subscriber that takes the `RUST_LOG` environment variable like so: ```rust tracing_subscriber::registry() .with(fmt::layer()) .with(EnvFilter::from_default_env()) .init(); ``` You can also apply a hard-coded filter programatically: ```rust use tracing_subscriber::filter::{EnvFilter, LevelFilter}; let my_filter = EnvFilter::builder() .with_default_directive(LevelFilter::ERROR.into()) .from_env_lossy(); tracing_subscriber::registry() .with(fmt::layer()) .with(filter) .init(); ``` You can also layer filters on top of each other! This is quite useful in case you want the effect of having multiple subscribers at the same time. If you need to export your logs somewhere, there's also the [tracing_appender](https://github.com/tokio-rs/tracing/tree/master/tracing-appender) crate. You would want to add this in with your tracing subscriber by using the `.with_writer()` method, like so: ```rust // create a file appender that rotates hourly let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log"); // make the file appender non-blocking // the guard exists to make sure buffered logs get flushed to output let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); // add the file appender to your tracing subscriber tracing_subscriber::fmt() .with_writer(non_blocking) .init(); ``` The `non_blocking` writer is built with a type that implements `std::io::Write` - so if you wanted to implement your own thing that implements `std::io::Write` (say you want a logging express that automatically exports all your stuff to BetterStack or Datadog) - you'd want to try this. See below: ```rust use std::io::Error; struct TestWriter; impl std::io::Write for TestWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { let buf_len = buf.len(); println!("{:?}", buf); Ok(buf_len) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } let (non_blocking, _guard) = tracing_appender::non_blocking(TestWriter); tracing_subscriber::fmt() .with_writer(non_blocking) .init(); ``` As you can see, the `tracing` family of crates offers a tonne of power in terms of what it can do and is robust enough for any web application and it's maintained by the Tokio team so it is sure to be supported for a long time. However, using it requires learning about how `tracing` works as it uses concepts that are not utilised in other logging crates - so you'll be locked in if you need to migrate from the crate for whatever reason and you're using spans. **Performance considerations:** The `tracing-appender` crate mentioned above provides non-blocking, out-of-thread logging which is particularly valuable for high-throughput applications where logging performance matters. The non-blocking writer offloads the I/O operations to a separate thread, preventing logging from blocking your application's critical path. **Compatibility note:** While `tracing` is its own ecosystem, you can make it work with `log`-based consumers using the `tracing-log` crate. This gives you flexibility - you can use tracing's powerful features while still integrating with libraries that use the standard `log` facade. Summary: - Requires some learning about spans, etc to utilise fully - Maintained by the Tokio team so more than likely will see LTS - Split crates means you don't have to install things you aren't going to use - `tracing-appender` provides out-of-thread logging for better performance - Compatible with `log` ecosystem via `tracing-log` - Probably the most complex system to use on the list due to the way it's built ## Choosing the Right Logging Crate With so many options, here's a comparison table to help you decide: | Crate | Best For | Complexity | Structured Logging | Key Strength | When to Use | | -------------- | ------------ | ---------- | ------------------ | ------------------------------------- | ------------------------------------------------------- | | **tracing** | Applications | High | Yes | Async-first, powerful instrumentation | Production apps, microservices, need structured logging | | **log** | Libraries | Low | No | Universal compatibility | Building a library that others will use | | **env-logger** | Applications | Very Low | No | Quick RUST_LOG setup | Development, prototypes, CLI tools | | **fern** | Applications | Low | No | Familiar API | Coming from JS/Python, need simple control | | **simplelog** | Applications | Very Low | No | Absolute simplicity | Just want it to work with minimal code | | **log4rs** | Applications | High | No | File rotation, YAML config | Need enterprise features, coming from Java | | **slog** | Applications | Medium | Yes | Structured logging (older) | Existing slog codebase (use tracing for new projects) | **Quick decision guide:** - **Building a library?** Use `log` - it's the universal standard and ensures maximum compatibility with any logging backend users might choose. - **Need structured logging?** Use `tracing` for new projects. It's the modern choice with the best async support and ecosystem. - **Just want simple logging for development?** Use `env-logger` or `simplelog` - both are trivial to set up and work great for quick projects. - **Need file rotation and enterprise features?** Use `log4rs` - it handles log archiving, rotation, and complex configurations out of the box. - **Coming from another language?** Use `fern` if you're familiar with Winston (JS) or Python's logging module - the builder pattern will feel natural. - **Performance-critical application?** Use `tracing` with `tracing-appender` for non-blocking, out-of-thread logging. The most important distinction: **libraries should use `log`** for compatibility, while **applications have more freedom** to choose based on their specific needs. ## Conclusions Thanks for reading! Now that we're at the end, I hope you have a better understanding of logging in Rust. With so many logging crates it's difficult to figure out which one you should use, but hopefully this article has provided some clarity into which crate is the best Rust logger for your use case. Did you like this article? Be sure to [give us a star on GitHub!](https://www.github.com/shuttle-hq/shuttle) --- # Writing a Web Scraper in Rust using Reqwest Source: https://www.shuttle.dev/blog/2023/09/13/web-scraping-rust-reqwest Date: 13 September 2023 Author: josh Tags: rust, web-scraping, tutorial Learn to leverage the power of Rust for web scraping. This article explores how you can competently create a web scraping service and host it online without hassle. ## Introduction Have you ever thought about making your own database of potential businesses for lead generation or product price data so you can get your products at the cheapest price without any effort? Web scraping is what lets you do that without having to do any of the manual work yourself. Rust makes this easier by allowing you to handle errors explicitly and run tasks concurrently, letting you do things like attaching a web service router to your scraper or a Discord bot that outputs the data. In this guide to Rust web scraping, we will write a Rust web scraper that will scrape Amazon for Raspberry Pi products and get their prices, then store them in a PostgresQL database for further processing. Stuck or want to see the final code? The Github repository for this article can be found [here.](https://github.com/joshua-mo-143/reqshuttle) ## Getting Started Let's make a new project by using `cargo shuttle init`. For this project we'll simply call it `webscraper` - you'll want the `none` option for the framework, which will spawn a new Cargo project with `shuttle-runtime` added (as we aren't currently using a web framework, we don't need to pick any of the other options). Let's install our dependencies with the following one liner: ```bash cargo add chrono reqwest scraper tracing shuttle-shared-db sqlx --features shuttle-shared-db/postgres,sqlx/runtime-tokio-native-tls,sqlx/postgres ``` We'll also want to install `sqlx-cli`, which is a useful tool for managing our SQL migrations. We can install it by running the following: ```bash cargo install sqlx-cli ``` If we then use `sqlx migrate add schema` in our project folder, we'll then get our SQL migration file, which can be found in the `migrations` folder! The file will be formatted with the date and time at which the migration was created, and then the name we gave it (in this case, `schema`). For our purposes, here are the migrations we'll be using: ```sql -- migrations/schema.sql CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, price VARCHAR NOT NULL, old_price VARCHAR, link VARCHAR, scraped_at DATE NOT NULL DEFAULT CURRENT_DATE ); ``` Before we get started, we'll want to make a struct that implements `shuttle_runtime::Service`, which is an async trait. We'll also want to set our user agent so that there is less chance of us getting blocked. Thankfully, we can do all of this by returning a struct in our main function, like so: ```rust // src/main.rs use reqwest::Client; use tracing::error; use sqlx::PgPool; struct CustomService { ctx: Client, db: PgPool } // Set up our user agent const USER_AGENT: &str = "Mozilla/5.0 (Linux x86_64; rv:115.0) Gecko/20100101 Firefox/115.0"; // note that we add our Database as an annotation here so we can easily get it provisioned to us #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool ) -> Result { // automatically attempt to do migrations // we only create the table if it doesn't exist which prevents data wiping sqlx::migrate!().run(&db).await.expect("Migrations failed"); // initialise Reqwest client here so we can add it in later on let ctx = Client::builder().user_agent(USER_AGENT).build().unwrap(); Ok(CustomService { ctx, db }) } #[shuttle_runtime::async_trait] impl shuttle_runtime::Service for CustomService { async fn bind(mut self, _addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { scrape(self.ctx, self.db).await.expect("scraping should not finish"); error!("The web scraper loop shouldn't finish!"); Ok(()) } } ``` Now that we're done, we can get started web scraping in Rust! ## Making our Web Scraper The first part of making our web scraper is making a request to our target URL so we can grab the response body to process. Thankfully, Amazon's URL syntax is quite simple, so we can easily customise the URL query parameters by adding the name of the search terms we want to look for. Because Amazon returns multiple pages of results, we also want to be able to set our page number as a mutable dynamic variable that will get incremented by 1 every time the request is successful. ```rust // src/main.rs use chrono::NaiveDate; #[derive(Clone, Debug)] struct Product { name: String, price: String, old_price: Option, link: String, } async fn scrape(ctx: Client) -> Result<(), String> { let mut pagenum = 1; let mut retry_attempts = 0; let url = format!("https://www.amazon.com/s?k=raspberry+pi&page={pagenum}"); let res = match ctx.get(url).send().await { Ok(res) => res, Err(e) => { error!("Error while attempting to send HTTP request: {e}"); break }}; let res = match res.text().await { Ok(res) => res, Err(e) => { error!("Error while attempting to get the HTTP body: {e}"); break } }; } ``` As you may have noticed, we added a variable named `retry_attempts`. This is because sometimes when we're scraping, Amazon (or any other site for that matter) may give us a 503 Service Unavailable, meaning that the scraping will fail. Sometimes this can be caused by server overload or us scraping too quickly, so we can model our error handling like this: ```rust // src/main.rs use reqwest::StatusCode; use std::thread::sleep as std_sleep; use tokio::time::Duration; let mut retry_attempts = 0; if res.status() == StatusCode::SERVICE_UNAVAILABLE { error!("Amazon returned a 503 at page {pagenum}"); retry_attempts += 1; if retry_attempts >= 10 { // take a break if too many retry attempts error!("It looks like Amazon is blocking us! We will rest for an hour."); // sleep for an hour then retry on current iteration std_sleep(Duration::from_secs(3600)); continue; } else { std_sleep(Duration::from_secs(15)); continue; } } retry_attempts = 0; ``` Assuming the HTTP request is successful, we'll get a HTML body that we can parse using `scraper`. If you go to Amazon in your browser and search for "raspberry pi", you'll then receive a product list. You can examine this product list by using the dev tools function on your browser (in this instance, it's the Inspect function in Firefox but you can also use Chrome Devtools, Microsoft Edge DevTools, etc...). It should look like the following: ![Devtools preview of webpage analysis for web scraping in a browser](/images/blog/web_scraper_amazon.png) You might notice that the `div` element has a data attribute of `data-component-type` for which the value of `s-search-result`. This is helpful for us as no other page components other than the ones we want to scrape have that attribute! Therefore, we can scrape the data by selecting it as a CSS selector (see below for more information). We'll want to make sure we prepare our HTML by parsing it as a HTML fragment, and then we can declare our initial `scraper::Selector`: ```rust // src/main.rs use scraper::{Html, Selector}; let html = Html::parse_fragment(&res); let selector = Selector::parse("div[data-component-type='s-search-result']").unwrap(); ``` As you can see, the `Selector` uses CSS selectors to be able to parse the HTML. In this case, we are specifically attempting to search for a HTML `div` element that has a data attribute called "data-component-type" with a value of "s-search-result". If you attempt to run our program now and `html.select(&selector)` as per the `scraper` documentation, you'll see that it returns an iterator over HTML elements. However, because the iteration count can also technically be zero, we'll want to make sure that there are actually things we can iterate over - so let's make sure we cover that point by adding an if statement to check for the iterator count: ```rust // src/main.rs if html.select(&selector).count() == 0 { error!("There's nothing to parse here!"); break }; ``` In our final iteration of the app, this should just break the loop as this will normally signal that there's no more products we can retrieve as in the first case there should always be product results. Now that we've done our respective error handling, we can iterate through the entries and create a Product, then append it to our vector of Products. ```rust // src/main.rs for entry in html.select(&selector) { // declaring more Selectors to use on each entry let price_selector = Selector::parse("span.a-price > span.a-offscreen").unwrap(); let productname_selector = Selector::parse("h2 > a").unwrap(); let name = entry.select(&productname_selector).next().expect("Couldn't find the product name").text.next().unwrap().to_string(); // Amazon products can have two prices: a current price, and an "old price". We iterate through both of these and map them to a Vec. let price_text = entry.select(&price_selector).map(|x| x.text().next().unwrap().to_string()).collect::>(); // get local date from chrono for database storage purposes let scraped_at = Local::now().date_naive(); // here we find the anchor element and find the value of the href attribute - this should always exist so we can safely unwrap let link = entry.select(&productname_selector).map(|link| {format!("https://amazon.co.uk{}", link.value().attr("href").unwrap())}).collect::(); vec.push(Product { name, price: price_text[0].clone(), old_price: Some(price_text[1].clone()), link, scraped_at, }); } pagenum += 1; std_sleep(Duration::from_secs(20)); ``` Note that in the above codeblock we use sleep from the standard library - if we attempt to use `tokio::time::sleep`, the compiler returns an error about holding a non-`Send` future across an await point. Now that we've written our code for processing the data we've gathered from the web page, we can wrap what we've written so far in a loop, moving our `Vec` and `pagenum` declarations to an outer loop that will run infinitely. Next, we'll want to make sure we have somewhere to save our data! We'll want to use a batched transaction here, which thankfully we can do by using `db.begin` and `db.commit`. Check the code out below: ```rust // src/main.rs let transaction = db.begin().await.unwrap(); for product in vec { if let Err(e) = sqlx::query("INSERT INTO products (name, price, old_price, link, scraped_at) VALUES ($1, $2, $3, $4, $5) ") .bind(product.name) .bind(product.price) .bind(product.old_price) .bind(product.link) .bind(product.scraped_at) .execute(&db) .await .unwrap() { error!("There was an error: {e}"); error!("This web scraper will now shut down."); transaction.rollback().await.unwrap(); break } } transaction.commit().await.unwrap(); ``` All we're doing here is just running a for loop over the list of scraped products and inserting them all into the database, then committing at the end to finalise it. Now ideally we'll want the scraper to rest for some time so that the pages are given time to update - otherwise, if you comb the pages all the time you will more than likely end up with a huge amount of duplicate data. Let's say we wanted to wanted it to rest until midnight: ```rust // src/main.rs use tokio::time::{sleep as tokio_sleep, Duration}; // get the local time, add a day then get the NaiveDate and set a time of 00:00 to it let tomorrow_midnight = Local::now() .checked_add_days(Days::new(1)) .unwrap() .date_naive() .and_hms_opt(0, 0, 0) .unwrap(); // get the local time now let now = Local::now().naive_local(); // check the amount of time between now and midnight tomorrow let duration_to_midnight = tomorrow_midnight.signed_duration_since(now).to_std().unwrap(); // sleep for the required time tokio_sleep(Duration::from_secs(duration_to_midnight.as_secs())).await; ``` Now we're pretty much done! Your final scraping function should look like this: ```rust // src/main.rs async fn scrape(ctx: Client, db: PgPool) -> Result<(), String> { debug!("Starting scraper..."); loop { let mut vec: Vec = Vec::new(); let mut pagenum = 1; let mut retry_attempts = 0; loop { let url = format!("https://www.amazon.com/s?k=raspberry+pi&page={pagenum}"); let res = match ctx.get(url).send().await { Ok(res) => res, Err(e) => { error!("Something went wrong while fetching from url: {e}"); StdSleep(StdDuration::from_secs(15)); continue; } }; if res.status() == StatusCode::SERVICE_UNAVAILABLE { error!("Amazon returned a 503 at page {pagenum}"); retry_attempts += 1; if retry_attempts >= 10 { error!("It looks like Amazon is blocking us! We will rest for an hour."); StdSleep(StdDuration::from_secs(3600)); continue; } else { StdSleep(StdDuration::from_secs(15)); continue; } } let body = match res.text().await { Ok(res) => res, Err(e) => { error!("Something went wrong while turning data to text: {e}"); StdSleep(StdDuration::from_secs(15)); continue; } }; debug!("Page {pagenum} was scraped"); let html = Html::parse_fragment(&body); let selector = Selector::parse("div[data-component-type= ' s-search-result ' ]").unwrap(); if html.select(&selector).count() == 0 { break; }; for entry in html.select(&selector) { let price_selector = Selector::parse("span.a-price > span.a-offscreen").unwrap(); let productname_selector = Selector::parse("h2 > a").unwrap(); let price_text = entry .select(&price_selector) .map(|x| x.text().next().unwrap().to_string()) .collect::>(); vec.push(Product { name: entry .select(&productname_selector) .next() .expect("Couldn't find the product name!") .text() .next() .unwrap() .to_string(), price: price_text[0].clone(), old_price: Some(price_text[1].clone()), link: entry .select(&productname_selector) .map(|link| { format!("https://amazon.co.uk{}", link.value().attr("href").unwrap()) }) .collect::(), }); } pagenum += 1; retry_attempts = 0; StdSleep(StdDuration::from_secs(15)); } let transaction = db.begin().await.unwrap(); for product in vec { if let Err(e) = sqlx::query( "INSERT INTO products (name, price, old_price, link, scraped_at) VALUES ($1, $2, $3, $4, $5)" ) .bind(product.name) .bind(product.price) .bind(product.old_price) .bind(product.link) .execute(&db) .await { error!("There was an error: {e}"); error!("This web scraper will now shut down."); break; } } transaction.commit().await.unwrap(); // get the local time, add a day then get the NaiveDate and set a time of 00:00 to it let tomorrow_midnight = Local::now() .checked_add_days(Days::new(1)) .unwrap() .date_naive() .and_hms_opt(0, 0, 0) .unwrap(); // get the local time now let now = Local::now().naive_local(); // check the amount of time between now and midnight tomorrow let duration_to_midnight = tomorrow_midnight .signed_duration_since(now) .to_std() .unwrap(); // sleep for the required time TokioSleep(TokioDuration::from_secs(duration_to_midnight.as_secs())).await; } Ok(()) } ``` And we're done! ## Deploying If you initialised your project on the Shuttle servers, you can get started by using `cargo shuttle deploy` (adding `--allow-dirty` if on a dirty Git branch). If not, you'll want to use `cargo shuttle project start --idle-minutes 0` to get your project up and running. ## Finishing Up Thanks for reading this article! I hope you have a more thorough understanding of how to start web scraping in Rust, using the Rust Reqwest and scraper crates. Ways to extend this article: - Add a frontend so you can show stats for your scraper bot - Add a proxy for your web scraper - Scrape more than one website --- # Semantic Search with Qdrant, OpenAI and Shuttle Source: https://www.shuttle.dev/blog/2023/09/08/building-semantic-search-in-rust Date: 8 September 2023 Author: stefan Tags: rust, ai, qdrant, semantic-search Explore the process of creating a semantic search with Qdrant, OpenAI, and Shuttle. The article provides a detailed guide on indexing blogs, using OpenAI for document queries, and deploying a web application. ## Introduction Large language models are mostly known for their use in chatbots like ChatGPT. Impressive as they are, that's not their only use case. A simple technique called "embedding" allows us to quantify any textual information and use it in all sorts of applications. For example, a semantic search engine, that understands our blog or documentation, and retrieves the most relevant information. As an added bonus, we want to create a summarization of the relevant contents. To do so, we need the following ingredients: 1. [OpenAI](https://openai.com) so we can use their language models to embed our sentences. 2. [Qdrant](https://qdrant.tech) as a database that allows us to search for similar textual information. 3. **Shuttle** so we can write an actual web application and deploy it in no time. ## The Architecture What is a semantic search? The gist is that you want to find documents and information based on a semantic connection rather than the words alone. The folks from Qdrant show it best in their presentation at [Rust Linz](https://www.youtube.com/watch?v=uS4yhtvjseM): If you search for "What is the capital of the United States?", you can either get information on Washington D.C., or on capital punishment for crimes in the U.S. The words alone can't differentiate, but the way the question is asked, there's no doubt that you want to know about Washington. Or maybe you use different words than indexed, that have a similar root or semantic value, but are nowhere found in your documents. This is where the semantic information is important. So how can we use a semantic search for our own documentation? ![A picture of how using an LLM for Semantic Search works](/images/blog/qdrant-architecture.png) The image above shows the architecture. What we are going to do is two-fold. First, we create embeddings of all our documents. Large language models not only allow us to generate text, they are also able to give us a numeric representation of text. Sentences and phrases with a similar mood, sentiment or content have similar vectors. A vector search engine can help us finding similar entries. The model we are using, Ada, generates vectors with 1500+ elements each. If a question is similar to a sentence in our documents, or has relevant and similar information, the vectors will be similar, too. Depending on your large language model, this vectors will also be similar if you use different words, expressions, or even languages. Second, we do the same process for single prompts by our users in our UI. We then search for the most similar vector in our database and return the corresponding document. As an added bonus, we create a summary through the GPT 3.5 model. ## Parsing As a first step, we need to create a database that holds the contents of our blog in a semantically searchable way. To do this, we want to parse a series of Markdown files and extract single sentences. We are not interested in comments, not even headlines. Headlines are usually too short to be useful for semantic search. We want to extract sentences that are meaningful and can be used to find similar sentences. We are interested in code-blocks, though, which we parse fully and take as a single "sentence". Note that the parsing process here is very rough and just "good enough" for my humble blog. This also works mostly because I write a new sentence with every new line. If you want to parse more complex Markdown files, or parse even completely different things, you need to improve the parsing process. We start by defining a struct that holds the path to the file, the contents of the file and the extracted sentences. ```rust pub struct File { pub path: String, pub contents: String, pub sentences: Vec, } ``` We also define a state-machine that helps us to parse the file. We start in the `None` state and then switch to `CodeBlock`, `Sentence` or `Comments` depending on the current line. ```rust enum FileState { None, CodeBlock, Sentence, Comments, } ``` The `parse` method starts with an empty contents vector and the `None` state. Then it iterates over all lines of the file and depending on the current state, it either switches to a new state or adds the current line to the current sentence. Once we extraced a full sentence, we add it to the contents vector and start a new sentence. ````rust pub fn parse(&mut self) { let mut contents = Vec::new(); let mut state = FileState::None; let mut sentence = String::new(); for line in self.contents.lines() { match state { FileState::None => { if line.starts_with("```") { state = FileState::CodeBlock; sentence = String::new(); sentence.push_str(line); sentence.push('\n'); } else if line.starts_with("---") { state = FileState::Comments; } else if !line.starts_with('#') && !line.is_empty() { state = FileState::Sentence; sentence = String::new(); sentence.push_str(line); sentence.push('\n'); } } FileState::CodeBlock => { sentence.push_str(line); if line.starts_with("```") { contents.push(sentence); sentence = String::new(); state = FileState::None; } } FileState::Comments => { if line.starts_with("---") { state = FileState::None; } } FileState::Sentence => { if line.is_empty() { state = FileState::None; contents.push(sentence); sentence = String::new(); } else { sentence.push_str(line); sentence.push('\n'); } } } } self.sentences = contents; } ```` With the `File` struct now representing a parsed file, we need to go from an actual file to a `File` struct. A little helper trait `HasFileExt` helps us to check if a file has a certain ending. We implement it for `Path` and use it to filter out files that do not have the correct ending. ```rust trait HasFileExt { fn has_file_extension(&self, ending: &str) -> bool; } impl HasFileExt for Path { fn has_file_extension(&self, ending: &str) -> bool { if let Some(path) = self.to_str() { return path.ends_with(ending); } false } } ``` The `load_files_from_dir` function takes a directory, an ending and a prefix. It then iterates over all files in the directory and if it finds a file with the correct ending, it parses it and adds it to the result vector. ```rust // Load files from directory by ending pub fn load_files_from_dir(dir: PathBuf, ending: &str, prefix: &PathBuf) -> Result> { let mut files = Vec::new(); for entry in fs::read_dir(dir)? { let path = entry?.path(); if path.is_dir() { let mut sub_files = load_files_from_dir(path, ending, prefix)?; files.append(&mut sub_files); } else if path.is_file() && path.has_file_extension(ending) { println!("Path: {:?}", path); let contents = fs::read_to_string(&path)?; let path = Path::new(&path).strip_prefix(prefix)?.to_owned(); let key = path.to_str().ok_or(NotAvailableError {})?; let mut file = File::new(key.to_string(), contents); file.parse(); files.push(file); } } Ok(files) } ``` Brilliant! Now that we can go from Markdown files to single sentences, we can start indexing! ## Embedding The next step is to embed the sentences. We use the [OpenAI API](https://beta.openai.com/docs/api-reference) to embed the sentences. The API is very simple and we can use the [openai](https://docs.rs/openai/latest/openai/) crate to interact with it. The Shuttle `SecretStore` helps us to load the API key from the environment. We use the `OPENAI_API_KEY` environment variable to setup the `openai` crate for our usage. All oyu need to do is to add the API key to your `Secrets.toml` file and then you can use it in your code. ```rust use shuttle_secrets::SecretStore; pub fn setup(secrets: &SecretStore) -> Result<()> { let openai_key = secrets .get("OPENAI_API_KEY") .ok_or(SetupError("OPENAI Key not available"))?; openai::set_key(openai_key); Ok(()) } ``` An `embed_file` function takes a `File` that we alrady parsed and returns a vector of `Embeddings`. We use the `openai::Embeddings::create` function to embed the sentences. Instead of just doing one sentence at a time, this function allows us to embed multiple sentences at once, giving us many vectors in return. We use the `text-embedding-ada-002` model. `"stefan"` is just a placeholder for a user that the OpenAI API requires. You can add whatever you like here. "Ada" is a very elaborate model for embedding sentences. It contains over 1500 values, allowing us to do very detailed comparisons between sentences. ```rust pub async fn embed_file(file: &File) -> Result { let sentence_as_str: Vec<&str> = file.sentences.iter().map(|s| s.as_str()).collect(); Embeddings::create("text-embedding-ada-002", sentence_as_str, "stefan") .await .map_err(|_| EmbeddingError {}.into()) } ``` Once we have all the embeddings, we need to store them somewhere. We use the [Qdrant](https://qdrant.tech/) database for this. It is a very simple vector database that allows us to store vectors and then query them. We use the [qdrant_client](https://docs.rs/qdrant-client/latest/qdrant_client/) crate to interact with the database. We define a `VectorDB` struct that holds the `QdrantClient` and the `id` of the collection we want to use. We use the `QDRANT_TOKEN` and `QDRANT_URL` environment variables to setup the `QdrantClient`. Again, you can add these to your `Secrets.toml` file and then use them in your code. The constant `COLLECTION` defines the name of the collection we want to use ```rust static COLLECTION: &str = "docs"; pub struct VectorDB { client: QdrantClient, id: u64, } impl VectorDB { pub fn new(secrets: &SecretStore) -> Result { let qdrant_token = secrets .get("QDRANT_TOKEN") .ok_or(SetupError("QDRANT_TOKEN not available"))?; let qdrant_url = secrets .get("QDRANT_URL") .ok_or(SetupError("QDRANT_URL not available"))?; let mut qdrant_config = QdrantClientConfig::from_url(&qdrant_url); qdrant_config.set_api_key(&qdrant_token); let client = QdrantClient::new(Some(qdrant_config))?; Ok(Self { client, id: 0 }) } // ... } ``` We define a `reset_collection` function that deletes the collection if it exists and then creates a new one. Usually, you only do this once and then you just add new vectors to the collection. Since my blog always pushes an entire set of rendered Markdown files, I just delete the collection and recreate it every time. This is not very efficient, but it works for my use case. ```rust pub async fn reset_collection(&self) -> Result<()> { self.client.delete_collection(COLLECTION).await?; self.client .create_collection(&CreateCollection { collection_name: COLLECTION.to_string(), vectors_config: Some(VectorsConfig { config: Some(Config::Params(VectorParams { size: 1536, distance: Distance::Cosine.into(), hnsw_config: None, quantization_config: None, on_disk: None, })), }), ..Default::default() }) .await?; Ok(()) } ``` The `upsert_embedding` function takes an `Embedding` and a `File` and stores the embedding in the database. We use the `file.path` as the `id` of the vector. This allows us to later query the database and get the `File` back. We use the `Embedding` as the vector and store it in the database. ```rust pub async fn upsert_embedding(&mut self, embedding: Embedding, file: &File) -> Result<()> { let payload: Payload = json!({ "id": file.path.clone(), }) .try_into() .map_err(|_| EmbeddingError {})?; println!("Embedded: {}", file.path); let vec: Vec = embedding.vec.iter().map(|&x| x as f32).collect(); let points = vec![PointStruct::new(self.id, vec, payload)]; self.client.upsert_points(COLLECTION, points, None).await?; self.id += 1; Ok(()) } ``` Alright! Now our database is ready and we can start searching for similar sentences. ## Searching The idea is that our users are asking our search engine a question in natural language, and we return the most relevant documents. Similar to our `embed_file` function, we create a function called `embed_sentence` that takes a user's prompt and returns a single `Embedding`. ```rust pub async fn embed_sentence(prompt: &str) -> Result { Embedding::create("text-embedding-ada-002", prompt, "stefan") .await .map_err(|_| EmbeddingError {}.into()) } ``` This single embedding then is used to fetch relevant documents from Qdrant. Remember, all embeddings have the same length. We look for sentences that are similar to the one raised by our question. If we get good hits, then the document attached to it is probably relevant to the question. The search function we write here is pretty simple and can be made much better. We look for the most similar sentence and return the `ScoredPoint`. This also includes the file path, so we have everything we need to get the relevant information. A search function that checks if there is more than one relevant sentence or that returns the top 10 results would be much better. But for now, this is good enough. It was important that we dropped all the headlines while indexing. Headlines contain condensed information which might give a good hit when searching, but ultimately lacks required context. So the real and relevant stuff is in the paragraphs! ```rust pub async fn search(&self, embedding: Embedding) -> Result { let vec: Vec = embedding.vec.iter().map(|&x| x as f32).collect(); let payload_selector = WithPayloadSelector { selector_options: Some(SelectorOptions::Enable(true)), }; let search_points = SearchPoints { collection_name: COLLECTION.to_string(), vector: vec, limit: 1, with_payload: Some(payload_selector), ..Default::default() }; let search_result = self.client.search_points(&search_points).await?; let result = search_result.result[0].clone(); Ok(result) } ``` We not only want to retrieve the relevant document, we also want to have our search answer the question based on the document it retrieves. We use OpenAI again, this time with the `chat` endpoint and the `gpt-3.5-turbo` model. The key here lies in the very first line. This is the message we send to OpenAI: 1. The Question, the "prompt" from the user. 2. The Context, the relevant document we retrieved from Qdrant. 3. A hint to be concise. With that little message, GPT 3.5. will answer the question based on the document we found, and it will give a short answer. This is exactly what we want. ```rust pub async fn chat_stream(prompt: &str, contents: &str) -> Result { let content = format!("{}\n Context: {}\n Be concise", prompt, contents); ChatCompletionBuilder::default() .model("gpt-3.5-turbo") .temperature(0.0) .user("stefan") .messages(vec![ChatCompletionMessage { role: openai::chat::ChatCompletionMessageRole::User, content, name: Some("stefan".to_string()), }]) .create_stream() .await .map_err(|_| EmbeddingError {}.into()) } ``` We use the streaming API to get single tokens, so everything flows together nicely. ## Putting it all together We have all the pieces we need to build our search engine. We can embed files, we can search for relevant documents, and we can ask OpenAI to answer questions based on the documents we found. Now it's time to put it all together in a little web application based on [Axum](https://github.com/tokio-rs/axum). We start by defining our `AppState`. This is the state of our application. It contains the files we indexed, and the database we use to search for relevant documents. Next, we define our routes. We have two routes. `/prompt`, that takes a `POST` request. The prompt is the question the user asks. We then embed the prompt, search for relevant documents, and ask OpenAI to answer the question based on the document we found. The route `/embed` is a `GET` route that starts the indexing process for the entire documentation. ```rust #[shuttle_runtime::main] async fn axum( #[shuttle_static_folder::StaticFolder(folder = "static")] static_folder: PathBuf, #[shuttle_static_folder::StaticFolder(folder = "docs")] docs_folder: PathBuf, #[shuttle_static_folder::StaticFolder(folder = ".")] prefix: PathBuf, #[shuttle_secrets::Secrets] secrets: shuttle_secrets::SecretStore, ) -> shuttle_axum::ShuttleAxum { open_ai::setup(&secrets)?; let mut vector_db = VectorDB::new(&secrets)?; let files = contents::load_files_from_dir(docs_folder, ".mdx", &prefix)?; let app_state = AppState { files, vector_db }; let app_state = Arc::new(Mutex::new(app_state)); let router = Router::new() .route("/prompt", post(prompt)) .route("/embedd", get(embed)) .nest_service("/", ServeDir::new(static_folder)) .with_state(app_state); Ok(router.into()) } ``` The indexing process is pretty simple. We iterate over all the files, embed them, and store the embeddings in [Qdrant](https://qdrant.tech). We use the `upsert` function, so we can run the indexing process multiple times. If we add new files, they will be added to the index. If we change existing files, the embeddings will be updated. ```rust async fn embed_documentation(app_state: &mut AppState) -> anyhow::Result<()> { for file in app_state.files { let embeddings = open_ai::embed_file(file).await?; println!("Embedding: {:?}", file.path); for embedding in embeddings.data { app_state.vector_db.upsert_embedding(embedding, file).await?; } } Ok(()) } async fn embed(State(app_state): State>>) -> Result<()> { let mut app_state = app_state.lock().await?; app_state.vector_db.reset_collection().await?; embed_documentation(&mut app_state).await?; } ``` The prompt route is a little more complicated. We first embed the prompt, then we search for relevant documents, and then we ask OpenAI to answer the question based on the document we found. We use the [`axum_streams` crate](https://docs.rs/axum-streams/latest/axum_streams/) to stream the response back to the user. This is important, because the response can be quite large. We don't want to wait for the entire response to be generated before we send it back to the user. Instead, we stream the response back to the user as it is generated. ```rust async fn get_contents( prompt: &str, app_state: &AppState, ) -> anyhow::Result> { let embedding = open_ai::embed_sentence(prompt).await?; let result = app_state.vector_db.search(embedding).await?; println!("Result: {:?}", result); let contents = app_state .files .get_contents(&result) .ok_or(PromptError {})?; open_ai::chat_stream(prompt, contents.as_str()).await } async fn prompt( State(app_state): State>, Json(prompt): Json, ) -> impl IntoResponse { let prompt = prompt.prompt; let chat_completion = get_contents(&prompt, &app_state).await; if let Ok(chat_completion) = chat_completion { return axum_streams::StreamBodyAs::text(chat_completion_stream(chat_completion)); } axum_streams::StreamBodyAs::text(error_stream()) } ``` And that's all there is to it. We have a fully functional semantic search engine, that finds the most relevant documents and that can answer questions based on the indexed documentation. One thing that's missing is the front-end. A tiny little HTML and JavaScript does the trick. This is the entire front-end: ```html

Shuttle Semantic Search

``` ## Try it out yourself! Try it for yourself. You can find the entire [program on GitHub](https://github.com/ddprrt/shuttle-qdrant-openai). The example contains everything you need to deploy your own semantic search engine, and it is fully prepared to run on Shuttle. You need to: 1. Get developer access at [OpenAI](https://platform.openai.com/) 2. Sign up for a database at [Qdrant](https://cloud.qdrant.io/) 3. Get your Shuttle account! But I'm sure you have one already, don't you? 4. [Install Shuttle](https://docs.shuttle.dev/getting-started/installation) Once you have everything, enter your access tokens to a `Secrets.toml` file and execute `shuttle deploy` in your favorite command line. If you want to see the results upfront, check out [our video from the workshop we did together](https://www.youtube.com/watch?v=YLWSeiDh2o0)! --- # How to Implement OAuth in Rust Source: https://www.shuttle.dev/blog/2023/08/30/using-oauth-with-axum Date: 30 August 2023 Author: josh Tags: rust, tutorial, auth Learn what OAuth2 can do for your web apps. Explore how you competently can use OAuth in your web application without stress. Make it easier than ever for your users to use your application. In this post, we'll be learning how to implement OAuth 2.0 in Rust by writing a backend service that will interact with [Google OAuth](https://developers.google.com/identity/protocols/oauth2) and will interact with [OpenID Connect](https://auth0.com/docs/authenticate/protocols/openid-connect-protocol) ("OIDC") service from Google to retrieve a user's email. We'll first learn to use the [oauth2](https://github.com/ramosbugs/oauth2-rs) library to authorise our users using database-backed sessions to keep them authenticated with a private cookie jar, then we'll use a middleware for Rust authentication to authenticate users and insert an extension to the request from the middleware. The final code for the repository can be found [here.](https://github.com/shuttle-hq/shuttle-examples/tree/main/axum/oauth2) ## Set Up Before we get started, you'll want the following: - A project in Google Cloud Console (you can get started [here](https://console.cloud.google.com) - it's free!) We'll want to also install [`sqlx-cli`](https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md), which we can do by running the following command: ```bash cargo install sqlx-cli ``` Once we've created our project, we'll want to use `sqlx migrate add schema` to create our initial schema file which you will be able to find in the migrations folder. Once you open the file, it'll be empty with a simple comment to add your migrations - in which we'll add the following: ```sql CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS sessions ( id SERIAL PRIMARY KEY, user_id INT NOT NULL UNIQUE, session_id VARCHAR NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ); ``` When we run our app we'll run the migrate macro, which will automatically attempt to run our migrations and add a new migration entry to the table so it won't automatically try to run the migration again. ## Getting Started To get started, you'll want to create a new project by running the following: ```bash shuttle init ``` We'll want to pick "axum" as the framework. For the purposes of the project we will refer to the project name as "oauth-rust". Looking to deploy? Make sure you enable initialising your project on the Shuttle servers! Next we'll want to install our dependencies - copy the script below to install everything in one go: ```bash cargo add axum --features multipart,macros cargo add axum-extra --features cookie-private cargo add chrono --features clock cargo add shuttle-shared-db --features postgres,sqlx cargo add sqlx --features runtime-tokio-rustls,macros,chrono cargo add tower-http --features cors,fs cargo add anyhow tracing oauth2 reqwest shuttle-secrets thiserror ``` Next you'll want to create a `Secrets.toml` file in the root of your backend that holds all of our secret variables - you'll want to make sure you have at least the following, in the following format: ```toml GOOGLE_OAUTH_CLIENT_ID = "Your key here" GOOGLE_OAUTH_CLIENT_SECRET = "Your key here" ``` Then we'll want to get started on setting up our main entrypoint function! We can get it set up like so: ```rust // main.rs use reqwest::Client as ReqwestClient; use sqlx::PgPool; use axum::extract::{cookie::Key, FromRef}; use axum::{Router, routing::get}; #[derive(Clone)] pub struct AppState { db: PgPool, ctx: ReqwestClient, key: Key } // implementing FromRef is required here so we can extract substate in Axum // read more here: https://docs.rs/axum/latest/axum/extract/trait.FromRef.html impl FromRef for Key { fn from_ref(state: &AppState) -> Self { state.key.clone() } } async fn hello_world() -> &'static str { "Hello world!" } #[shuttle_runtime::main] async fn axum( #[shuttle_shared_db::Postgres] db: PgPool, #[shuttle_secrets::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&db).await.expect("Failed migrations :("); // Getting secrets from our SecretsStore - safe to unwrap as they're required for the app to work let oauth_id = secrets.get("GOOGLE_OAUTH_CLIENT_ID").unwrap(); let oauth_secret = secrets.get("GOOGLE_OAUTH_CLIENT_SECRET").unwrap(); let ctx = ReqwestClient::new(); let state = AppState { db, ctx, key: Key::generate() }; let router = Router::new().route("/", get(hello_world)); // More info about this below - we will build an oauth client that can interface with any OAuth service // Depending on the URLs we pass into it - read more here: https://docs.rs/oauth2/latest/oauth2/struct.Client.html?search=bassiclient#method.new let client = build_oauth_client(oauth_id, oauth_secret); Ok(router.into()) } ``` Before we go any further, we should set up our error handling type so that we can propagate errors up the call stack instead of trying to either unwrap everything or manually handle every single error. ```rust // src/errors.rs use thiserror::Error; #[derive(Debug, Error)] pub enum ApiError { #[error("SQL error: {0}")] SQL(#[from] sqlx::Error), #[error("HTTP request error: {0}")] Request(#[from] reqwest::Error), #[error("OAuth token error: {0}")] TokenError( #[from] oauth2::RequestTokenError< oauth2::reqwest::Error, oauth2::StandardErrorResponse, >, ), #[error("You're not authorized!")] Unauthorized, #[error("Attempted to get a non-none value but found none")] OptionError, #[error("Attempted to parse a number to an integer but errored out: {0}")] ParseIntError(#[from] std::num::TryFromIntError), #[error("Encountered an error trying to convert an infallible value: {0}")] FromRequestPartsError(#[from] std::convert::Infallible), } ``` Here, note that the `#[from]` attribute allows us to directly implement `From` for our enum. The `#[error("...")]` attribute allows us to write an error message while still including the original error. To make our error type compatible with Axum, we need to implement the `IntoResponse` trait. We can do this like so: ```rust // src/routes/errors.rs use axum::{response::IntoResponse, Response, http::StatusCode}; impl IntoResponse for ApiError { fn into_response(self) -> Response { let response = match self { Self::SQL(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::Request(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::TokenError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized!".to_string()), Self::OptionError => ( StatusCode::INTERNAL_SERVER_ERROR, "Attempted to get a non-none value but found none".to_string(), ), Self::ParseIntError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::FromRequestPartsError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), }; response.into_response() } } ``` ## But how do I use OAuth? First, we will need to write a function to create an `oauth2::BasicClient`. This client can take any OAuth authorization endpoint URL and token endpoint URL (as long as they're both from the same OAuth service). We can pass in our Google OAuth secrets that we created earlier, as you'll be able to see below. Our redirect URL should be an endpoint that we create on our side so when the user gets successfully authorised, they get sent back to our application with a code we can exchange for a token that allows a user to stay authenticated. ```rust // src/main.rs use oauth::{TokenUrl, AuthUrl, basic::BasicClient, ClientId, ClientSecret, RedirectUrl }; fn build_oauth_client(client_id: String, client_secret: String) -> BasicClient { // In prod, http://localhost:8000 would get replaced by whatever your production URL is let redirect_url = "http://localhost:8000/api/auth/google_callback".to_string(); // If you're not using Google OAuth, you can use whatever the relevant auth/token URL is for your given OAuth service let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()) .expect("Invalid authorization endpoint URL"); let token_url = TokenUrl::new("https://www.googleapis.com/oauth2/v3/token".to_string()) .expect("Invalid token endpoint URL"); BasicClient::new( ClientId::new(client_id), Some(ClientSecret::new(client_secret)), auth_url, Some(token_url), ) .set_redirect_uri(RedirectUrl::new(redirect_url).unwrap()) } ``` Now that we've created our BasicClient, we can use it anywhere we wish! Before we set up our OAuth callback route though, let's first examine how OAuth works. First we need to make up our link to the Google OAuth for our backend. Here we have a premade route that has the oauth ID inserted in for you already (click [here](https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient) to find out more about customising your OAuth URL): ```rust // src/main.rs use axum::{response::Html, Extension}; async fn homepage( Extension(oauth_id): Extension ) -> Html { Html(format!("

Welcome!

Click here to sign into Google! ")) } ``` To get this route to work, you'll want to create a Router that layers an `axum::Extension`, then nest it onto your main router: ```rust // Use the oauth_id from earlier in your main function let homepage_router = Router::new() .route("/", get(homepage)) .layer(Extension(oauth_id)); ``` Once we allow the application to use our user's credentials, Google will fire a GET request to our chosen OAuth redirect URI as seen in the homepage router, with some URI query parameters. Although there's multiple parameters returned, for us we only need the code response given back by Google so we can exchange it for an access token. We can make a struct to extract the query parameters: ```rust use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct AuthRequest { code: String } ``` Then we need to exchange the code for a token by using our `BasicClient` we made earlier: ```rust // src/routes/oauth.rs use axum_extra::extract::cookie::PrivateCookieJar; use axum::extract::{State, Query}; use axum::Extension; use oauth2::AuthorizationCode; use crate::routes::errors::ApiError; // "async_http_client" is from oauth2::reqwest::async_http_client pub async fn google_callback( State(state): State, jar: PrivateCookieJar, Query(query): Query, Extension(oauth_client): Extension, ) -> Result { let token = oauth_client .exchange_code(AuthorizationCode::new(query.code)) .request_async(async_http_client) .await?; // .. rest of the function } ``` The token returned by exchanging the token holds all of the information from the response and has methods to get all of the required fields that we need (life duration of the access token, the code, etc...). Because we requested OpenID privileges earlier, we can now access any of Google's OpenID using the access token that was given to us (that we required permissions for, as per the redirect URI). Thankfully, this is pretty simple to do without the oauth2 crate and we only need to use a simple `Reqwest` client with bearer auth to get the user profile data, like so: ```rust #[derive(Deserialize, sqlx::FromRow, Clone)] pub struct UserProfile { email: String } // Note that in the full code, the reqwest client is already created in the main function // and passed to the AppState. Rather than initializing a Reqwest client with a connection pool // for every request, we share it in the router state. let profile = state.ctx.get("https://openidconnect.googleapis.com/v1/userinfo") .bearer_auth(token.access_token().secret().to_owned()) .send().await?; let profile = profile.json::().await.unwrap(); ``` As you can see, we needed to create a struct for the type. This particular OIDC endpoint returns much more than just an email and you'll be able to see that if you use `.text().await.unwrap()` instead of trying to convert the response to JSON - however, for our purposes currently we only need the email for verification - `serde` ignores unknown fields (unless [deny_unknown_fields](https://serde.rs/container-attrs.html#deny_unknown_fields) is enabled), so this is safe to do. ## Using OAuth with Axum Extensions Now that we've got our access token, all we need to do is store the token somewhere our service can access it. We can do this with SQLx and usage of the `PrivateCookieJar` type from `axum_extra`, which uses cryptographically secure cookies. Let's have a look at what the code would look like: ```rust let Some(secs) = token.expires_in() else { return Err(ApiError::OptionError); } let secs: i64 = secs.as_secs().try_into().unwrap(); let max_age = Local::now().naive_local() + Duration::seconds(secs); let cookie = Cookie::build("sid", token.access_token().secret().to_owned()) .domain(".app.localhost") .path("/") .secure(true) .http_only(true) .max_age(TimeDuration::seconds(secs)); sqlx::query("INSERT INTO users (email) VALUES ($1) ON CONFLICT (email) DO NOTHING") .bind(profile.email.clone()) .execute(&state.db) .await?; sqlx::query("INSERT INTO sessions (user_id, session_id, expires_at) VALUES ( (SELECT ID FROM USERS WHERE email = $1 LIMIT 1), $2, $3) ON CONFLICT (user_id) DO UPDATE SET session_id = excluded.session_id, expires_at = excluded.expires_at") .bind(profile.email) .bind(token.access_token().secret().to_owned()) .bind(max_age) .execute(&state.db) .await?; ``` Now that we've done everything, we want to make sure to include our token addition in the response and a redirect: ```rust Ok(( jar.add(cookie), Redirect::to("/protected") )) ``` Of course, our "protected" route doesn't actually exist yet - we'll create it in a moment. But first of all, let's see what the final OAuth callback handler looks like: ```rust pub async fn google_callback( State(state): State, jar: PrivateCookieJar, Query(query): Query, Extension(oauth_client): Extension, ) -> Result { let token = oauth_client .exchange_code(AuthorizationCode::new(query.code)) .request_async(async_http_client) .await?; let profile = state .ctx .get("https://openidconnect.googleapis.com/v1/userinfo") .bearer_auth(token.access_token().secret().to_owned()) .send() .await?; let profile = profile.json::().await?; let Some(secs) = token.expires_in() else { return Err(ApiError::OptionError); }; let secs: i64 = secs.as_secs().try_into()?; let max_age = Local::now().naive_local() + Duration::try_seconds(secs).unwrap(); let cookie = Cookie::build(("sid", token.access_token().secret().to_owned())) .domain(".app.localhost") .path("/") .secure(true) .http_only(true) .max_age(TimeDuration::seconds(secs)); sqlx::query("INSERT INTO users (email) VALUES ($1) ON CONFLICT (email) DO NOTHING") .bind(profile.email.clone()) .execute(&state.db) .await?; sqlx::query( "INSERT INTO sessions (user_id, session_id, expires_at) VALUES ( (SELECT ID FROM USERS WHERE email = $1 LIMIT 1), $2, $3) ON CONFLICT (user_id) DO UPDATE SET session_id = excluded.session_id, expires_at = excluded.expires_at", ) .bind(profile.email) .bind(token.access_token().secret().to_owned()) .bind(max_age) .execute(&state.db) .await?; Ok((jar.add(cookie), Redirect::to("/protected"))) } ``` To be able to authenticate users more easily, we will implement `FromRequest` for `UserProfile`. This will allow us to directly call the database while extracting the body. We then return the user profile of the person who just authenticated. ```rust #[axum::async_trait] impl FromRequest for UserProfile { type Rejection = ApiError; async fn from_request(req: Request, state: &AppState) -> Result { let state = state.to_owned(); let (mut parts, _body) = req.into_parts(); let cookiejar: PrivateCookieJar = PrivateCookieJar::from_request_parts(&mut parts, &state).await?; let Some(cookie) = cookiejar.get("sid").map(|cookie| cookie.value().to_owned()) else { return Err(ApiError::Unauthorized); }; let res = sqlx::query_as::<_, UserProfile>( "SELECT users.email FROM sessions LEFT JOIN USERS ON sessions.user_id = users.id WHERE sessions.session_id = $1 LIMIT 1", ) .bind(cookie) .fetch_one(&state.db) .await?; Ok(Self { email: res.email }) } } ``` Now we just need to add the protected route! ```rust pub async fn protected(profile: UserProfile) -> impl IntoResponse { (StatusCode::OK, profile.email) } ``` Now that we've filled out everything we need, we can come back to the main entrypoint function and fill back in all of our routes so that we can use them: ```rust #[shuttle_runtime::main] async fn axum( #[shuttle_shared_db::Postgres] db: PgPool, #[shuttle_secrets::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&db).await.expect("Failed migrations :("); let oauth_id = secrets.get("GOOGLE_OAUTH_CLIENT_ID").unwrap(); let oauth_secret = secrets.get("GOOGLE_OAUTH_CLIENT_SECRET").unwrap(); let ctx = Client::new(); let state = AppState { db, ctx, key: Key::generate() }; let oauth_client = build_oauth_client(oauth_id.clone(), oauth_secret); let router = init_router(state, oauth_client, oauth_id); Ok(router.into()) } fn init_router(state: AppState, oauth_client: BasicClient, oauth_id: String) -> Router { let auth_router = Router::new() .route("/auth/google_callback", get(oauth::google_callback)); let protected_router = Router::new() .route("/", get(oauth::protected)) .route_layer(middleware::from_fn_with_state(state.clone(), oauth::check_authenticated)); let homepage_router = Router::new() .route("/", get(homepage)) .layer(Extension(oauth_id)); Router::new() .nest("/api", auth_router) .nest("/protected", protected_router) .nest("/", homepage_router) .layer(Extension(oauth_client)) .with_state(state) } ``` ## Deploying to Production Once we're done implementing OAuth, all you need to do is use `shuttle deploy` (with `--allow-dirty` if you're working on a dirty Git branch) and it'll work! ## Finishing Up Thanks for reading! I hope you enjoyed this guide to implementing OAuth in Rust and leveraging the [oauth2](https://github.com/ramosbugs/oauth2-rs) library for Rust auth. Some extra ideas if you'd like to extend this article: - Silent token rotation - Add more functionality so users don't have to go through the whole OAuth process every single time - Try implementing refresh tokens (make sure they're implemented securely!) --- # Best Rust Web Frameworks to Use in 2023 Source: https://www.shuttle.dev/blog/2023/08/23/rust-web-framework-comparison Date: 23 August 2023 Author: stefan Tags: rust, tutorial, comparison Explore the top Rust web frameworks and their advantages and drawbacks. Discover the best choices for your projects. A comprehensive comparison to help you make informed decisions. ## Introduction In the dynamic landscape of web development, Rust has emerged as a language of choice for building safe and performant applications. As Rust's popularity grows, so does the array of web frameworks designed to harness its strengths. This article compares some of the best Rust frameworks highlighting their respective advantages and drawbacks to help you make informed decisions for your projects. It also takes a lookout on frameworks to look out for, as they might change how we build web applications in Rust. Since most of the web frameworks feel very similar in use at a first glance, the differences are much more nuanced and in detail. I hope to highlight the most important differences in text, but to give you a better idea, I also show example code with every framework that does more than a simple hello world. All the examples are taken from the respective GitHub repos. Also note that this list is by no means exhaustive, and I definitely missed some of the frameworks that are out there. If you want to have your favourite framework included, please reach out to me on [Twitter](https://twitter.com/ddprrt) or [Mastodon](https://mastodon.social/@deadparrot). ## The Popular Rust Frameworks ### Axum Axum is a web application framework with a special standing in the Rust ecosystem. It is part of the [Tokio](https://tokio.rs/) project, which is the runtime for writing asynchronous network applications with Rust. Not only does Axum use Tokio as its asynchronous runtime, but it also integrates with other libraries from the Tokio ecosystem, making use of _Hyper_ as its HTTP server and _Tower_ for middleware. In doing so, developers are able to reuse existing libraries and tools from the Tokio ecosystem. Axum also strives to deliver a best in class developer experience without relying on macros, but rather leveraging Rust's type system to provide a safe and ergonomic API. This is achieved by using traits to define the core abstractions of the framework, such as the `Handler` trait, which is used to define the core logic of an application. This approach allows developers to easily compose applications from smaller components, which can be reused across multiple applications. A handler in Axum is a function that takes a request and returns a response. This is similar to other backend frameworks, but with Axum's `FromRequest` trait, developers can specify the types of data that should be extracted from the request. The return type needs to implement the `IntoResponse` trait, and there are already a number of types that implement this trait, including tuple types that allow to easily change e.g. the status code of a response. If you've ever worked with Rust's type system, generics and especially async methods in traits (or more concretely: a returned `Future`), you know how complex Rust's error messages can get when you don't satisfy a trait bound. Especially when you try to match abstract trait bounds, it happens ever so often that you get a wall of text that is hard to decipher. Change the order of a few lines, and nothing works anymore! Axum provides a library with helper macros that put the error to where it actually happens, making it easier to understand what went wrong. Axum does a lot of things right, and it's very easy to get applications started that do _a lot_. However, there are some things that you need to look out for. The version is still below 1.0, and the Axum team takes the liberty to change APIs fundamentally between versions, which can cause your apps to break big time. That's the deal with 0.x versions, we know, but some changes appear to be so subtle, yet require you to develop a different mental model of how things work underneath. If you included a `Timeout` layer (built-in in Tower, yay!), it worked easily in one version, needed a catch-all error handler in another, and an tied error handler in the next. This is not a big deal, but it can be frustrating when you're trying to get things done or you start a project with the latest version and things suddenly work differently. Also while you are able to leverage the entire Tokio ecosystem, you sometimes need to deal with glue types and traits, rather then going to the Tokio functions directly. One example is the use of anything stream and (web) socket related. The good examples help, but you need to keep track. Nonetheless, Axum is my personal favourite, and also the framework I use for [Shuttle Launchpad](https://www.shuttle.dev/launchpad). I love the expressiveness and the concepts underneath, and there hasn't been a thing that I wanted to solve that I couldn't do inutitively by understanding the right concepts. If you want to get to know the Axum concepts, check out my [slides from my Tokio + Microservices workshop](https://fettblog.eu/slides/microservices-with-rust-and-tokio/). #### Axum Example An abbreviated example from the [Axum repo](https://github.com/tokio-rs/axum/blob/main/examples/testing-websockets/src/main.rs) showing a WebSocket handler that echos any message it receives. ```rust #[tokio::main] async fn main() { let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); println!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app()).await.unwrap(); } fn app() -> Router { // WebSocket routes can generally be tested in two ways: // // - Integration tests where you run the server and connect with a real WebSocket client. // - Unit tests where you mock the socket as some generic send/receive type // // Which version you pick is up to you. Generally we recommend the integration test version // unless your app has a lot of setup that makes it hard to run in a test. Router::new() .route("/integration-testable", get(integration_testable_handler)) .route("/unit-testable", get(unit_testable_handler)) } // A WebSocket handler that echos any message it receives. // // This one we'll be integration testing so it can be written in the regular way. async fn integration_testable_handler(ws: WebSocketUpgrade) -> Response { ws.on_upgrade(integration_testable_handle_socket) } async fn integration_testable_handle_socket(mut socket: WebSocket) { while let Some(Ok(msg)) = socket.recv().await { if let Message::Text(msg) = msg { if socket .send(Message::Text(format!("You said: {msg}"))) .await .is_err() { break; } } } } ``` #### Axum in a Nutshell - Macro-free API. - Strong ecosystem by leveraging Tokio, Tower, and Hyper. - Great developer experience. - Still in 0.x, so breaking changes can happen. ### Actix Web Actix Web is one of Rust's web frameworks that has been around for a while, and is thus very popular. Like any good open source project it has seen many iterations, but it has reached major versions other than 0 and keeps its stability guarantees: Within a major version, you can be sure that there are no breaking changes. When we talk [Actix Web](https://actix.rs), it's easy to assume that it is based on the `actix` actor runtime. However, this has not been the case for over 4 years; the only remaining part of Actix Web which requires actors are WebSockets, but work is ongoing to remove its usage completely since `actix` cannot play nicely with the modern async Rust world. The wider Actix project and GitHub org provides a number of libraries for building concurrent applications, spanning from lower-level TCP server builders, through the HTTP / web layer, up to static file providers and session management crates. At a first glance, Actix Web looks very familiar to other web frameworks in Rust. You use macros to define HTTP methods and routes (like Rocket), and you use extractors to get data from the request (like Axum). The similarities with Axum are striking, also in how they name concepts and traits. The biggest difference is that Actix Web does not tie itself too strongly to the Tokio ecosystem. While Tokio is still the runtime underneath Actix Web, the framework comes with its own abstractions and traits, and also with its own ecosystem of crates. This has pros and cons. On one hand, you can be sure that things usually work well together, on the other hand you might miss out on a lot of things that are already available in the Tokio ecosystem. One thing that strikes me odd is that Actix Web implements its own Service trait, which is basically the same as Tower's, but still incompatible. Which means that most of the available Middleware in the Tower ecosystem is not available for Actix. What's also interesting is that if you need some special tasks in Actix Web that you need to implement on your own, you might get confronted with the Actor model that runs everything in the framework. This might add some layers of complexity that you might not want to deal with. But the community around Actix Web delivers. The framework supports HTTP/2 and Websocket upgrades, it has crates and guides for the most common tasks in a web framework, excellent (and I mean _excellent_) documentation, and it's fast. Actix Web is popular for a reason, and if you need to keep version guarantees, it might be your best choice right now. #### Actix Web Example A simple [WebSocket echo server](https://actix.rs/docs/websockets) in Actix Web looks like this: ```rust use actix::{Actor, StreamHandler}; use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer}; use actix_web_actors::ws; /// Define HTTP actor struct MyWs; impl Actor for MyWs { type Context = ws::WebsocketContext; } /// Handler for ws::Message message impl StreamHandler> for MyWs { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { match msg { Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), Ok(ws::Message::Text(text)) => ctx.text(text), Ok(ws::Message::Binary(bin)) => ctx.binary(bin), _ => (), } } } async fn index(req: HttpRequest, stream: web::Payload) -> Result { let resp = ws::start(MyWs {}, &req, stream); println!("{:?}", resp); resp } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().route("/ws/", web::get().to(index))) .bind(("127.0.0.1", 8080))? .run() .await } ``` #### Actix Web in a Nutshell - Strong, self-contained ecosystem. - Actor model based. - Stable API via major version guarantees. - Fantastic documentation. ### Rocket Rocket has been the star in the Rust web framework ecosystem for quite a while, with it's unapologetic approach to developer experience, the reliance on familar and existing concepts, and its ambitious goal to provide a batteries-included experience. You can see its ambitions when you enter their beautiful [website](https://rocket.rs): Macro-based routing, built-in form handling, support for databases and state management, and its own version of templating! Rocket really tries to get everything done that you need to build a web application. However, Rocket's ambitions take their toll. While still being actively developed, the releases are not as frequent as they used to be. Which means that users of the framework miss out on a lot of important stuff. Also, with it's batteries-included approach, you also need to learn how Rocket does things. Rocket apps have a lifecycle, the building blocks are connected in a particular way, and if something goes wrong, you need to understand what goes wrong. Rocket is a great framework, and if you want to get started with Rust web development, it's a great choice. Personally, I have a soft spot for Rocket and I hope that development picks up. For many of us, Rocket was the first segue into Rust, and it's still fun to develop with it. Nonetheless, I usually rely on features that are not available in Rocket, and thus I don't use it in production. #### Rocket Example An abbreviated example of a Rocket application that deals with forms from their [example repo](https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/examples/forms/src/main.rs): ```rust #[derive(Debug, FromForm)] struct Password<'v> { #[field(validate = len(6..))] #[field(validate = eq(self.second))] first: &'v str, #[field(validate = eq(self.first))] second: &'v str, } #[derive(Debug, FromForm)] #[allow(dead_code)] struct Submission<'v> { #[field(validate = len(1..))] title: &'v str, date: Date, #[field(validate = len(1..=250))] r#abstract: &'v str, #[field(validate = ext(ContentType::PDF))] file: TempFile<'v>, ready: bool, } #[derive(Debug, FromForm)] #[allow(dead_code)] struct Account<'v> { #[field(validate = len(1..))] name: &'v str, password: Password<'v>, #[field(validate = contains('@').or_else(msg!("invalid email address")))] email: &'v str, } #[derive(Debug, FromForm)] #[allow(dead_code)] struct Submit<'v> { account: Account<'v>, submission: Submission<'v>, } #[get("/")] fn index() -> Template { Template::render("index", &Context::default()) } // NOTE: We use `Contextual` here because we want to collect all submitted form // fields to re-render forms with submitted values on error. If you have no such // need, do not use `Contextual`. Use the equivalent of `Form>`. #[post("/", data = "
")] fn submit<'r>(form: Form>>) -> (Status, Template) { let template = match form.value { Some(ref submission) => { println!("submission: {:#?}", submission); Template::render("success", &form.context) } None => Template::render("index", &form.context), }; (form.context.status(), template) } #[launch] fn rocket() -> _ { rocket::build() .mount("/", routes![index, submit]) .attach(Template::fairing()) .mount("/", FileServer::from(relative!("/static"))) } ``` #### Rocket in a Nutshell - Batteries-included approach. - Great developer experience. - Not as actively developed as it used to be. - Still a great choice for beginners. ## The Lesser Known but Still Exciting Rust Frameworks ### Warp Oh, Warp! You are a beautiful, strange, and powerful beast. Warp is a web framework that is built on top of Tokio, and it's a very good one. It's also very different from the other frameworks that we have seen so far. [Warp](https://github.com/seanmonstar/warp) shares a few common traits (haha!) with Axum: It's built on Tokio and Hyper, and makes use of Tower middleware. However, it's very different in its approach. Warp is built on top of the `Filter` trait. In Warp, you build a pipeline of filters that are applied to the incoming request, and the request is passed through the pipeline until it reaches the end. Filters can be chained, and they can be composed. This allows you to build very complex pipelines that are still easy to understand. Warp is also a bit closer to the Tokio ecosystem than Axum, which means that you might deal with more Tokio structs and concepts without any glue traits. Warp takes a very functional approach and if that's your style of programming you are going to love the expressiveness and composability of Warp. When you look at a piece of Warp code, it often reads like a story of what's happening, and it's fun and amazing that this works in Rust. You might want to turn off inlay hints in your Rust Analyzer setting, though. With all those different functions and filters being chained, the types in Warp get very long and very complex, and also hard to decipher. Same goes for error messages, which can be pages of text that are hard to understand. Also, while the filter concept is great once you get through, sometimes you want to have the declarative router, handler and extractor style that you get with, well, all the other frameworks. Warp is a great framework, and I love it. However, it's not the most beginner-friendly framework, and it's also not the most popular one. Which means that you might have a harder time finding help and resources. But it's fun for quick and little apps, and it's experimental style might give you new ideas! #### Warp Example An abbreviated example of a websocket chat from [their example repo](https://github.com/seanmonstar/warp/blob/master/examples/websockets_chat.rs): ```rust static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); /// Our state of currently connected users. /// /// - Key is their id /// - Value is a sender of `warp::ws::Message` type Users = Arc>>>; #[tokio::main] async fn main() { let users = Users::default(); // Turn our "state" into a new Filter... let users = warp::any().map(move || users.clone()); // GET /chat -> websocket upgrade let chat = warp::path("chat") // The `ws()` filter will prepare Websocket handshake... .and(warp::ws()) .and(users) .map(|ws: warp::ws::Ws, users| { // This will call our function if the handshake succeeds. ws.on_upgrade(move |socket| user_connected(socket, users)) }); // GET / -> index html let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML)); let routes = index.or(chat); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } async fn user_connected(ws: WebSocket, users: Users) { // Use a counter to assign a new unique ID for this user. let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); eprintln!("new chat user: {}", my_id); // Split the socket into a sender and receive of messages. let (mut user_ws_tx, mut user_ws_rx) = ws.split(); let (tx, rx) = mpsc::unbounded_channel(); let mut rx = UnboundedReceiverStream::new(rx); tokio::task::spawn(async move { while let Some(message) = rx.next().await { user_ws_tx .send(message) .unwrap_or_else(|e| { eprintln!("websocket send error: {}", e); }) .await; } }); // Save the sender in our list of connected users. users.write().await.insert(my_id, tx); // Return a `Future` that is basically a state machine managing // this specific user's connection. // Every time the user sends a message, broadcast it to // all other users... while let Some(result) = user_ws_rx.next().await { let msg = match result { Ok(msg) => msg, Err(e) => { eprintln!("websocket error(uid={}): {}", my_id, e); break; } }; user_message(my_id, msg, &users).await; } // user_ws_rx stream will keep processing as long as the user stays // connected. Once they disconnect, then... user_disconnected(my_id, &users).await; } async fn user_message(my_id: usize, msg: Message, users: &Users) { // Skip any non-Text messages... let msg = if let Ok(s) = msg.to_str() { s } else { return; }; let new_msg = format!(": {}", my_id, msg); // New message from this user, send it to everyone else (except same uid)... for (&uid, tx) in users.read().await.iter() { if my_id != uid { if let Err(_disconnected) = tx.send(Message::text(new_msg.clone())) { // The tx is disconnected, our `user_disconnected` code // should be happening in another task, nothing more to // do here. } } } } async fn user_disconnected(my_id: usize, users: &Users) { eprintln!("good bye user: {}", my_id); // Stream closed up, so remove from the user list users.write().await.remove(&my_id); } ``` #### Warp in a Nutshell - Functional approach. - Very expressive. - Strong ecosystem by being close to Tokio, Tower, and Hyper. - Not the most beginner-friendly framework. ### Tide [Tide](https://github.com/http-rs/tide) is a very minimalistic web framework that is built on top of the `async-std` runtime. The minimalistic approach means that you get a very small API surface. Handler functions in Tide are `async fn`s that take a `Request` and return a `tide::Result` of a `Response`. Extracting data or sending the right response format is up to you. While this is arguably more work for you, it's also a lot more direct, meaning that you have full control over what's happening. For some cases, being able to be so close to HTTP request and response is a delight and makes things easier. Its middleware approach is similar to what you know from Tower, but Tide exposes the [async trait crate](https://github.com/dtolnay/async-trait) to make implementation a lot easier. Since Tide is implement by folks who are also involved in the Rust async ecosystem, you can expect things like proper [async methods in traits](https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizing-async-fn-in-trait.html) which recently landed in Nightly, to be adopted quickly. #### Tide Example User sessions example from their [example repo](https://github.com/http-rs/tide/blob/main/examples/sessions.rs). ```rust #[async_std::main] async fn main() -> Result<(), std::io::Error> { femme::start(); let mut app = tide::new(); app.with(tide::log::LogMiddleware::new()); app.with(tide::sessions::SessionMiddleware::new( tide::sessions::MemoryStore::new(), std::env::var("TIDE_SECRET") .expect( "Please provide a TIDE_SECRET value of at \ least 32 bytes in order to run this example", ) .as_bytes(), )); app.with(tide::utils::Before( |mut request: tide::Request<()>| async move { let session = request.session_mut(); let visits: usize = session.get("visits").unwrap_or_default(); session.insert("visits", visits + 1).unwrap(); request }, )); app.at("/").get(|req: tide::Request<()>| async move { let visits: usize = req.session().get("visits").unwrap(); Ok(format!("you have visited this website {} times", visits)) }); app.at("/reset") .get(|mut req: tide::Request<()>| async move { req.session_mut().destroy(); Ok(tide::Redirect::new("/")) }); app.listen("127.0.0.1:8080").await?; Ok(()) } ``` #### Tide in a Nutshell - Minimalistic approach. - Uses `async-std` runtime. - Simple handler functions. - Playground of async features. ### Poem > A program is like a poem, you cannot write a poem without writing it. --- Dijkstra [Poem](https://github.com/poem-web/poem)'s Readme file greets you with these words. Poem claims to be a fully featured yet easy to use web framework. Bold claims, but Poem seems to deliver. At a first glance, it's usage is very similar to Axum, with the only difference that you need to mark handler functions with the respective macro. It also builds on Tokio and Hyper, and is fully compatible with Tower middleware, while still exposing its own middleware trait. Poem's middleware trait is also dead-simple to use. You can either implement the trait directly for all or specific `Endpoint`s (Poem's way of expressing everything that can handle HTTP requests), or you just write an async function that accepts an `Endpoint` as parameter. After dealing and sometimes struggling with [Tower and the Service Trait](https://www.youtube.com/watch?v=z78_RnUPnpY) for such a long time, this is a breath of fresh air. Not only is Poem compatible with a lot of features from the broader ecosystem, it's also jam-packed with features itself, including full support for OpenAPI and Swagger docs. And it's not limited to HTTP based web services, it can also be used for gRPC services based on Tonic, or even in Lambda functions, without the need to switch frameworks. Add support for OpenTelemetry, Redis, Prometheus, and a lot more, and you check off all the boxes of a modern web framework for enterprise grade applications. Poem is still in a 0.x version, but if it keeps momentum and delivers a solid 1.0, this is a framework to look out for! #### Poem Example An abbreviated version of the websocket chat from [their example repo](https://github.com/poem-web/poem/blob/master/examples/poem/websocket-chat/src/main.rs): ```rust #[handler] fn ws( Path(name): Path, ws: WebSocket, sender: Data<&tokio::sync::broadcast::Sender>, ) -> impl IntoResponse { let sender = sender.clone(); let mut receiver = sender.subscribe(); ws.on_upgrade(move |socket| async move { let (mut sink, mut stream) = socket.split(); tokio::spawn(async move { while let Some(Ok(msg)) = stream.next().await { if let Message::Text(text) = msg { if sender.send(format!("{name}: {text}")).is_err() { break; } } } }); tokio::spawn(async move { while let Ok(msg) = receiver.recv().await { if sink.send(Message::Text(msg)).await.is_err() { break; } } }); }) } #[tokio::main] async fn main() -> Result<(), std::io::Error> { let app = Route::new().at("/", get(index)).at( "/ws/:name", get(ws.data(tokio::sync::broadcast::channel::(32).0)), ); Server::new(TcpListener::bind("127.0.0.1:3000")) .run(app) .await } ``` #### Poem in a Nutshell - Vast feature set. - Compatible with the Tokio ecosystem. - Easy to use. - Adaptable for gRPC and Lambda. ## On the Lookout ### Pavex Initially I said that all Rust web frameworks look very similar at first glance. They are different in nuances, and sometimes do things better than others. Pavex is the exception to that rule. [Pavex](https://github.com/LukeMathWalker/pavex) is currently being implemented by no other than Luca Palmieri, the author of the popular [Zero To Production](https://www.zero2prod.com/index.html) book. You can say without a doubt that Luca knows what he is doing, and all his ideas and experience are going into Pavex. Pavex is significantly different as it sees itself as a _specialized compiler_ for building Rust APIs. It takes a high-level description of what your application should do, and the compiler generates a standalone API Server SDK crate, ready to be configured and launched. Pavex is still in its early stages, but it's definitely a project to keep an eye on. Check out Luca's [blog post](https://www.lpalmieri.com/posts/a-taste-of-pavex-rust-web-framework/) for more information. ## Conclusion As you can see, the world of Rust web frameworks is very diverse. There is no one-size-fits-all solution, and you need to pick the framework that fits your needs best. If you are just starting out, I recommend you to go with Actix or Axum, as they are the most beginner-friendly frameworks and they have gread documentation. Personally, I'm interested on what Pavex will bring to the table, and to be honest, after being a long time Axum user, I'm really interested in checking out Poem. And obviously, all of the major Rust web frameworks work with Shuttle, so [try them out](https://docs.shuttle.dev/examples/) and see what works best for you! --- # Building & deploying a Rust REST API with Turso Source: https://www.shuttle.dev/blog/2023/07/28/turso-shuttle-integration-cats-api Date: 28 July 2023 Author: josh Tags: rust, turso, tutorial In this article, we are showcasing our latest integration, and that's Turso! In this article, we are showcasing our latest integration, and that's Turso! Turso is a service that describes itself as "SQLite for the edge", taking the power of SQLite to the extreme by hosting it on the cloud and using replicas to allow you to get a local replica of your database wherever you need it, saving time and money on read data. This is hugely useful for web applications where you might need to generate a lot of reports using read data from your SQL database as reads using SQLite are extremely cheap, or web services where most of the traffic will be GET requests for information. We'll be writing a web service in the form of a Cat Facts API service that utilises a Turso instance. We'll want the following functionality by the time our API is done: - Grab a single cat fact - Users can submit their own cat facts - Allow users to subscribe to a web service that will send out a daily cat fact Looking for the final example codebase? You can check it out here: https://github.com/joshua-mo-143/cat-facts-api ## Getting Started Before anything else, you'll probably want to install Turso so you can use Turso instances. You can do this with a scripted install, like so: ```rust curl -sSfL https://get.tur.so/install.sh | bash ``` Turso also offers an install via Homebrew: ```rust brew install chiselstrike/tap/turso ``` You can verify your installation of Turso by using `turso --version`. Next you'll want to use `turso auth signup` to sign up to Turso's service, which will ask you to log in via GitHub and request some permissions to your GitHub account in order for the service to work. A token will then be generated in your Turso install location which is used to grant access to Turso (make sure to not share this, as it will allow others to use Turso while pretending to be you!). This token will also expire after 7 days. To create a database using Turso, we'll want to use the following: ```rust turso create my-db ``` This will generate a database for you that you can explore by using `turso db shell my-db`. It will also provide a URL that we will want to note down for later - ideally somewhere safe, as others will be able to use your Turso database if they know what the token is. Then you'll want to create an API token for your database that we'll be using in our Shuttle app. To do that, you'll want to use `turso db tokens create my-db`, which will generate a token. We'll want to make sure to keep this token somewhere safe for later, as this allows people to work with your database if they know what the database URL is. Feeling stuck? You can find the Turso docs here: https://docs.turso.tech/tutorials/get-started-turso-cli/step-01-installation Next, let's install `cargo-shuttle` (Shuttle's CLI) if you haven't already by running the following: ```rust // run if using regular install cargo install cargo-shuttle // run if using cargo-binstall cargo binstall cargo-shuttle ``` After that, let's initiate our app: ```rust shuttle init ``` We can then follow the prompt to the end to create our app. This guide will assume you're using the Axum starter template. Once you're done with the initial prompt, we'll want to install our initial libraries. You can either use this one-liner or examine the crate dependencies below: ```rust cargo add anyhow chrono libsql-client@0.30.1 reqwest serde shuttle-secrets shuttle-turso --features serde/derive ``` Here is a list of the dependencies, which you'll be able to find in Cargo.toml after adding everything: ```toml [dependencies] anyhow = "1.0.72" # more convenient error handling axum = "0.6.18" # an easy to use framework chrono = "0.4.26" # allows us to get Time and check the time for background task libsql-client = "0.30.1" # allows us to use Turso lettre = {version = "0.10.4", features = ["tokio1-native-tls"] } serde = { version = "1.0.171", features = ["derive"] } # (de)serializtion of structs shuttle-axum = "0.21.0" # using shuttle with axum shuttle-runtime = "0.21.0" # required to use shuttle shuttle-secrets = "0.21.0" # required to use secrets with shuttle shuttle-turso = "0.21.0" # required to use turso with shuttle tokio = "1.28.2" # async runtime ``` Remember the Turso database URL and API token you got earlier? You'll want to store it in a `Secrets.toml` file at the root of your project. We'll also be using Gmail SMTP for sending emails through `lettre`, although the crate will work with pretty much any SMTP server as long as you have the credentials and the relay server information! The `Secrets.toml` file should look like this: ```toml TURSO_ADDR = "YOUR_TURSO_URL_HERE" TURSO_TOKEN = "YOUR_TURSO_TOKEN_HERE" GMAIL_USER = "YOUR_GMAIL_ADDRESS_HERE" GMAIL_PASSWORD = "YOUR_GMAIL_PASSWORD_HERE" ``` Now that we've set up everything we need, we can get started! ## Backend Our service will be split into two parts: - A web service - A background task that will check the time and if it matches what the time is, it'll send out a load of subscription emails to our subscribers Natively, Shuttle will give you types that you can use for easily supporting your web services. However, we don't want that - we want to run our web service and background task concurrently, which means we need to return a type that implements `shuttle_runtime::Service`. See below: ```rust // main.rs use libsql_client::client::Client; use shuttle_secret::SecretStore; use std::sync::Arc; use tokio::sync::Mutex; pub struct CustomService { db: Arc>, gmail_user: String, gmail_password: String } #[shuttle_runtime::main] async fn axum( #[shuttle_secrets::Secrets] store: SecretStore, #[shuttle_turso::Turso( addr = "{secrets.TURSO_ADDR}", token = "{secrets.TURSO_TOKEN}" )] db: Client ) -> Result { let gmail_user = store.get("GMAIL_USER").unwrap_or_else(|| "None".to_string()); let gmail_password = store.get("GMAIL_PASSWORD").unwrap_or_else(|| "None".to_string()); let db = Arc::new(Mutex::new(db)); Ok(CustomService { db, gmail_user, gmail_password }) } #[shuttle_runtime::async_trait] impl shuttle_runtime::Service for CustomService { async fn bind( mut self, addr: std::net::SocketAddr ) -> Result<(), shuttle_runtime::Error> { Ok(()) } } ``` Although we've successfully implemented the trait, our `bind` function is actually empty because we haven't written anything yet. We'll be filling this function out once we've implemented all the functionality we need. You might also have noticed that we've wrapped our database client connection in `Arc`. This is because although we want our database connection to be shared across the web service and background task, it doesn't implement `Clone` - fortunately, `Arc` is a great workaround for this in a web service use case and is a common pattern for situations like this (where we want more than one thread to have access to a variable, but it needs to be thread-safe). You can read more about Arcs and Mutexes from Mara Bos' book, Rust Atomics & Locks which does a great deep dive on this: https://marabos.nl/atomics/ Let's talk about our CRUD API routes and migrations before anything else. We will probably want to set up our migrations like so in the main function: ```rust #[shuttle_runtime::main] async fn axum( #[shuttle_secrets::Secrets] store: SecretStore, #[shuttle_turso::Turso( addr = "{secrets.TURSO_ADDR}", token = "{secrets.TURSO_TOKEN}" )] db: Client ) -> Result { db.batch([ "CREATE TABLE IF NOT EXISTS catfacts id integer primary key autoincrement, fact text not null, created_at datetime default current_timestamp )", "CREATE TABLE IF NOT EXISTS subscribers ( id integer primary key autoincrement, email text not null, created_at datetime default current_timestamp )" ]) .await .unwrap(); Ok(()) } ``` This pair of statements will create the tables in the database that currently don't exist yet - and if they do exist, do nothing. This means that if we want to run our app more than once, it won't randomly cause our tables to reset (though you may wish to consider commenting this part of the function out once your migrations are set up!). We will want to set up our state-wide variables struct for our Axum web service by using an `AppState` struct that we'll inject into our Axum web service so that our API routes can also access the database connection (we'll be expanding this with secret keys and our backend router as required): ```rust pub struct AppState { db: Arc>, } ``` As you can see, we re-use the `Arc` pattern to be able to implement the client from `libsql_client` in the app. Next we'll want to implement our API routes. We'll want a health check route and an initial route to welcome users to our web service and to let them easily use our web service: ```rust // main.rs // health check async fn health_check() -> impl IntoResponse { (StatusCode::OK, "It works!".to_string()) } async fn homepage() -> impl IntoResponse { r#"Welcome to the Cat Facts API! Here are the following routes: - GET /health - Health check route. - GET /catfact - Get a random cat fact. - POST /catfact/create - Submit your own cat fact - Takes the following JSON parameters: "fact" - POST /subscribe - Subscribe to our free daily cat fact email service - Takes the following JSON parameters: "email" "# } ``` When we load our API home page route up on the browser, our front page will look like this: ![Preview of the cat facts API front page](/images/blog/cat-facts-frontpage.png) It's quite simple, but as this is intentionally meant to be an API for other developers to pull from, we really only need to add what routes are available and write about for other developers to pull from, we really only need to add what routes are available and write about how to use them.how to use them. Now that we've written our health check route and our initial homepage route, we can move onto our routes for submitting and getting single cat facts. We'll initially want to make sure that we declare a struct that can be serialized to JSON, as well as being deserialized from JSON - thankfully, `serde` makes it really easy to do so by declaring the macros above the struct, like so: ```rust // using derive macros from Serde allows us to easily // convert to and from JSON #[derive(Deserialize, Serialize)] pub struct CatFact { fact: String } ``` You can find more about the Serde derive macros here: We can then write our routes - we'll need one for getting a fact and one for users to be able to submit their own facts: ```rust // main.rs // get a record pub async fn get_record( State(state): State>, ) -> Result { // we have to lock the mutex to allow us to use the inner value let res = match state.db.lock().await.execute(" SELECT fact FROM catfacts order by random() limit 1").await { Ok(res) => res, Err(e) => return Err( (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) )}; let res = CatFact { fact: res.rows[0].values[0].to_string() }; Ok((StatusCode::OK, Json(res))) } // create a record pub async fn create_record( State(state): State>, Json(json): Json, ) -> Result { // since we are inserting into a table, we don't need an OK result // so we can only need to check whether there's an error match state .db .lock() .await .execute(Statement::with_args( "INSERT into CATFACTS (fact) VALUES (?)", &[json.fact], )) .await { Ok(_) => Ok(StatusCode::OK), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) } } ``` Now we need to add our router to the main function: ```rust // main.rs pub struct CustomService { db: Arc>, gmail_user: String, gmail_password: String, router: Router } #[shuttle_runtime::main] async fn axum( #[shuttle_secrets::Secrets] store: SecretStore, #[shuttle_turso::Turso( addr = "{secrets.TURSO_ADDR}", token = "{secrets.TURSO_TOKEN}")] db: Client, ) -> Result { let gmail_user = store .get("GMAIL_USER") .unwrap_or_else(|| "None".to_string()); let gmail_password = store .get("GMAIL_PASSWORD") .unwrap_or_else(|| "None".to_string()); db.batch([ "CREATE TABLE IF NOT EXISTS catfacts ( id integer primary key autoincrement, fact text not null, created_at datetime default current_timestamp )", "CREATE TABLE IF NOT EXISTS subscribers ( id integer primary key autoincrement, email text not null, created_at datetime default current_timestamp )", ]) .await .unwrap(); let db = Arc::new(Mutex::new(db)); let state = Arc::new(AppState { db: db.clone(), }); let router = Router::new() .route("/", get(homepage)) .route("/health", get(health_check)) .route("/catfact", get(get_record)) .route("/catfact/create", post(create_record)) .route("/subscribe", post(subscribe)) .with_state(state); Ok(CustomService { db, gmail_user, gmail_password, router }) } ``` After this, we'll want to write a route for adding subscribers. It's functionally the same as our `create_record` function with regards to executing a database query then returning either the error or a `StatusCode::CREATED`, but instead of inserting into the `CatFacts` table we're inserting into the `Subscribers` table, like so: ```rust // main.rs // add an email subscriber to our mailing list pub async fn subscribe( State(state): State>, Json(req): Json, ) -> Result { if let Err(e) = state .db .lock() .await .execute(Statement::with_args( "INSERT INTO subscribers (email) VALUES (?)", &[req.email], )) .await { return Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())); }; Ok((StatusCode::CREATED, "You're now subscribed!".to_string()) } ``` Once this is done, that's pretty much it for our web service! We can now move onto our background task, which will be the main meat of our service. Logically speaking, all we need to do is write a function that will loop every second, and check what the time is. If it's a given time, then we can try to start sending out emails to our subscribers. The function would look like this at a basic level: ```rust // main.rs #[allow(unreachable_code)] pub async fn scheduled_tasks( db: Arc>, gmail_user: String, gmail_password: String, ) -> Result<(), anyhow::Error> { let creds = Credentials::new(gmail_user.to_owned(), gmail_password.to_owned()); // Open a remote connection to gmail let mailer: AsyncSmtpTransport = AsyncSmtpTransport::::relay("smtp.gmail.com") .unwrap() .credentials(creds) .build(); // set the time where we want to carry out our scheduled task let mut tomorrow_midnight = Local::now() .checked_add_days(Days::new(1)).unwrap() .date_naive().and_hms_opt(0, 0, 0).unwrap(); loop { // find the time diff for the scheduled time for the task let duration = calculate_time_diff(tomorrow_midnight); // if the diffed duration is zero, send mail if duration == std::time::Duration::ZERO { send_subscriber_mail(mailer, db, gmail_user, gmail_password).await; } // now that the function's been ran we can get the time/date // for the next night tomorrow_midnight = Local::now() .checked_add_days(Days::new(1)).unwrap() .date_naive().and_hms_opt(0, 0, 0).unwrap(); // calculate time difference again let duration = calculate_time_diff(tomorrow_midnight); // tell the thread to sleep until when we need to execute the task again sleep(TokioDuration::from_secs(duration.as_secs())).await; } Ok(()) } ``` At the moment, you can see we're defining the time for when we want to run the task (every day at midnight - we get the local time, convert it to a `NaiveDate` then add our hours, minutes and seconds). As you may have noticed, we're also using a function called `calculate_time_diff` to get the difference between when we want to run the function and now - firstly to calculate whether or not we should run the task, then secondly so we can tell the program to make the thread sleep until when the time between midnight and the current time is zero, which would look like this: ```rust // main.rs fn calculate_time_diff(midnight: NaiveDateTime) -> Duration { let now = Local::now().naive_local(); midnight .signed_duration_since(now) .to_std() .unwrap() } ``` All we need to do now is to define our function (`send_subscriber_mail`) to grab our required values from the database (a random fact, plus all the names of our subscribers) and then iterate through every subscriber email and send an email to them through SMTP, like below: ```rust // main.rs async fn send_subscriber_mail( mailer: AsyncSmtpTransport, db: Arc>, gmail_user: String, gmail_password: String, ) -> Result<(), anyhow::Error> { let db = db.lock().await; let rows = match db.execute("SELECT email FROM subscribers").await { Ok(res) => res.rows, Err(e) => return Err(anyhow!("Had an error while sending emails: {e}")), }; if !rows.is_empty() { let cat_fact = match db .execute("SELECT fact FROM catfacts order by random() limit 1") .await { Ok(res) => res.rows[0].values[0].to_string(), Err(e) => return Err(anyhow!("error when trying to get a cat fact: {e}")), }; for row in rows { let email = Message::builder() .from("Cat Facts".parse().unwrap()) .to(row.values[0].to_string().parse().unwrap()) .subject("Happy new year") .header(ContentType::TEXT_PLAIN) .body(format!("Hey there! You're receiving this message because you're subscribed to Cat Facts. \n\nDid you know {cat_fact}?")) .unwrap(); if let Err(e) = mailer.send(email).await { println!("Something went wrong while sending mail: {e}") } } } } ``` Now that we've written all of the functions we need, we can now combine everything together in our `bind` function! We'll want to create the Axum router and bind it to our given server address, then run it together in a `tokio::select!` macro with our background task. It'd look something like this below: ```rust // main.rs #[shuttle_runtime::async_trait] impl shuttle_runtime::Service for CustomService { async fn bind( mut self, addr: std::net::SocketAddr ) -> Result<(), shuttle_runtime::Error> { let router = axum::Server::bind(&addr) .serve(self.router.into_make_service()); // as these tasks run indefinitely until they crash, // running both concurrently in a tokio::select macro works tokio::select!( _ = router => {}, _ = scheduled_tasks( self.db, self.gmail_user, self.gmail_password ) => {} ); Ok(()) } } ``` ## Deployment Now that we've written everything, all you need to do is to use `shuttle deploy` (add `--allow-dirty` if on a Git branch with uncommitted changes) and if there are no problems, you'll be able to visit your web service at the provided URL in the terminal! The deployment at the end should look like something similar to this: ![Preview of the cat facts API deployment](/images/blog/cat-facts-deployed.png) ## Conclusion Thank you for reading! Hopefully this has given you a good high-level insight into what is and what isn't possible with Rust on the web. Writing Rust has become easier than ever with the addition of crates like Axum that make it easy to write code with easily readable syntax that allows you to get up and running quicker, and Turso is another step in that direction. If you'd like to extend this example, here are a few suggestions: - Email validation for adding subscribers - Make the subscriber email background task a job queue - Add some kind of approval functionality and admin approval for submissions - Add a full frontend using HTML templating or a Rust/Javascript front-end framework --- # What if machines did all the work? Source: https://www.shuttle.dev/blog/2023/06/07/Shuttle-AI Date: 7 June 2023 Author: ian Tags: shuttle-ai, ai What if we didn't have to write code anymore? What if we could rely on machines to do our work for us? But not just code - specifications, infrastructure, deployments. What might the future of development look like? Let's take a look. When building Shuttle, we've always focused on how we can help developers. With our infrastructure-from-code offering, we hoped to solve the challenge of provisioning and managing a complex infrastructure. We wanted to give developers the ability to build, test, and deploy their apps without having to worry about managing servers or drowning in console commands. But, we never thought about how we could help machines too. With the recent advent of LLM's and their - sometimes unbelievable - ability to "understand" human language and turn it into working code, it felt like we were on the verge of something revolutionary. It felt like the whole area of software development is about to shift. Yet, while they helped humans write code and develop faster than ever, LLM's still haven't broken the barrier of doing end-to-end work - writing down specifications, generating a whole app, provisioning services, deploying it. But while the code-generation is possible through a combination of model-chaining and output parsing, the latter is where it becomes problematic - you need to be able to provision resources, which means letting the model connect to your AWS account, your shell or manually provisioning services - and none of it sounds like too smart of an idea. We've all seen Terminator, we all know how giving an AI access to the shell turns out. Luckily, our ability to connect and provision infrastructure straight from code has put us in the perfect position to be the glue LLM's need to complete the end-to-end part of the equation. So we decided to try and see how we can help out the developers be more productive, and ended up taking a peek into the future of programming. And we're finally ready to give you a peek into it too - presenting ### Shuttle AI Repetitive work is one of the most annoying parts of every project. We all like doing the fun, challenging stuff but we sure don't enjoy reinventing the wheel for a hundredth time or writing the same boilerplate over and over again. When you have an idea, you don't want to spend time wrangling infrastructure or debugging authentication implementations. But, did you ever wonder what if we could skip the boilerplate? What if we could have an AI write it for us, going from an idea to a fully-fledged MVP in a few minutes? How much time could it save? How many new products could it help launch? Wouldn't it be cool if we could just like Tony Stark, wave our hands around and have Jarvis turn our ideas into reality? Well, wonder no more. In collaboration with [@Ian Rumac](https://ianrumac.com), we've developed a new GPT-4 powered tool to help you develop, run and deploy a whole Rust-based web app to the cloud with a single command. It's the fastest way to go from an idea to production, even faster than deploying a "hello world". ### How? You just need an idea - want to build a revolutionary new blogging service? Just write `shuttle-ai build "Make me a simple blogging service"` and while you go and grab a cup of coffee, Shuttle's AI agents will breakdown the project, generate the needed code, ensure it compiles, provision the infrustructure and deploy it to Shuttle's Cloud, returning you a _live, fully-working backend_ app and the generated code. Missing a feature? Just write `shuttle-ai add-feature "Add commenting support"`, sit back and watch as our AI agents analyse your code, update it and deploy the new changes to it. But enough talking, here's that sneak peek we promised: Now, while this is just a small preview of our new tool's capabilities, we will soon be opening it up to more testers, and we're hyped to see what everyone will be able to build with it. And no worries - we've all seen generated code before and know how messy it can get. That's why the generated code is standard Rust code and is completely human-readable and if you want to maintain it yourself, you will find it easy to understand and navigate. All of the infrastructure is provisioned via Shuttle, and your backend can also be ran locally using `shuttle run`. ### Choosing Rust While Rust is one of the most loved languages and praised for it's performance and reliability, it is also notorious for it's strictness and learning curve. That strictness makes it perfectly positioned as a language for code-generation - if it compiles, you have the confidence in the codebase and it's inner workings - while it does not provide the confidence of good test-coverage, it narrows the possible bug area. Using a dynamic language like Javascript would be able to make our job easier, but the confidence in the generated code would be quite low due to it's dynamic properties. On the other hand, while Rust has a large learning curve, more and more developers are trying it and joining the Rust community. So generating a simple service they can play with and experiment on seems like a perfect opportunity to help them get over the curve easily and quickly become productive in Rust. ### Behind the scenes Behind the scenes lies a process with multiple GPT Agents, working in tandem to generate code, make it deployable and fix any errors along the way. Let's take a look behind the curtain and see how the process works: ![](/images/blog/diagram.png) First, we start with an agent that expands upon the users prompt, generating us a JSON-based specification of the project. This allows us not just interact with the results, but to expand the context and strictly define the set of work the agents will perform. This includes a longer description of the app, a set of endpoints it will generate and a set of models necessary for your app to run, as well as any features you might need, i.e. a database. After this, we turn over to the generation agents themselves, starting with the necessary models - be it SQL schema, just entities or both - then continue to generate endpoints using those exact models, culminating with the generation of the main file and the cargo file itself after all of the dependencies are known. This part of the process is done in order to ensure proper knowledge exists in the context before generation occurs. Before we continue, we run it through implementation agents, which check if there is any code we haven't implemented yet and we generate it before the code leaves for deployment. To make it deployable, we developed "Shuttlify" agents, that update the code with necessary dependencies and annotations to provision Shuttle infrastructure. While this tool currently resides inside Shuttle AI, we find it is quite useful for our community too, and we will soon be extracting it to a standalone tool, so you can migrate your existing projects to Shuttle in no time. While LLM's are amazing and generate all this code for us - they still make mistakes - that's why before deployment, we check the code for compilation issues and if any are found, we push it to our compiler-agent feedback loop. This loop provides the errors to an agent which reasons about it, as in "what caused this error? what does that imply?" and creates a plan - "what are the steps to fixing it? " which we can parse and implement step by step, providing the agent that does the actual fixes with more context, ensuring better results. But, even with the best of reasoning, these agents can sometimes get stuck in a minima, trying to achieve impossible things or just hallucinating plainly wrong code. To get out of this, we ramp-up the temperature of the calls depending on the compilation results. While this is not a perfect solution, it often can help the machine break out of it's loop and start again with a clean slate and a chance to take a different path. Now, with all these agents, you sure must be wondering - what about the prompt limit? Well, while currently this is quite a limitation and generation of large-scale projects is still not feasible (except with access to Claude or GPT4-32k), we're managing to stay under it it by performing agent-splitting. Agent-splitting allows us to clone off an agent at a certain point in conversation, which can be used to either split the independent parts of the task into a new agent, parallelise work or imbue a new agent with the context of a previous one by copying it's history. ### Limitations While the technology is pretty amazing and we are enjoying watching it build a whole codebase out of a simple prompt, we are well aware of some of it's current limitations and challenges. We are working actively to resolve some of them, but also are looking forward to the latest LLM technology improvements allowing us to implement better solutions and larger projects. Some of the encountered limitations are: - _Context size_, i.e. token limits in models. While it is possible to stay under them using different techniques, they are still an issue when a sufficiently large project is in question. Since code heavily relies on other code, providing less code can limit the context in which an LLM operates and increase the changes of the code not applying to the codebase. While we are currently in alpha and don't expect anyone to generate large-scale production projects, we are already using some techniques and actively exploring alternative approaches and updates to our process to avoid this limitation. - _Business logic_ - while often times the logic will be correct, LLM's still make mistakes, especially without enough information. This can be avoided by giving a more detailed prompt, but also by writing tests. While we have currently not implemented a test-writing component, it is in our short-term roadmap, along with other new tools that will enable you to have more confidence in the generation results and in your code. - _Hallucinations and local minima_ - one of the things we have to admit to ourselves when using LLM's is that they often hallucinate. And if you use them in a conversational manner as a part of an automated process, a single hallucination can often lead the LLM into a path that ends up looping, i.e. it starts applying hallucinations to fix it's hallucination-caused errors. While this problem is natural due to the way of the technologies inner workings, it is also avoidable by steering the LLM with a set of right rules and examples, or raising the temperature to get a different response path. We're applying multiple of these techniques, which combined with [Shuttle's large repository of samples](https://github.com/shuttle-hq/shuttle-examples), help reduce hallucinations to a minimum. - _Costs and privacy of LLM's_ - While we are currently using GPT-4, we are well aware of it's costs and privacy issues. Yet, while this is still in an early phase, we do not expect anyone to use it to generate proprietary code or hundreds of codebases. To get around these issues, we are looking into using self-hosted models, or even fine-tuning existing ones. In the future releases, we will also look into the ability of allowing users to provide tokens or even use pick-an-LLM approach. ### The implications of LLM's & code generation While building a tool like this, it's not uncommon to wonder "what does this imply about our future as developers". While we often joke it's going to put us out of work, the realistic perspective is that it won't - it will just make us insanely more productive and let us enjoy our jobs even more. As developers, we find we enjoy solving challenges and improving things, not writing repetitive infrastructure and boilerplate to support our code. And LLM's are able to remove the repetitiveness out of the way on a larger scale than any other development tool before them. On the other hand, we have had code generation for years, just not in this amount of detail. Tools like wizards, scaffolders and bootstraping toolkits (such as Shuttle's recent NextJS & Rust SaaS boilerplate) have existed for a long time and have impacted the productivity of the developers in a huge way, creating more amazing products and companies along the way than it was possible ever before. We are happily looking forward to the future of this technology and all the potential innovations and products it will help create - and especially the ones you create with Shuttle AI! ### When can I use it? While Shuttle AI is currently in a private alpha, you can [join the waitlist](https://www.shuttle.dev/ai) to be among the first to get access to it or [join our Discord](https://discord.gg/shuttle) to find out more. If you have any questions about the tool, feel free to reach out to us on [Twitter](https://twitter.com/shuttle_dev) or [Discord](https://discord.gg/shuttle). We'd love to hear what you think and learn how we can make this even more useful for you! --- # Introducing Shuttle Batch 2.0 Source: https://www.shuttle.dev/blog/2023/04/14/Shuttle-Batch-2 Date: 15 April 2023 Author: nathan Tags: shuttle, shuttle-batch The immersive online program where Rust developers unite to learn, collaborate, and contribute will be running again from May 2nd - apply now! ### How it all started One of Y Combinator's main superpowers is forging enduring relationships among founders, that lead to lifelong connections, partnerships, and friendships that fuel success and over time foster a strong community. This was the case for us - and we loved this aspect of YC. The beauty of YC's batch system is in its built-in mechanism for forming relationships among people by putting them in well-structured cohorts, where folks who are seemingly working on different problems end up in the same boat - all rushing towards the Demo Day. This got us thinking 🤔 about how we can reproduce this kind of experience and thus its benefits for the many Rust enthusiasts who have been joining our community and looking for ways to contribute to Shuttle's codebase. It's not always easy to build meaningful relationships online and taking the first plunge into open-source contributions can be quite daunting. We decided to launch an experiment - Shuttle Batch 1 (a.k.a. SB-1). ### Blast Off with SB-1! 🚀 The first Shuttle Batch rocketed into action as a six-week online program, bringing together carefully selected Rust developers from across the globe (10 participants hailing from 8 different countries! 🌎). **What happened in Batch 1.0:** - **Dynamic Duos**: Participants joined forces in pairs, merging the expertise of seasoned developers with the fresh insights of newcomers. - **Project Selection**: Teams explored a set of curated projects, and picked the ones they wanted to work on for the next 6 weeks. - **Support Squad**: As pairs raced towards D-day, they were supported and mentored by our engineering team, through hands-on sessions, office hours, Q&A sessions and async communication on our discord server. - **Added Fun:** Throughout the batch we've maintained a light and friendly atmosphere, hosting virtual socials, games nights, and challenges for the batch-mates to mingle with each other and the rest of the Shuttle team. - **The grand finale**: Demo Day - complete with prizes, recognition for key contributors, and Shuttle swag for everyone! 👕 Batch 1.0 was a resounding success! Participants honed their Rust skills, some made first OSS contributions and built lasting relationships with fellow developers around the world. In our post-batch survey, 80% of respondents reported increased Rust knowledge, and 100% wanted to continue contributing to Shuttle. Furthermore, one standout participant, Oddbjorn, was immediately hired and joined the team full time after the batch owing to his outstanding dedication and contributions! Here's his take on the experience: "As someone without industry experience and minimal open-source exposure, I struggled to find a project I felt qualified to contribute to. The SB-1 batch was a fantastic opportunity to get comfortable in open-source, with Shuttle engineers offering mentorship and guidance on tasks ranging from very easy to very hard." We're over the moon about the outcome of the batch (esp. finding a great addition to our team) and all the glowing feedback from the batch participants - so we're doing it again! Get ready for SB-2 🎉 ### Get Ready for SB-2! Strap in for our upgraded and enhanced program (2.0), fine-tuned based on feedback from our fantastic first cohort while staying true to the core Batch DNA. Batch 2.0 is an 8-week online Rust program packed with entertaining experiences, designed to supercharge your learning and development journey. Prepare for: - Hands-on Rust workshops that'll ignite your skills 🔧 - Face-to-face mentorship with our experienced core team 🤓 - Interactive Q&A sessions and office hours to quench your curiosity 🎤 - Social events and games to bond with fellow Rustaceans 🎲 - Demo Day complete with epic prizes 🏅 - And surprise guest speakers that'll leave you starry-eyed! 🌟 These sessions will take place a couple of days each week on our community Discord server, approximately between 2PM - 4PM (UTC +1). By joining SB-2, you'll become part of a tribe of passionate learners and builders, all contributing to a cutting-edge backend platform! Here is what John had to say about his experience with the first batch: "I came into the first Batch with very little Rust experience, having read most of the Rust book I was looking to get involved in Open Source to better my knowledge. The relaxed but supportive vibe in Batch was the perfect way for me to get to know the language better, to mix with people at a similar skill level, and to get quality guidance from the more experienced guys at Shuttle." Don't miss your chance to join the show - the last batch got massively oversubscribed within a day - [apply for SB-2 now!](https://www.shuttle.dev/shuttle-batch) 🚀‼️ --- # Next.js and Rust | An Innovative Approach to Full-Stack Development Source: https://www.shuttle.dev/blog/2023/03/23/nextjs-and-rust Date: 23 March 2023 Author: josh Tags: rust, javascript Let's build a full-stack app with authentication using Next.js and Rust! Recently, we've released a Node.js [CLI package](https://www.npmjs.com/package/create-shuttle-app) that allows you to quickly bootstrap an application that uses a Next.js frontend with a Rust backend that uses [Axum](https://github.com/tokio-rs/axum/), a popular Rust web framework with easy-to-use, uncomplicated syntax. The app we'll be building will be a notes app with a login portal that can register users, as well as log in users and reset passwords and logged in users will be able to view, create, update and delete notes. This article will focus more on the Rust (backend) side and will assume that you have knowledge of using React.js/Next.js for your frontend. ![](/images/blog/login-preview.png) The repo containing all the code can be found [here.](https://github.com/joshua-mo-143/nodeshuttle-example) ### Getting Started We can simply get started by running the following command (note: Looking to bootstrap your frontend so you can focus on the backend? Feel free to skip to the frontend section): ```bash npx create-shuttle-app --ts ``` Once you press enter, it should ask you for a name - feel free to enter any name you want here, and then it should start installing Rust automatically for you and bootstrap an application that uses Next.js (with Typescript because of the additional flag) as well as Rust for the backend, along with relevant npm commands to allow us to quickly get started with developing both the back and frontend. The framework we will be using for the backend is [Axum](https://github.com/tokio-rs/axum/), which is a highly performant, flexible framework with simple syntax and is highly compatible with [`tower_http`](https://github.com/tower-rs/tower-http), which is another extremely strong library for creating middleware. [shuttle](https://www.shuttle.dev) is a cloud development platform that simplifies the deployment of your apps. What makes it stand out is its "infrastructure-from-code" approach, allowing you to define your infrastructure directly in your code without the need for complicated consoles or external yaml/config files. This approach not only improves the clarity of your code but also provides compile-time assurance that you'll get what you asked for. Need a Postgres instance? Just add an [annotation](https://docs.shuttle.dev/resources/shuttle-shared-db) and you're good to go. [shuttle](https://www.shuttle.dev) also supports secrets (environment variables), static file folders and state persistence. Next, we'll want to install [`sqlx-cli`](https://docs.rs/crate/sqlx-cli/latest) which is a great command line app for managing our database migrations. We can install this by simply running the following command: ```bash cargo install sqlx-cli ``` If we navigate to our backend directory in the project folder, we'll be able to create our database migrations by using `sqlx migrate add schema` which will add a migrations folder (if you don't have one already) with a file that follows the naming convention of `_schema.sql` because we named our migration "schema". This SQL file should have the following: ```sql -- backend/migrations/_schema.sql DROP TABLE IF EXISTS sessions; CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR UNIQUE NOT NULL, email VARCHAR UNIQUE NOT NULL, password VARCHAR NOT NULL, createdAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS notes ( id SERIAL PRIMARY KEY, message VARCHAR NOT NULL, owner VARCHAR NOT NULL, createdAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); INSERT INTO notes (message, owner) VALUES ('Hello world!', 'user'); CREATE TABLE IF NOT EXISTS sessions ( id SERIAL PRIMARY KEY, session_id VARCHAR NOT NULL UNIQUE, user_id INT NOT NULL UNIQUE ); ``` As a side note, we'll be running these migrations automatically but if you want to run them manually, you can use `sqlx migrate run --database-url `. The reason why we can do this is that we've set up our SQL file to be idempotent - this simply means that if the table already exists, we won't attempt to create it again. We drop the sessions table to force users to log back in once the app re-uploads as their cookies won't work. Now that we're set up, let's get started! ### Frontend For this app, we'll need several pages: - Pages for logging in and registering - A page for users to be able to reset forgotten passwords - A dashboard page to show records - Pages for editing and creating new records You can clone the frontend-only example for this article by cloning it like below (note: If you skipped straight to this section, you'll need to make sure you have Rust, `cargo-shuttle` and `sqlx-cli` installed and create the migrations from the previous section): ```bash git clone https://github.com/joshua-mo-143/nodeshuttle-example-frontend ``` The cloned repository will have an already pre-setup `src` directory that looks like this: ![Tree file screenshot](/images/blog/folder-structure.png) The components folder contains two layout components that we nest our page components inside of and a modal for editing records that we use in the Dashboard index page. The pages folder includes the relevant page components we'll be using in our app (where the file name indicates the route). We use TailwindCSS for the CSS, as well as using Zustand for easy, bare-bones state management that doesn't require much boilerplate. When the user logs in, they should see something like this if there are any messages: ![User logged in screenshot](/images/blog/app-preview.png) Once we build the backend, the user will be able to register, log in (using cookie session-based authentication) and view, create, edit and delete their own messages by using the frontend. Users will also be able to reset their password if they've forgotten it by entering their email. Looking to make your own frontend? Feel free to consult the [GitHub repo](https://github.com/joshua-mo-143/nodeshuttle-example) to check how the API calling and state management is set up. Now that we're done with this part, we can move on to writing the backend! ### Backend If you navigate to the backend folder, you should see a single file called `main.rs` with a function in it that creates a basic router with one function that returns "Hello, world!". We'll be using this file as the entry point for our application and then creating other files that we'll import call in our main function. You'll want to make sure you have the following contents in your Cargo.toml file: ```toml # Cargo.toml [package] name = "static-next-server" version = "0.1.0" edition = "2021" publish = false [dependencies] # the rust framework we will be using - https://github.com/tokio-rs/axum/ axum = "0.6.1" # extra functionality for Axum https://github.com/tokio-rs/axum/ axum-extra = { version = "0.4.2", features = ["spa", "cookie-private"] } # encryption hashing for passwords - https://github.com/Keats/rust-bcrypt bcrypt = "0.13.0" # used for writing the CORS layer - https://github.com/hyperium/http http = "0.2.9" # send emails over SMTP - https://github.com/lettre/lettre lettre = "0.10.3" # random number generator (for creating a session id) - https://github.com/rust-random/rand rand = "0.8.5" # used to be able to deserialize structs from JSON - https://github.com/serde-rs/serde serde = { version = "1.0.152", features = ["derive"] } # environment variables on shuttle shuttle-secrets = "0.12.0" # the service wrapper for shuttle shuttle-runtime = "0.12.0" # allow us to use axum with shuttle shuttle-axum = "0.12.0" # this is what we use to get a shuttle-provisioned database shuttle-shared-db = { version = "0.12.0", features = ["postgres"] } # shuttle static folder support shuttle-static-folder = "0.12.0" # we use this to query and connect to a database - https://github.com/launchbadge/sqlx/ sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] } # middleware for axum router - https://github.com/tower-rs/tower-http tower-http = { version = "0.4.0", features = ["cors"] } # pre-req for using shuttle runtime tokio = "1.26.0" # get a time variable for setting cookie max age time = "0.3.20" ``` Once we're done with this, we will want to set up our main function so that we can use the [`shuttle_shared_db`](https://docs.shuttle.dev/resources/shuttle-shared-db) and [`shuttle_secrets`](https://docs.shuttle.dev/resources/shuttle-secrets) crates to get a free [shuttle](https://www.shuttle.dev)\-provisioned database and be able to use secrets, like so (as well as setting up a crude implementation of cookie-based session storage): ```rust // main.rs #[derive(Clone)] pub struct AppState { postgres: PgPool, key: Key } impl FromRef for Key { fn from_ref(state: &AppState) -> Self { state.key.clone() } } #[shuttle_runtime::main] async fn axum( #[shuttle_static_folder::StaticFolder] static_folder: PathBuf, #[shuttle_shared_db::Postgres] postgres: PgPool, #[shuttle_secrets::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&postgres).await; let state = AppState { postgres, key: Key::generate() }; let router = create_router(static_folder, state); Ok(router.into()) } ``` Now we can start creating our router! Let's make a file called `router.rs` in the `src` folder of our backend directory. We will put the bulk of our router code in here and then import the function we'll be using to make the final router into our main file once we're ready. Let's open up our `router.rs` file and create a function that returns a router with routes for registering and logging in: ```rust // router.rs // typed request body for logging in - Deserialize is enabled via serde so it can be extracted from JSON responses in axum #[derive(Deserialize)] pub struct LoginDetails { username: String, password: String, } pub fn create_router(state: AppState, folder: PathBuf) -> Router { // create a router that will host both of our new routes once we create them let api_router = Router::new() .route("/register", post(register)) .route("/login", post(login)) .with_state(state); // return a router that nests our API router in an "/api" route and merges it with our static files Router::new() .nest("/api", api_router) .merge(SpaRouter::new("/", static_folder).index_file("index.html")) } ``` As you can see, all we need to do is write the functions that we'll be using in our router and include them in the router. We can also use multiple request methods in one route by simply chaining the methods (more on this later on once we finish writing all of the routes). ```rust // backend/src/router.rs pub async fn register( // this is the struct we implement and use in our router - we will need to import this from our main file by adding "use crate::AppState;" at the top of our app State(state): State, // this is the typed request body that we receive from a request - this comes from the axum::Json type Json(newuser): Json, ) -> impl IntoResponse { // avoid storing plaintext passwords - when a user logs in, we will simply verify the hashed password against the request. This is safe to unwrap as this will basically never fail let hashed_password = bcrypt::hash(newuser.password, 10).unwrap(); let query = sqlx::query("INSERT INTO users (username, , email, password) values ($1, $2, $3)") // the $1/$2 denotes dynamic variables in a query which will be compiled at runtime - we can bind our own variables to them like so: .bind(newuser.username) .bind(newuser.email) .bind(hashed_password) .execute(&state.postgres); // if the request completes successfully, return CREATED status code - if not, return BAD_REQUEST match query.await { Ok(_) => (StatusCode::CREATED, "Account created!".to_string()).into_response(), Err(e) => ( StatusCode::BAD_REQUEST, format!("Something went wrong: {e}"), ) .into_response(), } } ``` As you can see, we hash the password, set up a query via SQLx to create a new user and then if it's successful, return a 402 Created status code - if it's not successful, return a 400 Bad Request status code to indicate that something's wrong. Pattern matching is an extremely strong form of exhaustive error handling in Rust, and it comes in many forms: We can use [`if let else`](https://rust-lang.github.io/rfcs/3137-let-else.html) and [`let else`](https://rust-lang.github.io/rfcs/3137-let-else.html), both of which utilise pattern matching as you will see later on. ```rust // backend/src/router.rs pub async fn login( State(mut state): State, jar: PrivateCookieJar, Json(login): Json, ) -> Result<(PrivateCookieJar, StatusCode), StatusCode> { let query = sqlx::query("SELECT * FROM users WHERE username = $1") .bind(&login.username) .fetch_optional(&state.postgres); match query.await { Ok(res) => { // if bcrypt cannot verify the hash, return early with a BAD_REQUEST error if bcrypt::verify(login.password, res.unwrap().get("password")).is_err() { return Err(StatusCode::BAD_REQUEST); } // generate a random session ID and add the entry to the hashmap let session_id = rand::random::().to_string(); sqlx::query("INSERT INTO sessions (session_id, user_id) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET session_id = EXCLUDED.session_id") .bind(&session_id) .bind(res.get::("id")) .execute(&state.postgres) .await .expect("Couldn't insert session :("); let cookie = Cookie::build("foo", session_id) .secure(true) .same_site(SameSite::Strict) .http_only(true) .path("/") .finish(); // propogate cookies by sending the cookie as a return type along with a status code 200 Ok((jar.add(cookie), StatusCode::OK)) } // if the query fails, return status code 400 Err(_) => Err(StatusCode::BAD_REQUEST), } } ``` As you can see, the requests simply take a JSON request body of whatever type we've decided to give it (so because we have given both a type of [`axum::Json`](https://docs.rs/axum/latest/axum/struct.Json.html) for the request body, it will only accept requests with a JSON request body of "username" and "password"). Structs used in this way must implement [`serde::Deserialize`](https://docs.rs/serde/1.0.155/serde/de/trait.Deserialize.html) as we need to be able to pull the data from JSON, as well as the JSON request argument itself being the final argument we pass into the route function. You may notice we've used a struct called [`PrivateCookieJar`](https://docs.rs/axum-extra/latest/axum_extra/extract/cookie/struct.PrivateCookieJar.html) in our login request. This is simply a way to be able to automatically handle HTTP cookies without having to explicitly set headers for them - to be able to propagate any changes in them however, we need to set them as a return type and return the changes. When the user wants to access a protected route, all we need to do is grab the value from the cookie jar and validate it against the session IDs we've saved in our database. Because we're using a private cookie jar, any cookies saved on the client side will be encrypted with the key we've created in our initial struct which will generate a new key each time we start our app up. Now that we've added a route to be able to log in, let's have a look at adding a route for logging out as well as some middleware for validating a session: ```rust // backend/src/router.rs pub async fn logout(State(state): State, jar: PrivateCookieJar) -> Result { let Some(cookie) = jar.get("foo").map(|cookie| cookie.value().to_owned()) else { return Ok(jar) }; let query = sqlx::query("DELETE FROM sessions WHERE session_id = $1") .bind(cookie) .execute(&state.postgres); match query.await { Ok(_) => Ok(jar.remove(Cookie::named("foo"))), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR) } } pub async fn validate_session( jar: PrivateCookieJar, State(state): State, // Request and Next are required types for middleware from a function in axum request: Request, next: Next, ) -> (PrivateCookieJar, Response) { // attempt to get the cookie - if it can't find a cookie, return 403 let Some(cookie) = jar.get("foo").map(|cookie| cookie.value().to_owned()) else { println!("Couldn't find a cookie in the jar"); return (jar,(StatusCode::FORBIDDEN, "Forbidden!".to_string()).into_response()) }; // attempt to find the created session let find_session = sqlx::query("SELECT * FROM sessions WHERE session_id = $1") .bind(cookie) .execute(&state.postgres) .await; // if the created session is OK, carry on as normal and run the route - else, return 403 match find_session { Ok(res) => (jar, next.run(request).await), Err(_) => (jar, (StatusCode::FORBIDDEN, "Forbidden!".to_string()).into_response()) } } ``` As you can see above - for the logout route we simply attempt to destroy the session and then return the cookie removal, and then for the validation route we attempt to get the session cookie and then make sure the cookie session is valid in our database. Let's have a look at creating basic CRUD functionality for some records in our database. We'll want to make a struct that utilizes [`sqlx::FromRow`](https://docs.rs/sqlx/latest/sqlx/trait.FromRow.html) so that we can easily pull records from our database, like so: ```rust // src/backend/router.rs #[derive(sqlx::FromRow, Deserialize, Serialize)] pub struct Note { id: i32, message: String, owner: String, } ``` Then we can simply just use [`sqlx::query_as`](https://docs.rs/sqlx/latest/sqlx/fn.query_as.html) while typing the variable as a vector of the struct to get what we want, like so: ```rust // src/backend/router.rs pub async fn view_records(State(state): State) -> Json> { let notes: Vec = sqlx::query_as("SELECT * FROM notes ") .fetch_all(&state.postgres) .await.unwrap(); Json(notes) } ``` As you can see, all we need to do is simply use the query with our database connection while making sure the struct we've typed our return as has the [`sqlx::FromRow`](https://docs.rs/sqlx/latest/sqlx/trait.FromRow.html) derive macro on it. Using what we know here, we can also quite simply make our other routes like so: ```rust // backend/src/router.rs #[derive(Deserialize)] pub struct RecordRequest { message: String, owner: String } pub async fn create_record( State(state): State, Json(request): Json, ) -> Response { let query = sqlx::query("INSERT INTO notes (message, owner) VALUES ($1, $2)") .bind(request.message) .bind(request.owner) .execute(&state.postgres); match query.await { Ok(_) => (StatusCode::CREATED, "Record created!".to_string()).into_response(), Err(err) => ( StatusCode::BAD_REQUEST, format!("Unable to create record: {err}"), ) .into_response(), } } // note here: the "path" is simply the id URL slug, which we will define later pub async fn edit_record( State(state): State, Path(id): Path, Json(request): Json, ) -> Response { let query = sqlx::query("UPDATE notes SET message = $1 WHERE id = $2 AND owner = $3") .bind(request.message) .bind(id) .bind(request.owner) .execute(&state.postgres); match query.await { Ok(_) => (StatusCode::OK, format!("Record {id} edited ")).into_response(), Err(err) => ( StatusCode::BAD_REQUEST, format!("Unable to edit message: {err}"), ) .into_response(), } } pub async fn destroy_record(State(state): State, Path(id): Path) -> Response { let query = sqlx::query("DELETE FROM notes WHERE id = $1") .bind(id) .execute(&state.postgres); match query.await { Ok(_) => (StatusCode::OK, "Record deleted".to_string()).into_response(), Err(err) => ( StatusCode::BAD_REQUEST, format!("Unable to edit message: {err}"), ) .into_response(), } } ``` Now we've created all of our basic functionality for our web app! However, we are missing one last thing before we combine all of our routes. What if a user wants to reset their password? Surely we should have a self-service route for that? Let's make that route now. ```rust // backend/src/router.rs pub async fn forgot_password( State(state): State, Json(email_recipient): Json, ) -> Response { let new_password = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); let hashed_password = bcrypt::hash(&new_password, 10).unwrap(); sqlx::query("UPDATE users SET password = $1 WHERE email = $2") .bind(hashed_password) .bind(email_recipient) .execute(&state.postgres) .await; let credentials = Credentials::new(state.smtp_email, state.smtp_password); let message = format!("Hello!\n\n Your new password is: {new_password} \n\n Don't share this with anyone else. \n\n Kind regards, \nZest"); let email = Message::builder() .from("noreply ".parse().unwrap()) .to(format!("<{email_recipient}>").parse().unwrap()) .subject("Forgot Password") .header(ContentType::TEXT_PLAIN) .body(message) .unwrap(); // build the SMTP relay with our credentials - in this case we'll be using gmail's SMTP because it's free let mailer = SmtpTransport::relay("smtp.gmail.com") .unwrap() .credentials(credentials) .build(); // this part x`doesn't really matter since we don't want the user to explicitly know if they've actually received an email or not for security purposes, but if we do then we can create an output based on what we return to the client match mailer.send(&email) { Ok(_) => (StatusCode::OK, "Sent".to_string()).into_response(), Err(e) => (StatusCode::BAD_REQUEST, format!("Error: {e}")).into_response(), } } ``` We'll also want to use a `Secrets.toml` as well as `Secrets.dev.toml` file at the `Cargo.toml` level to add secrets that we'll need. We should use the following format for this: ```ini # Secrets.toml SMTP_EMAIL="your-email-goes-here" SMTP_PASSWORD="your-email-password-goes-here" DOMAIN=".shuttleapp.rs" # You can create a Secrets.dev.toml to use secrets in a development environment - in this case, you can set domain to "127.0.0.1" and copy the other two variables as required. ``` Now our all of our apps are done, we should probably have a look at creating the router out of all of our apps. We can simply nest our routing and include the middleware by appending it to our protected routes, like so: ```rust // backend/src/router.rs pub fn api_router(state: AppState) -> Router { // CORS is required for our app to work let cors = CorsLayer::new() .allow_credentials(true) .allow_methods(vec![Method::GET, Method::POST, Method::PUT, Method::DELETE]) .allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT]) .allow_origin(state.domain.parse::().unwrap()); // declare the records router let notes_router = Router::new() .route("/", get(view_records)) .route("/create", post(create_record)) .route( // you can add multiple request methods to a route like this "/:id", get(view_one_record).put(edit_record).delete(destroy_record), ) .route_layer(middleware::from_fn_with_state( state.clone(), validate_session, )); // the routes in this router should be public, so no middleware is required let auth_router = Router::new() .route("/register", post(register)) .route("/login", post(login)) .route("/forgot", post(forgot_password)) .route("/logout", get(logout)); // return router that uses all routes from both individual routers, but add the CORS layer as well as AppState which is defined in our entrypoint function Router::new() .route("/health", get(health_check)) .nest("/notes", notes_router) .nest("/auth", auth_router) .with_state(state) .layer(cors) } ``` As you can see, we can create an API router by simply defining two routers, each with their own routes (one router with protected routes that will only run if the session is validated), and then simply returning a router that has a health check route, nests our two previous routes and then adds the CORS and app state to the router. Our final router function can simply then look like this: ```rust // backend/src/router.rs pub fn create_router(static_folder: PathBuf, state: AppState) -> Router { let api_router = api_router(state); // merge our static file assets Router::new() .nest("/api", api_router) .merge(SpaRouter::new("/", static_folder).index_file("index.html")) } ``` We will use this function in our initial entry point function in our main function (in `lib.rs`) to generate the router, like so: ```rust mod router; use router::create_router; #[derive(Clone)] pub struct AppState { postgres: PgPool, key: Key, smtp_email: String, smtp_password: String, domain: String, } impl FromRef for Key { fn from_ref(state: &AppState) -> Self { state.key.clone() } } #[shuttle_runtime::main] async fn axum( #[shuttle_static_folder::StaticFolder] static_folder: PathBuf, #[shuttle_shared_db::Postgres] postgres: PgPool, #[shuttle_secrets::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!() .run(&postgres) .await .expect("Something went wrong with migrating :("); let smtp_email = secrets .get("SMTP_EMAIL") .expect("You need to set your SMTP_EMAIL secret!"); let smtp_password = secrets .get("SMTP_PASSWORD") .expect("You need to set your SMTP_PASSWORD secret!"); // we need to set this so we can put it in our CorsLayer let domain = secrets .get("DOMAIN") .expect("You need to set your DOMAIN secret!"); let state = AppState { postgres, key: Key::generate(), smtp_email, smtp_password, domain, }; let router = create_router(static_folder, state); Ok(router.into()) } ``` Note that for importing functions from files, you need to define them in your `lib.rs` file if they're in the same file directory like above (`use router`); this also applies to trying to import functions from one file into another file that is also not the main entrypoint file. [This link explains it quite well if you need clarification.](https://users.rust-lang.org/t/how-to-call-a-function-in-another-file-but-the-same-crate/15214) Now we're done with the programming section! We can finally look at deploying. ### Deployment Deploying with [shuttle](https://www.shuttle.dev), thankfully, is quite easy - you just need to run `npm run deploy` in the root directory of your project and if there aren't any issues, you should be able to see that shuttle has launched your app and it will return a list of information about your deployment followed by the database connection string for your shuttle-provisioned database. If you need to find this database string again, you can run `shuttle resource list --show-secrets` in the backend directory of your project and it will find it for you. You may wish to run `cargo fmt` and `cargo clippy` before you deploy as any warnings or errors will appear while your web service is being built. If you don't have either of these components, you can use `rustup component add rustfmt` and `rustup component add clippy` respectively - both of these tools are a great addition to any Rust developer's toolbox and I would highly recommend using both of them. ### Finishing Up Thank you for reading my article! I hope this has given you some insight into how building a Rust webservice can be made easily and without hassle. Rust has evolved significantly in the past couple of years, making it much more approachable for new learners. If you've been hesitant to try it out, now is a great time to give it a go and see for yourself how powerful and user-friendly Rust can be. --- # Getting Started with Rust & GPT-3 Source: https://www.shuttle.dev/blog/2023/03/01/getting-started-with-rust-and-gpt Date: 23 December 2022 Author: josh Tags: rust, tutorial Quick guide on how to get started with Rust & GPT-3 by building & deploying a simple app. Rust is a language that is starting to gain more and more traction within many large tech companies as it is starting to become more widely adopted due to its high level of efficiency, memory safety as well as the ability to inter-op with other languages like C and JavaScript making it easy to add in without requiring a full rewrite in Rust. We can also leverage the power of AI and language models through OpenAI's GPT-3 to create web apps that can generate useful text, which we will be covering in this tutorial. The final example code for this article can be found [here](https://github.com/joshua-mo-143/react-rust-gpt3-example). We will be building a web app that will call use GPT-3 to generate a random name, pass it back to the frontend and then display it in the browser. ![Screenshot of the GPT-3 example](/images/blog/rust-gpt-example.png) If you don't have Rust installed already, you can find out how to install it [here](https://www.rust-lang.org/tools/install). This will install Rust as well as the respective Rust package manager, Cargo. You'll also need [cargo-shuttle](https://github.com/shuttle-hq/shuttle/tree/main/cargo-shuttle) which you can simply install with the following command: ```rust cargo install cargo-shuttle ``` Or you can install the binary by installing `binstall` and then running the following command: ```rust cargo binstall cargo-shuttle ``` You will need to set up your login for `cargo-shuttle` by simply going to our [login](https://www.shuttle.dev), logging in via GitHub and then using the login command with the API key flag, as without this you can't deploy anything. Before we start you'll also need to grab an API key from OpenAI's API by doing the following: 1. Go to the [OpenAI API dashboard](https://platform.openai.com/overview) and sign in (create an account by signing in via Google or any other available method if you don't have one yet) 2. Click your profile in the top-right hand corner and select "View API Keys" 3. Create a new secret key and store it somewhere for safekeeping as we'll be using this later on. Now we're ready to get started! ### Getting Started ### Frontend We'll want to start our project by initialising a React project using Vite, which is a fast development tool for rapid web development. We can run the following command below to initialise our project: ```bash npm create vite@latest react-rust-gpt-example -- --template react-ts ``` Now we should have a new folder in our current working directory that holds a default React project that we'll use as a base directory. You can quickly navigate to it by running the following command: ```bash cd react-rust-gpt-example ``` For this example, we'll be using TailwindCSS for our classes. You can find out how to install TailwindCSS for Vite [here](https://tailwindcss.com/docs/guides/vite). You will probably want to delete your `App.css` file and the relevant line to import it into the `App.ts` file, as this file won't be necessary once you start using Tailwind classes. Once that's done, we can fill out our App.ts function component like so: ```typescript // App.ts function App() { return (

Name Generator

Try generating a name!

) } ``` Now we just simply need to add an function that makes an async/await API call to our backend (which we haven't created yet!), and if the API call is successful, we will return the data and append the response to HTML like so: ```typescript // App.ts - Add this to your function component before the return const [prompt, setPrompt] = React.useState(""); const fetchData = async () => { const res = await fetch("/api/prompt"); const data = await res.json(); return data; }; const setText = () => { let text = document.querySelector("#prompt-text") as HTMLParagraphElement; text.innerText = prompt; }; const handleSubmit = async (e: React.SyntheticEvent) => { e.preventDefault(); try { fetchData().then((data) => { setPrompt(data); setText(); }); } catch (err: any) { console.log(`Error: ${err}`); setPrompt(`Error: ${err}`); setText(); } }; ``` That's it! Our frontend is done. Now we can focus on the meat of the app, which will be the backend. ### Backend Ok so now that we're finished with our frontend, we can now write a backend that we'll be using for the prompt. We will want to use the following command to initialise our project: ```rust shuttle init --axum ``` You'll be prompted to enter a name for your project, where you want to initialise the directory (we will be calling this folder "API" for the sake of clarity) and whether or not you want to start a project environment on shuttle. On initialisation, we will be including Axum, which is a strong, easy to use framework. Once the project's been created, you'll have the following: ```rust // main.rs #[shuttle_runtime::main] async fn axum() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/hello", get(hello_world)); Ok(router.into()) } ``` ```rust // Cargo.toml [package] name = "api" version = "0.1.0" edition = "2021" publish = false [dependencies] axum = "0.6.10" shuttle-axum = { version = "0.14.0" } shuttle-runtime = { version = "0.14.0" } tokio = { version = "1.26.0" } ``` Once you've checked that you have the above files, you'll want to add some more dependencies to make our final backend by pasting this as the dependencies into your Cargo.toml (they will get automatically built on compile): ```rust // Cargo.toml [dependencies] axum = "0.6.10" axum-extra = { version = "0.5.0", features = ["spa"] } axum-macros = "0.3.4" openai-api = "0.1.4" serde = { version = "1.0.152", features = ["derive"] } shuttle-axum = { version = "0.14.0" } shuttle-runtime = { version = "0.14.0" } shuttle-secrets = "0.14.0" shuttle-static-folder = "0.14.0" tokio = { version = "1.26.0" } ``` Once this is done, we can then create the routes we'll be using in our Axum router. We'll be using only a single route in our Axum router and then adding our compiled frontend assets with the `SpaRouter` type provided by `axum-extra` later on. Before we write our API route though, we should allow our web service to use our API key that we retrieved from GPT-3 by storing it in a `Secrets.toml` file at the `Cargo.toml` level - it should look like this: ```toml GPT_API_KEY="YOUR_KEY_HERE" ``` Now we can add our secrets to our main entry point function, like so: ```rust // main.rs #[shuttle_runtime::main] async fn axum( // https://docs.shuttle.dev/resources/shuttle-secrets #[shuttle_secrets::Secrets] secrets: SecretStore, // https://docs.shuttle.dev/examples/axum-static-files #[shuttle_static_folder::StaticFolder] static_folder: PathBuf, ) -> shuttle_axum::ShuttleAxum { let gpt_token = secrets .get("GPT_API_KEY") .expect("You need to set GPT_API_KEY in your Secrets.toml file!"); let router = handle_router(gpt_token); Ok(router.into()) } ``` Let's have a look at what our API route would look like: ```rust // router.rs pub async fn generate_prompt(State(state): State) -> impl IntoResponse { let prompt = "Generate a random name. Example: Name: John Doe Name:"; match state.client.complete_prompt_sync(prompt) { Ok(result) => (StatusCode::OK, Json(result.choices[0].text.clone())).into_response(), Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!(":( There was an error: {err}")).into_response() } } ``` Now that that's done, we should write our router handling function to take this route like so: ```rust // router.rs pub fn handle_router(api_key: String) -> Router { let prompt_client = AppState { client: Client::new(&api_key), }; Router::new() .route("/api/prompt", get(generate_prompt)) .with_state(prompt_client) } ``` ### Notes On GPT-3 Prompting So now we're done with the coding itself for the most part, we should probably explore how to get better at AI prompting. Prompting plays a very significant part in being able to get GPT-3 to give us an answer that we are able to meaningfully use in a web app. Although GPT-3 can give a rough approximation of an answer if we only try to prompt it in plain English, we can do much better by giving it an exact criteria of what we want by using specific descriptors, such as format, style, how we want the result to be formatted, and so on. So instead of just writing a vague description of what we want, like this: ```bash Generate a creative brief that uses 3 colors and 2 shapes. ``` Ideally we should tell it what we want in a list format, as well as the style and format we want it in, and then we can give it an example of what it would look like and then add an extra copy of the fields that we'd want it to give to us below, like so: ```bash Generate a creative brief that uses the following: Example: Design Brief: Design a logo for a website. Colors: #000000 (Black), #FFFFFF (White), #0000FF (Blue) Shapes: Square, Circle Style: Minimal Design Brief: Colors: Shapes: Style: ``` Now when we pass this as the prompt to GPT-3 instead of the message we put in before, we should get a much more descriptive response! This should now be the final part of the coding part of our app done. If we wanted to extend this, we could make it so only authorised users could access the prompt generator, or we could allow users to submit responses to prompts they've generated. ### Integrating Front & Backend Now that we've created both of our front and backend components, let's have a look at integrating them both together. You'll want to make sure your `vite.config.ts` file (which can be found at the `Packages.json` level) contains the following: ```typescript // vite.config.ts export default defineConfig({ base: "", plugins: [react()], build: { outDir: "API/static", emptyOutDir: true, }, }); ``` Then make sure your build script in `packages.json` looks like this: ```json // package.json "scripts": { // ... your other npm scripts if you have any "build": "tsc && vite build --emptyOutDir", // ... your other npm scripts if you have any }, ``` This will allow our frontend to compile directly to the static folder that our backend will be using, while also making sure it's empty before compiling so that we don't end up with multiple copies of compiled files. If we run `npm run build`, assuming there were no build errors we should now have a subfolder in our API directory called "static", which will hold all of our static assets that we can refer to in our Rust project. Let's implement our static folder by using the `shuttle_shared_folder` package: ```rust // main.rs #[shuttle_runtime::main] async fn axum( #[shuttle_secrets::Secrets] secrets: SecretStore, #[shuttle_static_folder::StaticFolder] static_folder: PathBuf, ) -> shuttle_axum::ShuttleAxum { let gpt_token = secrets .get("GPT_API_KEY") .expect("You need to set GPT_API_KEY in your Secrets.toml file!"); let router = handle_router(gpt_token, static_folder); Ok(router.into()) } ``` ```rust // router.rs pub fn handle_router(api_key: String, static_folder: PathBuf) -> Router { let prompt_client = AppState { client: Client::new(&api_key), }; let spa = SpaRouter::new("/", static_folder); Router::new() .merge(spa) .route("/api/prompt", get(generate_prompt)) .with_state(prompt_client) } ``` ### Deploying Before we deploy, we will probably want to compile our frontend for our backend to be able to use it. We can do this by simply just using the following command: ```bash npm run build ``` Now our frontend assets will compile into the static folder of our Rust project, which means whenever we want to run our Rust project locally, we'll have a static frontend we can work with and (more importantly) we can put our front and backend on one deployment. Now our app is ready to deploy, so once we're ready we can finalise the process by running the following command: ```rust shuttle deploy ``` If there's no issues, it should deploy! We'll be able to view our app at the link that was given to us in the terminal, and it should work with no issues whatsoever. ### Finishing Up Now that we're done, there's quite a few ways we could easily extend this example. If you're looking to take this example further and generate a fully working app, here's a few ideas you could try: - A web app that will generate a PDF or word document based on what you want the document to contain. - A random color scheme generator that will generate random colour schemes. - A random password generator based on some random criteria. Working with GPT-3 has never been easier, and with some simple prompt refinement we can get results that we can turn into meaningful web applications for end users. We've also recently released a new version (v0.12.0) that implements some really cool new features like a Node CLI to easily bootstrap a Next.js + Rust application, as well as local secrets, so if you'd like to use it for other things, now is a better time than ever to try it out! --- # Reflection in Rust with procedural macros Source: https://www.shuttle.dev/blog/2022/12/23/procedural-macros Date: 23 December 2022 Author: ben Tags: rust, tutorial, macros Comparing runtime reflection in JavaScript against Rust's compile time procedural derive macros ## Introduction Procedural macros are one of the more complex but powerful parts of Rust. For me, it's one of the features that really sets Rust apart from other languages. If you have ever seen this syntax and left scratching your head, then this post is for you: ```rust #[derive(derive_macros::MyTrait)] // << 🤨 struct X {} ``` This article will cover the concept of macros and some interesting use cases, and you certainly don't need to be a Rust expert to follow along. However, the example section assumes you have written _some_ Rust (`if let` , `struct`, `trait` etc). In this post we'll compare how Rust's compile time, token based approach to object reflection is different to the approach in JavaScript's runtime approach to reflection. ## What are Rust macros? Macros are a way of generating Rust code. They use _tokens_ which are small sections of syntax / grouped characters. Keywords, identifiers and operators are examples can be considered as tokens. Token streams are vectors / ordered collections of tokens. In Rust, some tokens are grouped together and thus the stream is not always flat. Macros take an input token stream and output _another_ token stream. Macros are _expanded_ at compile time so the output is checked syntactically and type checked. They are very powerful, so it's important to use them in a way for programs to still be understandable and maintainable. Rust offers [`macro_rules!`](https://doc.rust-lang.org/rust-by-example/macros.html) for creating macros using a pattern matching syntax that's bespoke to Rust. These are currently limited to just expression and statement invocations using `my_macro!` syntax. An example of an expression based macro is `println!`. Designing a function to print results to the terminal difficult to get right using just a function. Instead, it is implemented as a macro. This is very powerful, for example this allows writing a formatting string that interpolates variables in the scope (e.g. `println!("{my_var}");`). Also, as the input for macros is just a token stream, it is up to the macro to decide what commas mean and so `println!` arguments act _variadic-ly_. `macro_rules!` are easier to get started with as they can be written and used anywhere inside the same crate. However, as we'll see they only work for user token inputs (not on existing items) and their pattern syntax is limited. In this article we'll be focusing exclusively on the more advanced procedural macros. ### Procedural macros Compared to `macro_rules!` procedural macros are much more powerful in that they process token streams using Rust code instead of just using pattern matching: ```rust #[proc_macro] pub fn my_macro(input: TokenStream) -> TokenStream { input } ``` Procedural macros are different to `macro_rules!` in that they can **additionally** work on the tokens of existing structures. This includes `fn` ,`trait` , `struct` and `enum` declarations. They also require creating a separate crate for the function. This article will walk through all the steps and file structure required to add a proc (from "procedural") macro to a crate. ## Runtime reflection and JavaScript Before we start with writing procedural macros, let's take a look at closely related concept called reflection, what reflection is and how it is implemented in the JavaScript language. Reflection refers to code that may introspect and generate its own structure and behavior. There are various points to introspect such as the name of declarations, the structure of fields. In the following example we will be looking at the fields of an _object_ in JavaScript. JavaScript objects can be inspected at runtime. There are no fixed structures in JavaScript. Every object can have properties added or removed (unless [sealed](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal)). On the other hand Rust structures are static, that is they are determined at compile time. Rust _packs_ field data together to build structures and turns keys into offsets in memory. The actual fields, the count, the names are all lost at runtime under regular operation. On the other hand JavaScript objects at the surface level are all maps from keys to other JavaScript values. The names of properties are kept in memory at runtime. In Rust the rough equivalent type of a JS object would be: `HashMap<&'static str, Box>` (ignoring object prototypes). ## Our reflection example In our first example we have an array of JavaScript objects: ```javascript const countries = [ { name: "Japan", population: 124_214_766, above_equator: true }, { name: "Mexico", population: 129_150_971, above_equator: true }, { name: "Australia", population: 26_020_300, above_equator: false }, ... ]; ``` In our app we want to serialize this list to send it over the wire or to store locally in the browser. Our first instinct is to use JSON, but however we would ideally like a more space-efficient. We can take advantage of the fact that every object in the list has the same keys, we can take advantage of this and not serialize the keys over and over again. Another invariant we can take advantage of is that these objects are shallow (not nested) values with types which are known beforehand. We can write a function that: - Encodes the length of the array - Investigates the properties of the first objects (as the list is homogeneous, these facts apply to all objects in the array) - Loops over items in the array - Reads each field name in the object - Based on the type, encodes the value into a low level representation ```javascript const float32toString = (number) => String.fromCharCode(...new Uint16Array(new Float32Array([number]).buffer)); function arrayToString(array) { if (array.length === 0) { return ""; } const entries = Object.entries(array[0]); const fields = entries.map(([name, value]) => ({ name, ty: typeof value })); let buf = float32toString(array.length); for (const item of array) { for (const { name, ty } of fields) { const value = Reflect.get(item, name); if (value === null) { throw Error("Property value is null"); } switch (ty) { case "string": const length = float32toString(new Blob([value]).size); buf += length + value; break; case "number": buf += float32toString(value); break; case "boolean": buf += value ? "\0" : "1"; break; } } } return buf; } ``` Here with `Object.entries` we can inspect the _shape_ of an object at runtime. Using the `typeof` operator we can get the type of the value. (Again assuming that all objects have the same type): ```javascript > Object.entries(countries[0]) [ [ "name", "Japan" ], [ "population", 124_214_766 ], [ "above_equator", true ] ] > Object.entries(countries[0]).map(([name, value]) => ({ name, ty: typeof value })) [ { name: "name", ty: "string" }, { name: "population", ty: "number" }, { name: "above_equator", ty: "boolean" } ] ``` Assuming homogeneity of elements in the array, we can do reflection only once outside the main loop. Another fact is the fields are serialized in the same order. If we did reflection on each we would have to be careful of retaining the order. Objects keys are in order of declaration, so this can cause some problems with a subset of the reflection API: ```javascript > Object.keys({a: 3, b: 2}); [ "a", "b" ] > Object.keys({b: 3, a: 2}); [ "b", "a" ] ``` We also use `Reflect.get` to get a property under a given string key (`item[name]` is equivalent). > The idea of the example is not to show how to do low-level byte conversion in JavaScript but to show how you can mix in the the introspection logic. As we will see later runtime reflection is very difficult to do in Rust as there are no equivalent `Object.entries`, `Reflect.get` functions or a `typeof` operator in the language. Now that we can serialize the array with `arrayToString(countries)`, we want a way to reverse the process! However, the deserialization process be a little problematic, reflection in JS can only be done when we have a existing structure in inspect. As there are no type/shape declarations in plain JavaScript, there is no reference of the shape of the object we want to deserialize our serialized string into. If we were using JSON we would be okay as the keys are embed into the serialized format, in our example we don't save the keys. Instead we can send a representation array of the key type pairs we want the objects to look like. Using `fields` and with a bit of conversion from our low level formatted string we have the following: ```javascript const stringToFloat32 = (string, offset) => { const u16 = new Uint16Array([ string.charCodeAt(offset), string.charCodeAt(offset + 1), ]).buffer; return new Float32Array(u16)[0]; }; function arrayFromString(string, fields) { let i = 0; const entries = stringToFloat32(string, i); i += 2; const array = []; for (let arrayIndex = 0; arrayIndex < entries; arrayIndex++) { const object = {}; for (const { name, ty } of fields) { let value; switch (ty) { case "string": const length = stringToFloat32(string, i); i += 2; value = String.fromCharCode( ...Array.from({ length }, (_, j) => string.charCodeAt(j + i)), ); i += length; break; case "number": value = stringToFloat32(string, i); i += 2; break; case "boolean": value = string.charCodeAt(i) === 0; i++; break; } Reflect.set(object, name, value); } array.push(object); } return array; } ``` Here we're using another part of reflection `Reflect.set` which allows us to set a property of an existing object based on a string key (`name`). ### Problems with reflection in JavaScript The `arrayToString` function assumes that the caller has passed a standard array where every object has the same type. `Reflect.get` will fail at runtime as if the property doesn't exist. The downside of this is that both the dynamic property lookup and null check is expensive at runtime. One way to catch property errors ahead of time is to use a type system on top of JavaScript such as TypeScript. ## Writing a Rust procedural macro To get started, if you are not already in a cargo project you can create one with `cargo new ` command. Before we start generating code we should declare a trait as a target for our macros output. ### The `Binary` trait First, we need to create a `trait` which describes the requirements for serializing and deserializing objects - let's call this the `Binary` trait. Serialization will require adding information on the structure into a buffer. Deserialization will require pulling from an iterator (which iterates over bytes of a serialized buffer) and producing a term of `Self`. _We will assume the deserialize input is well-formed and panic at runtime rather than proper handling with `Result` when deserializing._ ```rust pub trait Binary { fn serialize(self, buf: &mut Vec); fn deserialize>(iter: &mut I) -> Self; } ``` We can implement the `Binary` trait for the primitives that will be in our structures: ```rust impl Binary for bool { fn serialize(self, buf: &mut Vec) { buf.push(self as u8) } fn deserialize>(iter: &mut I) -> Self { iter.next().unwrap() == (true as u8) } } impl Binary for u64 { fn serialize(self, buf: &mut Vec) { buf.extend_from_slice(&self.to_le_bytes()); } fn deserialize>(iter: &mut I) -> Self { let mut buf = [0; u64::BITS as usize / 8]; buf.fill_with(|| iter.next().unwrap()); u64::from_le_bytes(buf) } } impl Binary for String { fn serialize(self, buf: &mut Vec) { (self.len() as u64).serialize(buf); buf.extend_from_slice(self.as_bytes()); } fn deserialize>(iter: &mut I) -> Self { let length = u64::deserialize(iter) as usize; String::from_utf8(iter.take(length).collect()).unwrap() } } ``` ### The problem Next we would want to implement the same logic for structs. ```rust struct Country { name: String, population: u64, above_equator: bool } ``` We could implement `Binary` for `Country`, writing a bespoke implementation for it manually: ```rust impl Binary for Country { fn serialize(self, buf: &mut Vec) { self.name.serialize(buf); self.population.serialize(buf); self.above_equator.serialize(buf); } fn deserialize>(iter: &mut I) -> Self { Self { name: Binary::deserialize(iter), population: Binary::deserialize(iter), above_equator: Binary::deserialize(iter), } } } ``` This is great and we have our desired functionality. In this code we have to be careful we serialize and fields in the same order. **Writing this `impl` block out for many structs with many fields would get tedious. If we add another `struct` we want to be serializable we don't want to have to have to copy the implementation over.** With some idea of the code we want to write, we can get Rust to generate the above code for us using just the information in the struct definition. This is where proc macros come in... ### Procedural macro time! Now we know what code we want to generate, we can write some Rust code to handle a structure and generate the an output token stream. Rust procedural macros require their own crate for their definition due to constraints on how they are compiled. The unofficial Rust convention for derive macros is the name of trait or crate name + derive. So let's run the following from our current folder `cargo new --lib binary-derive`. We need to let `cargo` know that this crate is a proc macro by defining it in its `Cargo.toml`. We'll also add the dependencies [syn](https://github.com/dtolnay/syn) for parsing the contents of our structure and [quote](https://github.com/dtolnay/quote) for generating the output: ```toml [package] name = "binary-derive" version = "0.1.0" edition = "2021" [lib] # Important \/\/\/ proc-macro = true [dependencies] # Dependencies we use when writing the macro \/\/\/ quote = "1.0.23" syn = "1.0.107" ``` In our macro we want to parse the input into a structure that we can read information from. In the below we can read `.fields` directly without having to understand what tokens refer to field names and such. ````rust use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput, Data}; #[proc_macro_derive(Binary)] pub fn my_macro(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); // Get the name of the structure the derive is on. For example if we have: // ```rust // #[derive(Binary)] // struct MyStruct; // ``` // `name` will be "MyStruct" let name = input.ident; // Only look at structs for now. // Challenge for the reader to add the logic for enums! let Data::Struct(struct_data) = input.data else { unimplemented!("enums"); }; // Produce a iterator of expressions for serializing each field let serialize_fields = struct_data.fields.iter().map(|field| { // `field.ident` is the name of the field let Some(ident) = &field.ident else { unimplemented!("tuple structs"); }; quote!( self.#ident.serialize(buf); ) }); // Produce a key value pair deserializing each field // VERY important that the iterator is in the same order as the above serialize iterator! let deserialize_fields = struct_data.fields.iter().map(|field| { // Same process as for serialization but generating constructor // field.ident instead of calling expressions let Some(ident) = &field.ident else { unimplemented!("tuple structs"); }; quote!( #ident: Binary::deserialize(iter) ) }); // Finally produce a `impl` block for our trait. Using `#` to interpolate are above token iterators let expanded = quote! { #[automatically_derived] impl Binary for #name { fn serialize(self, buf: &mut Vec) { #( #serialize_fields )* } fn deserialize>(iter: &mut I) -> Self { Self { #( #deserialize_fields ),* } } } }; TokenStream::from(expanded) } ```` Our macro starts with a `pub` function with an attribute `#[proc_macro_derive(Binary)]` to show this is the derive macro for `Binary`. In the code we use two iterators. One that generates `serialize` calls on fields and one that deserializes fields and assigns them to fields. Throughout we use `quote!` (which yes is another macro 🤯). With `quote` we can describe the tokens we want to generate in a declarative form exactly the same as Rust's syntax. The `expanded` variable is code that is a copy of the above however using the _hash_ `#` character we can specify variables we want to interpolate. For example our struct name is interpolated into `impl Binary for #name`. We then interpolate our iterators. Using parenthesis and asterisks we specify run through the items from the iterator with a separator in between. In the first case the `serialize` call statements are separated with semicolons `#( #serialize_fields );*`. ## Using the procedural macro With our macro written, we include our crate using a path dependency ```toml [dependencies] binary-derive = { path = "./binary-derive" } ``` Similar to a function or other items we can import and reference the macro. It does not clash as macros and types have different name-spaces therefore we can have a macro and trait with the same name in the scope. (traits exist in the type namespace). We can tell the compiler that we want to generate code for `Country` using the `#[derive(...)` attribute on our struct. ```rust use binary_derive::Binary; #[derive(Binary)] struct Country { name: String, population: u64, above_equator: bool, } ``` ### cargo expand-ing our macro (optional) We can debug the output by installing [cargo-expand](https://github.com/dtolnay/cargo-expand) (`cargo install cargo-expand`). Running `cargo expand` we see the result which is the automatic generated what we wrote manually: ![](/images/blog/proc-macro-cargo-expand-output.png) > `cargo-expand` requires Rust nightly and can require running `cargo clean` if switching back and forth between stable. Alternatively, adding `eprintln!("{}", expanded)` to the end of your macro code and then running `cargo check` also helps for debugging. The output is not as readable but it works for malformed token streams. We can now emulate what we were doing with JavaScript arrays using regular generics and trait logic in Rust: ```rust pub fn serialize_list(items: Vec) -> Vec { let mut buf = Vec::new(); buf.extend_from_slice(&items.len().to_le_bytes()); for item in items { item.serialize(&mut buf); } buf } ``` ![](/images/blog/proc-macro-encoding-binary-output.png) ## Considerations As procedural macros are expanded at parse-time, there is no type information available to the macro. We can examine type references syntactically but you can't rely on the characteristics as the following is valid Rust code: ```rust type String = (); struct X(String); ``` Additionally, macros can result in increased compile time. When building the crate all the macros need to be run and the output token streams need to be parsed. Another problem is that the trait and its corresponding macro are split across crates. If like me you like keeping similar logic together then you might be a bit annoyed that it is impossible to have the trait definition and macro automatic implementation in the same file. The other difficulty is publishing the crate to [crates.io](https://crates.io/) (with `cargo publish`). For your crate to work its derive macro crate also needs to be published. If you update the trait and the macro you must first release the proc macro crate and then afterwards update the dependency version before publishing the main crate. ### syn-helpers [syn-helpers](https://github.com/kaleidawave/syn-helpers) is a framework I have been working to abstract common derive patterns. Our short example works great for our `Country` struct. However our macros should but doesn't currently work across enums, items with generics, etc. With syn-helpers it abstracts the item the derive is on and gives a really simple way to add logic to fields. It's still a work in progress but not all proc macros need long and complex implementations. ### Inner annotations Sometimes we want to have some custom behavior for a field. Rust allows for writing attributes in lots of places. The attribute can be read from the syntax tree that syn parses and are simple to lookup. They can contain token stream arguments if even more information needs to be given to the macro. In our example if there was a field we didn't want to end up in the output and that could be generated at runtime using default. We could add the following attribute and in the logic of the macro doing some different handling here. ```rust struct Country { // ... #[serialization_skip(using_default)] ignored_field: TypeThatImplementsDefault } ``` Just remember when doing so to register with Rust that the attribute belongs to the derive macro by registering attributes in the `proc_macro_derive` attribute. E.g. `#[proc_macro_derive(Binary, attributes(serialization_skip))]` ## Conclusion ### Performance characteristics As code is generated at compile time. It can be faster than having to do the work at runtime. Compared to JavaScript reflection, the properties are known at compile time, and we can read them using memory offsets. We won't cover it here as it is difficult to benchmark the difference between JavaScript's runtime reflection and Rust with proc macro compile time reflection, without the results including other differences in the languages. ### Real world procedural macro examples Proc macros are used throughout Rust. Most standard library `derive`s are proc macro could be implemented as proc macros. ### Serde [Serde](https://serde.rs/), a serialization and deserialization library is a prime example. It is feature-complete and supports a bunch of different serialization formats like JSON, YAML, etc. ### Shuttle Shuttle which is a Rust-native cloud development platform uses procedural macros to define how a service runs and what features it needs. It works a bit differently using _attribute_ macros (as opposed to the _derive_ macros we used in our example) which work on more items that just `structs` and `enum`s. They allow complete rewrites of token streams as opposed to just generating additional code. Here macros enable users to define how the service runs along-side business logic, rather than having to manage configuration files separately. Long live infrastructure for code! ## In summary procedural macros are good for - Common operations over structures, reducing boilerplate - Performant reflection. Reduced worked compared to dynamic/runtime lookup - Type checked, we can't run code that look on non-existent fields Hopefully if you were previously perplexed, this post cleared up the why's and how's of procedural macros. If you make anything cool with Rust and need a place why not try Shuttle! --- This blog post is powered by shuttle! If you have any questions, or want to provide feedback, join our [Discord server](https://discord.gg/shuttle)! ## [Shuttle](https://www.shuttle.dev/): The Rust-native, open source, cloud development platform. Deploying and managing your Rust web apps can be an expensive, anxious and time consuming process. If you want a batteries included and ops-free experience, [try out Shuttle](https://github.com/shuttle-hq/shuttle).
--- # It's time to rethink how we use virtualization in backends Source: https://www.shuttle.dev/blog/2022/10/21/shuttle-next Date: 21 October 2022 Author: brokad Tags: rust, startup, opinion Virtual machines and containers have improved development in a lot of ways, but over time they have also created a lot of problems. We believe it's time to rethink how we use virtualization for backend development.

Virtual machines and containers have improved backends in a lot of ways, but over time they have also created a lot of problems. We believe it's time to rethink how we use virtualization for backend development.

We're building a backend framework that shifts the scope of virtualization from processes down to service components.

In web applications nowadays, you can sort any component somewhere in a broad spectrum from client-side to server-side. On the client-side, there's everything that runs on people's devices, most likely a browser or an app. On the server-side, there's everything that runs in the cloud. That includes databases, authentication management, batch jobs, events handling etc. Each web framework squarely fits somewhere on that line. React, the most popular front-end web framework out there, is wholly client-side. Express, one of the most popular backend web frameworks, is wholly server-side. Client-side has been historically dominated by JavaScript frameworks. This is not surprising since every client ships a powerful JavaScript engine and that is the best way to make a web page interactive. On the server-side, things are more fragmented. This is also not surprising: backend services are just plain native processes that use their environment's network stack to respond to requests. And there is a world of different ways to write and run these: literally the history of computing. As in many other scenarios in software engineering and computer science, this huge free space of options is also the cause of a lot of problems. To understand why, we need to talk about containers. ## Containers are a solution and a problem On its way to settling in its standards, the cloud - epitomized by AWS - has evolved massively over the past decade. My co-founder has written a [post on this](https://www.shuttle.dev/blog/2022/05/09/ifc) previously. Today we, as software engineers, deal with it as it is: the result of incremental changes on top of a status quo. And it is not ideal. What starts life as physical machines in a data center gets split up into tens, sometimes hundreds, of virtual machines in the AWS console. But VMs are heavy, slow to start and it's difficult to make a lot of them coexist without wasting resources like RAM and storage. Then came along containers. Building on top of the Linux kernel's namespacing features, they made images smaller and runtimes more efficient than VMs. The genius of it is to move the virtualization layer from the hardware - where the kernel itself runs virtualized - to the software - where only processes run "virtualized". With containers, virtualized processes run natively in the host kernel, like any other. Except that their I/Os are carefully kept segregated from others in the host system. Any bit of compiled code that is executable on the host can be run in a container. And you can run processes in a container without a separate boot sequence and a full-fledged virtualized operating system with its own heavy machinery like a scheduler and dedicated virtualized hardware. Containers are actually much older than a lot of people realise, going as far as 2008 with LXC in Linux's case (even more in the case of FreeBSD). Their popularity, however, really took off with the arrival of Docker. The execution of Docker as a platform-as-a-service product was so good it took over software engineering practices for the following decade. And it is still the gold standard today in terms of usage. Of course, companies were quick to build products on top of containers. They basically pass through the benefits of containers to their paying customers. Heroku is one of the most notable example. And while containers delivered most of us, directly or indirectly, from having to deal with VMs as a unit of deployment, they certainly have their issues. The biggest one being their size. VMs have to run an entire operating system, containers don't. So they're quite a lot smaller. But container images still have to contain enough userspace to make the things you want to run actually runnable. For the way most people use them in deployments of web apps, this is generally still quite a lot! The heavier your containers are, the more difficult everything else becomes. They take longer to build, they need more resources to run, they are more expensive to store, etc. At [shuttle](https://www.shuttle.dev/) we're convinced that a lot of the pains experienced by software engineers in the post-Docker world can be traced back to that very simple statement: containers are often too heavy for the job. ## Replacing containers You're probably thinking: it's nice and optimistic to say containers are too heavy, but what do you replace them with? Well first, as an open-source company, you avoid making the same mistake Docker made. If you make the scope of virtualization too broad, you will end up with the same result as containers. The root cause behind the heavy weight of containers is that they have been built for too many usecases. They layer virtualization on top of _all_ the I/Os of a native Linux process: their usecase is just about anything that runs. We're concerned with the backend services most people write. These are HTTP request/response handlers, with or without state. And for that specific usecase, most projects just end up worse off by handing over backend services as container images to their deployment platform of choice. So we need to restrict the scope of virtualization to something more specific to web app backends. This is a trade-off of course, like most things in software engineering. By restricting the scope of a tool, you lose the ability to do certain things. But like most of these trade-offs, you usually are better served by erring on the side of simplicity unless you have specific needs that require extra complexity. In other words: use heavy machinery when you actually have a need for it, not before. Where does that leave us then? We need a new take on virtualization. One that has, perhaps, simplified I/Os and is engineered for backend services. Thankfully, we don't have to invent most of that wheel: let's talk about WASI. ## WASM and WASI [WebAssembly][webassembly] (abbreviated WASM) is an instruction set for extremely lightweight virtual machines. Its most common use is to speed up client-side interactivity. This is made possible as popular browsers have rolled out WASM runtimes a few years back. WASM is made for fast sandboxing. However, without any extension, it is unable to perform even simple I/O operations like reading data from a file descriptor. This is not a big deal if WASM is used _in the browser_ - we definitely don't want to let browsers freely provide file system access to web apps. But it is a serious limitation if WASM is to be used server-side - how else are you going to serve endpoints without that? Therefore, the introduction of WASM was followed, a short while later, by WASI - the [WebAssembly System Interface][wasi]. WASI is a standard API to give WASM code the ability to do system-level I/O. This allows WASM code running in a WASI-compliant runtime to do a lot of what a native process can do through syscalls. The really powerful thing about WASM is that it is a very common compilation target. Major languages (and commonly associated frameworks) now support building WASM as a target, just the same way you build for amd64 or arm. And a lot of standard libraries have added support for WASI-based I/Os. [This](#wasm-and-wasi) is what Docker's founder had to say about WASI, back in 2019. And we agree with them. At the end of the day containers are, really, just I/O-level virtualization. Now, a few years after its initial introduction, WASM runtimes have stabilised their support of WASI. This creates a prime environment to engineer, on top of WASI, a solution to containers' biggest drawbacks. ## Changing virtualization for backends When we launched [shuttle](https://www.shuttle.dev/) for its early alpha, back in March 2022, our purpose was to address the issues people face when building and deploying web app backends. So we created an open-source infrastructure-from-code platform with which you don't need to write Containerfiles and orchestrate images, starting with support for Rust. Since then, more than 1.2k people starred the [shuttle repo](https://github.com/shuttle-hq/shuttle) and hundreds joined our discord community. And we've seen more than 2000 deployments and hundreds of users! From which we received a ton of feedback. What we quickly realized is that while we simplified the process of getting started implementing your own backend and setting up its infrastructure, we completely failed to solve two core problems: long build and deploy times. Rust has notoriously long build times (this probably has to do with static linking and heavy reliance on compile-time code generation). And while it supports incremental compilation out of the box, in a containerized environment, missing the cache for an image layer means having to rebuild from scratch. We've found that no matter how much we tweaked our internal caching, too often users had to wait too long for their projects to build and deploy - something that can take minutes in the simplest projects, and closer to half an hour in complex ones. The reason was simple: our execution of our idea for shuttle is built on top of containers. And no matter how much we try to distance containers from our users, their limitations always surface back. It was time for a complete rethink, so we took a radical view: let's start from the services people are writing, distilling what they need done quickly and easily. And let's make it our mission to optimize the hell out of the entire stack. We thought that if the execution of that idea is done right, it'd let us trim the dependency tree of services our users deploy and slim the runtime that every service ships with. > What we quickly realized is that while we trimmed down the process of getting started implementing your own backend and setting up its infrastructure, we completely failed to solve two core problems: long build and deploy times. After all, a major culprit of these long build and deploy times in the real world is the large number of heavy dependencies of even simple projects. There's not much you can do about this: most services have a pretty big runtime that includes heavy machinery like an asynchronous executor (e.g. [tokio](https://tokio.rs)), a web server (e.g. [hyper](https://github.com/hyperium/hyper)), database drivers (e.g. [sqlx](https://github.com/launchbadge/sqlx)) and more. And on every deploy you need to re-build them and hope artifact caches are hit in order to get an incremental build. And it's not just building either, the running time of tests is also impacted by this. The closure of the codebase you're engaging in those tests is very large indeed as it follows that of your dependencies. This stuff materializes itself everywhere. Just try taking this hello world snippet: ```rust use axum::{Router, routing::get}; async fn get_hello() -> &'static str { "You're slow, Heroku!" } #[tokio::main] async fn main() { let port = std::env::var("PORT").unwrap(); let router = Router::new() .route("/", get(get_hello)) .into_make_service(); hyper::Server::bind(&format!("127.0.0.1:{port}").parse().unwrap()) .serve(router) .await .unwrap(); } ``` and deploy it to Heroku: To try to address this, we wanted to **move all these heavy dependencies to a common runtime across services**. So your tokio, hyper, sqlx and co (in the case of Rust), now all belong to a long-lived containerized process running persistently in the cloud. Whereas all your service logic, database and endpoint code build into lightweight WASM modules that are dynamically loaded in-place by this global persistent process. That way "building" means compiling a very lightweight codebase with a small dependency footprint. And "deploying" means calling upon the control plane of that long-lived process to replace service components without rolling out new images, containers or VMs. This leaves us with a trimmed down user-facing API that still uses familiar objects like `PgClient`s and axum-style routes with guards: Except that now the virtualization platform in which your services are run is responsible for instantiating these objects and calling these functions. With this approach, the component of virtualization that you end up deploying on a daily basis is much smaller than traditional VMs and containers. In a way we can say this makes the virtualization layer more adapted to the specific needs of backend services running in the cloud. It's an optimized I/O surface between backend service components that change a lot (e.g. endpoint implementations) and their environing long-lived runtimes that don't (e.g. tokio/hyper/sqlx). This results in "images" that are effectively up to **100x smaller** because of the switch from container images to WASM binaries. And super fast to deploy too, from tens of minutes sometimes to **less than a second** all the time. All because when things are _really_ incremental, you don't have to build and test a large codebase with its large userspace dependencies on every push. You just need to build and test the code you're writing and the changes you've made. Our vision for this new way of doing backend development is shuttle-next: a next-generation backend framework with the fastest build, test and deployment times ever. We believe that scoping down virtualization to the level of service components will eventually become the norm for backend development. In the same way we all think it's often not best to setup and start a VM only to run a single process, we will eventually all think it's misguided to build and start a container only to run a single service. We are launching shuttle-next as part of our closed beta for shuttle later this month, with the public release coming soon after. In the meantime, check out [shuttle's GitHub repo](https://github.com/shuttle-hq/shuttle) and [Twitter](https://twitter.com/shuttle_dev) for updates. If you'd like to support us, please star the repo and/or join the [shuttle Discord community](https://discord.gg/shuttle)! [webassembly]: http://webassembly.org/ [wasi]: https://wasi.dev --- # Building a Discord bot in Rust Source: https://www.shuttle.dev/blog/2022/09/14/serenity-discord-bot Date: 14 September 2022 Author: ben Tags: rust, tutorial A tutorial on building and deploying an interactive bot in Rust with Serenity & shuttle In this post, we will look at a simple way to add custom functionality to a Discord server using a bot written in Rust. We will first register a bot with Discord, then go about how to create a Serenity application that will later run on shuttle. Finally, we will make the bot do something useful, writing some Rust code to get information from an external service. The full code can be found in [this repository](https://github.com/kaleidawave/discord-weather-bot). ### Registering our bot Before we start making our bot, we need to register it for Discord. We do that by going to [https://discord.com/developers/applications](https://discord.com/developers/applications) and creating a new application. ![](/images/blog/discord-bot-screenshots/application-registration.png) The application process is also used for adding functionality to Discord but we will be only using the bot offering. Fill in the basic details and you should get to the following screen: ![](/images/blog/discord-bot-screenshots/application_id.png) You want to copy the Application ID and have it handy, because we will use it to add our bot to a test server. Next, we want to create a bot. You can set its public username here: ![](/images/blog/discord-bot-screenshots/bot-name.png) You want to click the reset token and copy this value (we will use it in a later step). This value represents the username and password as a single value that Discord uses to authenticate that our server is the one controlling the bot. You want to keep this value secret. You also want to tick the `MESSAGE CONTENT INTENT` setting so it can read the commands input. To add the bot to the server we will test on, we can use the following URL (replace `*application_id*` in the URL with the ID you copied beforehand): ```tsx https://discord.com/oauth2/authorize?client_id=*application_id*&scope=bot&permissions=8 ``` Here, we create it with `permissions=8` so that it can do everything on the server. If you are adding to another server, select only the permissions it needs. We now have a bot on our server: ![](/images/blog/discord-bot-screenshots/bot-is-offline.png) Oh, they're offline 😢 ## Getting a bot online At this moment, our bot is not running because there is no code. We will have to write it and run it before we can start interacting with it. ### [Serenity](https://docs.rs/serenity/latest/serenity/index.html) Serenity is a library for writing Discord bots (and communicating with the Discord API). We can create a new Serenity project which is readily deployable on shuttle with: `shuttle init --serenity` If you don't have shuttle yet, you can install it with `cargo install cargo-shuttle`. Afterwards, run the following in an empty directory: ``` shuttle init --serenity ``` After running it you, should see the following generated in `src/lib.rs`: ```rust use anyhow::anyhow; use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::prelude::*; use shuttle_secrets::SecretStore; use tracing::{error, info}; struct Bot; #[async_trait] impl EventHandler for Bot { async fn message(&self, ctx: Context, msg: Message) { if msg.content == "!hello" { if let Err(e) = msg.channel_id.say(&ctx.http, "world!").await { error!("Error sending message: {:?}", e); } } } async fn ready(&self, _: Context, ready: Ready) { info!("{} is connected!", ready.user.name); } } #[shuttle_service::main] async fn serenity( #[shuttle_secrets::Secrets] secret_store: SecretStore, ) -> shuttle_service::ShuttleSerenity { // Get the discord token set in `Secrets.toml` let token = if let Some(token) = secret_store.get("DISCORD_TOKEN") { token } else { return Err(anyhow!("'DISCORD_TOKEN' was not found").into()); }; // Set gateway intents, which decides what events the bot will be notified about let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let client = Client::builder(&token, intents) .event_handler(Bot) .await .expect("Err creating client"); Ok(client) } ``` ### Building an interaction for our bot We want to call our bot when chatting in a text channel. Discord enables this with [slash commands](https://discord.com/blog/slash-commands-are-here). Slash commands can be server-specific (servers are named as `guilds` in Discords API documentation) or application specific (across all servers the bot is in). For testing, we will only enable it on a single guild/server. This is because the application-wide commands can take an hour to fully register whereas the guild/server specific ones are instant, so we can test the new commands immediately. You can copy the guild ID by right-clicking here on the server name and click `copy ID` (you will need developer mode enabled to do this): ![](/images/blog/discord-bot-screenshots/guild-id.png) Now that we have the information for setup, we can start writing our bot and its commands. We will first get rid of the `async fn message` hook as we won't be using it in this example. In the `ready` hook we will call `set_application_commands` with a `GuildId` to register a command with Discord. Here we register a `hello` command with a description and no parameters (Discord refers to these as options). ```rust #[async_trait] impl EventHandler for Bot { async fn ready(&self, ctx: Context, ready: Ready) { info!("{} is connected!", ready.user.name); let guild_id = GuildId(*your guild id*); let commands = GuildId::set_application_commands(&guild_id, &ctx.http, |commands| { commands.create_application_command(|command| { command.name("hello").description("Say hello") }) }).await.unwrap(); info!("{:#?}", commands); } } ``` > Serenity has a bit of a different way of registering commands using a callback. If you are working on a larger command application, [poise](https://docs.rs/poise/latest/poise/) (which builds on Serenity) might be better suited. With our command registered, we will now add a hook for when these commands are called using `interaction_create`. ```rust #[async_trait] impl EventHandler for Bot { async fn ready(&self, ctx: Context, ready: Ready) { // ... } async fn interaction_create(&self, ctx: Context, interaction: Interaction) { if let Interaction::ApplicationCommand(command) = interaction { let response_content = match command.data.name.as_str() { "hello" => "hello".to_owned(), command => unreachable!("Unknown command: {}", command), }; let create_interaction_response = command.create_interaction_response(&ctx.http, |response| { response .kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|message| message.content(response_content)) }); if let Err(why) = create_interaction_response.await { eprintln!("Cannot respond to slash command: {}", why); } } } } ``` ### Trying it out Now with the code written we can test it locally. Before we do that we have to authenticate the bot with Discord. We do this with the value we got from "Reset Token" on the bot screen in one of the previous steps. To register a secret with shuttle we create a `Secrets.toml` file with a key value pair. This pair is read by the `secret_store.get("DISCORD_TOKEN")` call in the `ready` hook: ``` # Secrets.toml DISCORD_TOKEN="*your discord token*" DISCORD_GUILD_ID="*the guild we are testing on*" ``` `shuttle run` We should see that our bot now displays as online: ![](/images/blog/discord-bot-screenshots/bot-is-online.png) When typing, we should see our command come up with its description: ![](/images/blog/discord-bot-screenshots/command-description.png) Our bot should respond with "hello" to our command: ![](/images/blog/discord-bot-screenshots/command-result.png) Wow! Let's make our bot do something a little more useful. ### Making the bot do something [There is plenty of free APIs](https://github.com/public-apis/public-apis) that can be used for getting information on a variety of topics. For this demo, we are going to build a bot that gives a forecast for a location. I used the [AccuWeather API](https://developer.accuweather.com/) for this demo. If you are following this tutorial 1:1 you can go and register an application to get an access key. If you are using a different API this is still the sort of process you would follow. To get a forecast using the API requires two requests: 1. Get a location ID for a named location 2. Get the forecast at the location ID The API requires making network requests and it returns a JSON response. We can make the requests with `cargo add reqwest -F json` and deserialize the results to structures using serde, with `cargo add serde`. We will then have a function that chains the two requests together and deserializes the forecast to a readable result. > You can skip some of the boilerplate by using [direct access on untyped values](https://docs.rs/serde_json/latest/serde_json/#operating-on-untyped-json-values). But we will opt for the better strongly typed structured approach. Here we type some of the structures returned by the API and add `#[derive(Deserialize)]` so they can be decoded from JSON. All the keys are in _`PascalCase`_ so we use the `#[serde(rename_all = "PascalCase")]` helper attribute to stay aligned with Rust standards. Some are completely different from the Rust field name so we use `#[serde(alias = ...)]` on the field to set its matching JSON representation. ```rust // In weather.rs use serde::Deserialize; #[derive(Deserialize, Debug)] #[serde(rename_all = "PascalCase")] pub struct Location { key: String, localized_name: String, country: Country, } impl Display for Location { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}, {}", self.localized_name, self.country.id) } } #[derive(Deserialize, Debug)] pub struct Country { #[serde(alias = "ID")] pub id: String, } #[derive(Deserialize, Debug)] #[serde(rename_all = "PascalCase")] pub struct Forecast { pub headline: Headline, } #[derive(Deserialize, Debug)] pub struct Headline { #[serde(alias = "Text")] pub overview: String, } ``` > The above skips _a lot of the fields returned by the API_, only opting for the ones we will use in this demo. If you wanted to type all the fields you could try the new [type from JSON feature in rust-analyzer](https://rust-analyzer.github.io/thisweek/2022/08/15/changelog-142.html#new-features) to avoid having to write as much. Our location request call also fails if the search we put in returns no places. We will create an intermediate type that represents this case and implements `std::error::Error`: ```rust // Again in weather.rs use std::fmt::Display; #[derive(Debug)] pub struct CouldNotFindLocation { place: String, } impl Display for CouldNotFindLocation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Could not find location '{}'", self.place) } } impl std::error::Error for CouldNotFindLocation {} ``` Now with all the types written, we create a new `async` function that, given a place and a client, will return the forecast along with the location: ```rust // Again in weather.rs pub async fn get_forecast( place: &str, api_key: &str, client: &Client, ) -> Result<(Location, Forecast), Box> { // Endpoints we will use const LOCATION_REQUEST: &str = "http://dataservice.accuweather.com/locations/v1/cities/search"; const DAY_REQUEST: &str = "http://dataservice.accuweather.com/forecasts/v1/daily/1day/"; // The URL to call combined with our API_KEY and the place (via the q search parameter) let url = format!("{}?apikey={}&q={}", LOCATION_REQUEST, api_key, place); // Make the request we will call let request = client.get(url).build().unwrap(); // Execute the request and await a JSON result that will be converted to a // vector of locations let resp = client .execute(request) .await? .json::>() .await?; // Get the first location. If empty respond with the above declared // `CouldNotFindLocation` error type let first_location = resp .into_iter() .next() .ok_or_else(|| CouldNotFindLocation { place: place.to_owned(), })?; // Now have the location combine the key/identifier with the URL let url = format!("{}{}?apikey={}", DAY_REQUEST, first_location.key, api_key); let request = client.get(url).build().unwrap(); let forecast = client .execute(request) .await? .json::() .await?; // Combine the location with the foreact Ok((first_location, forecast)) } ``` Now we have a function to get the weather, **given a `reqwest` client and a place**, we can wire that into the bots logic. ### Setting up the reqwest client Our `get_forecast` requires a `reqwest` Client and the weather API key. We will add some fields to our bot for holding this data and initialize this in the `shuttle_service::main` function. Using the secrets feature we can get our weather API key: ```rust // In lib.rs struct Bot { weather_api_key: String, client: reqwest::Client, discord_guild_id: GuildId, } #[shuttle_service::main] async fn serenity(#[shuttle_secrets::Secrets] secret_store: SecretStore) -> shuttle_service::ShuttleSerenity { // Get the discord token set in `Secrets.toml` let token = secret_store .get("DISCORD_TOKEN") .context("'DISCORD_TOKEN' was not found")?; let weather_api_key = secret_store .get("WEATHER_API_KEY") .context("'WEATHER_API_KEY' was not found")?; let discord_guild_id = secret_store .get("DISCORD_GUILD_ID") .context("'DISCORD_GUILD_ID' was not found")?; // Set gateway intents, which decides what events the bot will be notified about let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let client = Client::builder(&token, intents) .event_handler(Bot { weather_api_key, client: reqwest::Client::new(), discord_guild_id: GuildId(discord_guild_id.parse().unwrap()) }) .await .expect("Err creating client"); Ok(client) } ``` ### Registering a /weather command We will add our new command with a place option/parameter. Back in the `ready` hook, we can add an additional command alongside the existing `hello` command: ```rust let commands = GuildId::set_application_commands(&guild_id, &ctx.http, |commands| { commands .create_application_command(|command| { command.name("hello").description("Say hello") }) .create_application_command(|command| { command .name("weather") .description("Display the weather") .create_option(|option| { option .name("place") .description("City to lookup forecast") .kind(CommandOptionType::String) .required(true) }) }) }).await.unwrap(); ``` Discord allows us to set the expected type and whether it is required. Here, the place needs to be a string and is required. Now in the interaction handler, we can add a new branch to the match tree. We pull out the option/argument corresponding to `place` and extract its value. Because of the restrictions made when setting the option we can assume that it is well-formed (unless Discord sends a bad request) and thus the unwraps here. After we have the arguments of the command we call the `get_forecast` function and format the results into a string to return. ```rust "weather" => { let argument = command .data .options .iter() .find(|opt| opt.name == "place") .cloned(); let value = argument.unwrap().value.unwrap(); let place = value.as_str().unwrap(); let result = weather::get_forecast(place).await; match result { Ok((location, forecast)) => format!( "Forecast: {} in {}", forecast.headline.overview, location ), Err(err) => { format!("Err: {}", err) } } } ``` ### Running Now, we have these additional secrets we are using and we will add them to the `Secrets.toml` file: ```toml # In Secrets.toml # Existing secrets: DISCORD_TOKEN="***" DISCORD_GUILD_ID="***" # New secret WEATHER_API_KEY="***" ``` With the secrets added, we can run the server: `shuttle run` While typing, we should see our command come up with the options/parameters: ![](/images/blog/discord-bot-screenshots/weather-input.png) Entering "Paris" as the place we get a result with a forecast: ![](/images/blog/discord-bot-screenshots/weather-forecast.png) And entering a location that isn't registered returns an error, thanks to the error handling we added to the `get_forecast` function: ![](/images/blog/discord-bot-screenshots/weather-error.png) ### Deploying on shuttle With all of that setup, it is really easy to get your bot hosted and running without having to run your PC 24/7. To deploy your app, all you need to do is: ```bash shuttle deploy ``` And you are good to go. Easy-pease, right? You could now take this idea even further: - Use a different API to create a bot that can return [new spaceflights](https://spaceflightnewsapi.net/) - Maybe you could use one of shuttle's provided databases to remember certain information about a user - Expand on the weather forecast idea by adding more advanced options and follow-ups to command options - Use the [localization information](https://discord.com/developers/docs/interactions/application-commands#localization) to return information in other languages --- This blog post is powered by shuttle! If you have any questions, or want to provide feedback, join our [Discord server](https://discord.gg/shuttle)! ## [Shuttle](https://www.shuttle.dev/): The Rust-native, open source, cloud development platform. Deploying and managing your Rust web apps can be an expensive, anxious and time consuming process. If you want a batteries included and ops-free experience, [try out Shuttle](https://github.com/shuttle-hq/shuttle).
--- # Building an authentication system in Rust using session tokens Source: https://www.shuttle.dev/blog/2022/08/11/authentication-tutorial Date: 17 August 2022 Author: Ben Tags: rust, guide, axum, sql Building authentication into a website with Rust and SQL Most websites have some kind of user system. But implementing authentication can be a bit complex. It requires several things working together. Making sure the system is secure is daunting. How do we know others cannot easily log into accounts and make edits on other people's behalf? And building stateful systems is difficult. Today we will look at a minimal implementation in Rust. For this demo we won't be using a specific authentication library, instead writing from scratch using our own database and backend API. We will be walking through implementing the system including a frontend for interacting with it. We will be using Axum for routing and other handling logic. The [source code for this tutorial can be found here](https://github.com/kaleidawave/axum-shuttle-postgres-authentication-demo). We will then deploy the code on shuttle, which will handle running the server and giving us access to a Postgres server. To prevent this post from being an hour long, some things are skipped over (such as error handling) and so might not match up one-to-one with the tutorial. This post also assumes basic knowledge of HTML, web servers, databases and Rust. This isn't verified to be secure, use it at your own risk!! ## Let's get started First, we will install shuttle for creating the project (and later for deployment). If you don't already have it you can install it with `cargo install cargo-shuttle`. We will first go to a new directory for our project and create a new Axum app with `shuttle init --axum`. You should see the following in `src/lib.rs`: ```rust use axum::{routing::get, Router}; use sync_wrapper::SyncWrapper; async fn hello_world() -> &'static str { "Hello, world!" } #[shuttle_service::main] async fn axum() -> shuttle_service::ShuttleAxum { let router = Router::new().route("/hello", get(hello_world)); let sync_wrapper = SyncWrapper::new(router); Ok(sync_wrapper) } ``` ### Templates For generating HTML we will be using [Tera](https://docs.rs/tera/latest/tera/), so we can go ahead and add this with `cargo add tera`. We will store all our templates in a `template` directory in the project root. We want a general layout for our site, so we create a base layout. In our base layout, we can add specific tags that will apply to all pages such as a [Google font](https://fonts.google.com/). With this layout all the content will be injected in place of `{% block content %}{% endblock content %}`: ```html Title {% block content %}{% endblock content %} ``` And now we can create our first page that will be displayed under the `/` path ```html {% extends "base.html" %} {% block content %}

Hello world

{% endblock content %} ``` Now we have our template, we need to register it under a Tera instance. Tera has a nice [filesystem-based registration system](https://docs.rs/tera/1.16.0/tera/struct.Tera.html#method.new), but we will use the [`include_str!`](https://doc.rust-lang.org/std/macro.include_str.html) macro so that the content is in the binary. This way we don't have to deal with the complexities of a filesystem at runtime. We register both templates so that the `index` page knows about `base.html`. ```rust let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("base.html", include_str!("../templates/base.html")), ("index", include_str!("../templates/index.html")), ]) .unwrap(); ``` We add it via an [Extension](https://docs.rs/axum/latest/axum/struct.Extension.html) (wrapped in `Arc` so that extension cloning does not deep clone all the templates) ```rust #[shuttle_service::main] async fn axum() -> shuttle_service::ShuttleAxum { let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("base.html", include_str!("../templates/base.html")), ("index", include_str!("../templates/index.html")), ]) .unwrap(); let router = Router::new() .route("/hello", get(hello_world)) .layer(Extension(Arc::new(tera))); let sync_wrapper = SyncWrapper::new(router); Ok(sync_wrapper) } ``` ### Rendering views Now we have created our Tera instance we want it to be accessible to our get methods. To do this in Axum, we add the extension as a parameter to our function. In Axum, an [Extension](https://docs.rs/axum/latest/axum/struct.Extension.html) is a unit struct. Rather than dealing with `.0` to access fields, we use destructuring in the parameter (if you thought that syntax looks weird). ```rust async fn index( Extension(templates): Extension, ) -> impl IntoResponse { Html(templates.render("index", &Context::new()).unwrap()) } ``` ### Serving assets We can create a `public/styles.css` file ```css body { font-family: "Karla", sans-serif; font-size: 12pt; } ``` And easily create a new endpoint for it to be served from: ```rust async fn styles() -> impl IntoResponse { Response::builder() .status(http::StatusCode::OK) .header("Content-Type", "text/css") .body(include_str!("../public/styles.css").to_owned()) .unwrap() } ``` Here we again are using `include_str!` to not have to worry about the filesystem at runtime. [ServeDir](https://docs.rs/tower-http/latest/tower_http/services/struct.ServeDir.html) is an alternative if you have a filesystem at runtime. You can use this method for other static assets like JavaScript and favicons. ## Running We will add our two new routes to the router (and remove the default "hello world" one) to get: ```rust let router = Router::new() .route("/", get(index)) .route("/styles.css", get(styles)) .layer(Extension(Arc::new(tera))); ``` With our main service we can now test it locally with `shuttle run`. ![](/images/blog/authentication-demo-screenshot.png) Nice! ## Adding users We will start with a user's table in SQL. ([this is defined in schema.sql](https://github.com/kaleidawave/axum-shuttle-postgres-authentication-demo/blob/main/schema.sql)). ```sql CREATE TABLE users ( id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, username text NOT NULL UNIQUE, password text NOT NULL ); ``` The `id` is generated by the database using a sequence. The `id` is a primary key, which we will use to reference users. It is better to use a fixed value field for identification rather than using something like the `username` field because you may add the ability to change usernames, which can leave things pointing to the wrong places. ### Registering our database Before our app can use the database we have to add sqlx with some features: `cargo add sqlx -F postgres runtime-tokio-native-tls`. We will also add the `shuttle-shared-db` crate with `cargo add shuttle-shared-db -F postgres`. Now back in the code we add a parameter with `#[shuttle_shared_db::Postgres] pool: Database`. The `#[shuttle_shared_db::Postgres]` annotation tells shuttle to provision a Postgres database using the [infrastructure from code design](https://www.shuttle.dev/blog/2022/05/09/ifc)! ```rust type Database = sqlx::PgPool; #[shuttle_service::main] async fn axum( #[shuttle_shared_db::Postgres] pool: Database ) -> ShuttleAxum { // Build tera as before // Run the schema.sql migration with sqlx to create our users table pool.execute(include_str!("../schema.sql")) .await .map_err(shuttle_service::error::CustomError::new)?; let router = Router::new() .route("/", get(index)) .route("/styles.css", get(styles)) .layer(Extension(Arc::new(tera))) .layer(pool); // Wrap and return router as before } ``` ### Signup For getting users into our database, we will create a post handler. In our handler, we will parse data using multipart. [I wrote a simple parser for multipart that we will use here](https://github.com/kaleidawave/axum-shuttle-postgres-authentication-demo/blob/main/src/utils.rs#L45-L64). The below example contains some error handling that we will ignore for now. ```rust async fn post_signup( Extension(database): Extension, multipart: Multipart, ) -> impl IntoResponse { let data = parse_multipart(multipart) .await .map_err(|err| error_page(&err))?; if let (Some(username), Some(password), Some(confirm_password)) = ( data.get("username"), data.get("password"), data.get("confirm_password"), ) { if password != confirm_password { return Err(error_page(&SignupError::PasswordsDoNotMatch)); } let user_id = create_user(username, password, database); Ok(todo!()) } else { Err(error_page(&SignupError::MissingDetails)) } } ``` #### Creating users and storing passwords safety When storing passwords in a database, for security reasons we don't want them to be in the exact format as plain text. To transform them away from the plain text format we will use a [cryptographic hash function](https://en.wikipedia.org/wiki/Cryptographic_hash_function) from [pbkdf2](https://github.com/RustCrypto/password-hashes/tree/master/pbkdf2) (`cargo add pbkdf2`): ```rust fn create_user(username: &str, password: &str, database: &Database) -> Result { let salt = SaltString::generate(&mut OsRng); // Hash password to PHC string ($pbkdf2-sha256$...) let hashed_password = Pbkdf2.hash_password(password.as_bytes(), &salt).unwrap().to_string(); // ... } ``` With hashing, if someone gets the value in the password field they cannot find out the actual password value. The only thing this value allows is whether a plain text password matches this value. And with [salting]() different names are encoded differently. Here all these passwords were registered as _"password"_, but they have different values in the database because of salting. ```sql postgres=> select * from users; id | username | password ----+----------+------------------------------------------------------------------------------------------------ 1 | user1 | $pbkdf2-sha256$i=10000,l=32$uC5/1ngPBs176UkRjDbrJg$mPZhv4FfC6HAfdCVHW/djgOT9xHVAlbuHJ8Lqu7R0eU 2 | user2 | $pbkdf2-sha256$i=10000,l=32$4mHGcEhTCT7SD48EouZwhg$A/L3TuK/Osq6l41EumohoZsVCknb/wiaym57Og0Oigs 3 | user3 | $pbkdf2-sha256$i=10000,l=32$lHJfNN7oJTabvSHfukjVgA$2rlvCjQKjs94ZvANlo9se+1ChzFVu+B22im6f2J0W9w (3 rows) ``` With the following simple database query and our hashed password, we can insert users. ```rust fn create_user(username: &str, password: &str, database: &Database) -> Result { // ... const INSERT_QUERY: &str = "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id;"; let fetch_one = sqlx::query_as(INSERT_QUERY) .bind(username) .bind(hashed_password) .fetch_one(database) .await; // ... } ``` And we can handle the response and get the new user id with the following: ```rust fn create_user(username: &str, password: &str, database: &Database) -> Result { // ... match fetch_one { Ok((user_id,)) => Ok(user_id), Err(sqlx::Error::Database(database)) if database.constraint() == Some("users_username_key") => { return Err(SignupError::UsernameExists); } Err(err) => { return Err(SignupError::InternalError); } } } ``` Great now we have the signup handler written, let's create a way to invoke it in the UI. ### Using HTML forms To invoke the endpoint with multipart we will use an HTML form. ```html {% extends "base.html" %} {% block content %}
{% endblock content %} ``` Notice the action and method that correspond to the route we just added. Notice also the `enctype` being multipart, which matches what we are parsing in the handler. The above has a few attributes to do some client-side validation, but [in the full demo it is also handled on the server](https://github.com/kaleidawave/axum-shuttle-postgres-authentication-demo/blob/ba71a914055f312636581f5e82172b1078e7b9eb/src/authentication.rs#L124-L133). We create a handler for this markup in the same way as done for our index with: ```rust async fn get_signup( Extension(templates): Extension, ) -> impl IntoResponse { Html(templates.render("signup", &Context::new()).unwrap()) } ``` We can add `signup` to the Tera instance and then add both the get and post handlers to the router by adding it to the chain: ```rust .route("/signup", get(get_signup).post(post_signup)) ``` ### Sessions Once signed up, we want to save the logged-in state. We don't want the user to have to send their username and password for every request they make. ### Cookies and session tokens Cookies help store the state between browser requests. When a response is sent down with [Set-Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie), then any subsequent requests the browser/client makes will send cookie information. We can then pull this information off of headers on requests on the server. Again, these need to be safe. We don't want collisions/duplicates. We want it to be hard to guess. For these reasons, we will represent it as a 128-bit unsigned integer. This has 2^128 options, so a very low chance of a collision. We want to generate a "session token". We want the tokens to be cryptographically secure. Given a session id, we don't want users to be able to find the next one. A simple globally incremented u128 wouldn't be secure because if I know I have _session 10_ then I can send requests with _session 11_ for the user who logged in after. With a cryptographically secure generator, there isn't a distinguishing pattern between subsequently generated tokens. We will use the [ChaCha](https://github.com/rust-random/rand/tree/master/rand_chacha) algorithm/crate (we will add `cargo add rand_core rand_chacha`). [We can see that it does implement the crypto marker-trait confirming it is valid for cryptographic scenarios](https://docs.rs/rand_chacha/0.3.1/rand_chacha/struct.ChaCha8Rng.html#impl-CryptoRng). This is unlike [Pseudo-random number generators where you can predict the next _random_ number given a start point and the algorithm](https://www.youtube.com/watch?v=-h_rj2-HP2E). This could be a problem if we have our token we can get the session token of the person who logged in after us really easy and thus impersonate them. To initialize the random generator we use [SeedableRng::from_seed](https://docs.rs/rand_core/latest/rand_core/trait.SeedableRng.html#tymethod.from_seed). The seed in this case is an initial _state_ for the generator. Here we use [OsRng.next_u64()](https://docs.rs/rand_core/latest/rand_core/struct.OsRng.html) which _retrieves randomness from the operating system_ rather a seed. We will be doing something similar to the creation of the Tera instance. We must wrap it in an arc and a mutex because generating new identifiers requires mutable access. We now have the following main function: ```rust #[shuttle_service::main] async fn axum( #[shuttle_shared_db::Postgres] pool: Database ) -> ShuttleAxum { // Build tera and migrate database as before let random = ChaCha8Rng::seed_from_u64(OsRng.next_u64()) let router = Router::new() .route("/", get(index)) .route("/styles.css", get(styles)) .route("/signup", get(get_signup).post(post_signup)) .layer(Extension(Arc::new(tera))) .layer(pool) .layer(Extension(Arc::new(Mutex::new(random)))); // Wrap and return router as before } ``` #### Adding sessions to signup As well as creating a user on signup, we will create the session token for the newly signed-up user. We post it to the table with our `user_id` ```rust type Random = Arc>; pub(crate) async fn new_session( database: &Database, random: Random, user_id: i32 ) -> String { const QUERY: &str = "INSERT INTO sessions (session_token, user_id) VALUES ($1, $2);"; let mut u128_pool = [0u8; 16]; random.lock().unwrap().fill_bytes(&mut u128_pool); // endian doesn't matter here let session_token = u128::from_le_bytes(u128_pool); let _result = sqlx::query(QUERY) .bind(&session_token.to_le_bytes().to_vec()) .bind(user_id) .execute(database) .await .unwrap(); session_token } ``` In the full demo, we use the [new type pattern](https://www.shuttle.dev/blog/2022/07/28/patterns-with-rust-types#the-new-type-pattern) over a u128 to make this easier, but we will stick with a u128 type here. Now we have our token, we need to package it into a cookie value. We will do it in the simplest way possible, using `.to_string()`. We will send a response that does two things, sets this new value and returns/redirects us back to the index page. We will create a utility function for doing this: ```rust fn set_cookie(session_token: &str) -> impl IntoResponse { http::Response::builder() .status(http::StatusCode::SEE_OTHER) .header("Location", "/") .header("Set-Cookie", format!("session_token={}; Max-Age=999999", session_token)) .body(http_body::Empty::new()) .unwrap() } ``` Now we can complete our signup handler by adding random as a parameter and returning our set cookie response. ```rust async fn post_signup( Extension(database): Extension, Extension(random): Extension, multipart: Multipart, ) -> impl IntoResponse { let data = parse_multipart(multipart) .await .map_err(|err| error_page(&err))?; if let (Some(username), Some(password), Some(confirm_password)) = ( data.get("username"), data.get("password"), data.get("confirm_password"), ) { if password != confirm_password { return Err(error_page(&SignupError::PasswordsDoNotMatch)); } let user_id = create_user(username, password, &database); let session_token = new_session(database, random, user_id); Ok(set_cookie(&session_token)) } else { Err(error_page(&SignupError::MissingDetails)) } } let session_token = new_session(database, random, user_id); ``` ### Using the session token Great so now we have a token/identifier for a _session_. Now we can use this as a key to get information about users. We can pull the cookie value using the following spaghetti of iterators and options: ```rust let session_token = req .headers() .get_all("Cookie") .iter() .filter_map(|cookie| { cookie .to_str() .ok() .and_then(|cookie| cookie.parse::().ok()) }) .find_map(|cookie| { (cookie.name() == USER_COOKIE_NAME).then(move || cookie.value().to_owned()) }) .and_then(|cookie_value| cookie_value.parse::().ok()); ``` #### Auth middleware [In the last post, we went into detail about middleware. You can read more about it in more detail there](https://www.shuttle.dev/blog/2022/08/04/middleware-in-rust). In our middleware, we will get a little fancy and make the user pulling lazy. This is so that requests that don't need user data don't have to make a database trip. Rather than adding our user straight onto the request, we split things apart. We first create an `AuthState` which contains the session token, the database, and a placeholder for our user `(Option)` ```rust #[derive(Clone)] pub(crate) struct AuthState(Option<(u128, Option, Database)>); pub(crate) async fn auth( mut req: http::Request, next: axum::middleware::Next, database: Database, ) -> axum::response::Response { let session_token = /* cookie logic from above */; req.extensions_mut() .insert(AuthState(session_token.map(|v| (v, None, database)))); next.run(req).await } ``` Then we create a method on `AuthState` which makes the database request. Now we have the user's token we need to get their information. We can do that using SQL joins ```rust impl AuthState { pub async fn get_user(&mut self) -> Option<&User> { let (session_token, store, database) = self.0.as_mut()?; if store.is_none() { const QUERY: &str = "SELECT id, username FROM users JOIN sessions ON user_id = id WHERE session_token = $1;"; let user: Option<(i32, String)> = sqlx::query_as(QUERY) .bind(&session_token.to_le_bytes().to_vec()) .fetch_optional(&*database) .await .unwrap(); if let Some((_id, username)) = user { *store = Some(User { username }); } } store.as_ref() } } ``` Here we cache the user internally using an Option. With the caching in place if another middleware gets the user and then a different handler tries to get the user it results in one database request, not two! We can add the middleware to our chain using: ```rust #[shuttle_service::main] async fn axum( #[shuttle_shared_db::Postgres] pool: Database ) -> ShuttleAxum { // tera,random creation and db migration as before let middleware_database = database.clone(); let router = Router::new() .route("/", get(index)) .route("/styles.css", get(styles)) .route("/signup", get(get_signup).post(post_signup)) .layer(axum::middleware::from_fn(move |req, next| { auth(req, next, middleware_database.clone()) })) .layer(Extension(Arc::new(tera))) .layer(pool) .layer(Extension(Arc::new(Mutex::new(random)))); // Wrap and return router as before } ``` #### Getting middleware and displaying our user info Modifying our index Tera template, we can add an "if block" to show a status if the user is logged in. ```html {% extends "base.html" %} {% block content %}

Hello world

{% if username %}

Logged in: {{ username }}

{% endif %} {% endblock content %} ``` Using our middleware in requests is easy in Axum by including a reference to it in the parameters. We then add the username to the context for it to be rendered on the page. ```rust async fn index( Extension(current_user): Extension, Extension(templates): Extension, ) -> impl IntoResponse { let mut context = Context::new(); if let Some(user) = current_user.get_user().await { context.insert("username", &user.username); } Html(templates.render("index", &context).unwrap()) } ``` ### Logging in and logging out Great we can signup and that now puts us in a session. We may want to log out and drop the session. This is very simple to do by returning a response with the cookie `Max-Age` set to 0. ```rust pub(crate) async fn logout_response() -> impl axum::response::IntoResponse { Response::builder() .status(http::StatusCode::SEE_OTHER) .header("Location", "/") .header("Set-Cookie", "session_token=_; Max-Age=0") .body(Empty::new()) .unwrap() } ``` For logging in we have a very similar logic for signup with pulling multipart information of a post request. Unlike signup, we don't want to create a new user. We want to check the row with that username has a password that matches. If the credentials match then we create a new session: ```rust async fn post_login( Extension(database): Extension, multipart: Multipart, ) -> impl IntoResponse { let data = parse_multipart(multipart) .await .map_err(|err| error_page(&err))?; if let (Some(username), Some(password)) = (data.get("username"), data.get("password")) { const LOGIN_QUERY: &str = "SELECT id, password FROM users WHERE users.username = $1;"; let row: Option<(i32, String)> = sqlx::query_as(LOGIN_QUERY) .bind(username) .fetch_optional(database) .await .unwrap(); let (user_id, hashed_password) = if let Some(row) = row { row } else { return Err(LoginError::UserDoesNotExist); }; // Verify password against PHC string let parsed_hash = PasswordHash::new(&hashed_password).unwrap(); if let Err(_err) = Pbkdf2.verify_password(password.as_bytes(), &parsed_hash) { return Err(LoginError::WrongPassword); } let session_token = new_session(database, random, user_id); Ok(set_cookie(&session_token)) } else { Err(error_page(&LoginError::NoData)) } } ``` Then we refer back to the [signup section](#using-html-forms) and replicate the same HTML form and handler that renders the Tera template as seen before but for a login screen. At the end of that we can add two new routes with three handlers completing the demo: ```rust #[shuttle_service::main] async fn axum( #[shuttle_shared_db::Postgres] pool: Database ) -> ShuttleAxum { // tera, middleware, random creation and db migration as before let router = Router::new() // ... .route("/logout", post(logout_response)) .route("/login", get(get_login).post(post_login)) // ... // Wrap and return router as before } ``` ## Deployment This is great, we now have a site with signup and login functionality. But we have no users, our friends can't log in on our localhost. We want it live on the interwebs. Luckily we are using shuttle, so it is as simple as: To deploy your app, all you need to do is: ``` shuttle deploy ``` Because of our `#[shuttle_service::main]` annotation and out-the-box Axum support our deployment doesn't need any prior config, it is instantly live! Now you can go ahead with these concepts and add functionality for listing and deleting users. [The full demo implements these if you are looking for clues](https://github.com/kaleidawave/axum-shuttle-postgres-authentication-demo). ## Thoughts building the tutorial and other ideas on where to take it This demo includes the minimum required for authentication. Hopefully, the concepts and snippets are useful for building it into an existing site or for starting a site that needs authentication. If you were to continue, it would be as simple as more fields onto the user object or building relations with the id field on the user's table. I will leave it out with some of my thoughts and opinions while building the site as well as things you could try extending it with. For templating Tera is great. I like how I separate the markup into external files rather than bundling it into `src/lib.rs`. Its API is easy to use and is well documented. However, it is quite a simple system. I had a few errors where I would rename or remove templates and because the template picker for rendering uses a map it can panic at runtime if the template does not exist. It would be nice if the system allowed checking that templates exist at compile time. The data sending works on serde serialization, which is a little bit more computation overhead than I would like. It also does not support streaming. With streaming, we could send a chunk of HTML that doesn't depend on database values first, and then we can add more content when the database transaction has gone through. If it supported streaming we could avoid the all-or-nothing pages with white page pauses and start connections to services like Google Fonts earlier. Let me know what your favorite templating engine is for Rust and whether it supports those features! For working with the database, sqlx has typed macros. I didn't use them here but for more complex queries you might prefer the type-checking behavior. Maybe 16 bytes for storing session tokens is a bit overkill. You also might want to try sharding that table if you have a lot of sessions or using a key-value store (such as Redis) might be simpler. We also didn't implement cleaning up the sessions table, if you were storing sessions using Redis you could use the [EXPIRE command](https://redis.io/commands/expire/) to automatically remove old keys. This blog post is powered by shuttle! The serverless platform built for Rust. ## [Shuttle](https://www.shuttle.dev/): Stateful Serverless for Rust Deploying and managing your Rust web apps can be an expensive, anxious and time consuming process. If you want a batteries included and ops-free experience, [try out shuttle](https://docs.rs/shuttle-service/latest/shuttle_service/).
--- # Implementing Middleware in Rust Source: https://www.shuttle.dev/blog/2022/08/04/middleware-in-rust Date: 4 August 2022 Author: ben Tags: rust, tutorial, middleware This article explores how you can use and write your own middleware in Rust web servers, using Rocket and Axum as examples. In this post we will take a general look into what middleware in Rust is, the benefits of using middleware and then how to use middleware in a Rust server application. ## What is middleware? A web server generally provides responses to requests. Very often, the protocol of choice is HTTP. A handler (sometimes called a response callback) is a function which takes a request's data and returns a response. Most server frameworks have a system called a 'router' which routes requests based on various parameters - usually the URL path. In HTTP routing is typically a combination of the path and the request method (GET, POST, PUT etc.). The benefit of a router is that it allows splitting logic up by path, which makes building large systems with lots of endpoints easier to manage. Individual path handlers are great, but sometimes you want logic which applies to a group of paths or indeed all paths. This is where **middleware** comes in. Unlike a handler, middleware is called on **every request and path** that it's registered on. Like handlers, middleware are functions. Middleware is very much **implementor dependent**. We will have a look at some concrete examples, but different frameworks have opted for different tradeoffs in their middleware implementation. Some middleware implementations work on an immutable state and act as a transformer on request and responses. Other frameworks treat the inputs as mutable and can freely modify / mutate the request objects. Some frameworks implement Rust middleware that can fail or short circuit. ### Middleware as a stack Middleware tends to be well-ordered. That is, a request or response passes through middleware in a well-defined order, as each layer processes the request or response and passes it onto the next layer: ``` requests | v +----- layer_three -----+ | +---- layer_two ----+ | | | +-- layer_one --+ | | | | | | | | | | | handler | | | | | | | | | | | +-- layer_one --+ | | | +---- layer_two ----+ | +----- layer_three -----+ | v responses ``` ### Applications of middleware #### Authentication Many routes may want user information. The incoming request contain user information via cookies or http authentication. Rather than every path handler having to deal with extracting the information we can abstract this logic to a request middleware and pass it down to subsequent handlers. #### Logging Information about which paths users are going to and when can be very useful. With logging middleware we can log and store request information for later analysis. Similar to logging is [_server response timings_](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing). This is a field / http header, which is standardized for holding timing information about requests. Here our middleware can log the start time of an incoming _request_ and the end time on the _response_. Then the middleware can modify the outgoing response to include the timing. This header is often highlighted in developer tools, which can be useful while debugging. It can also be used in chunked / streamed responses where the header of a request might have already been sent by using [Trailer](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer)s. #### Compression and other response optimizations Middleware can also amend outgoing responses and compress the output via algorithms like gzip and brotli. This removes the responsibility out of handlers and provides a convenient default for all responses. And it doesn't have to just be compression of responses, another use case is image resizing. Identifying mobile viewports using information on the request, outgoing responses can instead return smaller images rather than huge 4k images, in the end reducing bandwidth. ### Structuring applications As mentioned above the benefits of the middleware system is that while it is possible to do this stuff individually in each handler, abstracting it moves the responsibility away from the handlers. This can make management simpler and fewer lines of code! ```rust fn index() { let index_page = "..."; return compress(index_page); } fn about() { let about_page = "..."; return compress(about_page); } fn search() { let search_page = "..."; return compress(search_page); } Application::build() .routes([index, about, search]) ``` vs ```rust fn index() { return "..."; } fn about() { return "..."; } fn search() { return "..."; } Application::build() .routes([index, about, search]) .add_middleware(CompressionMiddleware::new()) ``` ### Separating out code The benefit of middleware _just_ being functions is that they can be separated out to different modules or even crates. Many 3rd party services may choose to expose their service as a middleware rather than a system of complicated functions, and having to deal with users passing the correct state into them. ```rust .add_middleware(hot_new_server_logging_framework_start_up::Middleware::new()) ``` ## Comparing middleware implementations in libraries ### [Rocket](https://rocket.rs/) Rocket is a server framework. Rocket's middleware implementation is known as fairings (yes there are many rocket related puns in the crate). From Rocket's fairing documentation: > Rocket's fairings are a lot like middleware from other frameworks, but they bear a few key distinctions: > > Fairings cannot terminate or respond to an incoming request directly. > Fairings cannot inject arbitrary, non-request data into a request. > Fairings can prevent an application from launching. > Fairings can inspect and modify the application's configuration. To make a fairing in Rocket you have to implement the fairing trait: ```rust struct MyCounterFairing { get_requests: AtomicUsize, } #[rocket::async_trait] impl Fairing for MyCounterFairing { fn info(&self) -> Info { Info { name: "GET Counter", kind: Kind::Request } } async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) { if let Method::Get = request.method() { self.get.fetch_add(1, Ordering::Relaxed); } } } ``` Using the `.attach` method it's really simple to add a fairing to a application. ```rust #[launch] fn rocket() -> _ { rocket::build() .attach(MyCounterFairing { get_requests: AtomicUsize::new(0), }) .attach(other_fairing) } ``` Rocket middleware has several hooks. Each of them has a default implementation so can be left out (you don't have to explicitly write a method for each hook). #### Requests using `on_request` This fires when a request is received. This hook has a mutable reference to the request and so **can modify the request**. "It cannot abort or respond directly to the request; these issues are better handled via request guards or via response callbacks.". As an aside, Rocket has a different non-middleware implementation that can be better suited for handlers that might short circuit an error rather than running a handler afterwards. We won't go into it here but if your middleware is fallible request guards might be a better option #### Response using `on_response` Similar to `on_request` this has mutable access to the response object (it also has immutable access to the request). Using this hook you can **inject headers** or amend **partial responses (aka 404)**. #### General server hooks Rocket's fairings go beyond request and responses and can act as hooks into application startup and closing: - Ignite (`on_ignite`). Runs before starting the server. Can validate config values, set initial state or abort. - Liftoff (`on_liftoff`). After server has launched (started) "A liftoff callback can be a convenient hook for launching services related to the Rocket application being launched." - Shutdown (`on_shutdown`). This hook can be used to wind down services and save state before the application closes. Runs concurrently and no requests are returned before. All Rocket fairings have a [info field](https://api.rocket.rs/v0.5/rocket/fairing/trait.Fairing#tymethod.info). The kind property decides which hooks the fairing can fire. #### Ad hoc fairings Simpler middleware using functions can be added using ad-hoc fairings. If the fairing doesn't have state / data with it, you can bypass needing to create a structure and writing a trait implementation for it and instead write a function. Using `AdHoc` and any of the names of the above mentioned hooks we can instead creating a function using a function (+ a string info): ```rust .attach(AdHoc::on_liftoff("Liftoff Printer", |_| Box::pin(async move { println!("...annnddd we have liftoff!"); }))) ``` ### [Axum](https://docs.rs/axum/latest/axum/index.html) Similar to Rocket, Axum is a HTTP framework for web applications. [Axum middleware](https://docs.rs/axum/latest/axum/index.html#middleware) is based of [tower](https://github.com/tower-rs/tower) which is a separate crate which deals with lower level base for networking in Rust. Axum and tower middleware is referred to a 'layers'. There are several ways to write middleware in Axum. Similar to standard fairings you can create a type that implements the [Layer trait](https://docs.rs/tower/0.4.13/tower/trait.Layer.html). The layer trait decorates / acts apon the [Service trait](https://docs.rs/tower/0.4.13/tower/trait.Service.html). This demo was taken from the [Tower docs](https://docs.rs/tower/0.4.13/tower/trait.Layer.html#log) **and before you get scared off we will see a much simpler way to implement middleware shortly**. ```rust pub struct LogLayer { target: &'static str, } impl Layer for LogLayer { type Service = LogService; fn layer(&self, service: S) -> Self::Service { LogService { target: self.target, service } } } // This service implements the Log behavior pub struct LogService { target: &'static str, service: S, } impl Service for LogService where S: Service, Request: fmt::Debug, { type Response = S::Response; type Error = S::Error; type Future = S::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.service.poll_ready(cx) } fn call(&mut self, request: Request) -> Self::Future { // Insert log statement here or other functionality println!("request = {:?}, target = {:?}", request, self.target); self.service.call(request) } } ``` We can register our mew layer (middleware) on a to a Axum application using `.layer` (similar to `.attach` in Rocket). ```rust use axum::{routing::get, Router}; async fn handler() {} let app = Router::new() .route("/", get(handler)) .layer(LogLayer { target: "our site" }) // `.route_layer` will only run the middleware if a route is matched .route_layer(TimeOutLayer) ``` There is also [`ServiceBuilder`](https://docs.rs/tower/0.4.13/tower/struct.ServiceBuilder.html) which is the recommended way to chain layers. They are executed in the reverse order to which they are attached (`layer_one` runs first). ```rust Router::new() .route("/", get(handler)) .layer( ServiceBuilder::new() .layer(layer_three) .layer(layer_two) .layer(layer_one) ) ``` #### A simpler way Similar to Rocket's trait fairings and ad hoc fairings there are two ways to write middleware for Axum using [middleware::from_fn](https://docs.rs/axum/latest/axum/middleware/fn.from_fn.html). Using a demo from the [Axum docs](https://docs.rs/axum/latest/axum/middleware/index.html#writing-middleware). ```rust async fn auth(req: Request, next: Next) -> Result { let auth_header = req.headers() .get(http::header::AUTHORIZATION) .and_then(|header| header.to_str().ok()); match auth_header { Some(auth_header) if token_is_valid(auth_header) => { Ok(next.run(req).await) } _ => Err(StatusCode::UNAUTHORIZED), } } ``` ```rust let app = Router::new() .route("/", get(|| async { /* ... */ })) .route_layer(middleware::from_fn(auth)); ``` #### Existing ready to use layers: As Axum is built on `tower` there are some great readily importable middleware that can be added as layers. One of those is that [TraceLayer](https://docs.rs/tower-http/0.3.4/tower_http/trace/index.html) that logs requests coming in and responses going out: ``` Mar 05 20:50:28.523 DEBUG request{method=GET path="/foo"}: tower_http::trace::on_request: started processing request Mar 05 20:50:28.524 DEBUG request{method=GET path="/foo"}: tower_http::trace::on_response: finished processing request latency=1 ms status=200 ``` There are a [bunch of layers in the tower_http crate](https://docs.rs/tower-http/0.3.4/tower_http/trace/index.html?search=struct%3ALayer) that can be used instead of writing your own. ## Building authentication using our own middleware Let's play around with a realistic example and build a middleware layer for our own application that manages authentication. In our route handlers we might want to know detailed information about the user that made the request. Rather than having to deal with passing around request information we can encapsulate this logic in middleware. We'll be using Axum for this demo. The demo is not public at the moment, look out for a future post about authentication for when the full demo is made public! ### Cookies as user state Cookies can be used for maintaining user state. When a user cookie is set on the frontend it's sent with every request. We'll skip over how the cookie got there 😅 and leave it for a future tutorial. Either way we want to add middleware which _injects_ the following the struct into current request. ```rust #[derive(Clone)] struct AuthState(Option<(SessionId, Arc>)>, Database); ``` We have got a bit fancy here. Rather than making a database request on every request we instead save the database pool in a mutable store ([OnceCell](https://docs.rs/once_cell/latest/once_cell/sync/struct.OnceCell.html)) together with the session id. With all this information it means that getting user state can be lazy or not done at all. We will build an `auth` function which builds up this lazy `AuthState` struct with the required information by parsing the headers of a request. ```rust async fn auth( mut req: Request, next: Next, database: Database, ) -> axum::response::Response { // Assuming we only have one cookie let key_pair_opt = req .headers() .get("Cookie") .and_then(|value| value.to_str().ok()) .map(|value| value .split_once(';') .map(|(left, _)| left) .unwrap_or(value) ) .and_then(|kv| kv.split_once('=')); let auth_state = if let Some((key, value)) = key_pair_opt { if key != USER_COOKIE_NAME { None } else if let Ok(value) = value.parse::() { Some(value) } else { None } } else { None }; req.extensions_mut().insert(AuthState( auth_state .map(|v| ( v, Arc::new(OnceCell::new()), database )), )); next.run(req).await } ``` _this is a bit ad hoc parsing, proper parsing should account for multiple cookies etc and could be neater 😆_. At the end we do two **important things**. First we _extend_ the request with this lazy auth state: `req.extensions_mut().insert(...)`. Secondly we run the rest of the request stack: `next.run(req).await`. Unlike Rocket fairings, in Axum we could return our own Response from the middleware and not run the handler by skipping `next.run(req).await`. ### Attaching the middleware We first attach it to our Axum application using: ```rust let middleware_database = database_pool.clone(); Router::new() .layer(middleware::from_fn(move |req, next| { auth(req, next, middleware_database.clone()) })) ``` Because our middleware also needs application state (in this case the database pool), we create a intermediate function which pulls that in. ### Using the middleware We can now use the state injected by the middleware using the [Extension](https://docs.rs/axum/latest/axum/struct.Extension.html) parameter. ```rust async fn me( Extension(current_user): Extension, ) -> Result { if let Some(user) = current_user.get_user().await { Ok(show_user(user)) } else { Err(error_page("Not logged in")); } } ``` I was actually surprised when this worked, Axum's handler parameter system is quite magic. ## Conclusion I hope you enjoyed reading this guide to using middleware in Rust! In summary, middleware helps you abstract common logic for paths into reusable stateful and stateless objects. Middleware might not be applicative for every scenario but when you need it, it is super useful! Did this article help you? Feel free to [give us a star on GitHub!](https://www.github.com/shuttle-hq/shuttle) --- # Patterns with Rust types Source: https://www.shuttle.dev/blog/2022/07/28/patterns-with-rust-types Date: 28 July 2022 Author: ben Tags: rust, tutorial Patterns to use types for better safety and design This post introduces some patterns and tricks to better utilise Rust's type system for clean and safe code. This post is on the advanced side and in general there are no absolutes - these patterns usually need to be evaluated on a case-by-case basis to see if the cost / benefit trade-off is worth it. ## The new type pattern The new type pattern provides encapsulation as well as a guarantee that the right type of value is supplied at compile time. There are several uses and benefits for the new type pattern - let's take a look at some examples. ### Identifier Separation A common representation of an identifier is a number - in this case let's use the unsigned integer type `usize`. Let's say we have a function that receives an identifier for a **User** from a database by username. By using a unique username our API retrieves the identifier of the user: ```rust fn get_user_id_from_username(username: &str) -> usize ``` Let's say we have a similar mechanism for another entity, `Post`. If our application is performing operations involving posts **and** users, the logic can get in a mix: ```rust let user_id: usize = get_user_id_from_username(username); let post_id: usize = get_last_post(); fn delete_post(post_id: usize) { // ... } delete_post(user_id); ``` Here `get_user_id_from_username` and `get_last_post` both return `usize`s while `delete)_post` also takes a usize. In this code we can accidentally call `delete_post` with a `user_id`, there's nothing in the type system that would stop us from doing that. To differentiate between these two identifiers we can use the new type pattern: The new type pattern boils down to creating **a new tuple struct with a single item**, in this case `usize` ```rust struct UserId(pub usize); ``` Now we can change our library definition to return a `UserId` instead of `usize` ```rust fn get_user_id_from_username(username: String) -> UserId { let user_id: usize = ... UserId(user_id) } ``` Doing similar for the posts system with a `PostId`, when now compiling we get an error on when calling `get_post`. ```rust | 14 | get_post(x); | ^ expected struct `PostId`, found struct `UserId` ``` The new-type pattern enforces type-safety at compile time without any performance overhead at runtime. ### Re-adding functionality to our type After creating this new _wrapper_ type, we may need to implement some of the behaviour of the type it is encapsulating to appease our compiler. For example consider a set of 'banned' users: ```rust let banned_users: HashSet = HashSet::new(); ``` The above doesn't compile because our new type `UserId` doesn't implement equality and hashing behaviour whereas `usize` did. To add these traits back we can use the inbuilt derive macro, which generates implementations for our struct based on the single and only field. ```rust #[derive(PartialEq, Eq, Hash)] struct UserId(usize); ``` And we're good to go! ### Contract based programming in Rust / sub-typing The new type pattern can also be used to constrain types to only take 'valid' values. In the above example we used a wrapper type to enforce _flow_ of values, this method also enforces the _content_ of the value. In our application we only want usernames to contain **lowercase** alphabetic characters. Wrapping over String we can do this: ```rust struct Username(String); ``` The only way to create a Username is using the `TryFrom` trait. ```rust impl TryFrom for Username { type Error = String; fn try_from(value: String) -> Result { if value.chars().all(|c| matches!(c, 'a'..='z')) { Ok(Username(value)) } else { Err(value) } } } ``` This implementation returns a new `Username` if _all_ the characters are lowercase. Else the string is returned and can be reused in logic possibly displaying an error. As the string field is private a `Username` cannot be created with `Username(my_string)`. It also cannot be modified by outsiders and invalidate our contract. We can now use this structure as an argument to our API. ```rust fn create_user(db: &mut DB, username: Username) -> Result<(), CreationError> { // ... } ``` Since the username is validated to be lowercase ahead of time, the `create_user` function doesn't care about whether the username is valid inside in its own scope. This can lead to easier error handling. `CreationError` doesn't have to include a variant for the if the username has invalid characters. Although **the only safe way** to construct if through the validator `TryFrom` trait, the `Username` can be created through unsafe transmute (casting the bits of one value to the type of another without checks). This is normally fine though as with unsafe you are introducing undefined behaviour anyway. ```rust let string = String::new("muahahaha 👿"); let bad_username = unsafe { std::mem::transmute::(string) }; dbg!(bad_username); ``` ### Wrapping vs canonical type Our wrapped type is great from the outside, however we are relying on logic internal to the type to validate our contract. If we want to we can be really drill down on the structure of our username. Here we also enforce that the username has to be between four and ten letters. ```rust #[rustfmt::skip] enum Alphabet { A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z } enum Username { FourLetters([Alphabet; 4]), FiveLetters([Alphabet; 5]), SixLetters([Alphabet; 6]), SevenLetters([Alphabet; 7]), EightLetters([Alphabet; 8]), NineLetters([Alphabet; 9]), TenLetters([Alphabet; 10]), } ``` Even though there is no way to make an invalid username (except for unsafe) this is a little over the top 😂. In some edge cases it can be beneficial but in the example above this is clearly overkill. ### Working with foreign traits on foreign types Traits are great. They can be defined on structs and enums, but you may run into some issues when implementing a foreign trait on a foreign type. This is by design, and here's why: ![](/images/blog/rust-trait-rules-diagram.png) In our crate the compiler doesn't know when calling `MyTrait` methods on `MyStruct` whether to use the implementation defined in `crate 3` or `crate 4`! [Rust has a set of orphan rules](https://github.com/Ixrec/rust-orphan-rules) to prevent this situation from happening. In the situation where you'd like to implement a foreign trait on a foreign type - the 'new type' pattern can come to the rescue yet again: ```rust // lives in crate X trait ToTree { // ... } fn very_useful_function(something: impl ToTree) -> () { // .. } // Our crate struct Wrapper(pub crate_y::MyType); impl ToTree for Wrapper { // ... } // Yay very_useful_function(Wrapper(foreign_value)) ``` One of the gotchas with this is that you have to manually implement the trait. You can't use derive macros, e.g. `#[derive(PartialEq)]` and reach through to the declaration of the wrapped type and read its declaration. You also have to make sure that you can properly implement the trait on the item. `crate_y::MyType` might hide information needed for the implementation 😕. Ok - enough with the new type pattern. Let's leave it for a minute and look at some other tricks when working with types in Rust. ## Using either to unify different types Sometimes we have a case where we have a complicated data type ```rust enum PostUser { Single { username: UserId }, Group { usernames: HashSet } } ``` We'd like a method that returns an iterator, but we're stuck since we either return a single once iterable ([std::iter::Once](https://doc.rust-lang.org/std/iter/struct.Once.html)) or an iterator over a hashset. These iterators are different types and have different properties, so Rust doesn't like when we try to build a function returning both. A Rust function / method can only return one type: ```rust impl PostUser { fn iter(&self) -> impl Iterator + '_ { match self { PostUser::User { username } => std::iter::once(username), PostUser::Group { usernames } => usernames.into_iter(), } } } ``` The following will fail because the match arms have different types. ```rust | 17 | / match self { 18 | | PostUser::User { username } => std::iter::once(username), | | ------------------------- this is found to be of type `std::iter::Once<&UserId>` 19 | | PostUser::Group { usernames } => usernames.into_iter(), | | ^^^^^^^^^^^^^^^^^^^^^ expected struct `std::iter::Once`, found struct `std::collections::hash_set::Iter` 20 | | } | |_________- `match` arms have incompatible types | = note: expected struct `std::iter::Once<&UserId>` found struct `std::collections::hash_set::Iter<'_, UserId>` ``` The [`either` crate](https://github.com/bluss/either) offers a general purpose sum type [that implements many traits](https://docs.rs/either/1.7.0/either/enum.Either.html#trait-implementations). Using `either::Left` for the once iterator and `either::Right` we can build two iterators into what Rust considers as a single type. ```rust impl PostUser { fn iter(&self) -> impl Iterator + '_ { match self { PostUser::User { username } => either::Left(std::iter::once(username)), PostUser::Group { usernames } => either::Right(usernames.into_iter()), } } } ``` We could have instead boxed the results and returned `Box>`. The benefit of using either is that it uses static dispatch rather than dynamic dispatch. [enum_dispatch has good performance comparison for using static dispatch over dyn](https://docs.rs/enum_dispatch/latest/enum_dispatch/#the-benchmarks) so if you are on a critical hot path, and you know all the returned types it is faster to use enums to unify types rather than dynamic trait dispatching. ## Extension traits When creating a library we may add some functions for working with existing types (whether in the standard library or a different crate). Let's say we are writing a library on top of _serenity_ which has models for discord servers (discord refers to them as guilds). Let's write a helper function that gets the number of channels in a [Guild](https://docs.rs/serenity/0.11.4/serenity/model/guild/struct.Guild.html). ```rust async fn get_number_of_channels( guild: &serenity::model::Guild, http: impl AsRef ) -> serenity::Result ``` When calling the function we **have to** pass the guild as the first argument. ```rust let guild: serenity::model::Guild = // ... get_number_of_channels(&guild, client); ``` But from a design perspective we might prefer to use member notation instead: `guild.get_number_of_channels(client)`. We can't use add a direct implementation for a type defined outside our current crate. ```rust / impl serenity::model::Guild { | fn number_of_channels>(&self, http: T) -> serenity::Result { | todo!() | } | } |_^ impl for type defined outside of crate. ``` To define an associated method on a type outside the crate we must instead make an intermediate 'Extension' trait: ```rust trait GuildExt { fn number_of_channels>(&self, http: T) -> serenity::Result; } impl GuildExt for serenity::model::Guild { fn number_of_channels>(&self, http: T) -> serenity::Result { // ... } } ``` Using the intermediate trait the compiler can reason about when the method exists. To use the method syntax and show the compiler that the extension exists we must import the trait into our scope: ```rust use crate::GuildExt; let guild: serenity::model::Guild = // ... let number_of_channels = guild.get_number_of_channels(client); ``` This pattern is used in the futures crate with the [FutureExt trait](https://docs.rs/futures/0.3.21/futures/future/trait.FutureExt.html). Here using the trait `FutureExt` provides additional methods to the existing [`Future` trait in Rust's standard library](https://doc.rust-lang.org/std/future/trait.Future.html). Aside from syntax aesthetics, it becomes much easier to find object-specific functions when using an IDE. You can use the [easy_ext](https://docs.rs/easy-ext/1.0.0/easy_ext/) for doing this pattern on a single type without having to write the trait / trait definition is generated for you. ## Conclusion We saw how we can use various patterns like the new-type pattern and extension pattern to make our Rust code more ergonomic and take advantage of the type system and compiler to write better code. There is a great book out on [Rust design patterns](https://rust-unofficial.github.io/patterns/intro.html) which covers some of these and many more patterns in Rust. What are your favourite design patterns in Rust? Let us know and we'll cover them next time! --- # More than you've ever wanted to know about errors in Rust Source: https://www.shuttle.dev/blog/2022/06/30/error-handling Date: 30 June 2022 Author: ben Tags: rust, tutorial A (mostly) complete guide to error handling in Rust To quote the Rust Book, 'errors are a fact of life in software'. This post goes over how to handle them. Before talking about recoverable errors and the `Result` type, let's first touch on unrecoverable errors - a.k.a panics. ## An Introduction to Unrecoverable Errors [Panics](https://doc.rust-lang.org/std/macro.panic.html) are exceptions a program can throw. It stops all execution in the current thread. When a panic is thrown it returns a short description of what went wrong as well as information about the position of the the panic. ```rust fn main() { panic!("error!"); println!("Never reached :("); } ``` Running the above causes: ``` thread 'main' panicked at 'error!', examples\panics.rs:2:5 ``` They are similar to `throw` in JavaScript and other languages, in that they don't require an annotation on the function to run and they can pass through function boundaries. However in Rust, panics cannot be recovered from, there is no way to incept a panic in the current thread. ```rust fn send_message(s: String) { if s.is_empty() { panic!("Cannot send empty message"); } else { // ... } } ``` The `send_message` function is fallible (can go wrong). If this is called with an empty message then the program stops running. There is no way for the callee to track that an error has occurred. For recoverable errors, Rust has a type for error handling in the standard library called a **`Result`**. It is a generic type, which means the result and error variant can basically be whatever you want. ```rust pub enum Result { Ok(T), Err(E), } ``` ## Basic Error Creation and Handling At the moment our `send_message` function doesn't return anything. This means no information can be received by the callee. We can change the definition to instead return a `Result` and rather than panicking we can early return a `Result::Err`. ```rust fn send_message(s: String) -> Result<(), &'static str> { if s.is_empty() { // Note the standard prelude includes `Err` so the `Result::Err` and `Err` are equivalent return Result::Err("message is empty") } else { // ... } Ok(()) } ``` Now our function actually returns information about what went wrong we can handle it when we call it: ```rust if let Err(send_error) = send_message(message) { show_user_error(send_error); } ``` ### Dealing With Unused Results In the above example we inspect the value of the item and branch on it. However, if we didn't inspect and handle the returned Result then the Rust compiler gives us a helpful warning about it so that you don't forget to explicitly deal with errors in your program. ``` | send_message(); | ^^^^^^^^^^^^^^^ = note: `#[warn(unused_must_use)]` on by default = note: this `Result` may be an `Err` variant, which should be handled ``` The `Result` type can be found in most libraries. One of my favorite examples is the return type of the [FromStr::from_str](https://doc.rust-lang.org/std/str/trait.FromStr.html#tymethod.from_str) trait method. With [str::parse](https://doc.rust-lang.org/std/primitive.str.html#method.parse) (which uses the `FromStr` trait) we can do the following: ```rust fn main() { let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); match input.trim_end().parse::() { Ok(number) => { dbg!(number); } Err(err) => { dbg!(err); } }; } ``` (We'll ignore the `unwrap` for now 😉) ```js $ cargo r --example input -q 10 [examples\input.rs:7] number = 10.0 $ cargo r --example input -q 100 [examples\input.rs:7] number = 100.0 $ cargo r --example input -q bad [examples\input.rs:10] err = ParseFloatError { kind: Invalid, } ``` Here we can see when we type in a number we get a `Ok` variant with the number else we get a [ParseFloatError](https://doc.rust-lang.org/std/num/struct.ParseFloatError.html) ## Files, Networks and Databases **All errors occur when you interact with the outside world or things outside the Rust runtime**. One of the places where a lot of errors can occur is interacting with the file system. The `File::open` function attempts to open a file. This can fail for a variety of reasons. The filename is invalid, the file doesn't exist or you simply don't have permission to read the file. Notice the errors are well-defined and known before-hand. You can even access the error variants with the [`kind`](https://doc.rust-lang.org/std/io/struct.Error.html#method.kind) function and in order to implement your program logic or return an instructive error message to the user. ### Aliasing Results and Errors When you're working on a project you'll often find yourself repeating yourself when it comes to return types in function signatures: ```rust fn foo() -> Result { ... } ``` To give a concrete example, all functions which operate on the file system have the same errors (file not exists, invalid permissions). [io::Result](https://doc.rust-lang.org/std/io/type.Result.html) is a alias over a result but means that every function does not have to specify the error type: ```rust pub type Result = Result; ``` If you have an API which has a common error type, you may want to consider this pattern. ### The Question Mark Operator One of the best things about Results is the question mark operator, The question mark operator can short circuit Result error values. Let's look at a simple function which uploads text from a file. This can error in a bunch of different ways: ```rust fn upload_file() -> Result<(), &'static str> { let text = match std::fs::read_to_string("file.txt").map_err(|_| "read file error") { Ok(value) => value, Err(err) => { return err; } }; if let Err(err) = upload_text(text) { return err } Ok(()) } ``` Hang on, we're writing Rust not Go! If a `?` is postfixed on to a Result (or anything that implements [`try`](https://doc.rust-lang.org/std/ops/trait.Try.html) so also `Option`) we can obtain a functionally equivalent outcome with a much more readable and concise syntax. ```rust fn upload_file() -> Result<(), &'static str> { let text = std::fs::read_to_string("file.txt").map_err(|_| "read file error")?; upload_text(text)?; Ok(()) } ``` As long as the calling function also returns a `Result` with the same `Error` type, `?` saves a ton of explicit code being written. Moreover, the question-mark implicitly runs [Into::into](https://doc.rust-lang.org/std/convert/trait.Into.html#tymethod.into) (which is automatically implemented for [From](https://doc.rust-lang.org/std/convert/trait.From.html) implementors) on the error value. So we don't have to worry about converting the error before we use the operator: ```rust // This derive an into implementation for `std::io::Error -> MyError` #[derive(derive_enum_from_into::EnumFrom)] enum MyError { IoError(std::io::Error) // ... } fn do_stuff() -> Result<(), MyError> { let file = File::open("data.csv")?; // ... } ``` We will look at more patterns for combining error types later! ## Introducing the Error Trait The [Error](https://doc.rust-lang.org/std/error/trait.Error.html#) trait is defined in the standard library. It basically represents the expectations of error values - values of type `E` in `Result`. [The Error trait is implemented for many errors](https://doc.rust-lang.org/std/error/trait.Error.html#implementors) and provides a unified API for information on errors. The Error trait is a bit needy and requires that the error implements both [Debug](https://doc.rust-lang.org/std/fmt/trait.Debug.html) and [Display](https://doc.rust-lang.org/std/fmt/trait.Display.html). While it can be cumbersome to implement we will see some helper libraries for doing so later on. In the standard library [VarError](https://doc.rust-lang.org/std/env/enum.VarError.html) (for reading environment variables) and [ParseIntError](https://doc.rust-lang.org/std/num/struct.ParseIntError.html) (for parsing a string slice as a integer) are different errors. When we interact them we need to differentiate between the types because they have different properties and different stack sizes. To build a combination of them we could build a sum type using an enum. Alternatively we can use dynamically dispatched traits which handle varying stack sized items and other type information. Using the above mentioned try syntax (`?`) we can convert the above errors to be dynamically dispatched. This makes it easy to handle different errors without building enums to combine errors. ```rust fn main() -> Result<(), Box> { let key = std::env::var("NUMBER_IN_ENV")?; let number = key.parse::()?; println!("\"NUMBER_IN_ENV\" is {}", number); Ok(()) } ``` While this is an easy way to handle errors, it isn't easy to differentiate between the types and can make handling errors in libraries hard. More information on this later. ### The Error trait vs Results and enums One thing when using an enum is we can use `match` to branch on the enum error variants. On the other hand, with the `dyn` trait unless you go down the down casting path it is very hard to get specific information about the error: ```rust match my_enum_error { FsError(err) => { report_fs_error(err) }, DbError(DbError { err, database }) => { report_db_error(database, err) }, } ``` For reusable libraries it is better to use enums to combine errors so that users of your library can handle the specifics themselves. But for CLIs and other applications using the trait can be a lot simpler. ## Methods on Result Result and Option contains many useful functions. Here are some functions I commonly use: ### Result::map() [Result::map](https://doc.rust-lang.org/std/result/enum.Result.html#method.map) maps or converts the `Ok` value if it exists. This can be more concise than using the `?` operator. ```rust fn string_to_plus_one(s: &str) -> Result { s.parse::().map(|num| num + 1) } ``` ### Result::ok() [Result::ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok) is useful for converting Results to Options ```rust assert_eq!(Ok(2).ok(), Some(2)); assert_eq!(Err("err!").ok(), None); ``` ### Option::ok_or_else() [Option::ok_or_else](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or_else) is useful for going the other way in converting from Options to Results ```rust fn get_first(vec: &Vec) -> Result<&i32, NotInVec> { vec.first().ok_or_else(|| NotInVec) } ``` ### Error handling for iteration Using results in iterator chains can be a little confusing. Luckily `Result` implements [collect](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect). We can use this to short circuit an iterator if an error occurs. In the following, if all the `parse`s succeed then we get collected vec of numbers result. If one fails then it instead returns a Result with the failing Err. ```rust fn main() { let a = ["1", "2", "not a number"] .into_iter() .map(|a| a.parse::()) .collect::, _>>(); dbg!(a); } ``` ``` [examples\iteration.rs:6] a = Err( ParseFloatError { kind: Invalid, }, ) ``` Removing the `"not a number"` entry ``` [examples\iteration.rs:3] a = Ok( [ 1.0, 2.0, ], ) ``` Because Rust iterators are _piecewise_ and lazy the iterator can short circuit without evaluating parse on any of the later items. ## More Panic ### Special panics `todo!()`, `unimplemented!()`, `unreachable!()` are all wrappers for `panic! ()` which but are specialized to their situation. Panics have a special [`!`](https://doc.rust-lang.org/reference/types/never.html) type, called the 'never type', which represents the result of computations that never complete (also means it can be passed anywhere): ```rust fn func_i_havent_written_yet() -> u32 { todo!() } ``` Sometimes there is Rust code which the compiler cannot properly infer is valid. For this type of situation, the `unreachable!` panic can be used: ```rust fn get_from_vec_else_zero(a: Vec) -> i32 { if let Some(value) = a.get(2) { if let Some(prev_value) = a.get(1) { prev_value } else { unreachable!() } } else { 0 } } ``` ### Unwrapping `unwrap` is a method on `Result` and `Option`. They return the `Ok` or `Some` variant or else panic... ```rust // result.unwrap() let value = if let Ok(value) = result { value } else { panic!("Unwrapped!") }; ``` The uses-cases for this are developer error and situations the compiler can't quite figure out. If you are just trying something and don't want to set up a full error handling system then they can be used to ignore compiler warnings. Even if the situation calls for `unwrap` you are better off using `expect` which has an accompanying message - you'll be thanking your past self when the `expect` error message helps you find the root cause of an issue 2 weeks down the line. ### Panics in the standard library It is important to note that some of the APIs in the standard library _can_ panic. You should look out for these annotations in the docs. One of them is [Vec::remove](https://doc.rust-lang.org/std/vec/struct.Vec.html#panics-6). If you use this you should ensure that the argument is in its indexable range. ```rust fn remove_at_idx(a: usize, vec: &mut Vec) -> Option { if a < idx.len() { Some(vec.remove(a)) } else { None } } ``` ## Handling Multiple Errors and Helper Crates Handling errors from multiple libraries and APIs can become challenging as you have to deal with a bunch of different types. They are different sizes and contain different information. To unify the types we have to build a sum type using an enum, in order to ensure they have the same size at compile time. ```rust enum Errors { FileSystemError(..), StringParseError(..), NetworkError(..), } ``` Some crates for making creating these unifying enums easier: ### thiserror [`thiserror`](https://github.com/dtolnay/thiserror) provides a derive implementation which adds the Error trait for us. As previously mentioned, to implement Error we have to implement display and thiserrors' `#[error]` attributes provide templating for the displayed errors. ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum DataStoreError { #[error("data store disconnected")] Disconnect(#[from] io::Error), #[error("the data for key `{0}` is not available")] Redaction(String), #[error("invalid header (expected {expected:?}, found {found:?})")] InvalidHeader { expected: String, found: String, }, #[error("unknown data store error")] Unknown, } ``` ### anyhow [`anyhow`](https://github.com/dtolnay/anyhow) provides an ergonomic and idiomatic alternative to explicitly handling errors. It is similar to the previously mentioned error trait but has additional features such as adding context to thrown errors. This is really, really, useful when you want to convey errors to an application's users in a context-aware fashion: ```rust use anyhow::{bail, Result, Context}; fn main() -> Result<()> { println!("Hello World!"); func1().context("while calling func1")?; Ok(()) } fn func1() -> Result<()> { func2().context("while calling func2") } fn func2() -> Result<()> { bail!("Hmm something went wrong ") } ``` ``` Error: while calling func1 Caused by: 0: while calling func2 1: Hmm something went wrong ``` Similar to the `Error` trait, `anyhow` suffers from the fact you can't match on `anyhow`'s result error variant. This is why it is suggested in `anyhow`'s docs to use `anyhow` for applications and `thiserror` for libraries. ### eyre Finally, [`eyre`](https://github.com/eyre-rs/eyre) is a fork of [`anyhow`](https://github.com/dtolnay/anyhow) and adds more backtrace information. It's highly customisable and using [color-eyre](https://lib.rs/crates/color-eyre) we get colors in our panic messages - a little color always brightens up the dev experience. ``` The application panicked (crashed). Message: test Location: examples\color_eyre.rs:6 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⋮ 13 frames hidden ⋮ 14: core::ops::function::FnOnce::call_once,eyre::Report>, 1, 18446744073709551615, Err> (*)(),tuple$<> > at /rustc/7737e0b5c4103216d6fd8cf941b7ab9bdbaace7c\library\core\src\ops\function.rs:227 ⋮ 17 frames hidden ⋮ ``` ## Finishing Up Thank you for reading this article! Error handling can be tough, but with this guide hopefully you'll have a better idea of how to ensure you can reliably track errors and make debugging your Rust apps that much easier. --- # Generative metatag images in Rust Source: https://www.shuttle.dev/blog/2022/06/23/generative-metatag-images Date: 23 June 2022 Author: ben Tags: rust, tutorial Creating images in Rust using svgs This blog post is powered by shuttle! The serverless platform built for Rust. ### What open graph tags are Links are bare and unreadable. They can contain symbols to parse and cannot contain spaces. `https://www.shuttle.dev/blog/2022/06/16/a-short-introduction-to-async-rust` The above url isn't the most user friendly way of understanding what the post contains. With referrer parameters and such it only gets more unreadable. Meta tags are special HTML elements that you can add to HTML responses which show nicer previews: ![](https://i.imgur.com/RzgMfUs.png) You can get this additional preview by setting the following HTML elements inside the `` tag. ```html ``` These tags are easily scrapable[^scrapable] by bots which allow them to be added to places where links can be shared, such as messages and tweets. Unfortuantly meta tags don't really have a specification which is why it is best to include both the [open graph protocol](https://ogp.me/) and the [twitter card](https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started) specific tags. When links are shared with these tags in the response, the platform can add the adornments to the message. These previews make it easier to see what the content is before following the link. ### Creating open graph tags and images The specific tag of interest here is: ```html ``` Here the `content` is a url to an image. In the case of a blog post, we create specific graphics for it, upload it as an asset then set the URL to the path of the uploaded asset. This is fine for static content. However for _dynamic_ pages which may be user generated content, manual image creation isn't really possible. You can also use this method if you don't have individual custom meta images for each of your static posts. A while back GitHub added custom images for links on pull request, which includes information about the pull request. They even wrote a [blog post about how they did it](https://github.blog/2021-06-22-framework-building-open-graph-images/). I really like the result, however wondered if there was a alternative to the way they implemented it. GitHub uses a headless browser[^headless-browser] to do this which is less portable and includes spinning up a execessive process to generate a simple image. In this post we'll attempt to create similar graphics using lowerlevel libraries and Rust. ### Image generation in Rust To easily create graphics we will be using SVG. It's the most used format for vector graphics and supports embedding text, images and shapes. Since it's a vector graphic, shapes and text remain crisp no matter the size of the output image. It's readable and easily modifiable. The problem is that open graph images don't support displaying SVGs in previews due to the fact they are more complex of a format to render. So we have to make a step to turn our SVGs into another image format. ### The scalable vector graphic format We will start exploring the format with a simple SVG with three shapes in different colors and a rectangle used to give the graphic a white background. ```svg ``` SVGs renders line by line, so the white box will be at the behind, rendering the shapes in front. ![](/images/blog/metatag-shapes.png) #### Turning SVGS into WEBP images with Rust We can draw images in Rust using [resvg](https://docs.rs/resvg/latest/resvg) which handles rendering SVGs. It expects a parsed svg tree from [usvg](https://docs.rs/usvg/latest/usvg) so we'll also be needing that. Internally it uses [tiny_skia](https://docs.rs/tiny-skia/latest/tiny_skia/) which is a "tiny Skia subset ported to Rust". Resvg and tiny skia have all the building blocks we need to do basic image generation. We'll also use Pixmap which holds the pixels that we will be generating and then encode it in webp format[^png-output] to minimize output file size. `cargo add resvg tiny-skia usvg webp` ```rust use resvg::render; use std::{error::Error, fs, time::Instant}; use tiny_skia::{Pixmap, Transform}; use usvg::{Options, Tree}; use std::fs; const WIDTH: u32 = 1200; const HEIGHT: u32 = 630; fn main() -> Result<(), Box> { // Read in the svg template we have let svg = include_str!("shapes.svg"); // Create a new pixmap buffer to render to let mut pixmap = Pixmap::new(WIDTH, HEIGHT) .ok_or("Pixmap allocation error")?; // Use default settings let mut options = Options::default(); // Build our string into a svg tree let tree = Tree::from_str(svg, &options.to_ref())?; // Render our tree to the pixmap buffer, using default fit and transformation settings render( &tree, usvg::FitTo::Original, Transform::default(), pixmap.as_mut(), ); // Encode our pixmap buffer into a webp image let encoded_buffer = webp::Encoder::new(pixmap.data(), webp::PixelLayout::Rgba, WIDTH, HEIGHT).encode_lossless(); let result = encoded_buffer.deref(); // Write the result fs::write("image.webp", result)?; Ok(()) } ``` The above code generate a `image.webp` with the colorful shape image shown above. ## Going further Lets add some text to the graphic. We could use the default Times New Roman font - but let's get a little more fancy. [Google Fonts](https://fonts.google.com/) is a great resource for free font files. You can download any of the families on there and extract the specific `.ttf` font you want in and include it in the binary using `include_bytes!()`. In this demo I am using [Inter](https://fonts.google.com/specimen/Inter). ```rust // ... let mut options = Options::default(); options .fontdb .load_font_data(include_bytes!("Inter.ttf").to_vec()); // ... ``` ### Templating As our page will be dynamic, we'd like to insert strings defined in our Rust code onto the SVG. To do this we'll use the templating engine [liquid](https://github.com/cobalt-org/liquid-rust). (`cargo add liquid`) ```svg {{ text }} ``` We can add a new `text` node that is positioned in the center of the graphic. Using `font-family="Inter"` we can specify the font to be Inter. Liquid uses double braces `{{ ... }}` for interpolation. In our Rust code we'll change the string we use to build the tree to be the output of our of liquid template. The `liquid::object!` macro sets the data we we'll be rendering. ```rust let template = liquid::ParserBuilder::with_stdlib() .build() .unwrap() .parse(include_str!("template.svg")) .unwrap(); let globals = liquid::object!({ "text": "test" }); let svg = template.render(&globals).unwrap(); // Build our string into a svg tree let tree = Tree::from_str(&svg, &options.to_ref())?; ``` Which will render the following: ![](/images/blog/metatag-test-text.png) ### Adding images So far we have seen shapes and text. We're gonna step it up a bit by adding an image to the SVG. There are many ways to add a images to an SVG but we will use `` ```svg {{ text }} ``` The pattern includes a `` with a href that points to the one and only ferris. However `resvg` looks up images in the filesystem by default so we have to rewrire the handler which turns paths into binary image representation. We can do that using `reqwest` and its blocking client (add `reqwest` using `cargo add reqwest -F blocking`). We change the options to a custom function which gets the response, figures out the encoding and pulls out the images bytes: ```rust let mut options = Options { image_href_resolver: ImageHrefResolver { resolve_string: Box::new(move |path: &str, _| { let response = reqwest::blocking::get(path).ok()?; let content_type = response .headers() .get("content-type") .and_then(|hv| hv.to_str().ok())? .to_owned(); let image_buffer = response.bytes().ok()?.into_iter().collect::>(); match content_type.as_str() { "image/png" => Some(ImageKind::PNG(Arc::new(image_buffer))), // ... excluding other content types _ => None, } }), ..Default::default() }, ..Default::default() }; ``` And now we have: ![](/images/blog/metatag-ferris.png) ## Benchmarking Compared to the headless browser technique this process is faster. While doing a lot of the same things that the headless browser process was doing, we have picked out the only part we wanted, the svg renderer. With some rough benchmarks the Rust loading, rendering and encoded was two times faster than the nodejs puppeteer equivalent (100ms vs 200ms). This time accounts for the startup time in the nodejs version. If you aren't retaining the browser window then the results are even more noticeable. Generating one of images (_cold start_) the Rust version is 7x faster. Aside from rendering performance the Rust version is self contained, only the compiled binary is needed to generate the image. No having to worry about whether chromium is in the environment. This is huge benefit if you are doing image generation in a serverless environment. Also the headless browser version was harder to work with, importing fonts and setting the output size was considerably more complicated. ## Conclusion Hopefully this was a interesting post and taught some things about generating images in Rust. This technique can be used for other types of image generation not just for meta tag results. To complete the result all you need to do is hook up a service for the url in the meta tag. Rather than saving the image to the file system you would then return send the bytes back over the wire. For images that aren't updated you should be caching the images that are generated as to not regenerate them on every request. [Full code for the demo is here](https://github.com/kaleidawave/image-generation-rust) And if you are looking for a service to host your new procedurally generated meta tag images, why not try shuttle: ## [Shuttle](https://www.shuttle.dev/): Stateful Serverless for Rust Deploying and managing your Rust web apps can be an expensive, anxious and time consuming process. If you want a batteries included and ops-free experience, [try out Shuttle](https://docs.rs/shuttle-service/latest/shuttle_service/).
[^png-output]: If you just want png output then: `let encoded_buffer = pixels.encode_png().unwrap();` [^scrapable]: HTML elements in a response can easily parsed without having to run JavaScript [^headless-browser]: A browser which is controlled via code. [puppeteer](https://developer.chrome.com/docs/puppeteer/) and [selenium](https://www.selenium.dev/) are good examples. --- # Getting started with Async Rust Source: https://www.shuttle.dev/blog/2022/06/16/a-short-introduction-to-async-rust Date: 16 June 2022 Author: ben Tags: rust, tutorial, async Discover asynchronous programming in Rust with this comprehensive guide. Learn to use async code effectively and understand key concepts for efficient concurrent task handling. In this article, we'll take a closer look at async programming in Rust. Until now, my experience with Rust async was mainly copying code from Stack Overflow. This article aims to help you understand what async code is and how to use it effectively. ## What is asynchronous code? To understand what asynchronous code is - let's first talk about synchronous code. In synchronous code, statements run in a sequential order: ```rust println!("Hello World"); let cargo_toml_content = std::fs::read_to_string("Cargo.toml").unwrap(); println!("'Cargo.toml':\n{}", cargo_toml_content); ``` The above statements are executed in a well-defined order, one after the other, from top to bottom." `Hello World` is printed, followed by the contents of `Cargo.toml` being read and then printed. This paradigm is perfectly fine under normal operation - but sometimes our code requires the current context to stop while it _waits_ for something else - this is generally known as **blocking**. In other words, when a piece of code is blocked, it's essentially on hold, waiting for a particular operation to complete before it can proceed. This can occur when, for example, we're waiting for the file system, network communication, a database transaction, or even a specified amount of time to pass. During this blocked state, the program remains idle and cannot perform other tasks concurrently. In the earlier example, the loop can't move on to the next iteration until the request in the previous iteration has finished. This can lead to inefficiencies, especially when dealing with a significant number of such requests. In the example below, every loop iteration a request is made to the infamous `example.com`. ```rust for index in 1..=100 { let result = sync_http_client.get(format!("www.example.com/items/{}", index)); } ``` The problem here is that `sync_http_client.get` is blocking. Blocking can occur for lots of reasons: - waiting for the file system - waiting for the network - waiting for some database transaction - waiting for some time to occur - etc. When a program is blocked it is doing nothing but waiting for a response to return to continue execution. If we need to work on anything else - we're kinda stuck. In this example the loop cannot run the next iteration / index until the request in the previous one has fully finished. While making and reading a single request is relatively fast, the code in the loop runs 100 times and makes 100 requests so the whole loop takes a while to run. **What if there was a way to start additional requests without having to wait for the previous to have finished its request?** This is where asynchronous programming comes in. Asynchronous programming is about _not_ blocking. Let's say you've ordered a mountain bike for a ride on the weekend. You don't need to spend all your time on the doorstep waiting for the delivery - you can continue living your life doing whatever. An async runtime allows you to continue whatever you are doing and serves as a notification, _awaking_ you to the door when the delivery arrives. We will get more in to how to write async later but the essence is that we can change the loop to the following to start up 100 requests without having requiring the previous request to have finished: ```rust let mut handles = Vec::new(); for index in 1..=100 { let handle = tokio::spawn( async_http_client.get(format!("www.example.com/items/{}", index)) ); handles.push(handle); } for handle in handles { let result = handle.await; } ``` ### Parallelization and concurrency Before we go further we should note that async is _not_ for processing expensive operations. It's only beneficial for IO in which data comes from somewhere further away than the RAM and when there is a lot of it. Parallelization is beneficial for computationally expensive operations. **Parallelization is running multiple things at the same time. Concurrency is handling multiple things at the same time.** Async is designed for concurrency. Tokio's default runtime utilises threads so we also benefit from parallelization. ### Benchmarking Comparing an example written using async vs the same example written synchronously - for a large numbers of concurrent web requests, the async version is ~60% faster that synchronous requests and ~20% faster than spinning up a thread for each request[^benchmarks]. | Command | Mean [s] | Min [s] | Max [s] | Relative | | :---------------- | ------------: | ------: | ------: | ----------: | | `./sync` | 1.070 ± 0.013 | 1.060 | 1.085 | 1.65 ± 0.09 | | `./threads` | 0.787 ± 0.007 | 0.782 | 0.795 | 1.22 ± 0.06 | | `./async` | 0.732 ± 0.016 | 0.721 | 0.750 | 1.13 ± 0.06 | | `./async_threads` | 0.646 ± 0.033 | 0.612 | 0.677 | 1.00 | ## Getting started with async Rust Rust does not have a runtime[^rust-runtime] and so doesn't have a standard executor (at least for now). There are several popular executor runtimes. These are crates like any other library so you can use them by adding them to the `Cargo.toml`. For this demo we will pick Tokio Rust (Tokio-rs) - [https://tokio.rs/](https://tokio.rs/) as it the most popular executor. Other runtimes exist and prioritize different things. For example [async-std](https://docs.rs/async-std/latest/async_std/index.html) is focused on an async version of Rust's standard library and [smol](https://docs.rs/smol/1.2.5/smol/) which is focused on being lightweight. Overall Rust is designed to stay out the way, so it lets you pick which executor you run. To start we will run `cargo new`. Then add `tokio = { version = "1.19", features = ["full"] }` to `Cargo.toml` (or if you have [cargo-edit](https://github.com/killercup/cargo-edit) installed: `cargo add tokio -F full`) ```rust #[tokio::main] async fn main() { println!("Hello from an async function"); } ``` ### Async functions In Rust, functions that incorporate asynchronous operations are identified by the `async` keyword. To declare such a function, simply prefix it with `async` as shown below: ```rust async fn do_thing() { let result = some_async_function().await; println!("{}", result); } ``` Within an async function, you have the ability to employ `.await`. This is appended to the end of an asynchronous function call, and it plays a vital role in non-blocking execution. When you use `.await`, it temporarily halts execution and retrieves the actual result value. Now, let's go a bit deeper. Async functions, as well as async blocks, return Futures. A [Future](https://doc.rust-lang.org/std/future/trait.Future.html) is a function which returns a [Poll](https://doc.rust-lang.org/std/task/enum.Poll.html). Poll is a bit like a `Result` or `Option`, it has two variants one is a final value and the other variant is that the value is still pending. Futures are lazy, there are two ways to run a future: `tokio::spawn` to spawn eagerly and get a [JoinHandle](https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html) or `.await`. Rust warns against _unawaited_ futures. ### Writing async operations Let's check out the code snippet below: ```rust let contents = tokio::fs::read("Cargo.toml").await; ``` Within this code snippet, you may be curious about `tokio::fs::read` and its resemblance to the `std::fs::read` function in Rust's standard library. This is where Tokio proves its utility. Tokio provides asynchronous counterparts to the synchronous input and output (IO) operations found in Rust's standard library. Specifically, `tokio::fs::read` represents an asynchronous file reading operation. What makes it special is its asynchronous nature; it enables your program to read file contents without blocking other tasks. While it waits for the file read to complete, your program can continue executing other tasks concurrently. This non-blocking behavior is a fundamental aspect of asynchronous programming in Rust, safeguarding your program against unresponsiveness during IO operations. ### Writing concurrency As discussed earlier, the issue with blocking calls is that they allow only one task to run at a time. ```rust let weather = client.get("https://api.darksky.net/forecast").await; let news = client.get("https://api.nytimes.com/svc/topstories").await; ``` With `tokio::join!`, we can initiate both requests and await their results concurrently. ```rust let weather = client.get("https://api.darksky.net/forecast"); let news = client.get("https://api.nytimes.com/svc/topstories"); let (weather, news) = tokio::join!(weather, news).await; ``` What `tokio::join`! does there is initiate multiple asynchronous tasks simultaneously and then await their results concurrently. In essence, it starts both the weather and news requests at the same time and then waits for both responses without waiting for one to finish before starting the other. This concurrent approach is significantly different from sequential execution, where you would request weather first, then wait for it to complete, and only after that request news. By leveraging `tokio::join!`, you're able to efficiently utilize your program's time, improving performance when dealing with multiple asynchronous operations.
To keep this post short and to the basics we will stop here. If you want to read more about writing async the there is the [official Rust async book](https://rust-lang.github.io/async-book/) and [Tokio has a brilliant tutorial](https://tokio.rs/tokio/tutorial). ## Conclusion Async Rust is a practical and evolving aspect of the Rust language. While the async features continue to develop, there is room for improvement in the future. You can check the current status of async features and other aspects of the async ecosystem at [areweasyncyet.rs](https://areweasyncyet.rs). This post provides an introductory guide to writing async Rust code, so you can definitely _await_ a future post that digs deeper into async in Rust with topics such as; Rust streams, error handling in async code, advanced concurrency patterns, and practical examples of async Rust in real-world applications.
[^rust-runtime]: Technically there are panic handlers and things which is runtime https://doc.rust-lang.org/reference/runtime.html [^benchmarks]: We had a bit of difficult showing beneficial results for async and still unsure whether these results are a good reflection of the benefits of async. If you're interested, feel free to have a look at [the full benchmarking code.](https://github.com/kaleidawave/sync-vs-threads-vs-async-rust-bench) --- # Builders in Rust Source: https://www.shuttle.dev/blog/2022/06/09/the-builder-pattern Date: 9 June 2022 Author: ben Tags: rust, tutorial In this post we do a deep dive into the builder pattern - an easy way to write cleaner and more readable code. This blog post is powered by shuttle! The serverless platform built for Rust. In this post, we'll be going over the "builder pattern". The builder pattern is an API design pattern for constructing instances of Rust structures. We'll be going over where it makes sense to use it and some of the benefits of applying it to your structs. ## Examples Here are some examples of the builder pattern in common Rust crates: [`Command`](https://doc.rust-lang.org/std/process/struct.Command.html) from the Rust standard library ```rust Command::new("cmd") .args(["/C", "echo hello"]) .output() ``` [`Rocket`](https://api.rocket.rs/v0.5/rocket/struct.Rocket) in Rocket ```rust rocket::build() .mount("/hello", routes![world]) .launch() ``` [`Response`](https://docs.rs/http/latest/http/response/struct.Response.html#method.builder) in the HTTP crate ```rust Response::builder() .status(200) .header("X-Custom-Foo", "Bar") .header("Set-Cookie", "key=2") .body(()) .unwrap(); ``` [Cargo uses the pattern internally for tests](https://github.com/rust-lang/cargo/blob/c6745a3d7fcea3a949c3e13e682b8ddcbd213add/tests/testsuite/build.rs#L74-L91) Ok - so let's dive into _what_ the builder pattern actually is. ## What is the builder pattern Given the following struct representation: ```rust struct Message { from: String, content: String, attachment: Option } ``` Using struct initialization syntax: ```rust Message { from: "John Smith".into(), content: "Hello!".into(), attachment: None } ``` Using a builder pattern: ```rust Message::builder() .from("John Smith".into()) .content("Hello!".into()) .build() ``` The builder pattern consists of: - A function that generates a _intermediate builder structure_ (`Message::builder()`) - A chain of methods which set values on the builder: (`.from("John Smith".into()).content("Hello!".into())`) - A final method which builds the final value from the intermediate structure `.build()` The structure of the builder pattern follows the functional programming design and has likeness of building iterators. The setting methods take a mutable reference to the builder and return the same reference (thus for chaining to work). The handy part about working with mutable references is that it can be shared around between functions and if statements: ```rust fn build_message_from_console_input( builder: &mut MessageBuilder ) -> Result<(), Box> { let mut buffer = String::new(); let mut stdin = std::io::stdin(); stdin.read_line(&mut buffer).unwrap(); let split = buffer.rsplit_once("with attachment: "); if let Some((message, attachment_path)) = split { let attachment = std::fs::read_to_string(attachment_path).unwrap(); builder .content(message.into()); .attachment(attachment); } else { builder.text_filter(buffer); } } ``` Next we'll explore some places where the builder pattern can offer a lot of benefits. #### Constraints and computed data Given the following struct which represents running a certain function at a certain time: ```rust struct FutureRequest { at: chrono::DateTime, func: T } ``` We don't want the program to be able to create a `FutureRequest` for a time in the past. With regular struct initialisation and public fields there isn't a good way to constrain the values being given to the struct[^type_constraints] ```rust let fq = FutureRequest { at: chrono::DateTime::from_utc( chrono::NaiveDate::from_ymd(-112, 2, 18) .and_hms(11, 5, 6), Utc ), func: || println!("𓅥𓃶𓀫"), } ``` However with the builder pattern and a method for setting the time we can validate the value before it is assigned ```rust #[derive(Debug)] struct SchedulingInPastError; impl ()> FutureRequestBuilder { fn at( &mut self, date_time: chrono::DateTime ) -> Result<&mut Self, SchedulingInPastError> { if date_time < Utc::now() { Err(SchedulingInPastError) } else { self.at = date_time; Ok(self) } } } ``` Maybe we don't even want an absolute time - but a relative time at some point in the future. ```rust impl ()> FutureRequestBuilder { fn after(&mut self, duration: std::time::Duration) -> &mut Self { self.at = Utc::now() + chrono::Duration::from_std(duration).unwrap(); self } } ``` #### Encapsulating data Sometimes - we want to keep some fields hidden from the user: ```rust struct Query { pub on_database: String, // ... } fn foo(query: &mut Query) { // You want mutable access to call mutable methods on the query // but want to prevent against: query.on_database.drain(..); } ``` So you could make the fields private and create a function which constructs the value (known as a constructor): ```rust impl Query { fn new( fields: Vec, text_filter: String, database: String, table: String, fixed_amount: Option, descending: bool, ) -> Self { unimplemented!() } } let query = Query::new( vec!["title".into()], "Morbius 2".into(), "imdb".into(), "films".into(), None, false ); ``` But this causes confusion at the call site. Its not clear whether "imdb" is the database, the table or the text_filter? [^vscode-inlay-hints]. The builder pattern makes it much easier to read and understand what's happening during initialisation: ```rust let query = Query::builder() .fields(vec!["title".into()]), .text_filter("Morbius 2".into()), .database("imdb".into()), .table("films".into()), .fixed_amount(None), .descending(false) .build(); ``` #### Enums and nested data So far we've just discussed structs - let's talk about enums: ```rust enum HTMLNode { Text(String), Comment(String), Element(HTMLElement) } struct HTMLElement { tag_name: String, attributes: HashMap>, children: Vec } ``` Here there is builder associated with each variant: ```rust HTMLNode::text_builder() .text("Some text".into()) .build() // vs HTMLNode::Text("Some text".into()) // -- HTMLNode::element_builder() .tag_name("p".into()) .attribute("class".into(), "big quote".into()) .attribute("tabindex".into(), "5".into()) .content("Some text") // vs HTMLNode::Element(HTMLElement { tag_name: "p".into(), attributes: [ ("class".into(), "big quote".into()), ("tabindex".into(), "5".into()) ].into_iter(), children: vec![HTMLNode::Text("Some text".into())] }) ``` ## Building our own builder pattern Now let's build our own builders (no pun intended). In this example we have some users: ```rust #[derive(Debug)] struct User { username: String, birthday: NaiveDate, } struct UserBuilder { username: Option, birthday: Option, } #[derive(Debug)] struct InvalidUsername; #[derive(Debug)] enum IncompleteUserBuild { NoUsername, NoCreatedOn, } impl UserBuilder { fn new() -> Self { Self { username: None, birthday: None, } } fn set_username(&mut self, username: String) -> Result<&mut Self, InvalidUsername> { // true if every character is number of lowercase letter in English alphabet let valid = username .chars() .all(|chr| matches!(chr, 'a'..='z' | '0'..='9')); if valid { self.username = Some(username); Ok(self) } else { Err(InvalidUsername) } } fn set_birthday(&mut self, date: NaiveDate) -> &mut Self { self.birthday = Some(date); self } fn build(&self) -> Result { if let Some(username) = self.username.clone() { if let Some(birthday) = self.birthday.clone() { Ok(User { username, birthday }) } else { Err(IncompleteUserBuild::NoCreatedOn) } } else { Err(IncompleteUserBuild::NoUsername) } } } ``` Some things to look out for: - Every set method must take a mutable reference in order to add the data to the backer - The method must then return the mutable reference it has to allow for them to be chained. There are clones in the `build` method but if that method is only called once then it is optimized out by Rust. ## Automatic approaches Similar to how Clone and Debug work, crates can create there own derive macros. [There are a lot of crates which can help with generating the builder pattern](https://lib.rs/keywords/builder). Let's take a look at a few: ### [derive_builder](https://lib.rs/crates/derive_builder) ```rust #[derive(Debug, derive_builder::Builder)] #[builder(build_fn(validate = "Self::validate"))] struct Query { fields: Vec, text_filter: String, database: String, table: String, fixed_amount: Option, descending: bool, } // Usage same as described patterns: let query = Query::builder() .table("...".into()) // ... .build() .unwrap(); ``` This derive macro generates a new struct named the same as the original structure but postfixed with `Builder` (in this case `QueryBuilder`). Derive builder has the downside of a whole object validation rather than per field. As well as the error variant of construction being a `String`, which makes it harder to match on the error or return error data compared to a error enum: ```rust impl Query { fn validate(&self) -> Result<(), String> { let valid = self .database .as_ref() .map(|value| value == "pg_roles") .unwrap_or_default(); if valid { Ok(()) } else { Err("Cannot construct Query on 'pg_roles'".into()) } } } ``` ### [typed-builder](https://lib.rs/crates/typed-builder) Typed-builder solves two problems with `derive_builder`: With `derive_builder` you can set a field twice (or more) ```rust Query::builder() .database("imdb".into()) // ... .database("fishbase".into()) ``` Which takes the value of the last set field which is likely a mistake. Although Rust can optimize out a write without a read it is very difficult to have a linter error for this mistake. `derive_builder` also delegates the check to whether all the required fields have been set to runtime. With `typed-builder` it has a very similar implementation but has a different output which Rust can reason about and check that they are no duplicate sets and the build is well formed (all the required fields have been set). The downside here is that it takes longer to expand the macros as there is more to generate. The added complexity also makes it more complicated to pass the builder around. ### [Buildstructor](https://lib.rs/crates/buildstructor) Buildstructor is a annotation for an existing impl block. Rather than using the fields on a structure (as seen in the previous two) to generate code it builds wrappers around existing constructor functions: ```rust struct MyStruct { sum: usize } #[buildstructor::buildstructor] impl MyStruct { #[builder] fn new(a: usize, b: usize) -> MyStruct { Self { sum: a + b } } } MyStruct::builder().a(1).b(2).build(); ``` Similar to `typed-builder` it generates intermediate staging structs for building which has the benefits of compile time checking that all the fields exist. However that comes again with the drawback of slower compile time and less flexibility when passing it around. Typed builder looks to be more compatible with the Rust language which allows it to support async builders! It's definitely the more interesting one of the bunch and I will be looking to play with with it future projects. ### Alternative patterns If you just want to build a struct which has a large amount of default fields, using `..` (base syntax) with the [Default](https://doc.rust-lang.org/std/default/trait.Default.html) trait (whether a custom implementation or the default one with `#[derive(Default)]`) will do: ```rust #[derive(Default)] struct X { a: u32, b: i32, c: bool, } X { a: 10, ..Default::default() } ``` If you want computation, constraints, encapsulation and named fields you could create a intermediate struct which can be passed to a constructor: ```rust struct Report { title: String, on: chrono::DateTime // ... } struct ReportArguments { title: String, on: Option // ... } impl Report { fn new_from_arguments(ReportArguments { title, on }: ReportArguments) -> Result { if title. .chars() .all(|chr| matches!(chr, 'a'..='z' | '0'..='9')) { Ok(Self { title, on: chrono.unwrap_or_else(|| todo!()) }) } else { Err("Invalid report name") } } } ``` However both of these don't the use the nice chaining syntax. ## Conclusion The builder pattern can help you write cleaner, more readable APIs, and it turn help the consumers of your APIs write better code. We can apply constraints to make sure that our structs are initialised correctly with a clean API enforcing the contract. One thing to remember is that code is read _much_ more than it's written - so it's worth going out of our way to make our code just that little bit more pleasant to read. ## [Shuttle](https://www.shuttle.dev/): Stateful Serverless for Rust Deploying and managing your Rust web apps can be an expensive, anxious and time consuming process. If you want a batteries included and ops-free experience, [try out Shuttle](https://docs.rs/shuttle-service/latest/shuttle_service/).
[^type_constraints]: I partially agree with this, there are ways to design your types to be constrained. Here we could create a `struct FutureEvent(chrono::DateTime)` structure where the constraint is constructing the `FutureEvent` type rather than leaving the constraint to the field. But there are lots of scenarios where that isn't the case. [^vscode-inlay-hints]: With vscode and rust analyzer there is a feature called [inlay hints](https://rust-analyzer.github.io/manual.html#inlay-hints) which shows the names of parameters in the editor. While this is great this is a feature specific to vscode at the moment. You won't see the hints on GitHub diffs and in other text editors. --- # Hyper vs Rocket - Low Level vs Batteries included Source: https://www.shuttle.dev/blog/2022/06/01/hyper-vs-rocket Date: 9 May 2022 Author: ben Tags: rust, rocket, hyper, comparison A comparison of using the low-level HTTP framework 'hyper' vs a batteries included framework like 'Rocket' In this post we're going to be comparing two popular Rust libraries used for building web applications. We'll be writing an example in each and compare their ergonomics and how they perform. The first library [Hyper](https://github.com/hyperium/hyper) is a low level HTTP library which contains the primitives for building server applications. The second library [Rocket](https://rocket.rs/) comes with more "batteries included" and provides a more declarative approach to building web applications. ## The Demo We're going to build a simple site to showcase how each libraries implements: ### Routing Routing decides what to respond for a given URL. Some paths are fixed, in our example we will have a fixed route `/` which returns `Hello World`. Some paths are dynamic and can have parameters. In the example we will have `/hello/*name*` which will response `Hello *name*` which will have _name_ substituted in each response. ### Shared state We want to have a central state for the application. In this demo we will have central site visitor counter which counts the number of requests. This number can be viewed as JSON on the `/counter.json` route. In this example we will be storing the counter in application memory. However if you were storing it in a database the shared state would be a database client. The are lots of other functionality necessary for a site such as handling HTTP methods, receiving data, rendering templates and error handling. But for the scope of this post and example we will only be comparing these two features. ### The rules The rules for this demonstration is to only use the specific library and any of its re-exported dependencies. So no additional libraries (except in the hyper example we need a `tokio::main`). ## Hyper Hyper's readme describes hyper as a "A fast and correct HTTP implementation for Rust with client and server APIs". For this demo we will be using the server side of the library. It has **9.7k** stars on GitHub and **48M** crates downloads. It is used as a often a dependency and many other libraries such as [`reqwest`](https://github.com/seanmonstar/reqwest) and [`tonic`](https://github.com/hyperium/tonic) build on top of it. In this example we see how far we can get with just using the library. This demo uses Hyper 0.14[^hyper-deps]. Below is the full code for the site: ```rust use hyper::server::conn::AddrStream; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use std::convert::Infallible; use std::sync::{atomic::AtomicUsize, Arc}; #[derive(Clone)] struct AppContext { pub counter: Arc, } async fn handle(context: AppContext, req: Request) -> Result, Infallible> { // Increment the visit count let new_count = context .counter .fetch_add(1, std::sync::atomic::Ordering::SeqCst); if req.method().as_str() != "GET" { return Ok(Response::builder().status(406).body(Body::empty()).unwrap()); } let path = req.uri().path(); let response = if path == "/" { Response::new(Body::from("Hello World")) } else if path == "/counter.json" { let data = format!("{{\"counter\":{}}}", new_count); Response::builder() .header("Content-Type", "application/json") .body(Body::from(data)) .unwrap() } else if let Some(name) = path.strip_prefix("/hello/") { Response::new(Body::from(format!("Hello, {}!", name))) } else { Response::builder().status(404).body(Body::empty()).unwrap() }; Ok(response) } #[tokio::main] async fn main() { let context = AppContext { counter: Arc::new(AtomicUsize::new(0)), }; let make_service = make_service_fn(move |_conn: &AddrStream| { let context = context.clone(); let service = service_fn(move |req| handle(context.clone(), req)); async move { Ok::<_, Infallible>(service) } }); let server = Server::bind(&"127.0.0.1:3000".parse().unwrap()) .serve(make_service) .await; if let Err(e) = server { eprintln!("server error: {}", e); } } ``` At the top we define a `handle` function which processes all the requests. Routing is done through the chain of ifs and elses in the `handle` function. First the path of the request (e.g `/` for the index) is extracted using `req.uri().path()`. Fixed routes are easy to branch on using string comparison like `path == "/"`. For routes which match multiple paths such as the `/hello/` route it uses [`str::strip_prefix`](https://doc.rust-lang.org/std/primitive.str.html#method.strip_prefix) which returns a `None` if the path doesn't start with the prefix or `Some` if the path starts with the prefix along with a slice that proceeds the prefix. ```rust "/".strip_prefix("/hello/") == None "/test".strip_prefix("/hello/") == None "/hello/jack".strip_prefix("/hello/") == Some("jack") ``` The function has a early return for requests with a method other than GET because there are no POST routes or others for this example. If the site accepted different requests types and had to add additional guards then we could additional clauses to the if statement. Although you could see how expand on the if chain would get more complex and verbose. To return a response, Hyper re-exports [`Response`](https://docs.rs/hyper/0.14.19/hyper/struct.Response.html) (from the [http crate](https://docs.rs/http/latest/http/)). It has a nice simple builder pattern for building the responses. The serialization code is hand written using `format!`. Of course we could import serde but that's against the rules. The counter is done by creating a struct in the initializing code and cloning it on every request to send to the handler function. Without going into the details it uses `Arc` instead of a `usize` as the atomic variant has special properties for when multiple handlers are using and mutating it. The code increments the visitor counter before anything else in the handler function so that a visit is recorded for all requests. ### Hyper Verdict In terms of development (on a low end machine we used for profiling[^profile-machine]), a debug build (without any of the build artifacts) takes **79.0s**. After the initial compilation, incremental compilation takes only **1.9s**. For building a `release` build with further optimizations (on top of the debug build artifacts) it takes **32.5s**. The initialization code was take from [Hyper's server docs](https://docs.rs/hyper/latest/hyper/server/index.html) and is quite verbose and out of the box for Hyper there are no logs or server information. In terms of runtime performance over three 30 second connections Hyper responded to on average **74,563** requests per second on the index route on the above code. Which is incredible quick! ## Rocket Rocket is a "web framework for Rust with a focus on ease-of-use, expressibility, and speed". It has **17.4k** github stars and **1.7M** crates downloads. Rocket internally uses Hyper. For this demo we are using the `0.5.0-rc2` version of Rocket[^rocket-deps] which builds on Rust stable. ```rust use rocket::{ fairing::{Fairing, Info, Kind}, get, launch, routes, serde::{json::Json, Serialize}, Config, Data, Request, State, }; use std::sync::atomic::AtomicUsize; #[derive(Serialize, Default)] #[serde(crate = "rocket::serde")] struct AppContext { pub counter: AtomicUsize, } #[launch] fn rocket() -> _ { let config = Config { port: 3000, ..Config::debug_default() }; rocket::custom(&config) .attach(CounterFairing) .manage(AppContext::default()) .mount("/", routes![hello1, hello2, counter]) } struct CounterFairing; #[rocket::async_trait] impl Fairing for CounterFairing { fn info(&self) -> Info { Info { name: "Request Counter", kind: Kind::Request, } } async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) { request .rocket() .state::() .unwrap() .counter .fetch_add(1, std::sync::atomic::Ordering::SeqCst); } } #[get("/")] fn hello1() -> &'static str { "Hello World" } #[get("/hello/")] fn hello2(name: &str) -> String { format!("Hello, {}!", name) } #[get("/counter.json")] fn counter(state: &State) -> Json<&AppContext> { Json(state.inner()) } ``` In Rocket we describe each endpoint using a function. The `get` macro attribute handles path routing and http method constraint. No need to add early returns for methods and dealing with raw string slices. It takes the declarative approach, `#[get("/hello/")]` is more descriptive and less verbose than `if let Some(name) = path.strip_prefix("/hello/")`. The functions are registered using `.mount("/", routes![hello1, hello2, counter])`. The application has a state defined here: ```rust #[derive(Serialize, Default)] #[serde(crate = "rocket::serde")] struct AppContext { pub counter: AtomicUsize, } ``` And it is created and registered using `.manage(AppContext::default())`. Rocket re-exports the serialization library serde so we can use `#[derive(Serialize)]` to generate serialization logic for the counter state, so no hand writing the serialization code unlike first method. In Rocket endpoint functions can just return `String` or `str` slices and Rocket handles it automatically. Rocket also comes with a `Json` return type and reusing the fact that `AppContext` implements `Serialize` we can freely build a `Json` response from it. The `Json` structure handles setting the `Content-Type` header automatically for us. Rocket has a middleware implementation which it calls "fairings". In the example it defines a `CounterFairing` which on every request modifies the counter state. The initialization code is really slim, it sets up a config and a Rocket structure is created using a builder pattern. Annotating the main function with `#[launch]` helps Rocket find the entry point and abstracts how the server is span up. Rocket also has really nice built in logs which are great for development.

A stack of logs generated by a Rocket Rust web service

### Rocket Verdict Since Rocket has more dependencies and requires more macro expansion it takes a bit longer build taking **141.9s** (2m 21.9s) on a cold start to compile. A release builds on top of debug artefact takes **147.0s** (2m 27.0s) to compile. Incremental builds are still fast taking **3.3s** to compile after a small change to the response of a endpoint. Using the same benchmark as Hyper, on average Rocket returned **43,899** requests per second in a release build with the logging disabled - roughly **60%** of Hyper's throughput. ## Conclusion Writing both of these examples were fun to build and there weren't any frustrations or problems using them. Both are plenty fast for performance to be a concern. Rockets documentation is very good and explanatory. All of Hyper's api is well documented on its [docs.rs page](https://docs.rs/hyper/latest/hyper/). Both libraries are actively developed with many commits and pull requests made in the last month. Do you prefer the control and speed of Hyper or prefer the expressiveness of Rocket? ## [Shuttle](https://www.shuttle.dev/): Stateful Serverless for Rust Deploying and managing your Rust web apps can be an expensive, anxious and time consuming process. If you want a batteries included and ops-free experience, [try out Shuttle](https://docs.rs/shuttle-service/latest/shuttle_service/).
[^profile-machine]: The build and request profile machine is a vm with 2 cores and 7 GB RAM. Take the numbers with a grain of salt [^hyper-deps]: The dependencies for building the project `hyper = { version = "0.14", features = ["server", "tcp", "http1"] }` and `tokio = { version = "1.18.2", features = ["rt", "macros", "rt-multi-thread"] }` [^rocket-deps]: The dependencies for building the project `rocket = { version = "0.5.0-rc.2", features = ["json"] }` --- # Infrastructure From Code Source: https://www.shuttle.dev/blog/2022/05/09/ifc Date: 9 May 2022 Author: nodar Tags: infra-from-code, rust, startup A new paradigm for building on the cloud In the early days of Facebook (back when it was still called `thefacebook.com`), Mark Zuckerberg hosted it on Harvard's university servers. Back then companies used to buy or rent physical servers to run their software on. The advent of the cloud in the mid-2000s changed the game. The elasticity that this enabled has in big part enabled the rapid progress that we've all enjoyed since then. What we demand from software has increased tremendously, and correspondingly its architecture has become much more elaborate. The power of flexibility came at a price though - the complexity of wiring code with infrastructure. That price is even higher today. ### The Container Hero Heroku became part of the cloud-native lore as the first incredibly successful attempt at tackling this complexity. They led the first crusade to rid software developers of the infrastructure complexity dragon. People loved it. Heroku pioneered the wildly popular container-based approach to deployment that abstracted away the burden of managing virtual machines. By being opinionated with the use of containers, Heroku was able to appeal to a broad set of customers looking to quickly build apps. Containers are mutually isolated processes, wired together by third-party configuration which does not belong in the application's code base - this design choice results in a lack of elasticity and granular control of your system. This results in a conservative outlook of dealing with infrastructure, constantly over-provisioning and hence overpaying to account for potential future load. Furthermore, infrastructure is still treated separately from code - the two worlds live separately and don't really know much about each other. There is much less wiring to do than with AWS for example, but what is left to do - and there's a lot of it - you still have to do yourself. Heroku trades off AWS's elasticity for ready-made building block components that are statically wired up together through a combination of CLI commands and dashboard operations. Of course, Heroku is limited by its founding principle: static containers as building blocks of applications. With Heroku, it is true you do not have to think about infrastructure - but only in the beginning. Once your application scales, your bills stack up and you're left without a choice: go back to AWS. ### The Serverless Conundrum We need to talk about serverless. Serverless (think AWS Lambda) was a new cloud computing execution model where machine allocation happens on-demand and the user is primarily abstracted away from the underlying servers. With it came a familiar promise - developers not needing to think about infrastructure at all. Despite its somewhat counterintuitive name (because, of course, there are always servers running somewhere), serverless sounds like a great ideal to strive towards. This is simple, developers want to spend as much time as possible on delivering business value by writing code, while companies would like to avoid spending fortunes on DevOps. This seems to be the holy grail, but there's a catch. You might ask, "if you say serverless is so great, why have we all not switched yet"? Well, serverless forces you to write application business logic as functions, rather than the more traditional idiom of stateful processes. To reap the benefits of serverless, you have to build your application as a multitude of stateless request or event handlers, often requiring a bottoms-up redesign of your system. For some use-cases the serverless paradigm works, but in many cases breaking things into discrete, decoupled functions may not be optimal or even feasible. The next question is, can we have our cake and eat it too? Can we maintain the paradigm of stateful processes and abstract away the underlying infrastructure and orchestration? ### Infrastructure from Code At shuttle we want to empower engineers by creating the best possible developer experience. We've already developed an annotation based system that enables Rust apps to be deployed with a one-liner, as well as dependencies like databases being provisioned through static analysis in real-time. ```rust #[shuttle_service::main] async fn rocket( pool: PgPool, // automatic db provisioning + hands you back an authenticated connection pool ) -> Result<...> { // application code } ``` Building on the phenomenal engineering done before us, we see a better future. One where developers don't need to do any "wiring" whatsoever when it comes to code and infrastructure. In this future, infrastructure can be defined directly from code. Not in the "Infrastructure as Code" kind of way though, but in the way that the code that developers write implicitly defines infrastructure. What your code actually needs in terms of infrastructure should be inferred as you build your application, instead of you having to think upfront about what infrastructure piece is needed and how to wire it up. This setup should also break the boundaries that keep containers isolated from each other (and thus make it difficult to orchestrate them), without necessarily getting rid of the paradigm of containers. It should not force you into any specific way of writing applications, but just be an extension of your workflow. ### Having your cake and eating it too When looking back at Heroku's success, it becomes apparent that focusing on one language, Ruby, which was becoming quite popular at the time - was a remarkable strategy. It enabled their team to focus acutely and produce an unparalleled experience for their users. At shuttle we are convinced Rust is the best language to start this journey with. It's been [the most loved](https://www.cantorsparadise.com/the-most-loved-programming-language-in-the-world-5220475fcc22) language by developers for many years in a row (as well as one of the fastest-growing languages). If you want to create the best developer experience - it makes sense to start with the most loved language. Indeed, Rust is the first language packed with such a powerful set of tools for static analysis and code generation, that are required to create the best developer experience when it comes to _Infrastructure ~~as~~ from Code_. Removing the burden of dealing with DevOps from developers, many of whom find it daunting and stressful, not only do we stand to make development more enjoyable and efficient, but also enable far more people to write and ship applications. From inception, all of us shared affection for open source software, not only from a philosophical standpoint. We have seen in practice that the best way to build software is together with the end-users. It all goes back to the idea of creating the best developer experience - so for us, this is a no-brainer. Our community is just as important to us, as our vision is, so if any of this resonates with you - [join us on discord](https://discord.gg/shuttle). Or check out our [jobs board](https://www.workatastartup.com/companies/shuttle). Also, if you're curious to learn more about _how_ we are building this - [check out our GitHub](https://github.com/getsynth/shuttle). --- # DevLog[1]: Building a serverless platform for Rust in 4 weeks - part deux Source: https://www.shuttle.dev/blog/2022/04/27/dev-log-1 Date: 27 April 2022 Author: christoshadjiaslanis Tags: rust, startup, devlog Designing and building a deployment system as a state machine `shuttle` is a serverless platform built for Rust. The goal of shuttle is to create the best possible developer experience for deploying Rust apps. Also, shuttle introduces a new paradigm for developing on the cloud called Infrastructure From Code (IFC). IFC uses application code as the source of truth for provisioning infrastructure. No longer are your applications and servers decoupled, the two go hand in hand. shuttle does this by doing static analysis of user code and generating the corresponding infrastructure in real time. A bit like this:

An image depicting Infrastructure from Code.

In the [previous DevLog](https://www.shuttle.dev/blog/2022/04/22/dev-log-0) we started the journey of building the shuttle MVP. We went over the design and implementation of the `cargo` subcommand which deploys cargo projects to shuttle. This has been a race against the clock, so corners were cut and tradeoffs were made. A similar theme emerges in this DevLog which covers the **deployment state machine**. We're going to think about compiling and deploying user code, while also covering one of my favourite design patterns in Rust. ## Deployment State shuttle exposes an HTTP endpoint under `POST /deploy`. This endpoint receives a series of bytes, from [`cargo shuttle`](https://github.com/shuttle-hq/shuttle/tree/main/cargo-shuttle), which correspond to a packaged cargo project (basically a compressed tarball with a bunch of `.rs` files). The aim of the game, is to convert that series of bytes into a deployed web service - how do we go about doing that? The deployment process is broken into 4 stages: 1. `Queued` - the cargo project is received and waiting to be compiled 2. `Built` - the cargo project is compiled successfully 3. `Loaded` - the output of the compilation is loaded as a dynamically-linked library 4. `Deployed` - the app inside the DLL is running and listening for connections Then life happens so you need a couple more states: 5. `Error` - there was an issue anywhere in the build process 6. `Deleted` - user-initiated deletion of the deploymentThis endpoint Which corresponds to: ![State Machine](/images/blog/state-machine.jpeg) All this can be expressed nicely in an enum since all these states are mutually exclusive: ```rust enum DeploymentState { Queued, Built, Loaded, Deployed, Error, Deleted } ``` Even though we have a nice representation of our states - these states don't actually hold any data yet and the state transitions are not defined. We would like the `DeploymentState` to own all the data that corresponds to the specific stage in it's deployment. We'll create some structs to hold the data required for each stage. First, the `QueuedState` just has a vector of bytes from the packaged cargo project that was received from `cargo-shuttle`: ```rust struct QueuedState { crate_bytes: Vec, } ``` When a deployment is queued, the shuttle build system writes the `crate_bytes` (just a tarball of a cargo project) to the file system. It then extracts the tarball and starts the compilation process by running `cargo::ops::compile`. The output of the build process is an `.so` file which is held in the next stage - the `BuildState`: ```rust struct BuiltState { so_path: PathBuf, } ``` So far so good. At this point we have a pointer to a compiled shared object file - next we need to load it into memory. `shuttle` uses the [`libloading`](https://github.com/nagisa/rust_libloading) crate to dynamically load from a `.so` file a value of a type implementing the [`Service`](https://docs.rs/shuttle-service/0.2.6/shuttle_service/trait.Service.html) trait. The `Service` trait is code-generated for the user via the `#[shuttle_service::main]` annotation and it's how shuttle interfaces with client apps. ```rust pub struct LoadedState { service: Box, so: Library, } ``` We keep the `Library` struct around since `Box` is just a pointer to data loaded and managed by `Library`. Library going out of scope deallocates that data; meaning service will be pointing to deallocated memory hence we get a `segfault`. So it's important to keep `Library` around for the lifetime of the deployment. Finally we find a free port, spin up a new tokio runtime (we keep the handle so that we can kill it in the future) and bind the service to the port. We'll be covering this stuff in depth on a future DevLog but if you're insatiably curious you can check out the [source](https://github.com/shuttle-hq/shuttle/tree/main/service). All of this is put into the `DeployedState` and we're done! ```rust struct DeployedState { so: Library, // remember if we drop this, weird undefined behaviour port: Port, handle: ServeHandle, } ``` To tie it all together, we modify our initial `DeploymentState` own the various states corresponding to the stages of the deployment process: ```rust enum DeploymentState { Queued(QueuedState), Built(BuiltState), Loaded(LoadedState), Deployed(DeployedState), Error(anyhow::Error), Deleted // doesn't have any state } ``` We also wrote a really light `impl` to define the state transitions: ```rust impl DeploymentState { fn queued(crate_bytes: Vec) -> Self { Self::Queued(QueuedState { crate_bytes }) } fn built(build: Build) -> Self { Self::Built(BuiltState { build }) } fn loaded(loader: Loader) -> Self { Self::Loaded(loader) } fn deployed( so: Library, port: Port, handle: ServeHandle ) -> Self { Self::Deployed(DeployedState { so, port, handle, }) } } ``` You'll also notice that there is no mutation happening here. We found it cleaner to simply drop the old state and construct a new one (although we did try). ## Conclusion In the case of shuttle, using enum variants and structs to represent states in a state machine seemed like the natural thing to do. The states were distinct and clear, and for the most part the transitions are clean and self-contained. So what do you think about enum variants as states in a state machine? What would you have done differently? ## Next Steps In the next DevLog we'll be looking at the implementation of our reverse proxy and routing table - how we keep a ledger of deployed services and route network calls appropriately. In the meantime, if you want to try out shuttle head over to the [getting started](https://docs.rs/shuttle-service/0.2.6/shuttle_service/) section! It's completely free while shuttle is still in Alpha. --- # DevLog[0]: Building a serverless platform for Rust in 4 weeks Source: https://www.shuttle.dev/blog/2022/04/22/dev-log-0 Date: 22 April 2022 Author: christoshadjiaslanis Tags: rust, startup, devlog DevLog[0] is the first in a series of posts about how we built the shuttle MVP Put yourself in this situation. Your startup company has come across a pretty obvious gap in the market. It's ambitious, maybe even a little crazy. You're going to toe-to-toe with AWS, Heroku, Google etc. You have 4 weeks to prove the concept. Go. In January we spent countless hours interviewing software engineers. A striking pattern emerged - no one liked dealing with the cloud. It is of course, much better than having physical servers in your basement or driving 45 minutes to your local datacenter to patch a service. But since AWS came along c. 2007, there was a longing for things to be done better. Most engineers (myself included) don't want to deal with infrastructure, we just want to write code that scales and focus on product. Infrastructure is a pre-requisite but not sufficient to build a great product. The folks at Heroku had this insight and essentially developed PaaS along with the beginnings of containerisation tech. Then Hashicorp built declarative abstractions to make the business of managing your infrastructure less of a headache. Then the serverless movement promised to be the final chapter of this saga; devs could wrap their business logic in neat functions which would scale for you. Yet here we are again, in the winter of our discontent. At shuttle we think there is a better paradigm for building applications. We call it Infrastructure From Code (IFC). IFC uses application code as the source of truth for provisioning infrastructure. No longer are your applications and servers decoupled, the two go hand in hand. Our plan was to achieve this by doing static analysis of user code and generating the corresponding infrastructure in real time. A bit like this: ```rust #[get("/hello")] fn index() -> &'static str { "Hello, world!" } #[shuttle_service::main] async fn rocket( pool: PgPool, // This will spin up a Postgres database, create an account and hand you an authenticated connection pool redis: redis::Client // This will spin up a Redis instance and hand you back a client ) -> Result<...> { // Application Code } ``` This isn't going to be everyone's cup of tea and this paradigm is probably not sufficient for every use-case. However we believe there exists a large class of products and teams which will benefit substantially from IFC. We have 4 weeks to prove the concept, let's get started. ## Developer Experience Our primary focus with the MVP was to provide the best possible developer experience. We wanted the end user experience to be as simple as possible. 1. A single annotation can transform your web app into a shuttle app: `#[shuttle_service::main]` 2. You can get started with a single cargo command: `$ cargo shuttle deploy` 3. Your shuttle app is automatically provisioned a subdomain `my-app.shuttleapp.rs` ## Engineering Design Our primary focus was simplicity - we didn't want anything too complicated to start with. For example, we decided to ditch Kubernetes for our API and deployment servers. We simply didn't need that scale until we proved the concept and the complexity overhead would have been detrimental to development velocity. The deployment process is the core piece of engineering we spent the most time on. It looks something like this: 1. `$ cargo shuttle deploy` runs a `cargo package` under the hood, zipping up the current cargo project into a tarball and shipping it to our API under the `/deploy` endpoint with a bearer token for authentication 2. The API receives the tarball and holds it in memory. The build is added to a job processor which acts as a build queue. 3. The job processor unpacks the tarball and writes it to disk, say under `/projects/my-app`. The build system is triggered to compile the unpacked cargo project 4. The output of the build process is a shared object file ('.so') which is then dynamically loaded by the API with its own runtime. The newly born web-server is assigned a free port which is not exposed to the outside world. 5. We update the routing table of our reverse proxy such that requests coming in with the host `my-app.shuttleapp.rs` are forwarded to the aforementioned port. And that's it! It turns out there are more than a few devils in the details here - but that was our plan in all it's glory. ## $ cargo shuttle deploy The cargo subcommand [`cargo shuttle`](https://github.com/getsynth/shuttle/tree/main/cargo-shuttle) seemed like the obvious place to start. To create a third-party cargo subcommand, the binary needs to be named `cargo-${command}` and it needs to be stored in `~./.cargo/bin`. The easiest way to do this is to create a binary called `cargo-shuttle` and publish it to `crates.io`. Then, `cargo install cargo-shuttle` will place it in `~./.cargo/bin`. Pretty simple. `cargo-shuttle` also needs an HTTP client to make requests against the API, as well as some config logic to hold API keys. Finally, `cargo-shuttle` needs to use the `cargo` crate to programmatically run cargo commands like `cargo package`. We had underestimated how easy this would be - it turns out even though the `cargo` binary has world-class documentation, the same is not true for the crate. After a day of grappling and digging into the `cargo` source code, `cargo-shuttle` was happily packaging up cargo projects and serializing them nicely into the body of a POST request. We had built the bare bones of our client, next up was build system. ## Next Steps In the next devlog we'll be exploring how we hacked together a build system to compile `cdylib`s for them to be dynamically linked to the API runtime. In the meantime, if you want to try out shuttle head over to the [getting started](https://docs.rs/shuttle-service/0.2.6/shuttle_service/) section! It's completely free while shuttle is still in Alpha. --- # Building and Deploying a URL shortener with Rust in 10 minutes or less Source: https://www.shuttle.dev/blog/2022/03/13/url-shortener Date: 13 April 2022 Tags: rust Terrence hacks together a URL shortener way past midnight I was trying to get to sleep on a Wednesday night - I check my phone, it's 2:54 AM. A feeling of dread comes over me as I realise I'm not going to be able to get more than 5 hours of sleep - again. I've been a software developer for close to 10 years now. How did it get to this? My deployment broke production at 10pm (because I never learn) and I had to deal with our infrastructure coupled with acute stress for the next 3 hours. As I sat there contemplating my life choices, I had an idea. Can we do better? I don't want to have to deal with Terraform and Kubernetes at midnight. I want to write scalable code and just get it deployed. I want my dependencies generated and managed for me. I want to be able to sleep at night. It's 2023. **Surely** we can do better. As I stared blankly at my white ceiling, I decided to see if it was possible. I suddenly sat up in bed. Can I create a useful app, with some sort of database state, a custom subdomain, focus _only_ on my application code without needing to worry about infrastructure _and_ get it done in 10 minutes or less? I'll write a URL shortener or something. I'll write in Rust. I'll write it tonight. ## Design I got out of bed and turned on the lights in my office. I sat down on my ergonomic chair and power up a comically large curved monitor. Arch boots up. I quickly message a friend of mine on Signal to remind them that I use Arch. Now I'm ready to code. Let's build this thing. The API is going to be simple. No reason for GUIs or anything like that - I am engineer, therefore I've convinced myself that UI's peaked with the 1970's teletype. Life's short so I'm going to build an HTTP API. The simplest thing I can come up with. You can shorten URLs like this: ```bash curl -X POST -d 'https://www.google.com' https://myapp.com https://myapp.com/uvAivJ ``` And you get redirected like this: ```bash curl https://myapp.com/uvAivJ < HTTP/2 301 ... ``` Yeah that'll work. Next I'll need some sort of database to store the urls. I briefly considered using a bijective compression scheme without needing database state, but let's face it I'm not really sure what a bijection is and it's already 3:02 AM. I'll just get a Postgres instance with a basic schema: ```sql CREATE TABLE urls ( id VARCHAR(6) PRIMARY KEY, url VARCHAR NOT NULL ); ``` Genius. I'll add an index or something so that the database doesn't do a linear search on every request. No one is really going to use this - but I can already feel the judgement of anyone who happens to glance over my source code. I need to be able to explain to people that you can search for urls in constant time, implying I understand complexity theory. Ok I'm ready. It's 3:05 AM. I have 10 minutes. I pick up my black vape and take a large hit. Smoke fills up the room and I can't see the screen any more. Whatever. I try to crack my fingers and neck for some dramatic flair, fail, and open a terminal. ## Building the Barebones - 09:59 minutes remaining I'm using [shuttle](https://www.shuttle.dev) for this project. It's a serverless platform built for Rust and I don't have to deal with provisioning databases, or subdomains or any of that gunk. I already have the CLI [installed](https://docs.rs/shuttle-service/0.2.5/shuttle_service/#deploying) and an [account](https://www.shuttle.dev/) so I simply: ```bash mkdir -p ~/projects/url-shortener && cd ~/projects/url-shortener && cargo init --lib ``` Ok we have our `cargo` project. I stop and think for a little bit - which web framework do I want to use? I think I'm going to go with [Rocket](https://rocket.rs/). It's pretty much production ready with a sweet API and I'm reasonably proficient with it. I open up `src/lib.rs` and overwrite it with the `shuttle` entrypoint code: ```rust #[macro_use] extern crate rocket; use rocket::{Build, Rocket}; #[get("/hello")] fn hello() -> &'static str { "Hello, world!" } #[shuttle_service::main] async fn init() -> Result, shuttle_service::Error> { let rocket = rocket::build().mount("/", routes![hello]); Ok(rocket) } ``` My IDE violently lights up with red syntax highlighting as I realise I haven't imported anything. The realities of software engineering hit me as I eye the bottle of whiskey next to me. 18 year old scotch. It turns out I'm grossly overpaid for the value I offer society. I grab a coffee mug and pour myself a small shot - liquid courage. Next I import all of the dependencies to get shuttle to work with Rocket - pretty simple. I open up `Cargo.toml` add a couple of lines: ```toml [package] name = "url-shortener" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] rocket = { version = "0.5.0-rc.4", features = ["json"] } shuttle-service = { version = "0.2", features = ["sqlx-postgres", "web-rocket"] } ``` My IDE quietens down as dependencies are resolved and a wave of relief washes over me. Let's deploy this thing. ```bash $ cargo shuttle deploy Packaging url-shortener v0.1.0 (/private/shuttle/examples/url-shortener) Archiving Cargo.toml Archiving Cargo.toml.orig Archiving src/lib.rs Compiling tracing-attributes v0.1.27 Compiling tokio-util v0.7.10 Compiling multer v2.1.0 Compiling hyper v0.14.27 Compiling rocket_http v0.5.0-rc.4 Compiling rocket_codegen v0.5.0-rc.4 Compiling rocket v0.5.0-rc.4 Compiling shuttle-rocket v0.32.0 Compiling shuttle-rutnime v0.32.0 Compiling url-shortener v0.1.0 (/opt/shuttle/crates/url-shortener) Finished dev [unoptimized + debuginfo] target(s) in 1m 01s Project: url-shortener Deployment Id: 3d08ac34-ad63-41c1-836b-99afdc90af9f Deployment Status: DEPLOYED Host: url-shortener.shuttleapp.rs Created At: 2022-04-13 03:07:34.412602556 UTC ``` Ok... this seemed a little too easy, let's see if it works. ``` $ curl -X https://url-shortener.shuttleapp.rs/hello Hello, world! ``` Hm, not bad. I pour myself another shot... ## Adding Postgres - 07:03 minutes remaining This is the part of my journey where I usually get a little flustered. I've set up databases before but it's always a pain. You need to provision a VM, make sure storage isn't ephemeral, install and spin up the database, create an account with the correct privileges and secure password, store the password in some sort of secrets manager in CI, add your IP address and your VM's IP address to the list of acceptable hosts etc etc etc. Oof that sounds like a lot of work. `shuttle` does a lot of this stuff for you - I just didn't remember how. I quickly head over to the [shuttle / sqlx](https://docs.rs/shuttle-service/0.2.5/shuttle_service/#using-sqlx) section in the docs. I added the `sqlx` dependency to `Cargo.toml` and change _one line_ in `lib.rs`: ```rust #[shuttle_service::main] async fn rocket(pool: PgPool) -> Result, shuttle_service::Error> { ``` By adding a parameter to the main `rocket` function, `shuttle` will automatically provision a Postgres database for you, create an account and hand you back an authenticated connection pool which is usable from your application code. Let's deploy it and see what happens: ```bash $ cargo shuttle deploy ... Finished dev [unoptimized + debuginfo] target(s) in 19.50s Project: url-shortener Deployment Id: 538e41cf-44a9-4158-94f1-3760b42619a3 Deployment Status: DEPLOYED Host: url-shortener.shuttleapp.rs Created At: 2022-04-13 03:08:30.412602556 UTC Database URI: postgres://***:***@pg.shuttle.rs/db-url-shortener ``` I have a database! I couldn't help but chuckle a little bit. So far so good. ## Setting up the Schema - 06:30 minutes remaining The database provisioned by `shuttle` is completely empty - I'm going to need to either connect to Postgres and create the schema myself, or write some sort of code to automatically perform the migration. As I start to ponder this seemingly existential question I decide not to overthink it. I'm just going to go with whatever is easiest. I connect to the database provisioned by shuttle using [pgAdmin](https://www.pgadmin.org/) using the provided database URI and run the following script: ```sql CREATE TABLE urls ( id VARCHAR(6) PRIMARY KEY, url VARCHAR NOT NULL ); ``` As I was ready to Google 'how to create index postgres' I realised that since the `id` used for the url lookup is a primary key, which is implicitly a 'unique' constraint, Postgres would create the index for me. Cool. ## Writing the Endpoints - 05:17 remaining The app's going to need two endpoints - one to `shorten` URLs and one to retrieve URLs and `redirect` the user. I quickly created two stubs for the endpoints while I thought about the actual implementation: ```rust #[get("/")] async fn redirect(id: String, pool: &State) -> Result { unimplemented!() } #[post("/", data = "")] async fn shorten(url: String, pool: &State) -> Result { unimplemented!() } ``` I decided to start with the shorten method. The simplest implementation I could think of is to generate a unique id on the fly using the [`nanoid`](https://github.com/nikolay-govorov/nanoid) crate and then running an `INSERT` statement. Hm - what about duplicates? I decided not to overthink it 🤷. ```rust #[post("/", data = "")] async fn shorten(url: String, pool: &State) -> Result { let id = &nanoid::nanoid!(6); let p_url = Url::parse(&url).map_err(|_| Status::UnprocessableEntity)?; sqlx::query("INSERT INTO urls(id, url) VALUES ($1, $2)") .bind(id) .bind(p_url.as_str()) .execute(&**pool) .await .map_err(|_| Status::InternalServerError)?; Ok(format!("https://url-shortener.shuttleapp.rs/{id}")) } ``` Next I implemented the `redirect` method in a similar spirit. At this point I started to panic as it was really getting close to the 10 minute mark. I'll do a `SELECT *` and pull the first url that matches with the query id. If the id does not exist, you get back a `404`: ```rust #[get("/")] async fn redirect(id: String, pool: &State) -> Result { let url: (String,) = sqlx::query_as("SELECT url FROM urls WHERE id = $1") .bind(id) .fetch_one(&**pool) .await .map_err(|e| match e { Error::RowNotFound => Status::NotFound, _ => Status::InternalServerError })?; Ok(Redirect::to(url.0)) } ``` Whoops there's a typo in the SQL query. After I fixed my typo and sorted out the various unresolved dependencies by letting my IDE do the heavy lifting for me, I deployed to shuttle for the last time. ## Moment of truth - 00:25 minutes remaining Feeling like an off-brand Tom Cruise in mission impossible I stared intently at the clock counting down as shuttle deployed my url-shortener. 19.3 seconds and we're live. As soon as the `DEPLOYED` dialog came up, I instantly tested it out: ```bash $ curl -X POST -d "https://google.com" https://url-shortener.shuttleapp.rs https://s.shuttleapp.rs/XDlrTB⏎ ``` I then copy/pasted the shortened URL to my browser and, lo an behold, was redirected to Google. I did it. ## Retrospective - 00:00 minutes remaining With a sigh of relief I pushed myself back from my desk. I refilled my mug, picked it up and headed to my derelict balcony. As I slid open the the windows and the cold air flowed into my apartment, I took two steps forward to rest my elbows and mug on the railing. I sat there for a while reflecting on what had just happened. I _had_ succeeded. I'd successfully built a somewhat trivial app quickly without needing to worry about provisioning databases or networking or any of that jazz. But how would this measure up in the real world? Real software engineering is complex, involving collaboration across different teams with different skill-sets. The entire world of software is barely keeping it together. Is it really feasible to replace our existing, tried and tested cloud paradigms with a new paradigm of not having to deal with infrastructure at all? What I knew for sure is I wasn't going to get to the bottom of this one tonight. As I went back to my bedroom and laid once more in bed, I noticed I was grinning. There's a chance we really can do better. Maybe we're not exactly there yet, but my experience tonight had given me a certain optimism that we aren't as far as I once thought. With the promise of a brighter tomorrow, I turned on my side and fell asleep. --- # Building a startup with Rust Source: https://www.shuttle.dev/blog/2021/10/08/building-a-startup-with-rust Date: 8 April 2022 Author: christoshadjiaslanis Tags: rust, startup This blog post is a compilation of thoughts around building a company with Rust When building a company you are setting out to fundamentally solve a problem. For this reason, engineers have been systematically attracted by this romantic idea of changing the world with your brain and a laptop. We are at heart problem solvers. As engineers, we can (and most of us have) become zealous at times about our solutions to these problems. We have pragmatists who just get stuff done - they address the symptom fast and effectively. We have idealists who will grind at an elegant scalable solution and try to treat the disease. Whichever camp you subscribe to, at a certain point you need to form an opinion about which technologies you are going to use to solve the problems you see in the world - and this opinion will inevitably cause contention. Conventional wisdom is to 'use the right tool for the job'. The choice of programming language for example, depends on the domain of the problem you are trying to solve. If you're implementing some algorithm, in a secluded project, it's easy to make the case about what the language for the job may be. You can run a benchmark and literally test the execution time for each candidate language (if you're optimising for execution time). You can persuade yourself you've made a rational and 'objectively correct' decision. However, in the context of building a business, your optimisation function is a high-dimensional mess involving performance, development velocity, hiring, server costs, ecosystem, tooling, support, licenses etc. You can assign weights to what is most important for your business, but at the end of the day the decision is inevitably qualitative. At `shuttle`, we're building a serverless platform for Rust. We made a conscious decision to write the platform itself in Rust as well. After more than a two years of building I've had the opportunity to see Rust at its best and worst in the context of starting a company - this post is a compilation of these (at times cynical) thoughts. ## Development Velocity Rust has a _really_ steep learning curve. Coming from an OO background it took me _months_ to become productive in Rust. This was incredibly frustrating for me as I felt that my lack of productivity was impacting the team, which it was. Even when you eventually do become productive (and you will), Rust forces you to really think deeply about what you're doing and things inevitably take longer to get over the line. A poorly thought out design decision today can come back to haunt you months later. What should be a simple change or refactor can end up resulting in complete tear down as you try to appease the borrow checker. This is deadly. The entire premise of a startup is that _you have to iterate rapidly_. Very few companies know what they should be building from day one. It's an iterative process involving a feedback loop of talking to users and making changes to reflect the feedback. The faster you can make that feedback loop, the higher probability you have of success. ## Correctness The evident hit in development velocity is redeemed to an extent by Rust's emphasis on writing correct programs. "if it compiles it works' so to speak. I've found this to be true for the most part while building with Rust and it is an absolute joy to work with for this reason. Even if your program is not perfect, you understand the failure modes much better. The set of unknown failure modes is reduced substantially as your program breaks in exactly the way you expect it to. The lack of null pointers in conjunction with the `Result` paradigm (vs say, exceptions) compels you to build correct programs where edge cases are well understood and are handled explicitly by you (or `unimplemented!` but no one is perfect). If you've reached product market fit - correctness may counteract the development velocity hit. When you know what you're building you need to iterate less. Your dev team is also going to be spending less time dealing with bugs as you've already dealt with that while trying to appease the compiler. If it compiles it works - and this is an invaluable asset when you're aggressively shipping code. ## Talent Getting great talent is unbelievably important for an early stage startup. The fact that the absolute number of competent and experienced Rust developers is so small initially seems detrimental to getting great people. This is exacerbated by Rust's steep learning curve as you need to hire someone with experience, or it's going to take months for them to become productive. However, this is not the full picture. In our experience the competence of your average Rust developer is much higher than more conventional programming languages. Something spoke to these individuals when they picked up Rust, and it's hard to put your finger on it but it's that same quality that makes a great engineer. It's also been a pleasant surprise to find out that really good engineers will seek you out as an employer _because you use Rust_. They don't want to work in \*script or Java or C++. They want to work with Rust because it's great. ## Open Source We've been really lucky to have a really active set of contributors - giving ideas, reporting bugs and contributing (at times very significant) code. It is hard to know for sure, but we have a strong hunch that a lot of the contributors are active because they have an interest in Rust projects specifically. A lot of our contributors are also interested in learning Rust - not necessarily being veterans of the language. This has worked out great as the more experienced members of our core team mentor and review code of young rustaceans, building a symbiotic positive feedback loop. Thank you to all our contributors - you know who you are and you guys are amazing. ## Libraries Rust has an ecosystem of incredibly high quality libraries. The Rust core team has led by example and focused on a high quality and tight standard library. The result of a highly focused standard library is (unfortunately) a lack of canonical libraries for doing things outside the standard library. So you want a webserver, pick from one of the 100s available. You want a crate (Rust lingo for library) for working with JWT tokens? Here's 9, pick one. I mean, even something as fundamental as an asynchronous runtime is split between `tokio` and `async-std` and others. As a young rustacean this can be overwhelming. What ends up happening over time is certain libraries become implicitly canonical as they receive overwhelming support and start becoming serious dependencies differentiating from their alternatives. Also in a project update from RustConf 2021 it [was mentioned](https://youtu.be/ylOpCXI2EMM?t=1048) that the idea of having 'recommended crates' may be visited in the future. The lack of canonical non-standard libraries is an issue when you're getting started - but over time this diminishes as you get a better understanding of the ecosystem. What _has_ been constantly detrimental to our development velocity has been the lack of _client_ libraries for Rust. We've had to write a bunch of different integrations ourselves, but they're often clunky as we don't have the time to invest in making them really high quality. For example most of Google's products have at best an unofficial code-generated crate maintained by the community, and at worst absolutely nothing. You need to write it from scratch. ## Should you build your startup with Rust? Well it depends. Assuming you're building a product in the right domain for Rust (say a CLI as opposed to a social media site), even then the answer is not clear-cut. If you don't have close to 100% conviction that you know what you're building, I would be inclined to say no. Development velocity and being able to make rapid iterations is so important for an early stage startup that it outweighs a lot of the benefits that Rust brings to the table. If your company is later stage, and you now understand exactly what you should be building (assuming this is ever the case) then I would say yes. The 'correctness' of Rust programs and the propensity of Rust to attract great engineers can help in building a great engineering culture and a great company. --- # Pages ## Home Source: https://www.shuttle.dev/ Shuttle - Build backends fast ## Pricing Source: https://www.shuttle.dev/pricing Shuttle pricing plans and information ## About Source: https://www.shuttle.dev/about About Shuttle and our mission ## Starters Source: https://www.shuttle.dev/starters Get started quickly with Shuttle starter templates ## Careers Source: https://www.shuttle.dev/careers Join the Shuttle team ## Contact Source: https://www.shuttle.dev/contact Get in touch with Shuttle ## Shuttle AI Source: https://www.shuttle.dev/ai AI-powered backend development ## Shuttle Batch Source: https://www.shuttle.dev/shuttle-batch Shuttle's accelerator program ## Shuttle Heroes Source: https://www.shuttle.dev/shuttle-heroes Community champions and contributors ## Launchpad Source: https://www.shuttle.dev/launchpad Shuttle's newsletter for backend developers ## Privacy Policy Source: https://www.shuttle.dev/privacy Shuttle privacy policy ## Terms of Service Source: https://www.shuttle.dev/terms Shuttle terms of service ## Acceptable Use Policy Source: https://www.shuttle.dev/acceptable-use Shuttle acceptable use policy ## Cookie Policy Source: https://www.shuttle.dev/cookies Shuttle cookie policy ## Data Processing Agreement Source: https://www.shuttle.dev/dpa Shuttle data processing agreement