autoworkflow 3.1.4 → 3.5.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.
Files changed (123) hide show
  1. package/.claude/commands/analyze.md +19 -0
  2. package/.claude/commands/audit.md +174 -11
  3. package/.claude/commands/build.md +39 -0
  4. package/.claude/commands/commit.md +25 -0
  5. package/.claude/commands/fix.md +23 -0
  6. package/.claude/commands/plan.md +18 -0
  7. package/.claude/commands/suggest.md +23 -0
  8. package/.claude/commands/verify.md +18 -0
  9. package/.claude/hooks/post-bash-router.sh +20 -0
  10. package/.claude/hooks/post-commit.sh +140 -0
  11. package/.claude/hooks/pre-edit.sh +129 -0
  12. package/.claude/hooks/session-check.sh +79 -0
  13. package/.claude/settings.json +40 -6
  14. package/.claude/settings.local.json +3 -1
  15. package/.claude/skills/actix.md +337 -0
  16. package/.claude/skills/alembic.md +504 -0
  17. package/.claude/skills/angular.md +237 -0
  18. package/.claude/skills/api-design.md +187 -0
  19. package/.claude/skills/aspnet-core.md +377 -0
  20. package/.claude/skills/astro.md +245 -0
  21. package/.claude/skills/auth-clerk.md +327 -0
  22. package/.claude/skills/auth-firebase.md +367 -0
  23. package/.claude/skills/auth-nextauth.md +359 -0
  24. package/.claude/skills/auth-supabase.md +368 -0
  25. package/.claude/skills/axum.md +386 -0
  26. package/.claude/skills/blazor.md +456 -0
  27. package/.claude/skills/chi.md +348 -0
  28. package/.claude/skills/code-review.md +133 -0
  29. package/.claude/skills/csharp.md +296 -0
  30. package/.claude/skills/css-modules.md +325 -0
  31. package/.claude/skills/cypress.md +343 -0
  32. package/.claude/skills/debugging.md +133 -0
  33. package/.claude/skills/diesel.md +392 -0
  34. package/.claude/skills/django.md +301 -0
  35. package/.claude/skills/docker.md +319 -0
  36. package/.claude/skills/doctrine.md +473 -0
  37. package/.claude/skills/documentation.md +182 -0
  38. package/.claude/skills/dotnet.md +409 -0
  39. package/.claude/skills/drizzle.md +293 -0
  40. package/.claude/skills/echo.md +321 -0
  41. package/.claude/skills/eloquent.md +256 -0
  42. package/.claude/skills/emotion.md +426 -0
  43. package/.claude/skills/entity-framework.md +370 -0
  44. package/.claude/skills/express.md +316 -0
  45. package/.claude/skills/fastapi.md +329 -0
  46. package/.claude/skills/fastify.md +299 -0
  47. package/.claude/skills/fiber.md +315 -0
  48. package/.claude/skills/flask.md +322 -0
  49. package/.claude/skills/gin.md +342 -0
  50. package/.claude/skills/git.md +116 -0
  51. package/.claude/skills/github-actions.md +353 -0
  52. package/.claude/skills/go.md +377 -0
  53. package/.claude/skills/gorm.md +409 -0
  54. package/.claude/skills/graphql.md +478 -0
  55. package/.claude/skills/hibernate.md +379 -0
  56. package/.claude/skills/hono.md +306 -0
  57. package/.claude/skills/java.md +400 -0
  58. package/.claude/skills/jest.md +313 -0
  59. package/.claude/skills/jpa.md +282 -0
  60. package/.claude/skills/kotlin.md +347 -0
  61. package/.claude/skills/kubernetes.md +363 -0
  62. package/.claude/skills/laravel.md +414 -0
  63. package/.claude/skills/mcp-browser.md +320 -0
  64. package/.claude/skills/mcp-database.md +219 -0
  65. package/.claude/skills/mcp-fetch.md +241 -0
  66. package/.claude/skills/mcp-filesystem.md +204 -0
  67. package/.claude/skills/mcp-github.md +217 -0
  68. package/.claude/skills/mcp-memory.md +240 -0
  69. package/.claude/skills/mcp-search.md +218 -0
  70. package/.claude/skills/mcp-slack.md +262 -0
  71. package/.claude/skills/micronaut.md +388 -0
  72. package/.claude/skills/mongodb.md +319 -0
  73. package/.claude/skills/mongoose.md +355 -0
  74. package/.claude/skills/mysql.md +281 -0
  75. package/.claude/skills/nestjs.md +335 -0
  76. package/.claude/skills/nextjs-app-router.md +260 -0
  77. package/.claude/skills/nextjs-pages.md +172 -0
  78. package/.claude/skills/nuxt.md +202 -0
  79. package/.claude/skills/openapi.md +489 -0
  80. package/.claude/skills/performance.md +199 -0
  81. package/.claude/skills/php.md +398 -0
  82. package/.claude/skills/playwright.md +371 -0
  83. package/.claude/skills/postgresql.md +257 -0
  84. package/.claude/skills/prisma.md +293 -0
  85. package/.claude/skills/pydantic.md +304 -0
  86. package/.claude/skills/pytest.md +313 -0
  87. package/.claude/skills/python.md +272 -0
  88. package/.claude/skills/quarkus.md +377 -0
  89. package/.claude/skills/react.md +230 -0
  90. package/.claude/skills/redis.md +391 -0
  91. package/.claude/skills/refactoring.md +143 -0
  92. package/.claude/skills/remix.md +246 -0
  93. package/.claude/skills/rest-api.md +490 -0
  94. package/.claude/skills/rocket.md +366 -0
  95. package/.claude/skills/rust.md +341 -0
  96. package/.claude/skills/sass.md +380 -0
  97. package/.claude/skills/sea-orm.md +382 -0
  98. package/.claude/skills/security.md +167 -0
  99. package/.claude/skills/sequelize.md +395 -0
  100. package/.claude/skills/spring-boot.md +416 -0
  101. package/.claude/skills/sqlalchemy.md +269 -0
  102. package/.claude/skills/sqlx-rust.md +408 -0
  103. package/.claude/skills/state-jotai.md +346 -0
  104. package/.claude/skills/state-mobx.md +353 -0
  105. package/.claude/skills/state-pinia.md +431 -0
  106. package/.claude/skills/state-redux.md +337 -0
  107. package/.claude/skills/state-tanstack-query.md +434 -0
  108. package/.claude/skills/state-zustand.md +340 -0
  109. package/.claude/skills/styled-components.md +403 -0
  110. package/.claude/skills/svelte.md +238 -0
  111. package/.claude/skills/sveltekit.md +207 -0
  112. package/.claude/skills/symfony.md +437 -0
  113. package/.claude/skills/tailwind.md +279 -0
  114. package/.claude/skills/terraform.md +394 -0
  115. package/.claude/skills/testing-library.md +371 -0
  116. package/.claude/skills/trpc.md +426 -0
  117. package/.claude/skills/typeorm.md +368 -0
  118. package/.claude/skills/vitest.md +330 -0
  119. package/.claude/skills/vue.md +202 -0
  120. package/.claude/skills/warp.md +365 -0
  121. package/README.md +135 -52
  122. package/package.json +1 -1
  123. package/system/triggers.md +152 -11
@@ -0,0 +1,129 @@
1
+ #!/bin/bash
2
+ # AutoWorkflow Pre-Edit Hook
3
+ # Runs on: PreToolUse for Write/Edit tools
4
+ # Purpose: Enforce plan approval before implementation
5
+ #
6
+ # This hook implements the plan_approval_gate enforcement
7
+ # It BLOCKS Write/Edit operations if plan hasn't been approved
8
+
9
+ # Colors
10
+ RED='\033[0;31m'
11
+ GREEN='\033[0;32m'
12
+ YELLOW='\033[1;33m'
13
+ CYAN='\033[0;36m'
14
+ BOLD='\033[1m'
15
+ DIM='\033[2m'
16
+ NC='\033[0m'
17
+
18
+ # State directory
19
+ STATE_DIR=".claude/.autoworkflow"
20
+
21
+ # State files
22
+ PHASE_FILE="$STATE_DIR/phase"
23
+ TASK_TYPE_FILE="$STATE_DIR/task-type"
24
+ PLAN_APPROVED_FILE="$STATE_DIR/plan-approved"
25
+
26
+ # Get current phase
27
+ get_phase() {
28
+ if [ -f "$PHASE_FILE" ]; then
29
+ cat "$PHASE_FILE"
30
+ else
31
+ echo "IDLE"
32
+ fi
33
+ }
34
+
35
+ # Get task type
36
+ get_task_type() {
37
+ if [ -f "$TASK_TYPE_FILE" ]; then
38
+ cat "$TASK_TYPE_FILE"
39
+ else
40
+ echo "unknown"
41
+ fi
42
+ }
43
+
44
+ # Check if plan is approved
45
+ is_plan_approved() {
46
+ if [ -f "$PLAN_APPROVED_FILE" ]; then
47
+ local status=$(cat "$PLAN_APPROVED_FILE")
48
+ [ "$status" = "true" ] && return 0
49
+ fi
50
+ return 1
51
+ }
52
+
53
+ # Check if task type requires approval
54
+ requires_approval() {
55
+ local task_type="$1"
56
+ case "$task_type" in
57
+ # These task types require plan approval
58
+ feature|fix|refactor|perf|security|test)
59
+ return 0
60
+ ;;
61
+ # These can proceed without explicit approval
62
+ docs|style|config|query)
63
+ return 1
64
+ ;;
65
+ # Unknown defaults to requiring approval
66
+ *)
67
+ return 0
68
+ ;;
69
+ esac
70
+ }
71
+
72
+ # Main check
73
+ main() {
74
+ local phase=$(get_phase)
75
+ local task_type=$(get_task_type)
76
+
77
+ # Skip check if no task is active
78
+ if [ "$phase" = "IDLE" ]; then
79
+ exit 0
80
+ fi
81
+
82
+ # Skip check for task types that don't require approval
83
+ if ! requires_approval "$task_type"; then
84
+ exit 0
85
+ fi
86
+
87
+ # Skip check if already in IMPLEMENT or later phases
88
+ case "$phase" in
89
+ IMPLEMENT|VERIFY|FIX|AUDIT|PRE_COMMIT|COMMIT|UPDATE)
90
+ # Already past approval, allow edits
91
+ exit 0
92
+ ;;
93
+ esac
94
+
95
+ # Check if we're in a pre-approval phase
96
+ if [ "$phase" = "ANALYZE" ] || [ "$phase" = "PLAN" ]; then
97
+ if ! is_plan_approved; then
98
+ echo ""
99
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
100
+ echo -e "${RED}${BOLD}⛔ AUTOWORKFLOW: PLAN APPROVAL REQUIRED${NC}"
101
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
102
+ echo ""
103
+ echo -e "${CYAN}Current Phase:${NC} $phase"
104
+ echo -e "${CYAN}Task Type:${NC} $task_type"
105
+ echo -e "${CYAN}Plan Approved:${NC} NO"
106
+ echo ""
107
+ echo "Cannot edit files before plan approval."
108
+ echo ""
109
+ echo -e "${BOLD}Required workflow:${NC}"
110
+ echo " 1. Complete ANALYZE phase (read relevant files)"
111
+ echo " 2. Present PLAN with suggestions"
112
+ echo " 3. Wait for user approval"
113
+ echo " 4. THEN implement"
114
+ echo ""
115
+ echo -e "${DIM}To approve, user must say: yes, proceed, approved, go ahead${NC}"
116
+ echo ""
117
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
118
+ echo ""
119
+
120
+ # Exit with error to BLOCK the edit
121
+ exit 1
122
+ fi
123
+ fi
124
+
125
+ # All checks passed, allow edit
126
+ exit 0
127
+ }
128
+
129
+ main
@@ -28,6 +28,78 @@ PHASE_FILE="$STATE_DIR/phase"
28
28
  TASK_TYPE_FILE="$STATE_DIR/task-type"
29
29
  BLUEPRINT_CHECK_FILE="$STATE_DIR/blueprint-checked"
30
30
  TASK_DESCRIPTION_FILE="$STATE_DIR/task-description"
31
+ PLAN_APPROVED_FILE="$STATE_DIR/plan-approved"
32
+ CHANGED_FILES_FILE="$STATE_DIR/changed-files"
33
+ SESSION_RESUMED_FILE="$STATE_DIR/session-resumed"
34
+
35
+ # Check for resumable session state
36
+ check_session_resume() {
37
+ # Only show resume prompt once per session
38
+ if [ -f "$SESSION_RESUMED_FILE" ]; then
39
+ return 1
40
+ fi
41
+
42
+ # Check if there's a previous session with active state
43
+ if [ -f "$PHASE_FILE" ]; then
44
+ local phase=$(cat "$PHASE_FILE")
45
+
46
+ # Only prompt for non-idle, non-complete states
47
+ if [ "$phase" != "IDLE" ] && [ "$phase" != "" ] && [ "$phase" != "COMMIT" ]; then
48
+ local task_type=""
49
+ local verify_iter="0"
50
+ local plan_approved="false"
51
+ local changed_count="0"
52
+
53
+ [ -f "$TASK_TYPE_FILE" ] && task_type=$(cat "$TASK_TYPE_FILE")
54
+ [ -f "$STATE_DIR/verify-iteration" ] && verify_iter=$(cat "$STATE_DIR/verify-iteration")
55
+ [ -f "$PLAN_APPROVED_FILE" ] && plan_approved=$(cat "$PLAN_APPROVED_FILE")
56
+ [ -f "$CHANGED_FILES_FILE" ] && changed_count=$(wc -l < "$CHANGED_FILES_FILE" 2>/dev/null | tr -d ' ')
57
+
58
+ echo ""
59
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
60
+ echo -e "${CYAN}${BOLD}📍 AUTOWORKFLOW: SESSION RESUME${NC}"
61
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
62
+ echo ""
63
+ echo -e "${BOLD}Previous session state detected:${NC}"
64
+ echo ""
65
+ echo -e " ${CYAN}Phase:${NC} $phase"
66
+ [ -n "$task_type" ] && echo -e " ${CYAN}Task type:${NC} $task_type"
67
+ echo -e " ${CYAN}Plan approved:${NC} $plan_approved"
68
+ echo -e " ${CYAN}Verify attempts:${NC} $verify_iter/10"
69
+ [ "$changed_count" -gt 0 ] 2>/dev/null && echo -e " ${CYAN}Changed files:${NC} $changed_count"
70
+ echo ""
71
+
72
+ # Show changed files if any
73
+ if [ -f "$CHANGED_FILES_FILE" ] && [ "$changed_count" -gt 0 ] 2>/dev/null; then
74
+ echo -e "${DIM}Pending changes:${NC}"
75
+ head -5 "$CHANGED_FILES_FILE" | while read file; do
76
+ echo -e " ${DIM}- $file${NC}"
77
+ done
78
+ [ "$changed_count" -gt 5 ] 2>/dev/null && echo -e " ${DIM}... and $((changed_count - 5)) more${NC}"
79
+ echo ""
80
+ fi
81
+
82
+ # Show workflow position
83
+ local workflow=$(get_workflow "$task_type")
84
+ echo -e "${DIM}Workflow: $workflow${NC}"
85
+ echo -e "${DIM} ↑ Current: $phase${NC}"
86
+ echo ""
87
+
88
+ echo "Continue from $phase phase, or start fresh?"
89
+ echo ""
90
+ echo -e "${DIM}(Say 'continue' to resume, or describe new task to start fresh)${NC}"
91
+ echo ""
92
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
93
+ echo ""
94
+
95
+ # Mark that we've shown the resume prompt
96
+ touch "$SESSION_RESUMED_FILE"
97
+ return 0
98
+ fi
99
+ fi
100
+
101
+ return 1
102
+ }
31
103
 
32
104
  # Generate session ID if new session
33
105
  init_session() {
@@ -39,6 +111,8 @@ init_session() {
39
111
  rm -f "$STATE_DIR/changed-files" 2>/dev/null
40
112
  rm -f "$BLUEPRINT_CHECK_FILE" 2>/dev/null
41
113
  rm -f "$TASK_TYPE_FILE" 2>/dev/null
114
+ rm -f "$PLAN_APPROVED_FILE" 2>/dev/null
115
+ rm -f "$SESSION_RESUMED_FILE" 2>/dev/null
42
116
  return 0 # New session
43
117
  fi
44
118
  return 1 # Existing session
@@ -335,6 +409,11 @@ main() {
335
409
  exit 0
336
410
  fi
337
411
 
412
+ # Check for session resume (only on existing sessions)
413
+ if [ "$is_new_session" = false ]; then
414
+ check_session_resume
415
+ fi
416
+
338
417
  # Check for missing blueprint (triggers auto-audit)
339
418
  check_blueprint
340
419
 
@@ -18,6 +18,30 @@
18
18
  ]
19
19
  }
20
20
  ],
21
+ "PreToolUse": [
22
+ {
23
+ "matcher": "Write|Edit",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "./.claude/hooks/pre-edit.sh",
28
+ "timeout": 5,
29
+ "statusMessage": "Checking plan approval..."
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "matcher": "Bash",
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "./.claude/hooks/pre-tool-router.sh \"$TOOL_INPUT\"",
39
+ "timeout": 300,
40
+ "statusMessage": "Checking workflow gates..."
41
+ }
42
+ ]
43
+ }
44
+ ],
21
45
  "PostToolUse": [
22
46
  {
23
47
  "matcher": "Write|Edit",
@@ -29,17 +53,15 @@
29
53
  "statusMessage": "Running verification..."
30
54
  }
31
55
  ]
32
- }
33
- ],
34
- "PreToolUse": [
56
+ },
35
57
  {
36
58
  "matcher": "Bash",
37
59
  "hooks": [
38
60
  {
39
61
  "type": "command",
40
- "command": "./.claude/hooks/pre-tool-router.sh \"$TOOL_INPUT\"",
41
- "timeout": 300,
42
- "statusMessage": "Checking workflow gates..."
62
+ "command": "./.claude/hooks/post-bash-router.sh \"$TOOL_INPUT\"",
63
+ "timeout": 30,
64
+ "statusMessage": "Checking post-command actions..."
43
65
  }
44
66
  ]
45
67
  }
@@ -56,6 +78,18 @@
56
78
  "ANALYZE → PLAN → CONFIRM → IMPLEMENT → VERIFY → AUDIT → COMMIT → UPDATE",
57
79
  "```",
58
80
  "",
81
+ "### Skill Loading (CRITICAL)",
82
+ "When executing ANY command from .claude/commands/:",
83
+ "1. Read the command's YAML frontmatter",
84
+ "2. Check for `skills_required` array",
85
+ "3. For EACH skill listed, READ the file from `.claude/skills/` BEFORE executing",
86
+ "4. Apply the knowledge from skills during command execution",
87
+ "",
88
+ "Example: `/audit` has `skills_required: [security.md, code-review.md]`",
89
+ "→ Read `.claude/skills/security.md` first",
90
+ "→ Read `.claude/skills/code-review.md` second",
91
+ "→ THEN execute the audit with this knowledge loaded",
92
+ "",
59
93
  "### Auto-Triggers (Hooks enforce these)",
60
94
  "1. **SESSION START**: If instructions/BLUEPRINT.md missing → AUTO-RUN audit (no permission needed)",
61
95
  "2. **AFTER CODE CHANGES**: Run `npm run verify` automatically. Fix errors until passing (max 10 iterations)",
@@ -11,7 +11,9 @@
11
11
  "Bash(npm run typecheck:*)",
12
12
  "Bash(git add:*)",
13
13
  "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(hooks\\): implement full auto-trigger system with blocking gates\n\n- Add 7 new hook scripts for workflow automation:\n - session-check.sh: Init, blueprint check, task classification\n - post-edit.sh: Auto-verify with loop tracking \\(max 10 iterations\\)\n - pre-tool-router.sh: Route Bash commands to appropriate checks\n - pre-commit-check.sh: All 7 gate checks with blocking \\(exit 1\\)\n - phase-transition.sh: State management and gate enforcement\n - audit-runner.sh: UI enforcement + circular dependency checks\n - blueprint-generator.sh: Auto-scan project structure\n\n- Pre-commit gate now checks:\n - TypeScript errors\n - ESLint warnings\n - TODO/FIXME comments\n - console.log statements\n - Orphan features \\(UI enforcement\\)\n - Circular dependencies\n - Conventional commit format\n\n- State tracking in .claude/.autoworkflow/:\n - phase, task-type, verify-iteration, audit-iteration\n - verify-status, audit-status, plan-approved\n\n- Updated CLAUDE.md files with:\n - Slash commands table\n - Hook files reference\n - Hook integration section\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
14
- "Bash(git commit:*)"
14
+ "Bash(git commit:*)",
15
+ "Bash(unzip:*)",
16
+ "Bash(node:*)"
15
17
  ]
16
18
  }
17
19
  }
@@ -0,0 +1,337 @@
1
+ # Actix Web Skill
2
+
3
+ ## Application Setup
4
+ \`\`\`rust
5
+ use actix_web::{web, App, HttpServer, middleware};
6
+ use actix_cors::Cors;
7
+
8
+ #[actix_web::main]
9
+ async fn main() -> std::io::Result<()> {
10
+ // Initialize logging
11
+ env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
12
+
13
+ // Create shared state
14
+ let db_pool = create_pool().await.expect("Failed to create pool");
15
+ let app_state = web::Data::new(AppState { db: db_pool });
16
+
17
+ HttpServer::new(move || {
18
+ // Configure CORS
19
+ let cors = Cors::default()
20
+ .allow_any_origin()
21
+ .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
22
+ .allowed_headers(vec!["Authorization", "Content-Type"])
23
+ .max_age(3600);
24
+
25
+ App::new()
26
+ .app_data(app_state.clone())
27
+ .wrap(middleware::Logger::default())
28
+ .wrap(middleware::Compress::default())
29
+ .wrap(cors)
30
+ .configure(configure_routes)
31
+ })
32
+ .bind("127.0.0.1:8080")?
33
+ .run()
34
+ .await
35
+ }
36
+
37
+ fn configure_routes(cfg: &mut web::ServiceConfig) {
38
+ cfg.service(
39
+ web::scope("/api/v1")
40
+ .service(
41
+ web::scope("/auth")
42
+ .route("/login", web::post().to(login))
43
+ .route("/register", web::post().to(register))
44
+ )
45
+ .service(
46
+ web::scope("/users")
47
+ .wrap(AuthMiddleware)
48
+ .route("", web::get().to(list_users))
49
+ .route("", web::post().to(create_user))
50
+ .route("/{id}", web::get().to(get_user))
51
+ .route("/{id}", web::put().to(update_user))
52
+ .route("/{id}", web::delete().to(delete_user))
53
+ )
54
+ );
55
+ }
56
+ \`\`\`
57
+
58
+ ## Shared State
59
+ \`\`\`rust
60
+ use sqlx::PgPool;
61
+
62
+ pub struct AppState {
63
+ pub db: PgPool,
64
+ pub config: AppConfig,
65
+ }
66
+
67
+ // Access in handlers
68
+ async fn get_user(
69
+ state: web::Data<AppState>,
70
+ path: web::Path<String>,
71
+ ) -> Result<HttpResponse, AppError> {
72
+ let id = path.into_inner();
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(HttpResponse::Ok().json(user))
79
+ }
80
+ \`\`\`
81
+
82
+ ## Request Extractors
83
+ \`\`\`rust
84
+ use actix_web::{web, HttpResponse};
85
+ use serde::Deserialize;
86
+ use validator::Validate;
87
+
88
+ // JSON body
89
+ #[derive(Deserialize, Validate)]
90
+ pub struct CreateUserRequest {
91
+ #[validate(email)]
92
+ pub email: String,
93
+ #[validate(length(min = 2, max = 100))]
94
+ pub name: String,
95
+ #[validate(length(min = 8))]
96
+ pub password: String,
97
+ }
98
+
99
+ async fn create_user(
100
+ state: web::Data<AppState>,
101
+ body: web::Json<CreateUserRequest>,
102
+ ) -> Result<HttpResponse, AppError> {
103
+ // Validate request
104
+ body.validate().map_err(|e| AppError::Validation(e.to_string()))?;
105
+
106
+ let user = User::new(body.email.clone(), body.name.clone(), &body.password)?;
107
+ // ... save user
108
+
109
+ Ok(HttpResponse::Created().json(user))
110
+ }
111
+
112
+ // Query parameters
113
+ #[derive(Deserialize)]
114
+ pub struct PaginationQuery {
115
+ #[serde(default = "default_page")]
116
+ pub page: u32,
117
+ #[serde(default = "default_per_page")]
118
+ pub per_page: u32,
119
+ }
120
+
121
+ fn default_page() -> u32 { 1 }
122
+ fn default_per_page() -> u32 { 20 }
123
+
124
+ async fn list_users(
125
+ state: web::Data<AppState>,
126
+ query: web::Query<PaginationQuery>,
127
+ ) -> Result<HttpResponse, AppError> {
128
+ let offset = (query.page - 1) * query.per_page;
129
+ let users = sqlx::query_as!(User,
130
+ "SELECT * FROM users LIMIT $1 OFFSET $2",
131
+ query.per_page as i64,
132
+ offset as i64
133
+ )
134
+ .fetch_all(&state.db)
135
+ .await?;
136
+
137
+ Ok(HttpResponse::Ok().json(users))
138
+ }
139
+
140
+ // Path parameters
141
+ async fn get_user(path: web::Path<(String,)>) -> Result<HttpResponse, AppError> {
142
+ let (id,) = path.into_inner();
143
+ // ...
144
+ }
145
+
146
+ // Multiple path params
147
+ async fn get_post_comment(
148
+ path: web::Path<(String, String)>,
149
+ ) -> Result<HttpResponse, AppError> {
150
+ let (post_id, comment_id) = path.into_inner();
151
+ // ...
152
+ }
153
+ \`\`\`
154
+
155
+ ## Custom Middleware
156
+ \`\`\`rust
157
+ use actix_web::{dev::{ServiceRequest, ServiceResponse, Transform, Service}, Error, HttpMessage};
158
+ use futures::future::{ok, Ready, LocalBoxFuture};
159
+ use std::rc::Rc;
160
+
161
+ pub struct AuthMiddleware;
162
+
163
+ impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
164
+ where
165
+ S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
166
+ B: 'static,
167
+ {
168
+ type Response = ServiceResponse<B>;
169
+ type Error = Error;
170
+ type Transform = AuthMiddlewareService<S>;
171
+ type InitError = ();
172
+ type Future = Ready<Result<Self::Transform, Self::InitError>>;
173
+
174
+ fn new_transform(&self, service: S) -> Self::Future {
175
+ ok(AuthMiddlewareService {
176
+ service: Rc::new(service),
177
+ })
178
+ }
179
+ }
180
+
181
+ pub struct AuthMiddlewareService<S> {
182
+ service: Rc<S>,
183
+ }
184
+
185
+ impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
186
+ where
187
+ S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
188
+ B: 'static,
189
+ {
190
+ type Response = ServiceResponse<B>;
191
+ type Error = Error;
192
+ type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
193
+
194
+ fn poll_ready(&self, ctx: &mut core::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
195
+ self.service.poll_ready(ctx)
196
+ }
197
+
198
+ fn call(&self, req: ServiceRequest) -> Self::Future {
199
+ let service = Rc::clone(&self.service);
200
+
201
+ Box::pin(async move {
202
+ // Extract and validate token
203
+ let auth_header = req.headers()
204
+ .get("Authorization")
205
+ .and_then(|h| h.to_str().ok());
206
+
207
+ let token = match auth_header {
208
+ Some(h) if h.starts_with("Bearer ") => &h[7..],
209
+ _ => return Err(AppError::Unauthorized.into()),
210
+ };
211
+
212
+ let claims = validate_token(token)
213
+ .map_err(|_| AppError::Unauthorized)?;
214
+
215
+ // Store claims in request extensions
216
+ req.extensions_mut().insert(claims);
217
+
218
+ service.call(req).await
219
+ })
220
+ }
221
+ }
222
+
223
+ // Access auth data in handler
224
+ async fn protected_handler(req: HttpRequest) -> Result<HttpResponse, AppError> {
225
+ let claims = req.extensions().get::<Claims>()
226
+ .ok_or(AppError::Unauthorized)?;
227
+
228
+ println!("User ID: {}", claims.user_id);
229
+ Ok(HttpResponse::Ok().finish())
230
+ }
231
+ \`\`\`
232
+
233
+ ## Error Handling
234
+ \`\`\`rust
235
+ use actix_web::{HttpResponse, ResponseError, http::StatusCode};
236
+ use thiserror::Error;
237
+
238
+ #[derive(Error, Debug)]
239
+ pub enum AppError {
240
+ #[error("Resource not found")]
241
+ NotFound,
242
+
243
+ #[error("Unauthorized")]
244
+ Unauthorized,
245
+
246
+ #[error("Validation error: {0}")]
247
+ Validation(String),
248
+
249
+ #[error("Database error")]
250
+ Database(#[from] sqlx::Error),
251
+
252
+ #[error("Internal error")]
253
+ Internal(String),
254
+ }
255
+
256
+ impl ResponseError for AppError {
257
+ fn status_code(&self) -> StatusCode {
258
+ match self {
259
+ AppError::NotFound => StatusCode::NOT_FOUND,
260
+ AppError::Unauthorized => StatusCode::UNAUTHORIZED,
261
+ AppError::Validation(_) => StatusCode::BAD_REQUEST,
262
+ AppError::Database(_) | AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
263
+ }
264
+ }
265
+
266
+ fn error_response(&self) -> HttpResponse {
267
+ let status = self.status_code();
268
+ let message = match self {
269
+ AppError::Database(_) | AppError::Internal(_) => "Internal server error".to_string(),
270
+ _ => self.to_string(),
271
+ };
272
+
273
+ HttpResponse::build(status).json(serde_json::json!({
274
+ "error": message
275
+ }))
276
+ }
277
+ }
278
+ \`\`\`
279
+
280
+ ## Testing
281
+ \`\`\`rust
282
+ #[cfg(test)]
283
+ mod tests {
284
+ use super::*;
285
+ use actix_web::{test, App};
286
+
287
+ #[actix_web::test]
288
+ async fn test_create_user() {
289
+ let app = test::init_service(
290
+ App::new()
291
+ .app_data(web::Data::new(create_test_state().await))
292
+ .configure(configure_routes)
293
+ ).await;
294
+
295
+ let req = test::TestRequest::post()
296
+ .uri("/api/v1/users")
297
+ .set_json(serde_json::json!({
298
+ "email": "test@example.com",
299
+ "name": "Test User",
300
+ "password": "password123"
301
+ }))
302
+ .to_request();
303
+
304
+ let resp = test::call_service(&app, req).await;
305
+ assert_eq!(resp.status(), StatusCode::CREATED);
306
+ }
307
+
308
+ #[actix_web::test]
309
+ async fn test_get_user_not_found() {
310
+ let app = test::init_service(
311
+ App::new()
312
+ .app_data(web::Data::new(create_test_state().await))
313
+ .configure(configure_routes)
314
+ ).await;
315
+
316
+ let req = test::TestRequest::get()
317
+ .uri("/api/v1/users/nonexistent")
318
+ .to_request();
319
+
320
+ let resp = test::call_service(&app, req).await;
321
+ assert_eq!(resp.status(), StatusCode::NOT_FOUND);
322
+ }
323
+ }
324
+ \`\`\`
325
+
326
+ ## ✅ DO
327
+ - Use \`web::Data<T>\` for shared application state
328
+ - Implement \`ResponseError\` for custom error types
329
+ - Use extractors (\`web::Json\`, \`web::Query\`, \`web::Path\`)
330
+ - Configure routes in separate function with \`ServiceConfig\`
331
+ - Use \`#[actix_web::test]\` for async tests
332
+
333
+ ## ❌ DON'T
334
+ - Don't clone \`web::Data\` unnecessarily (it's already \`Arc\`)
335
+ - Don't block the async runtime with sync operations
336
+ - Don't expose internal error details to clients
337
+ - Don't forget to handle all error cases in \`ResponseError\`