rust
57 lines · 9 steps
Typed error handling in an Axum handler
An Axum route extracts typed params, validates input, queries the database, and maps every failure to an HTTP response through one error enum.
Explained by
highlit
1use axum::{
2 extract::{Path, State},
3 http::StatusCode,
4 response::{IntoResponse, Response},
5 Json,
6};
7use serde::Deserialize;
8use uuid::Uuid;
9
10#[derive(Deserialize)]
11pub struct PostParams {
12 workspace_id: Uuid,
13 post_slug: String,
14}
15
16pub async fn get_post(
17 State(state): State<AppState>,
18 Path(PostParams { workspace_id, post_slug }): Path<PostParams>,
19) -> Result<Json<Post>, ApiError> {
20 if post_slug.len() > 128 || !post_slug.chars().all(|c| c.is_alphanumeric() || c == '-') {
21 return Err(ApiError::InvalidSlug);
22 }
23
24 let post = sqlx::query_as::<_, Post>(
25 "SELECT * FROM posts WHERE workspace_id = $1 AND slug = $2",
26 )
27 .bind(workspace_id)
28 .bind(&post_slug)
29 .fetch_optional(&state.db)
30 .await?
31 .ok_or(ApiError::NotFound)?;
32
33 Ok(Json(post))
34}
35
36pub enum ApiError {
37 InvalidSlug,
38 NotFound,
39 Database(sqlx::Error),
40}
41
42impl From<sqlx::Error> for ApiError {
43 fn from(err: sqlx::Error) -> Self {
44 ApiError::Database(err)
45 }
46}
47
48impl IntoResponse for ApiError {
49 fn into_response(self) -> Response {
50 let (status, message) = match self {
51 ApiError::InvalidSlug => (StatusCode::BAD_REQUEST, "invalid post slug"),
52 ApiError::NotFound => (StatusCode::NOT_FOUND, "post not found"),
53 ApiError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal error"),
54 };
55 (status, Json(serde_json::json!({ "error": message }))).into_response()
56 }
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A single error enum implementing IntoResponse centralizes how every failure becomes an HTTP status and body.
- 2Implementing From lets the `?` operator convert library errors into your domain error automatically.
- 3Destructuring extractors in the function signature pulls typed request data straight into named locals.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/typed-error-handling-in-an-axum-handler-explained-rust-28cf/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.