autoworkflow 3.1.5 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Axum Skill
|
|
2
|
+
|
|
3
|
+
## Application Setup
|
|
4
|
+
\`\`\`rust
|
|
5
|
+
use axum::{
|
|
6
|
+
Router,
|
|
7
|
+
routing::{get, post, put, delete},
|
|
8
|
+
middleware,
|
|
9
|
+
};
|
|
10
|
+
use tower_http::{cors::CorsLayer, trace::TraceLayer, compression::CompressionLayer};
|
|
11
|
+
use std::net::SocketAddr;
|
|
12
|
+
|
|
13
|
+
#[tokio::main]
|
|
14
|
+
async fn main() {
|
|
15
|
+
// Initialize tracing
|
|
16
|
+
tracing_subscriber::init();
|
|
17
|
+
|
|
18
|
+
// Create shared state
|
|
19
|
+
let db_pool = create_pool().await.expect("Failed to create pool");
|
|
20
|
+
let state = AppState { db: db_pool };
|
|
21
|
+
|
|
22
|
+
// Build router
|
|
23
|
+
let app = Router::new()
|
|
24
|
+
.nest("/api/v1", api_routes())
|
|
25
|
+
.layer(TraceLayer::new_for_http())
|
|
26
|
+
.layer(CompressionLayer::new())
|
|
27
|
+
.layer(CorsLayer::permissive())
|
|
28
|
+
.with_state(state);
|
|
29
|
+
|
|
30
|
+
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
|
31
|
+
tracing::info!("Listening on {}", addr);
|
|
32
|
+
|
|
33
|
+
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
34
|
+
axum::serve(listener, app).await.unwrap();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn api_routes() -> Router<AppState> {
|
|
38
|
+
Router::new()
|
|
39
|
+
.nest("/auth", auth_routes())
|
|
40
|
+
.nest("/users", user_routes())
|
|
41
|
+
.nest("/posts", post_routes())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn auth_routes() -> Router<AppState> {
|
|
45
|
+
Router::new()
|
|
46
|
+
.route("/login", post(login))
|
|
47
|
+
.route("/register", post(register))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn user_routes() -> Router<AppState> {
|
|
51
|
+
Router::new()
|
|
52
|
+
.route("/", get(list_users).post(create_user))
|
|
53
|
+
.route("/:id", get(get_user).put(update_user).delete(delete_user))
|
|
54
|
+
.layer(middleware::from_fn(auth_middleware))
|
|
55
|
+
}
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
## Shared State
|
|
59
|
+
\`\`\`rust
|
|
60
|
+
use axum::extract::State;
|
|
61
|
+
use sqlx::PgPool;
|
|
62
|
+
|
|
63
|
+
#[derive(Clone)]
|
|
64
|
+
pub struct AppState {
|
|
65
|
+
pub db: PgPool,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Access in handlers
|
|
69
|
+
async fn get_user(
|
|
70
|
+
State(state): State<AppState>,
|
|
71
|
+
Path(id): Path<String>,
|
|
72
|
+
) -> Result<Json<User>, AppError> {
|
|
73
|
+
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
|
74
|
+
.fetch_optional(&state.db)
|
|
75
|
+
.await?
|
|
76
|
+
.ok_or(AppError::NotFound)?;
|
|
77
|
+
|
|
78
|
+
Ok(Json(user))
|
|
79
|
+
}
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
## Extractors
|
|
83
|
+
\`\`\`rust
|
|
84
|
+
use axum::{
|
|
85
|
+
extract::{Path, Query, State, Json},
|
|
86
|
+
http::HeaderMap,
|
|
87
|
+
};
|
|
88
|
+
use serde::Deserialize;
|
|
89
|
+
|
|
90
|
+
// JSON body with validation
|
|
91
|
+
#[derive(Deserialize)]
|
|
92
|
+
pub struct CreateUserRequest {
|
|
93
|
+
pub email: String,
|
|
94
|
+
pub name: String,
|
|
95
|
+
pub password: String,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async fn create_user(
|
|
99
|
+
State(state): State<AppState>,
|
|
100
|
+
Json(body): Json<CreateUserRequest>,
|
|
101
|
+
) -> Result<(StatusCode, Json<User>), AppError> {
|
|
102
|
+
// Validate
|
|
103
|
+
if body.email.is_empty() {
|
|
104
|
+
return Err(AppError::Validation("Email is required".into()));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let user = User::new(body.email, body.name, &body.password)?;
|
|
108
|
+
// ... save user
|
|
109
|
+
|
|
110
|
+
Ok((StatusCode::CREATED, Json(user)))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Query parameters
|
|
114
|
+
#[derive(Deserialize)]
|
|
115
|
+
pub struct PaginationQuery {
|
|
116
|
+
#[serde(default = "default_page")]
|
|
117
|
+
pub page: u32,
|
|
118
|
+
#[serde(default = "default_per_page")]
|
|
119
|
+
pub per_page: u32,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn default_page() -> u32 { 1 }
|
|
123
|
+
fn default_per_page() -> u32 { 20 }
|
|
124
|
+
|
|
125
|
+
async fn list_users(
|
|
126
|
+
State(state): State<AppState>,
|
|
127
|
+
Query(query): Query<PaginationQuery>,
|
|
128
|
+
) -> Result<Json<Vec<User>>, AppError> {
|
|
129
|
+
let offset = (query.page - 1) * query.per_page;
|
|
130
|
+
let users = sqlx::query_as!(User,
|
|
131
|
+
"SELECT * FROM users LIMIT $1 OFFSET $2",
|
|
132
|
+
query.per_page as i64,
|
|
133
|
+
offset as i64
|
|
134
|
+
)
|
|
135
|
+
.fetch_all(&state.db)
|
|
136
|
+
.await?;
|
|
137
|
+
|
|
138
|
+
Ok(Json(users))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Multiple path parameters
|
|
142
|
+
async fn get_post_comment(
|
|
143
|
+
Path((post_id, comment_id)): Path<(String, String)>,
|
|
144
|
+
) -> Result<Json<Comment>, AppError> {
|
|
145
|
+
// ...
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Headers
|
|
149
|
+
async fn with_headers(headers: HeaderMap) -> impl IntoResponse {
|
|
150
|
+
let user_agent = headers
|
|
151
|
+
.get("user-agent")
|
|
152
|
+
.and_then(|v| v.to_str().ok())
|
|
153
|
+
.unwrap_or("unknown");
|
|
154
|
+
format!("User-Agent: {}", user_agent)
|
|
155
|
+
}
|
|
156
|
+
\`\`\`
|
|
157
|
+
|
|
158
|
+
## Custom Extractors
|
|
159
|
+
\`\`\`rust
|
|
160
|
+
use axum::{
|
|
161
|
+
async_trait,
|
|
162
|
+
extract::FromRequestParts,
|
|
163
|
+
http::{request::Parts, StatusCode},
|
|
164
|
+
RequestPartsExt,
|
|
165
|
+
};
|
|
166
|
+
use axum_extra::{
|
|
167
|
+
headers::{authorization::Bearer, Authorization},
|
|
168
|
+
TypedHeader,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
pub struct AuthUser {
|
|
172
|
+
pub user_id: String,
|
|
173
|
+
pub role: String,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[async_trait]
|
|
177
|
+
impl<S> FromRequestParts<S> for AuthUser
|
|
178
|
+
where
|
|
179
|
+
S: Send + Sync,
|
|
180
|
+
{
|
|
181
|
+
type Rejection = AppError;
|
|
182
|
+
|
|
183
|
+
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
|
184
|
+
// Extract Authorization header
|
|
185
|
+
let TypedHeader(Authorization(bearer)) = parts
|
|
186
|
+
.extract::<TypedHeader<Authorization<Bearer>>>()
|
|
187
|
+
.await
|
|
188
|
+
.map_err(|_| AppError::Unauthorized)?;
|
|
189
|
+
|
|
190
|
+
// Validate token
|
|
191
|
+
let claims = validate_token(bearer.token())
|
|
192
|
+
.map_err(|_| AppError::Unauthorized)?;
|
|
193
|
+
|
|
194
|
+
Ok(AuthUser {
|
|
195
|
+
user_id: claims.sub,
|
|
196
|
+
role: claims.role,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Use in handler
|
|
202
|
+
async fn protected_handler(auth: AuthUser) -> impl IntoResponse {
|
|
203
|
+
format!("Hello, user {}", auth.user_id)
|
|
204
|
+
}
|
|
205
|
+
\`\`\`
|
|
206
|
+
|
|
207
|
+
## Middleware
|
|
208
|
+
\`\`\`rust
|
|
209
|
+
use axum::{
|
|
210
|
+
middleware::Next,
|
|
211
|
+
http::Request,
|
|
212
|
+
response::Response,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Function-based middleware
|
|
216
|
+
async fn auth_middleware(
|
|
217
|
+
request: Request<axum::body::Body>,
|
|
218
|
+
next: Next,
|
|
219
|
+
) -> Result<Response, AppError> {
|
|
220
|
+
let auth_header = request
|
|
221
|
+
.headers()
|
|
222
|
+
.get("Authorization")
|
|
223
|
+
.and_then(|v| v.to_str().ok());
|
|
224
|
+
|
|
225
|
+
let token = match auth_header {
|
|
226
|
+
Some(h) if h.starts_with("Bearer ") => &h[7..],
|
|
227
|
+
_ => return Err(AppError::Unauthorized),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
validate_token(token).map_err(|_| AppError::Unauthorized)?;
|
|
231
|
+
|
|
232
|
+
Ok(next.run(request).await)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Apply to routes
|
|
236
|
+
fn protected_routes() -> Router<AppState> {
|
|
237
|
+
Router::new()
|
|
238
|
+
.route("/profile", get(get_profile))
|
|
239
|
+
.layer(middleware::from_fn(auth_middleware))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Request timing middleware
|
|
243
|
+
async fn timing_middleware(
|
|
244
|
+
request: Request<axum::body::Body>,
|
|
245
|
+
next: Next,
|
|
246
|
+
) -> Response {
|
|
247
|
+
let start = std::time::Instant::now();
|
|
248
|
+
let method = request.method().clone();
|
|
249
|
+
let uri = request.uri().clone();
|
|
250
|
+
|
|
251
|
+
let response = next.run(request).await;
|
|
252
|
+
|
|
253
|
+
let duration = start.elapsed();
|
|
254
|
+
tracing::info!("{} {} - {:?}", method, uri, duration);
|
|
255
|
+
|
|
256
|
+
response
|
|
257
|
+
}
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
## Error Handling
|
|
261
|
+
\`\`\`rust
|
|
262
|
+
use axum::{
|
|
263
|
+
response::{IntoResponse, Response},
|
|
264
|
+
http::StatusCode,
|
|
265
|
+
Json,
|
|
266
|
+
};
|
|
267
|
+
use thiserror::Error;
|
|
268
|
+
|
|
269
|
+
#[derive(Error, Debug)]
|
|
270
|
+
pub enum AppError {
|
|
271
|
+
#[error("Resource not found")]
|
|
272
|
+
NotFound,
|
|
273
|
+
|
|
274
|
+
#[error("Unauthorized")]
|
|
275
|
+
Unauthorized,
|
|
276
|
+
|
|
277
|
+
#[error("Validation error: {0}")]
|
|
278
|
+
Validation(String),
|
|
279
|
+
|
|
280
|
+
#[error("Database error")]
|
|
281
|
+
Database(#[from] sqlx::Error),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
impl IntoResponse for AppError {
|
|
285
|
+
fn into_response(self) -> Response {
|
|
286
|
+
let (status, message) = match &self {
|
|
287
|
+
AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
|
288
|
+
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
|
289
|
+
AppError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
|
290
|
+
AppError::Database(e) => {
|
|
291
|
+
tracing::error!("Database error: {:?}", e);
|
|
292
|
+
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into())
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
let body = Json(serde_json::json!({
|
|
297
|
+
"error": message
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
(status, body).into_response()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Handler returning Result
|
|
305
|
+
async fn get_user(
|
|
306
|
+
State(state): State<AppState>,
|
|
307
|
+
Path(id): Path<String>,
|
|
308
|
+
) -> Result<Json<User>, AppError> {
|
|
309
|
+
let user = find_user(&state.db, &id)
|
|
310
|
+
.await?
|
|
311
|
+
.ok_or(AppError::NotFound)?;
|
|
312
|
+
|
|
313
|
+
Ok(Json(user))
|
|
314
|
+
}
|
|
315
|
+
\`\`\`
|
|
316
|
+
|
|
317
|
+
## Testing
|
|
318
|
+
\`\`\`rust
|
|
319
|
+
#[cfg(test)]
|
|
320
|
+
mod tests {
|
|
321
|
+
use super::*;
|
|
322
|
+
use axum::{
|
|
323
|
+
body::Body,
|
|
324
|
+
http::{Request, StatusCode},
|
|
325
|
+
};
|
|
326
|
+
use tower::ServiceExt; // for oneshot
|
|
327
|
+
|
|
328
|
+
async fn create_test_app() -> Router {
|
|
329
|
+
let state = AppState {
|
|
330
|
+
db: create_test_pool().await,
|
|
331
|
+
};
|
|
332
|
+
Router::new()
|
|
333
|
+
.nest("/api/v1", api_routes())
|
|
334
|
+
.with_state(state)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[tokio::test]
|
|
338
|
+
async fn test_create_user() {
|
|
339
|
+
let app = create_test_app().await;
|
|
340
|
+
|
|
341
|
+
let response = app
|
|
342
|
+
.oneshot(
|
|
343
|
+
Request::builder()
|
|
344
|
+
.method("POST")
|
|
345
|
+
.uri("/api/v1/users")
|
|
346
|
+
.header("Content-Type", "application/json")
|
|
347
|
+
.body(Body::from(r#"{"email":"test@example.com","name":"Test","password":"password123"}"#))
|
|
348
|
+
.unwrap()
|
|
349
|
+
)
|
|
350
|
+
.await
|
|
351
|
+
.unwrap();
|
|
352
|
+
|
|
353
|
+
assert_eq!(response.status(), StatusCode::CREATED);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#[tokio::test]
|
|
357
|
+
async fn test_get_user_not_found() {
|
|
358
|
+
let app = create_test_app().await;
|
|
359
|
+
|
|
360
|
+
let response = app
|
|
361
|
+
.oneshot(
|
|
362
|
+
Request::builder()
|
|
363
|
+
.uri("/api/v1/users/nonexistent")
|
|
364
|
+
.body(Body::empty())
|
|
365
|
+
.unwrap()
|
|
366
|
+
)
|
|
367
|
+
.await
|
|
368
|
+
.unwrap();
|
|
369
|
+
|
|
370
|
+
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
\`\`\`
|
|
374
|
+
|
|
375
|
+
## ✅ DO
|
|
376
|
+
- Use \`Router::nest\` for modular route organization
|
|
377
|
+
- Implement \`IntoResponse\` for custom error types
|
|
378
|
+
- Use \`tower\` layers for cross-cutting concerns
|
|
379
|
+
- Use custom extractors for auth/validation
|
|
380
|
+
- Clone state only when needed (it should be \`Arc\` internally)
|
|
381
|
+
|
|
382
|
+
## ❌ DON'T
|
|
383
|
+
- Don't block async runtime with sync operations
|
|
384
|
+
- Don't forget to derive \`Clone\` for \`AppState\`
|
|
385
|
+
- Don't expose internal errors to clients
|
|
386
|
+
- Don't use \`.unwrap()\` in handlers - return \`Result\`
|