Claude Sonnet 4.5 Code Review Benchmark

Why benchmark LLMs for code review?

Most LLM benchmarks focus on code generation — writing new code from scratch, solving algorithmic puzzles, or completing functions. But code review is a fundamentally different task. A model that excels at generating code may perform poorly when asked to find subtle bugs in someone else’s code, assess security implications of a design choice, or evaluate whether a refactor actually improves maintainability.

Code review requires a different set of capabilities than code generation. When reviewing code, the model needs to:

  1. Understand intent from context. The model must infer what the code is supposed to do based on surrounding code, PR descriptions, commit messages, and file naming conventions — not from an explicit prompt.
  2. Identify what is wrong without being told what to look for. Unlike code generation where the task is clearly defined, code review is open-ended. The model needs to independently surface bugs, security issues, performance problems, and style violations.
  3. Distinguish real issues from false positives. This is arguably the most important skill. A model that flags everything is useless. A model that flags nothing is equally useless. The value is in high signal-to-noise ratio.
  4. Provide actionable feedback. Saying “this might have a bug” is not helpful. Explaining exactly what the bug is, why it matters, and how to fix it — that is useful code review.

Existing benchmarks like HumanEval, MBPP, and SWE-bench measure code generation and bug fixing. There is no widely accepted benchmark for code review quality. That is the gap we set out to fill.

We tested six leading LLMs on 54 real pull requests across five programming languages, measuring bug detection accuracy, security analysis, performance issue identification, false positive rates, and the actionability of review comments. This article presents the full methodology and results, with a particular focus on Claude Sonnet 4.5, Anthropic’s latest model at the time of testing.

Methodology

Benchmark integrity requires transparency about methodology. Here is exactly how we conducted this evaluation.

Test dataset: 54 real pull requests

We curated 54 pull requests from open-source repositories on GitHub. These were not synthetic examples or toy problems. Each PR was a real change submitted by a real developer to a real project. We selected PRs that had already been merged and had meaningful review history, so we could compare model outputs against actual human reviewer findings.

The PRs spanned five languages and multiple domains:

Language Number of PRs Repositories (examples)
Python 14 FastAPI, Django REST framework, scikit-learn
JavaScript 11 Next.js, Express, React ecosystem libraries
TypeScript 12 tRPC, Prisma, Angular
Java 9 Spring Boot, Apache Kafka, Elasticsearch
Go 8 Kubernetes, Docker, CockroachDB

We deliberately included a range of PR sizes:

  • Small PRs (under 100 lines changed): 16 PRs
  • Medium PRs (100-500 lines changed): 24 PRs
  • Large PRs (500+ lines changed): 14 PRs

Issue categories

Each PR was analyzed for issues across four categories:

  1. Bug detection — Logic errors, null pointer dereferences, race conditions, off-by-one errors, incorrect error handling, type mismatches
  2. Security analysis — SQL injection, cross-site scripting (XSS), authentication bypass, insecure deserialization, path traversal, hardcoded secrets, improper input validation
  3. Performance issues — N+1 queries, unnecessary memory allocations, algorithmic complexity problems, missing caching opportunities, blocking I/O in async contexts
  4. Code quality — Dead code, unclear naming, missing error propagation, violation of SOLID principles, inconsistent patterns within the codebase

Ground truth establishment

For each PR, we established ground truth through a three-step process:

  1. Existing review comments. We collected all issues identified by original human reviewers during the actual PR review process.
  2. Expert annotation. Two senior engineers (each with 10+ years of experience) independently reviewed each PR and documented all issues they found, including issues the original reviewers missed.
  3. Consensus labeling. The two annotators reconciled their findings. Any disagreements were discussed and resolved. The final ground truth for each PR was the union of confirmed issues, each labeled by category and severity (critical, major, minor).

This process yielded 247 confirmed issues across all 54 PRs: 89 bugs, 43 security issues, 52 performance problems, and 63 code quality concerns.

How we scored each model

Each model was given the same input for each PR: the full diff, the PR title and description, and relevant file context (up to 8,000 tokens of surrounding code for files touched by the PR). We used each model’s API directly with a standardized system prompt that instructed the model to perform a thorough code review and report issues by category.

For each model’s output, we measured:

  • True positive rate (recall) — Percentage of ground truth issues the model correctly identified
  • Precision — Percentage of the model’s flagged issues that were actual issues (inverse of false positive rate)
  • F1 score — Harmonic mean of precision and recall, providing a single balanced metric
  • Actionability score — Human-rated score (1-5) for how useful each review comment was, considering explanation quality, fix suggestions, and relevance

Two annotators independently scored each model’s output against ground truth. Inter-annotator agreement was 91%, and disagreements were resolved through discussion.

Transparency note

We are an independent review site. We are not affiliated with Anthropic, OpenAI, Google, or DeepSeek. We do not receive funding from any LLM vendor. We purchased API access to all models at standard pricing. Our goal is to provide useful, unbiased data to engineering teams making tooling decisions.

Models tested

We evaluated six models representing the current state of the art as of early 2026:

Model Provider Context Window Price (input / output per 1M tokens)
Claude Sonnet 4.5 Anthropic 200K $3.00 / $15.00
Claude Haiku 3.5 Anthropic 200K $0.80 / $4.00
GPT-4o OpenAI 128K $2.50 / $10.00
GPT-4o-mini OpenAI 128K $0.15 / $0.60
Gemini 2.5 Pro Google 1M $1.25 / $5.00
DeepSeek V3 DeepSeek 128K $0.27 / $1.10

All models were tested using their latest available versions as of February 2026. For Claude Sonnet 4.5, we tested both standard mode and extended thinking mode (which allows the model to reason through complex problems before responding). Temperature was set to 0 for all models to ensure reproducibility.

Results

Overall accuracy

The table below summarizes each model’s performance across all 247 confirmed issues in our dataset.

Model Recall Precision F1 Score Actionability (1-5)
Claude Sonnet 4.5 (extended thinking) 78.1% 82.4% 80.2% 4.3
GPT-4o 76.5% 78.9% 77.7% 4.1
Claude Sonnet 4.5 (standard) 74.9% 81.7% 78.2% 4.2
Gemini 2.5 Pro 73.3% 76.2% 74.7% 3.9
DeepSeek V3 68.4% 71.8% 70.1% 3.6
Claude Haiku 3.5 62.7% 74.5% 68.1% 3.5
GPT-4o-mini 59.1% 69.3% 63.8% 3.2

Key takeaways from overall results:

  • Claude Sonnet 4.5 with extended thinking achieved the highest F1 score (80.2%), edging out GPT-4o (77.7%) and standard Claude Sonnet 4.5 (78.2%).
  • The gap between extended thinking and standard mode for Claude Sonnet 4.5 was meaningful — an additional 3.2% recall and a 2 percentage point improvement in F1 score. Extended thinking particularly helped on complex multi-file bugs and architectural issues.
  • GPT-4o was competitive across the board and outperformed standard Claude Sonnet 4.5 on recall, though Claude maintained higher precision.
  • Gemini 2.5 Pro performed solidly in the middle tier, with particular strength in Go and Java codebases.
  • Budget models (Claude Haiku, GPT-4o-mini) were significantly behind the frontier models, but still useful for catching straightforward issues at a fraction of the cost.
  • DeepSeek V3 delivered impressive results for its price point, outperforming both budget models despite costing less than Claude Haiku.

Bug detection

Bug detection is the core value proposition of AI code review. We tested each model against the 89 confirmed bugs in our dataset.

Model Bugs Found Recall Precision Notable Strengths
Claude Sonnet 4.5 (extended) 73 82.0% 83.9% Race conditions, complex logic errors
GPT-4o 69 77.5% 79.5% Null safety, type mismatches
Claude Sonnet 4.5 (standard) 67 75.3% 82.7% Off-by-one errors, error handling
Gemini 2.5 Pro 63 70.8% 75.9% Concurrency bugs in Go/Java
DeepSeek V3 57 64.0% 70.4% Python-specific patterns
Claude Haiku 3.5 51 57.3% 73.6% Common null checks
GPT-4o-mini 47 52.8% 67.1% Simple pattern matching

Example: Race condition caught by Claude Sonnet 4.5 but missed by other models.

In a Go PR modifying a concurrent map access pattern in a Kubernetes controller, the following code was introduced:

func (c *Controller) reconcile(ctx context.Context, key string) error {
    obj, exists := c.cache[key]
    if !exists {
        return nil
    }

    // Process the object
    result, err := c.process(ctx, obj)
    if err != nil {
        return err
    }

    c.cache[key] = result
    return nil
}

Claude Sonnet 4.5 (with extended thinking) identified that c.cache was a map[string]Object accessed from multiple goroutines without synchronization. Its review comment explained the race condition, referenced Go’s memory model, and suggested using a sync.RWMutex or sync.Map. GPT-4o flagged it as a “potential concurrency concern” but did not explain the specific risk or provide a fix. Other models missed it entirely.

Example: Null pointer dereference caught by most models.

In a TypeScript PR:

async function getUserProfile(userId: string) {
  const user = await prisma.user.findUnique({ where: { id: userId } });
  const subscription = user.subscription; // user could be null
  return { name: user.name, plan: subscription?.tier ?? 'free' };
}

Every model except GPT-4o-mini caught the potential null dereference on user.subscription when findUnique returns null. This type of straightforward null safety check is where all models perform reasonably well — it is the more nuanced bugs where differentiation appears.

Security analysis

Security vulnerabilities are high-stakes findings. A missed SQL injection or authentication bypass can lead to a data breach. We tested each model against 43 confirmed security issues.

Model Issues Found Recall Precision Strengths
Claude Sonnet 4.5 (extended) 37 86.0% 88.1% Auth bypass, complex injection chains
GPT-4o 35 81.4% 83.3% XSS, SSRF, common OWASP patterns
Claude Sonnet 4.5 (standard) 34 79.1% 87.2% Input validation, deserialization
Gemini 2.5 Pro 32 74.4% 78.0% SQL injection, path traversal
DeepSeek V3 27 62.8% 71.1% Basic injection patterns
Claude Haiku 3.5 25 58.1% 75.8% Hardcoded secrets, obvious XSS
GPT-4o-mini 22 51.2% 66.7% Simple pattern-based detection

SQL injection detection. All frontier models (Claude Sonnet 4.5, GPT-4o, Gemini 2.5 Pro) caught direct SQL injection vulnerabilities with 100% recall. Differentiation appeared with indirect injection — cases where user input passed through multiple function calls before reaching a query. Claude Sonnet 4.5 with extended thinking traced data flow across three function boundaries in a Django application to identify an injection vector. GPT-4o caught the same issue when it spanned two functions but missed it at three. Gemini 2.5 Pro missed multi-hop injection entirely.

XSS detection. GPT-4o had a slight edge in detecting cross-site scripting vulnerabilities, catching 10 of 11 XSS issues compared to Claude Sonnet 4.5’s 9 of 11. GPT-4o was particularly good at identifying DOM-based XSS in React applications where dangerouslySetInnerHTML was used with insufficiently sanitized input.

Authentication bypass. Claude Sonnet 4.5 with extended thinking stood out in detecting authentication bypass vulnerabilities. In one Spring Boot PR, it identified that a new API endpoint was missing the @PreAuthorize annotation that all sibling endpoints had, and correctly flagged this as a critical security issue. It also caught a subtle IDOR (Insecure Direct Object Reference) vulnerability where user ID validation was present in the controller but missing in a newly added service method that was called from a different controller.

Performance issues

Performance bugs are often subtle and require understanding of runtime characteristics. We tested each model against 52 confirmed performance issues.

Model Issues Found Recall Precision
Claude Sonnet 4.5 (extended) 38 73.1% 79.2%
GPT-4o 37 71.2% 77.1%
Claude Sonnet 4.5 (standard) 35 67.3% 79.5%
Gemini 2.5 Pro 34 65.4% 73.9%
DeepSeek V3 28 53.8% 68.3%
Claude Haiku 3.5 24 46.2% 70.6%
GPT-4o-mini 21 40.4% 63.6%

N+1 query detection. This was the most commonly planted performance issue, appearing in 12 PRs across Python (Django/SQLAlchemy), TypeScript (Prisma), and Java (Hibernate). Claude Sonnet 4.5 caught 11 of 12 N+1 patterns, GPT-4o caught 10, and Gemini 2.5 Pro caught 9. The one N+1 issue that Claude missed was deeply nested inside a recursive function that loaded child entities conditionally — a pattern that all models struggled with.

Algorithmic complexity. We included 8 PRs with suboptimal algorithm choices (for example, using nested loops where a hash map lookup would reduce O(n^2) to O(n)). Claude Sonnet 4.5 with extended thinking caught 7 of 8, often providing the improved algorithm in its review comment. GPT-4o caught 6 of 8 but typically only mentioned “this could be optimized” without providing the specific improvement.

Memory leaks and resource management. In Go and Java PRs involving unclosed resources (database connections, file handles, HTTP response bodies), Claude and GPT-4o performed similarly, each catching roughly 75% of resource leak issues. Gemini 2.5 Pro was notably strong on Go-specific resource management, catching defer misuse and context cancellation issues that other models missed.

False positive rate

False positives are the silent killer of AI code review adoption. If a tool generates too many incorrect findings, developers learn to ignore it entirely, defeating its purpose. Lower false positive rates are better.

Model Total Comments True Positives False Positives FP Rate
Claude Sonnet 4.5 (standard) 226 185 41 18.1%
Claude Sonnet 4.5 (extended) 234 193 41 17.5%
Claude Haiku 3.5 195 155 40 20.5%
GPT-4o 259 205 54 20.8%
DeepSeek V3 232 175 57 24.6%
Gemini 2.5 Pro 268 198 70 26.1%
GPT-4o-mini 237 162 75 31.6%

Claude Sonnet 4.5 produced the fewest false positives of any model tested. Both in standard and extended thinking modes, Claude maintained a false positive rate below 19%, compared to GPT-4o at 20.8% and Gemini 2.5 Pro at 26.1%.

The most common types of false positives across all models were:

  1. Style preferences disguised as bugs (32% of all FPs) — flagging valid but unconventional code patterns as incorrect
  2. Missing context (28% of all FPs) — identifying an “issue” that was actually handled elsewhere in the codebase
  3. Overly cautious security warnings (22% of all FPs) — flagging already-sanitized input as a security risk
  4. Incorrect language semantics (18% of all FPs) — misunderstanding language-specific behavior and flagging correct code as buggy

Claude Sonnet 4.5’s lower false positive rate appears to stem from two factors. First, it produces fewer style-based false positives — it is more likely to note a stylistic observation without categorizing it as a bug. Second, its extended thinking mode allows it to reason through potential false positives before reporting them, effectively self-filtering its own output.

Response quality

Beyond raw accuracy, we evaluated the quality of each model’s review comments on a 1-5 scale across three dimensions:

Model Explanation Clarity Fix Quality Actionability Overall
Claude Sonnet 4.5 (extended) 4.5 4.4 4.3 4.3
Claude Sonnet 4.5 (standard) 4.4 4.2 4.2 4.2
GPT-4o 4.2 4.0 4.1 4.1
Gemini 2.5 Pro 3.9 3.9 3.9 3.9
DeepSeek V3 3.6 3.5 3.6 3.6
Claude Haiku 3.5 3.5 3.4 3.5 3.5
GPT-4o-mini 3.3 3.1 3.2 3.2

Explanation clarity. Claude Sonnet 4.5 consistently provided the most thorough explanations. When identifying a race condition, for example, it would explain the specific interleaving that causes the bug, reference the relevant memory model, and describe the potential consequences. GPT-4o’s explanations were accurate but more concise, sometimes to the point of being insufficient for junior developers to understand the issue.

Fix quality. Claude Sonnet 4.5 with extended thinking provided the most complete and correct fix suggestions. In 87% of cases where it suggested a fix, the fix was directly applicable without modification. GPT-4o’s fix rate was 79%, and Gemini 2.5 Pro’s was 74%. Budget models frequently suggested fixes that were syntactically correct but semantically incomplete — fixing the immediate symptom without addressing the underlying issue.

Actionability. This is a composite score reflecting whether a developer could take immediate action based on the review comment. Claude Sonnet 4.5 scored highest here because its comments typically included: (1) a clear description of the problem, (2) why it matters, (3) a suggested fix, and (4) a severity assessment. GPT-4o often omitted the severity assessment, and budget models frequently omitted the fix suggestion.

Cost analysis

Performance must be weighed against cost. We measured the total API cost for reviewing all 54 PRs with each model.

Model Total Cost (54 PRs) Avg Cost per PR Accuracy (F1) Cost-Effectiveness (F1 per $)
GPT-4o-mini $1.87 $0.03 63.8% 34.1
DeepSeek V3 $3.42 $0.06 70.1% 20.5
Claude Haiku 3.5 $5.94 $0.11 68.1% 11.5
Gemini 2.5 Pro $9.18 $0.17 74.7% 8.1
Claude Sonnet 4.5 (standard) $18.36 $0.34 78.2% 4.3
GPT-4o $21.06 $0.39 77.7% 3.7
Claude Sonnet 4.5 (extended) $27.54 $0.51 80.2% 2.9

Cost-effectiveness analysis. If your sole metric is accuracy per dollar, GPT-4o-mini and DeepSeek V3 win. But this metric is misleading for code review because the cost of a missed bug in production far exceeds the cost of a review API call. A single production incident can cost thousands of dollars in engineering time, and a security breach can cost orders of magnitude more.

For teams that need maximum accuracy, Claude Sonnet 4.5 with extended thinking costs roughly $0.51 per PR review. For a team merging 50 PRs per week, that is approximately $102 per month — less than half the cost of a single hour of senior engineer time. At this price point, the cost of the API is negligible compared to the value of catching even one additional critical bug per month.

The sweet spot for most teams is Claude Sonnet 4.5 in standard mode or GPT-4o, which deliver frontier-level accuracy at roughly $0.34 to $0.39 per PR. Teams on tight budgets should consider DeepSeek V3, which delivers surprisingly strong results at $0.06 per PR.

A practical tiered approach is to use a budget model (Claude Haiku or GPT-4o-mini) for initial screening on all PRs and escalate to a frontier model (Claude Sonnet 4.5 or GPT-4o) only for PRs that touch security-sensitive code or exceed a certain size threshold. This can reduce costs by 60-70% while maintaining high accuracy where it matters most.

Claude Sonnet 4.5 deep dive

Since Claude Sonnet 4.5 topped our overall benchmark, it warrants a deeper examination of its strengths, weaknesses, and ideal use cases.

Extended thinking for complex reviews

Claude Sonnet 4.5’s extended thinking capability is its most distinctive feature for code review. When enabled, the model takes additional time to reason through the code before generating its review. This manifests as a visible “thinking” phase where the model breaks down the problem, considers multiple interpretations, and evaluates potential issues before committing to its output.

In our testing, extended thinking added 15-30 seconds to review time but delivered measurable improvements:

  • +6.7% recall on bugs compared to standard mode (82.0% vs 75.3%)
  • +6.9% recall on security issues (86.0% vs 79.1%)
  • +5.8% recall on performance issues (73.1% vs 67.3%)
  • Lower false positive rate (17.5% vs 18.1%)

The improvement was most pronounced on complex, multi-file issues that require reasoning across multiple code paths. For simple issues like null checks and missing error handling, extended thinking provided minimal benefit over standard mode.

Strengths

  1. Lowest false positive rate. Claude Sonnet 4.5 consistently produced the cleanest output with the fewest incorrect findings. This matters enormously for adoption — developers who trust the tool will actually read and act on its findings.

  2. Best explanation quality. Claude’s review comments read like they were written by a thoughtful senior engineer. They explain not just what is wrong, but why it matters and what the consequences could be. This is particularly valuable for teams with junior developers who learn from review feedback.

  3. Strongest on nuanced bugs. Race conditions, subtle state management issues, and cross-function data flow bugs were Claude’s specialty. These are precisely the bugs that are hardest for human reviewers to catch and most expensive when they reach production.

  4. Consistent across languages. While some models showed significant performance variation across languages (Gemini was notably stronger on Go, DeepSeek on Python), Claude Sonnet 4.5 delivered consistent results across all five tested languages.

Weaknesses

  1. Slower than GPT-4o. Claude Sonnet 4.5 in standard mode was roughly 20% slower than GPT-4o for equivalent-length reviews. With extended thinking enabled, it was 40-60% slower. For teams that prioritize speed (for example, for real-time review-as-you-type workflows), this latency may matter.

  2. Higher cost. Claude Sonnet 4.5 with extended thinking was the most expensive option in our benchmark at $0.51 per PR. While we argue this cost is justified by accuracy gains, budget-constrained teams may find better value elsewhere.

  3. Occasional verbosity. Claude Sonnet 4.5 sometimes provided longer explanations than necessary for simple issues. A null check does not need three paragraphs of explanation. This verbosity can increase noise in review comments, partially offsetting the low false positive rate.

  4. Weaker on DOM-specific XSS. GPT-4o outperformed Claude on React-specific XSS detection, suggesting Claude’s training may have slightly less emphasis on frontend security patterns.

Best use cases for Claude Sonnet 4.5

Based on our benchmark results, Claude Sonnet 4.5 is the best choice for:

  • Security-critical codebases where missing a vulnerability is unacceptable
  • Backend services handling financial transactions, authentication, or sensitive data
  • Teams with mixed experience levels where detailed explanations help junior developers learn
  • Complex microservice architectures where bugs span multiple services and require cross-file reasoning
  • Final review gate in a tiered review pipeline where a budget model handles the first pass

Claude Sonnet 4.5 is not the best choice for:

  • High-volume, low-stakes reviews where cost and speed matter more than depth
  • Real-time code suggestions where latency needs to be under 2 seconds
  • Simple linting and formatting checks where a rule-based tool is faster and cheaper

Practical recommendations

Based on the full benchmark results, here are our recommendations for different team profiles.

Best model by use case

Use Case Recommended Model Why
Maximum accuracy Claude Sonnet 4.5 (extended) Highest F1 score, lowest FP rate
Best value for money Claude Sonnet 4.5 (standard) Near-top accuracy at moderate cost
Budget-conscious teams DeepSeek V3 Strong results at very low cost
Speed-critical workflows GPT-4o Fastest response with frontier accuracy
Open-source projects Claude Haiku or GPT-4o-mini Free tiers and low cost at scale
Security-focused review Claude Sonnet 4.5 (extended) Highest security issue detection rate

Cost-optimized configurations

Tiered review pipeline. Use Claude Haiku ($0.11/PR) as a first-pass reviewer on all PRs. For PRs that touch authentication, payment, or infrastructure code, escalate to Claude Sonnet 4.5 with extended thinking ($0.51/PR). This reduces average cost to roughly $0.15/PR while maintaining high accuracy on critical paths.

Size-based routing. Use GPT-4o-mini ($0.03/PR) for PRs under 100 lines. Use Claude Sonnet 4.5 standard ($0.34/PR) for PRs between 100-500 lines. Use Claude Sonnet 4.5 extended ($0.51/PR) for PRs over 500 lines. This matches model capability to review complexity.

Language-specific routing. For Go codebases, Gemini 2.5 Pro offers strong performance at lower cost than Claude or GPT-4o. For Python-heavy projects, DeepSeek V3 provides good value. For polyglot codebases, Claude Sonnet 4.5 offers the most consistent cross-language performance.

When to use expensive vs. budget models

Use a frontier model (Claude Sonnet 4.5, GPT-4o) when:

  • The PR touches security-sensitive code (authentication, authorization, input validation, cryptography)
  • The PR modifies financial calculations or billing logic
  • The PR introduces new architectural patterns or public APIs
  • The PR changes concurrency or distributed systems code
  • The PR is from an external contributor or junior developer

Use a budget model (Claude Haiku, GPT-4o-mini, DeepSeek V3) when:

  • The PR is a dependency update or version bump
  • The PR only changes tests or documentation
  • The PR is small (under 50 lines) with straightforward changes
  • The PR has already been thoroughly reviewed by a human
  • The PR is for internal tooling or non-production code

How tools use these models

Benchmarking raw LLM capabilities is useful, but most teams interact with these models through code review tools that add structure, context, and workflow integration on top of the base model.

CodeRabbit’s model selection

CodeRabbit supports multiple LLM backends and selects models dynamically based on the review context. It uses frontier models like Claude Sonnet 4.5 and GPT-4o for security-sensitive analysis and complex multi-file reviews, while routing simpler checks to faster, cheaper models. CodeRabbit also adds its own context layer — pulling in PR descriptions, linked issues, repository-level patterns, and learned team preferences — which can improve raw model performance by 10-15% compared to sending the diff alone.

CodeRabbit’s configuration allows teams to specify which model to prioritize, and its natural language instruction system means you can tell it to “focus on security issues and ignore style suggestions” to reduce noise. For teams that want the accuracy benefits of Claude Sonnet 4.5 without managing the API directly, CodeRabbit is one of the most accessible options.

PR-Agent’s model support

PR-Agent, the open-source AI code review tool by Qodo, supports Claude models through the Anthropic API. You can configure PR-Agent to use Claude Sonnet 4.5 as its primary review model, and it handles prompt construction, diff chunking, and review comment formatting automatically.

PR-Agent’s advantage is self-hosting — your code never leaves your infrastructure. This matters for teams with strict data handling policies. The trade-off is that you need to manage API keys, handle rate limits, and tune prompts yourself. PR-Agent’s default prompts are optimized for GPT-4o, so switching to Claude may require prompt adjustments for optimal results.

Building your own with APIs

For teams with specific requirements, building a custom review pipeline using the Claude API directly offers the most flexibility. The typical architecture involves:

  1. Webhook listener — A GitHub or GitLab webhook that triggers on PR events
  2. Context builder — A service that extracts the diff, gathers relevant file context, and constructs the prompt
  3. Model call — A call to the Anthropic API with the review prompt and code context
  4. Comment formatter — Parsing the model’s response and posting it as inline review comments

The Anthropic API supports streaming, which allows you to show review comments as they are generated rather than waiting for the full response. For large PRs, you can split the diff into chunks and review each chunk in parallel, then aggregate results.

A basic implementation takes 2-3 days for a senior engineer. Production-grade implementations that handle rate limiting, retry logic, cost tracking, and review comment deduplication typically take 1-2 weeks.

Conclusion

Claude Sonnet 4.5 earned the top position in our benchmark by delivering the highest overall accuracy, the lowest false positive rate, and the most actionable review comments. Its extended thinking capability provides a meaningful accuracy boost for complex reviews, and its consistent performance across languages makes it a reliable choice for polyglot codebases.

However, model selection for code review is not one-size-fits-all. GPT-4o is a strong alternative with faster response times. DeepSeek V3 offers remarkable value for budget-conscious teams. And for teams that want to avoid managing LLM infrastructure entirely, tools like CodeRabbit and PR-Agent abstract away model selection and provide turnkey code review with the best models under the hood.

The most important takeaway from our benchmark is that all frontier LLMs are now good enough to provide meaningful value in code review. The differences between Claude Sonnet 4.5, GPT-4o, and Gemini 2.5 Pro are real but incremental. The much larger gap is between using any AI code review and using none at all. Teams that are not yet using AI-assisted code review are leaving significant value on the table — regardless of which model they choose.

We will update this benchmark as new models are released. Our next round of testing will include Claude Opus, GPT-5, and Gemini 2.5 Ultra when they become available.

Frequently Asked Questions

Is Claude Sonnet 4.5 good for code review?

Yes. In our benchmark across 50+ real PRs, Claude Sonnet 4.5 ranked among the top models for code review accuracy, particularly excelling at bug detection and producing fewer false positives than competing models. Its extended thinking capability helps with complex architectural analysis.

How does Claude compare to GPT-4o for code review?

Claude Sonnet 4.5 and GPT-4o perform comparably for code review overall. Claude excels at nuanced bug detection and produces more actionable suggestions, while GPT-4o is slightly faster. Both significantly outperform smaller models for complex review tasks.

Which LLM is best for code review?

Based on our benchmark, Claude Sonnet 4.5 and GPT-4o are the top choices for code review accuracy. Claude Sonnet 4.5 offers the best balance of accuracy and cost. For teams prioritizing speed over depth, GPT-4o-mini or Claude Haiku provide good results at lower cost.

What tools use Claude for code review?

CodeRabbit uses Claude models as one of its AI backends. PR-Agent supports Claude through the Anthropic API. You can also build custom review workflows using the Claude API directly. Cursor and Windsurf use Claude for inline code suggestions.

Originally published at aicodereview.cc

Zod vs Typia vs AJV — I Built a Vite Plugin That Makes Zod 60x Faster With Zero Code Changes

When I first released Zod AOT, it required wrapping every schema with compile():

import { compile } from "zod-aot";

const UserSchema = compile(
  z.object({
    name: z.string().min(1),
    email: z.email(),
  })
);

The #1 feedback was: “I don’t want to change my code.”

Fair. So I removed that requirement. Zod AOT now has an autoDiscover mode — a Vite plugin that finds your Zod schemas at build time, compiles them into optimized validators, and replaces them in-place. Your schema files stay pure Zod. No imports from zod-aot. No wrappers. Just this:

// vite.config.ts
import zodAot from "zod-aot/vite";

export default defineConfig({
  plugins: [zodAot({ autoDiscover: true })],
});

That’s it. Every exported Zod schema in your project gets AOT-compiled at build time.

I also added Typia and AJV to the benchmarks, built a two-phase “Fast Path” validator, and shipped a diagnostic CLI. Here’s what changed.

autoDiscover: How It Works

The challenge is detecting Zod schemas without any marker in the source code. There’s no compile() call, no special import — just plain Zod:

// src/schemas.ts — plain Zod, no Zod AOT import
import { z } from "zod";

export const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(["admin", "editor", "viewer"]),
});

export const UpdateUserSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  email: z.email().optional(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(["admin", "editor", "viewer"]).optional(),
});

The plugin detects and compiles these in four steps:

Step 1: Regex Pre-filter

Before doing any work, the plugin checks if the file contains a runtime Zod import:

import { z } from "zod"   ✓  (runtime — schema definitions)
import type { z } from "zod"  ✗  (type-only — skip)

Files without a runtime Zod import are skipped entirely. This keeps build times fast — most files in your project aren’t schema files.

Step 2: Execute and Inspect Exports

The plugin loads the source file at build time using jiti and inspects each export:

function isZodSchema(value: unknown): boolean {
  if (typeof value !== "object" || value === null || !("_zod" in value))
    return false;
  const zod = (value as Record<string, unknown>)["_zod"];
  return typeof zod === "object" && zod !== null && "def" in zod;
}

Every Zod schema has a _zod.def structure — this is the internal definition that describes the schema’s type, checks, and children. If an export has this marker, it’s a Zod schema.

Step 3: Compile to Optimized Validators

Each discovered schema goes through the Zod AOT compilation pipeline:

Zod Schema → Extract IR → Generate Code → Wrap in IIFE

The pipeline walks the schema tree, extracts an intermediate representation, and generates flat, inlined validation code — the same process as before, but triggered automatically.

Step 4: AST-Based Source Replacement

The plugin uses acorn to parse expression boundaries in the source code:

// Before (source)
export const CreateUserSchema = z.object({ ... });

// After (build output)
export const CreateUserSchema = /* @__PURE__ */ (() => {
  // ... generated validation code ...
  var __w = Object.create(originalSchema);
  __w.parse = function(input) { /* optimized */ };
  __w.safeParse = __validate;
  __w.schema = originalSchema;
  return __w;
})();

The Object.create(originalSchema) is key — it preserves the full Zod API. Your compiled schema still has .shape, .keyof(), .pick(), .merge(), and everything else. Framework code that inspects schema metadata (like tRPC or React Hook Form resolvers) continues to work.

Build Output

With verbose: true, you see what happened:

[zod-aot] Auto-discovering: src/schemas.ts (4 Zod exports found)
[zod-aot]   ✓ CreateUserSchema
[zod-aot]   ✓ UpdateUserSchema
[zod-aot]   ✓ ListUsersSchema
[zod-aot]   ✓ UserIdSchema
[zod-aot] Build summary: 4/4 schemas optimized across 1 file(s)

Scoping

Not every file should be auto-discovered — you probably don’t want to execute test fixtures or files with side effects at build time. Use include and exclude:

zodAot({
  autoDiscover: true,
  include: ["src/schemas"],
  exclude: ["test", "mock"],
});

Two-Phase Validation: The Fast Path

The prior version of Zod AOT generated error-collecting validators — they always created an issues array and tracked every validation failure. That’s necessary for error reporting, but it’s overhead when the input is valid.

In production, most inputs are valid. So I added a Fast Path.

Phase 1: Boolean Expression Chain

For valid inputs, the entire schema is validated with a single boolean expression:

// Generated Fast Path for CreateUserSchema
typeof input==="object"&&input!==null&&!Array.isArray(input)&&
typeof input.name==="string"&&input.name.length>=1&&input.name.length<=100&&
typeof input.email==="string"&&__re0.test(input.email)&&
typeof input.age==="number"&&!Number.isNaN(input.age)&&
  Number.isSafeInteger(input.age)&&input.age>=0&&input.age<=150&&
(input.role==="admin"||input.role==="editor"||input.role==="viewer")

If this evaluates to true, the function returns { success: true, data: input } immediately.

Zero allocations. No issues array. No error objects. No path arrays. Just a boolean chain that short-circuits on the first failure.

Regex patterns and enum Sets are pre-compiled in a preamble:

var __re0 = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
// Small enums (≤3 values) use inline === checks instead of Set.has()

Phase 2: Error-Collecting Path (Slow Path)

If the Fast Path fails — meaning the input has at least one issue — the full error-collecting validator runs. This creates the standard { success: false, error: { issues: [...] } } response with codes, paths, and messages.

The key insight: the Slow Path only runs when it’s actually needed. For hot API endpoints where 95%+ of requests pass validation, the Fast Path is all that executes.

Fast Path Eligibility

Not every schema qualifies for a Fast Path. Schemas with .transform(), .refine(), .default(), .coerce(), or .catch() are ineligible because they need to modify the input or run opaque functions. The check command (see below) tells you which schemas qualify.

The 5-Way Benchmark

I added Typia and AJV to the benchmark suite. All five validators parse the same data with equivalent schemas:

  • Zod v3: Interpreter model, walks schema tree on every call
  • Zod v4: New architecture with JIT object compilation via new Function()
  • Zod AOT: Build-time compilation with two-phase validation
  • Typia: Compile-time code generation via TypeScript transformer
  • AJV: JSON Schema validator with ahead-of-time compilation

Environment: Node.js v22, Apple M-series, Vitest bench, 1M+ iterations with warmup.

Full Results

Schema Zod v3 Zod v4 Zod AOT Typia AJV
String min/max 8.3M 5.6M 10.6M 10.5M 8.8M
Enum 7.6M 9.3M 10.2M 10.8M 10.7M
Medium object (valid) 1.3M 1.7M 5.2M 7.0M 4.8M
Medium object (invalid) 365K 63K 471K 2.1M 4.7M
Large object — 100 items 8.6K 11.6K 627K 808K 89K
Discriminated union (3) 2.3M 3.0M 10.2M 10.6M 5.4M
Recursive tree — 121 nodes 23K 103K 733K 1.4M 251K
Set (20 items) 980K 461K 7.6M — —
Map (20 entries) 479K 235K 5.1M — —
Event log (mixed types) 263K 473K 4.5M — —
Partial fallback — 50 items 18K 30K 238K — —

ops/sec — higher is better. “—” = type not supported by the library.

What the numbers tell us:

Primitives — All AOT approaches converge around ~10.5M ops/sec. This is the V8 ceiling for simple type checks. No meaningful difference between Zod AOT, Typia, and AJV.

Objects — Typia leads (1.3x over Zod AOT on medium, 1.2x on large). But the gap narrows as objects grow — the Fast Path’s single boolean chain scales well. AJV collapses on large objects (9x slower than Typia at 100 items). Zod v4 is 3-54x slower than both.

Invalid inputs — AJV dominates the error path (4.7M ops/s) with minimal error construction. Zod v4 is slowest at 63K due to rich structured errors. In practice, most production inputs are valid — the Fast Path handles those.

Collections (Set/Map) — Zod AOT’s exclusive territory. Typia doesn’t validate Set or Map contents. AJV doesn’t support JS-native types. Zod AOT is 16-22x faster than Zod v4 here.

Recursive — Typia leads 1.9x over Zod AOT on deep trees. Zod AOT currently uses z.lazy() fallback for recursive references — improving this is on the roadmap.

Partial fallback — Schemas with .transform() can’t be fully AOT-compiled, but Zod AOT compiles everything it can and delegates the rest to Zod. Even partial compilation delivers 2.6-8x over Zod v4.

The Scoreboard

Category Winner Runner-up
Primitives Tie (all AOT ≈ 10.5M) —
Objects (valid) Typia Zod AOT (within 1.2x)
Objects (invalid) AJV Typia
Collections (Set/Map) Zod AOT (only competitor)
Discriminated unions Tie (Typia ≈ Zod AOT) —
Recursive Typia Zod AOT (1.9x gap)
Partial compilation Zod AOT (only competitor)

Zod AOT sits in a unique position: near-Typia performance with full Zod API compatibility. You don’t need type-level tags, a different schema DSL, or JSON Schema. You keep z.object(), z.email(), .transform(), .refine() — and the build plugin handles the rest.

The check Command: Know Your Coverage

Not sure what percentage of your schemas will be compiled? Run check:

npx zod-aot check src/schemas.ts
validateUser — 100% compiled (8/8 nodes) | Fast Path: eligible
  └─ ✓ object
     ├─ ✓ string .name
     ├─ ✓ string .email
     ├─ ✓ number .age
     ├─ ✓ enum .role
     ├─ ✓ boolean .isActive
     ├─ ✓ array .tags
     │  └─ ✓ string .tags[]
     └─ ✓ nullable .bio
        └─ ✓ string .bio

validateOrder — 80% compiled (4/5 nodes) | Fast Path: ineligible
  └─ ✓ object
     ├─ ✓ string .orderId
     ├─ ✓ number .amount
     ├─ ✓ enum .currency
     └─ ✗ fallback .slug (transform)
           hint: Extract transform into a separate post-processing step

Each schema gets a tree view showing:

  • Which nodes are fully compiled (✓) vs falling back to Zod (✗)
  • Compilation coverage percentage
  • Fast Path eligibility
  • Actionable hints for improving coverage

CI Integration

# Fail if compilation coverage drops below 80%
npx zod-aot check src/schemas/ --fail-under 80 --json

The --json flag outputs structured data for CI pipelines:

{
  "exportName": "validateOrder",
  "coverage": { "total": 5, "compilable": 4, "percent": 80 },
  "fastPath": { "eligible": false, "blocker": "fallback (transform)" },
  "fallbacks": [{
    "reason": "transform",
    "path": ".slug",
    "hint": "Extract transform into a separate post-processing step"
  }]
}

Framework Integration

autoDiscover works at the build layer — framework code doesn’t know or care:

tRPC: Schemas auto-compiled, router unchanged.

// router.ts — no changes needed
const appRouter = router({
  createUser: publicProcedure
    .input(CreateUserSchema)  // ← this is now AOT-compiled
    .mutation(({ input }) => db.users.create(input)),
});

Hono: Middleware validation auto-optimized.

app.post("/users", zValidator("json", CreateUserSchema), (c) => {
  // CreateUserSchema is AOT-compiled at build time
  const data = c.req.valid("json");
});

React Hook Form: Resolver schemas auto-compiled.

const { register } = useForm({
  resolver: zodResolver(CreateUserSchema), // AOT-compiled
});

The Object.create() wrapper ensures that framework code reading .shape, .keyof(), or other Zod metadata still works — the compiled schema inherits from the original.

What I Learned from the Competition

Typia is the performance king for objects and recursive structures. Its compile-time TypeScript transformer generates monolithic validation functions with zero runtime overhead. But:

  • It requires type-level tags (tags.MinLength<3>) instead of method chaining — a different mental model
  • It doesn’t validate Set or Map contents
  • No bigint support
  • No .transform() or .refine() — it’s a validator, not a parser

AJV has the best error-path performance and the broadest ecosystem (JSON Schema is a standard). But:

  • JSON Schema doesn’t support JavaScript-native types (Set, Map)
  • Verbose schema definitions — no chaining, no composition
  • No transforms or pipes — declarative validation only

Zod AOT fills the gap: Zod’s composable API, near-Typia performance, full JavaScript type support, and partial compilation for schemas that mix validation with transformation.

Get Started

npm install zod-aot zod@^4
// vite.config.ts — that's the entire setup
import zodAot from "zod-aot/vite";

export default defineConfig({
  plugins: [zodAot({ autoDiscover: true })],
});

Your existing Zod schemas are now AOT-compiled. No code changes.

Also works with webpack, esbuild, Rollup, Rolldown, rspack, and Bun via unplugin.

Methodology

All benchmarks use safeParse() on pre-created schema instances, measured with Vitest bench (1M+ iterations, with warmup). Numbers are steady-state throughput — JIT is already compiled after the first call. Typia uses createValidate<T>(). AJV uses ajv.compile(schema). Invalid-input benchmarks use all-fields-invalid payloads to stress the error path.

  • Environment: Node.js v22, Apple M-series
  • Libraries: Zod v3.24, Zod v4, Zod AOT 0.14, Typia 12, AJV 8
  • Raw data: benchmarks/ in the zod-aot repo

SQL Assertions, ANSI join, and ORA-08697

In the previous post on consistency boundaries, we saw that an updatable join view can hide a write-skew anomaly when the developer assumes the consistency boundary is a single row, even though there are actually two underlying rows and only one is locked. I wanted to see how the new SQL Assertions introduced in “Oracle AI Database 26ai Release 23.26.1.0.0” handle this, since they use a different locking mechanism.

TL;DR: they handle it correctly, but without ANSI joins.

Without assertions, for an employee with a salary of 1000 and a commission of 100, two concurrent users could each add 42 to both values because they couldn’t see each other’s changes, and updating one column didn’t lock the other. I then added a SQL assertion that doesn’t allow the commission to exceed 1150, so that only one session can add 42. This assertion works as expected, waiting on the other transaction before checking the sum:

CREATE ASSERTION salary_plus_commission_le_1150
CHECK (
  NOT EXISTS (
    SELECT 'violation'
    FROM emp_salary s, emp_commission c
    WHERE s.empno = c.empno
      AND s.salary + c.commission > 1150
  )
);

When the first transaction committed, the second transaction detected the write skew:

ERROR at line 1:
ORA-08601: SQL assertion (FRANCK.SALARY_PLUS_COMMISSION_LE_1150) violated.

I’m writing this blog post because I had trouble finding the correct declaration for this assertion, and I hope it helps others. My first attempt was simply:

CREATE ASSERTION salary_plus_commission_le_1150
CHECK (
  NOT EXISTS (
    SELECT 'violation'
    FROM emp_salary s join emp_commission c USING (empno)
    WHERE s.salary + c.commission > 1150
  )
);

This joins the two tables and raises a violation is the sum of salary and commission is higher than 1150, but the CREATE ASSERTION failed with:

ORA-08689: CREATE ASSERTION failed
ORA-08697: SYS owned tables are not supported.

I’m not using SYS, and cannot use SYS because I’m on the managed service “Autonomous Database” which doesn’t give SYSDBA privileges.

I reproduced this in a local environment to trace the SQL statements (ALTER SESSION SET EVENTS 'sql_trace bind=false, wait=false') and found that Oracle internally checked this constraint with:

/* SQL Analyze(250,0) */
SELECT /*+ ALL_ROWS BYPASS_RECURSIVE_CHECK */
1 FROM "SYS"."DUAL" WHERE (
  NOT EXISTS (
    SELECT 1
    FROM emp_salary s
    JOIN emp_commission c ON s.empno = c.empno
    WHERE s.salary + NVL(c.commission, 0) > 1000 + 100 + 42
  )
)

The good old DUAL table is owned by SYS and cannot be used in the assertion.

After trying many variations, I realized that SQL assertions are always evaluated with a SELECT from “SYS”.”DUAL”, even for cases that work, like in my previous post on SQL Assertions.

Then, I remembered what I’d learned from 30 years of working with Oracle Databases: if something fails with an ANSI join in Oracle, try the legacy join syntax (no JOIN clauses, only WHERE conditions). When ANSI joins were added in 9i, Oracle implemented them with internal transformations that affected other features. Most of those issues were fixed over the 20 years that followed, but SQL assertions seem to have fallen into that trap.

With the legacy join syntax, the internal query still reads “SYS”.”DUAL”, but it works:

=====================
PARSING IN CURSOR #140737366953320 len=208 dep=0 uid=136 oct=290 lid=136 tim=6202261308 hv=3501011950 ad='7ffff8be4960'
 sqlid='fzcfcfv8auczf'
CREATE ASSERTION salary_plus_commission_le_1150
CHECK (
  NOT EXISTS (
    SELECT 'violation'
    FROM emp_salary s, emp_commission c
    WHERE s.empno = c.empno
      AND s.salary + c.commission > 1150
  )
)
END OF STMT

...

=====================
PARSING IN CURSOR #140737364327008 len=286 dep=1 uid=136 oct=3 lid=0 tim=6202334328 hv=225567679 ad='783b9f10' sqlid='fv1va6n6r3sxz'
 /* SQL_ASSERTION obj#=72600 */ SELECT /*+  ALL_ROWS BYPASS_RECURSIVE_CHECK */ 1 FROM "SYS"."DUAL" WHERE (   NOT EXISTS (SELECT 'violation' "'VIOLATION'" FROM "FRANCK"."EMP_SALARY" "S","FRANCK"."EMP_COMMISSION" "C" WHERE "S"."EMPNO"="C"."EMPNO" AND "S"."SALARY"+"C"."COMMISSION">1150) )
END OF STMT

Two things to remember:

  • SQL Assertions provide correct consistency boundaries to avoid write skew in the absence of a serializable isolation level.
  • ANSI joins are nice, but Oracle was designed before this standard, with another syntax, and some bugs persist. Falling back to the old syntax may be safer.

Engineering Is a Team Sport. Your Incentives Aren’t.

I have a game tape dashboard. It tracks constraints and throughput across my engineering team … PR cycle times, review patterns, where work slows down and where it moves. I built it because I believe engineering is a team sport, and team sports need game film. You can’t coach what you can’t see.

A few years into leading my current team, I opened that dashboard and saw something I couldn’t ignore. Pull requests with over a thousand lines of code were taking, on average, five minutes longer to review than PRs with two hundred and fifty to five hundred lines. Five minutes. For four times the code.

The math told the story before anyone had to say a word out loud. PRs were sitting for days. When reviews did happen, they were almost always handled by the same one or two people. Everyone else had their own features to ship.

The math told the story before anyone had to say a word out loud.

The System I Inherited

I hadn’t built this team from scratch. I walked into an incentive structure that had been in place long enough to become invisible … one of those systems that nobody designed on purpose and nobody questions because it’s just always been there.

Engineers were measured by ticket throughput. How many you closed. How fast you moved from one to the next. Whoever did the most tickets won. That was the implicit scoreboard, even if nobody said it in a meeting or wrote it in a doc.

It’s a seductive way to measure a team when you’re under delivery pressure. Tickets closed feels like progress. Velocity trending upward feels like health. And for a while, it is. But a system designed to maximize individual throughput produces exactly one thing when applied to work at the team level like code review … engineers who are very good at protecting their time from other people’s problems.

When someone else’s PR sits in the queue, that’s not your problem. Your problem is the ticket in front of you. The one after that. The sprint goal attached to your name. Someone else being blocked is unfortunate, but it doesn’t change your scoreboard. So you keep moving.

Earlier in my career I was the manager who pulled up a velocity chart in planning and pushed for the number to go higher. The team complied. Velocity climbed. Leadership was happy. And quietly, underneath all of it, the codebase was accumulating debt that didn’t show up until something broke. I set the expectation and got exactly what I asked for.

Incentives work. Just not always in the direction you intended.

Why Every Process Fix Misses

The standard playbook for slow PR reviews follows a familiar script. Break your work into smaller PRs. Create better review templates. Set up a rotation schedule so the same two people aren’t carrying the load. Add SLAs with automated reminders. Some of these things have limited value. None of them address the actual problem.

If your performance is still measured by what you ship individually, a review rotation schedule just means it’s your turn to pull time away from shipping. The calendar entry exists. The incentive hasn’t moved. So the review gets done … technically. But it gets done the way you’d expect. Skim. Approve. Move on. The queue clears and the same pattern comes back within a quarter.

When you treat an incentive problem as a process problem, you get process-shaped results. The symptom quiets down. The cause stays exactly where it was.

What Actually Changed

My first instinct when I saw that dashboard data was to reach for a new metric. Measure review participation alongside ticket throughput. Track who was reviewing, how often, how quickly. Put the numbers somewhere visible.

The problem is that metrics can be bluffed. Give people a number to hit and they will find the path of least resistance. If review participation becomes a tracked metric, you’ll get reviews. You won’t necessarily get reviews that actually catch anything. You’ll get the five-minute rubber stamp dressed up as engagement, and your dashboard will look healthy while quality stays exactly where it was.

So instead of a new metric, we built a behavior.

When an engineer opens a PR, they drop it in a dedicated channel. The team sees it. Not as an item on a rotation list or an obligation attached to a schedule, but as a ball in play. The way I described it to the team was recovering a fumble in football. Somebody put something valuable on the field. The whole team swarms.

What made it work wasn’t the channel. It was what the channel represented … a shift from “your work is yours until it merges” to “the work belongs to all of us until it ships.” Reviewing a teammate’s PR wasn’t taking time away from your sprint anymore. It was part of your sprint. Getting someone unblocked counted the same as closing your own ticket, because the thing we were actually measuring was how fast the team moved work through the system.

The senior engineers who had been too busy to review suddenly had time. Because now someone else being blocked was their problem too.

We went from review cycles that ran three days to same day.

We went from review cycles that ran three days to same day.

The Number That Actually Matters

I come back to the game tape dashboard here. The metric I actually care about is team throughput, not individual velocity. How fast does work move from opened to merged to deployed, across the whole team, over a rolling period of time.

Individual velocity can look healthy while team throughput quietly collapses. One engineer closing six tickets while four of their teammates’ PRs sit unreviewed is not a win. It’s a transfer. You moved the constraint from one column to another and called it productivity.

The fastest teams I’ve built had one thing in common. Helping a teammate ship counted as shipping. Not rhetorically. Not in the values doc. In actual performance conversations, in how growth was talked about, in what got recognized out loud.

That’s where the behavior change lives. Not in the channel, not in the dashboard, not in any rotation schedule. In what your engineers believe will happen in their next performance conversation if they spend an afternoon doing thorough, careful reviews instead of closing their own tickets.

If the honest answer is “nothing good” … you already know what to fix.

It’s not the process.

One email a week from The Builder’s Leader. The frameworks, the blind spots, and the conversations most leaders avoid. Subscribe for free.

I built a lightweight HTTP client that lives inside Chrome — no Postman needed

I built a lightweight HTTP client that lives inside Chrome

I got tired of switching between the browser and Postman just to test a quick API call.

So I built HTTP Client — a Chrome extension that lets you send HTTP requests
directly from a side panel, without leaving your current tab.

What it does

  • All HTTP methods: GET, POST, PUT, DELETE, PATCH and more
  • Headers, Query Params, Request Body (JSON / plain text)
  • Authentication: Bearer token, Basic Auth, API Key
  • Save requests to Collections
  • Request History — every request is saved automatically
  • Environment Variables — use {{baseUrl}} style placeholders
  • Works as a Side Panel — stays open while you browse

Why not Postman?

Postman is powerful, but heavy. Sometimes you just need to hit an endpoint
and see the response. Opening a separate app breaks your flow.

HTTP Client stays inside Chrome, opens in seconds, and remembers your requests.
No account required. No telemetry. Completely free.

Try it

👉 Install from Chrome Web Store

Would love your feedback — what feature would make your API testing faster?

Architecting a Decoupled Stack: Next.js 15 and Django REST API

Architecting a Decoupled Stack: Next.js 15 and Django REST API
The landscape of web architecture has undergone a transformative shift from monolithic systems toward decoupled, distributed environments. This evolution is necessitated by the increasing complexity of user interfaces and the demand for backend services that are both secure and scalable. In contemporary engineering, the combination of Next.js 15 for the frontend and Django REST Framework for the backend emerges as a premier selection for architecting robust applications. This report provides an exhaustive technical analysis of this stack, examining the nuances of Next.js 15’s rendering model, the implementation of stateless authentication using JSON Web Tokens through secure cookie flows, and the deployment of these components within an integrated infrastructure on the DigitalOcean App Platform.
The Modern Architectural Shift
The traditional monolithic approach, where frontend logic, backend processing, and data management reside within a single codebase, served the industry for decades by providing a simplified development and deployment cycle. However, as applications scale in both traffic and complexity, the monolith often becomes a bottleneck. In-process calls within a monolith are efficient, yet the tight coupling of components restricts the ability of developers to adopt new technologies incrementally or to scale specific features independently.
Decoupled architectures resolve these limitations by separating the concerns of the user interface from the business logic. This separation allows specialized teams to focus on their respective domains—Next.js for high-performance rendering and SEO, and Django for complex relational data handling and secure backend logic. The transition to a decoupled stack, often referred to as a “headless” or “service-oriented” approach, introduces a balanced methodology that offers the scalability of microservices while retaining the structural integrity associated with mature frameworks.

Next.js 15: Technical Foundations
Next.js 15 represents a paradigm shift in how React applications are constructed, particularly through its refined App Router and deep integration with React 19. The framework emphasizes a server-first mental model, leveraging React Server Components (RSC) to minimize the amount of JavaScript sent to the client. Unlike traditional client-side rendering where the browser executes the entire component tree, Server Components execute exclusively on the server during build time or request time.
Server and Client Component Synergy
The architectural core of Next.js 15 relies on the distinction between Server and Client Components. Server Components are designed to fetch data directly from the backend or database, keeping database credentials and business logic entirely off the client device. Client Components, designated by the ‘use client’ directive, are reserved for interactivity and state management.
The Backend: Django REST Framework
Django remains a preferred backend for decoupled stacks due to its “batteries-included” philosophy, providing an integrated Object-Relational Mapping (ORM) system and a robust security framework. When exposing Django as an API, the Django REST Framework (DRF) is the industry standard, offering a modular architecture for serialization, authentication, and permission management.
Infrastructure and Deployment
Deploying a decoupled stack requires an infrastructure that supports multiple services, secure networking, and efficient scaling. The DigitalOcean App Platform is a Platform-as-a-Service (PaaS) that manages these complexities by allowing developers to deploy the Django backend and Next.js frontend as separate Service components within a single application.
Conclusion
The combination of Next.js 15 and Django REST Framework represents a mature and resilient architectural path for modern web development. By leveraging the high-performance rendering of React 19 and the comprehensive security of the Django ecosystem, developers can build applications that are both highly interactive and rigorously protected.

Claude Opus 4.6 vs. GPT 5.4

GIF

Claude Opus 4.6 vs. GPT 5.4: My Take as a C#/.NET Dev on AI Coding Companions

Alright team, let’s talk AI. As a senior engineer who’s spent more years than I care to admit wrangling C# and .NET, I’ve seen my fair share of “game-changing” tech. Most of it is just hype. But these new-gen LLMs? They’re different. We’re talking about legitimate productivity boosters, especially when you’re staring down a tricky bug or architecting a new microservice.

Lately, I’ve been putting the big two — Claude Opus 4.6 and GPT 5.4 — through their paces specifically for coding tasks. The question isn’t if they’re useful, but which one to bring to the fight, or if we should be thinking “both.” Let’s dive into my real-world experiences.

The Setup: My C#/.NET AI Playground

Before we get into the nitty-gritty, a quick word on my testing environment. I wasn’t just asking them to write “Hello World.” I was throwing real-world problems at them: building complex LINQ queries, designing a robust API controller, refactoring legacy code, even trying to get them to write xUnit tests for some tricky asynchronous logic.

I wanted to see how they handled:

  • Context: Can they keep track of a larger codebase or conversation?
  • Precision: Do they generate code that actually compiles and runs correctly the first time?
  • Nuance: Can they understand why I’m asking for something, not just what?
  • Debugging: How good are they at finding issues in their own code or mine?

GPT 5.4: The Speedy Generalist with a Few Surprises

GPT 5.4 feels like that incredibly bright junior developer who’s read every programming book but sometimes misses the specific context of our project. It’s fast, incredibly broad in its knowledge, and often provides surprisingly elegant solutions right out of the gate.

When I needed boilerplate code for a new DbContext or a standard ASP.NET Core controller, GPT 5.4 was lightning-fast and usually spot-on. It’s fantastic for generating common design patterns or even suggesting different approaches to a problem.

Where it Shines:

  • Broad Knowledge Base: If it’s a common C# pattern, a widely used .NET library, or a general algorithm, GPT 5.4 knows it.
  • Code Generation Speed: It often generates long code blocks quickly, perfect for getting a first draft down.
  • Exploration: Great for brainstorming different ways to solve a problem or exploring new libraries.

Where I Pump the Brakes:
Sometimes, GPT 5.4 can be a bit too confident. It occasionally generates plausible-looking code that has subtle bugs, or it might make assumptions about my project that aren’t true. I’ve also found it can “forget” earlier parts of our conversation if the thread gets too long. It’s like it gets distracted by the next shiny coding problem.

GIF

Claude Opus 4.6: The Meticulous Architect, Slow and Steady

Claude Opus 4.6, on the other hand, feels more like a seasoned architect. It’s often slower to respond, but its answers tend to be incredibly thoughtful, detailed, and deeply contextual. It seems to “think” more before responding, often asking clarifying questions or laying out its reasoning step-by-step.

For complex refactoring tasks, or when I was trying to optimize a specific piece of asynchronous code for performance, Claude truly stood out. It provided not just the code, but the rationale behind the choices, often citing best practices or potential pitfalls. It felt like pair programming with someone who meticulously considers every angle.

Where it Shines:

  • Contextual Understanding: It maintains context over very long conversations, making it excellent for multi-step tasks or complex debugging.
  • Deep Reasoning: Its explanations are often superb, breaking down complex problems and justifying its code choices.
  • Fewer Hallucinations: I’ve found it to be more reliable in generating correct, runnable code without subtle errors. It double-checks its work, which is invaluable.
  • Refactoring & Debugging: Excellent at identifying issues in existing code and suggesting robust improvements.

Where I Feel the Pinch:
The speed. Sometimes, when you just need a quick IEnumerable extension method or a simple DI setup, waiting for Claude’s detailed explanation can feel a bit overkill. It’s not a rapid-fire code generator in the same way GPT 5.4 can be.

The Verdict: Don’t Choose, Combine!

After weeks of real-world use, my conclusion is clear: you don’t have to pick just one. These aren’t competitors; they’re complementary tools in a modern software engineer’s arsenal.

Think of it this way:

  • Reach for GPT 5.4 when:

    • You need rapid prototyping or boilerplate generation.
    • You’re exploring new libraries or frameworks and need quick examples.
    • You’re stuck on a common problem and need a few different potential solutions fast.
    • You need simple, isolated code snippets.
  • Reach for Claude Opus 4.6 when:

    • You’re working on complex architectural decisions or refactoring significant parts of your codebase.
    • You need detailed explanations, best practices, and a deeper understanding of why certain code is structured a certain way.
    • You’re debugging persistent, tricky issues and need a methodical, logical approach.
    • You have a long, ongoing conversation about a specific problem and need the AI to maintain deep context.

Technical Image

I often start with GPT 5.4 for initial drafts or quick ideas. Then, if I hit a wall, or if the problem requires more nuanced reasoning, I’ll port the conversation (or at least the core problem) over to Claude Opus 4.6 for a more in-depth architectural review or a meticulous debugging session. It’s like having a brilliant junior dev for the grunt work and an experienced architect for the heavy lifting.

Your AI Pair Programming Partner(s)

Adopting these tools isn’t about replacing engineers; it’s about augmenting our capabilities. It’s like having a super-powered pair programmer who never gets tired and has read the internet. For us C# and .NET folks, understanding the strengths of both Claude Opus 4.6 and GPT 5.4 means we can write better code, faster, and with fewer headaches.

What are your experiences? Have you found one to be clearly superior for your specific tech stack, or are you also seeing the value in a multi-model approach? Let me know in the comments!

CA 04 – Two Sum & Sorted Two Sum

TWO SUM PROBLEM

I solved the Two Sum problem from LeetCode in java using brute force method. The problem gives an array of integers and a target value. I need to find two indices so that the numbers at those indices add to the target. and i can’t use the same element twice, and there will always be one valid answer.

To solve this, I used a simple method. I took one number from the array and compared it with every number that comes after it. For every pair i checked if their sum is equal to the target. If it is i need to return the indices of those two numbers.

I used two loops for this. The first loop picks one number, and the second loop checks the remaining numbers. As soon as it find a pair that matches the target, it return the answer.

This method works correctly but takes more time because it checks all pairs. The time complexity is O(n^2). The space complexity is O(1) because no extra space is used.

class Solution {
    public int[] twoSum(int[] nums, int target) {
        for(int i = 0; i < nums.length; i++) {
            for(int j = i + 1; j < nums.length; j++) {
                if(nums[i] + nums[j] == target) {
                    return new int[] {i, j};
                }
            }
        }
        return new int[] {};
    }
}

SORTED TWO SUM

The task is to find two numbers such that their sum equals the target and return their positions. I also need to add 1 to the indices before returning them.

To solve this, i used a simple brute force method. It checked every possible pair of numbers in the array and saw if their sum equals the target.

I used two loops for this. The first loop picks one number, and the second loop checks all the numbers after it. For each pair, It added the two numbers and compared the result with the target. If the sum matches, it return the indices. return i + 1 and j + 1.

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        for(int i = 0; i < numbers.length; i++) {
            for(int j = i + 1; j < numbers.length; j++) {
                if(numbers[i] + numbers[j] == target) {
                    return new int[] {i + 1, j + 1};
                }
            }
        }
        return new int[] {};
    }
}

This method works correctly but it takes more time. The time complexity is O(n^2) because of the nested loops. The space complexity is O(1) since no extra space is used.

Stop Fighting Zustand Context: Practical Store Scoping Patterns for React

Zustand is one of the rare state management libraries that feels good almost immediately. It is small, fast, and does not try to force a framework-sized architecture onto your app.

That simplicity is exactly why many teams adopt it quickly.

Then the app grows, and a different problem shows up: scoped state.

What happens when your app needs multiple, isolated instances of the same store? Imagine a dashboard where each complex “widget” needs its own independent state or a multi-step “wizard” where simultaneous tabs shouldn’t overwrite each other’s data.

The official Zustand documentation recommends using React Context for this, but doing it manually is a grind. You have to:

  1. Create a React Context.
  2. Create a factory function for the store instance.
  3. Build a wrapper Provider component.
  4. Manually rebuild strongly-typed selector hooks (useStore, useStoreApi) for consumers.
  5. Pepper your codebase with useShallow to prevent unnecessary re-renders when returning objects or arrays.

At that point, plain Zustand is still capable, but the implementation starts getting repetitive.

To reduce that boilerplate, I built @okyrychenko-dev/react-zustand-toolkit.

It gives you a few composable helpers around Zustand:

  • generated context providers
  • shallow-first selectors by default
  • “resolved” hooks that can read from either a scoped or global store
  • a small set of React 19 utilities

The goal of this article is not to oversell that toolkit. It is to show the real architectural cases where it helps, where it does not, and how its three main factory functions map to actual React state ownership patterns.

Before We Start: The Real Problem

Zustand itself is not the problem. In many apps, plain Zustand is already enough:

  • one global store
  • a few focused selectors
  • occasional middleware
  • no need for isolated store instances

The pain starts when your architecture stops being purely global.

That usually happens in one of these situations:

  • you render the same complex widget multiple times and each instance needs separate state
  • you build reusable modules that should work standalone and also inside a larger application
  • you want most of the app to read from one global store, but a subtree should temporarily override it
  • you are tired of repeating provider + context + hook wiring for every isolated Zustand use case

That is the exact gap this toolkit is trying to cover.

So while this article shows the library API, the more important takeaway is architectural:

  • use a plain global store when isolation is not needed
  • use scoped providers when identity and lifetime matter per subtree
  • use resolved hooks when consumers should not care where the state comes from

With that framing in place, the API makes much more sense.

1. The Global Singleton: createShallowStore

Let’s start with the simplest layer.

If you are just building a standard global store, the main reason to use this layer is shallow-first selectors.

In standard Zustand, if your selector returns a new object or array, your component will re-render every single time the store updates, even if the selected values haven’t changed. To fix this, you have to manually wrap your selectors:

// ❌ Standard Zustand requires boilerplate for shallow picks
import { useShallow } from 'zustand/react/shallow'

const { id, name } = useUserStore(
  useShallow((state) => ({ id: state.id, name: state.name }))
)

With createShallowStore, your generated hooks use zustand/shallow by default. You can pick objects and arrays freely without the boilerplate:

import { createShallowStore } from "@okyrychenko-dev/react-zustand-toolkit";

interface SessionStore {
  token: string | null;
  user: { name: string; role: string } | null;
  login: (token: string, user: { name: string; role: string }) => void;
}

const { useStore, useStorePlain, useStoreApi } = createShallowStore<SessionStore>((set) => ({
  token: null,
  user: null,
  login: (token, user) => set({ token, user }),
}));

// ✅ Object picks use shallow comparison by default.
function ProfileInfo() {
  const { token, user } = useStore((state) => ({
    token: state.token,
    user: state.user
  }));

  return <div>{user?.name}</div>;
}

If you ever need standard, strict-equality behavior, the toolkit always provides explicit useStorePlain alternatives.

Why this matters in practice

The shallow-first approach is especially useful when components naturally want to read small object bundles:

const { isLoading, error, reload } = useStore((state) => ({
  isLoading: state.isLoading,
  error: state.error,
  reload: state.reload,
}));

In plain Zustand, patterns like this often push teams into one of two habits:

  • wrapping selectors in useShallow
  • splitting every field into its own selector call

Both work. They are just noisy when repeated across a large codebase.

This helper does not replace selector discipline. It simply makes the common “pick a few fields” case less repetitive.

What it does not do

It is still important to be precise about the limits:

  • it does not make every selector free
  • it does not replace good store design
  • it does not solve deep comparison problems
  • it does not remove the need to think about derived data and subscription granularity

It mainly improves the ergonomics of shallow object and array picks.

2. Isolated Store Contexts: createStoreProvider

The next layer is where Zustand usually becomes a little more manual.

When you need true isolation, where every instance of a component must own a separate store, createStoreProvider removes most of the repetitive setup.

It generates the Context, the Provider component, and the typed consumer hooks in a single call.

import { createStoreProvider } from "@okyrychenko-dev/react-zustand-toolkit";

interface WizardStore {
  step: number;
  direction: 'forward' | 'backward';
  next: () => void;
}

// 1. Generate the provider and hooks
export const { 
  Provider: WizardProvider, 
  useContextStore,
  useContextStoreApi 
} = createStoreProvider<WizardStore>((set) => ({
  step: 1,
  direction: 'forward',
  next: () => set((state) => ({ 
    step: state.step + 1, 
    direction: 'forward' 
  })),
}), "Wizard");

// 2. Consume safely within the isolated tree
function WizardControls() {
  const step = useContextStore((state) => state.step);
  const next = useContextStore((state) => state.next);

  return (
    <div>
      <p>Current Step: {step}</p>
      <button onClick={next}>Next Step</button>
    </div>
  );
}

Why provider-scoped Zustand is useful

Context-scoped stores are not just an implementation detail. They model a different ownership pattern.

With a global singleton store:

  • the store exists once
  • every consumer shares the same data
  • state lifetime usually matches the application lifetime

With a provider-scoped store:

  • each provider instance owns one store
  • sibling subtrees can hold completely different values
  • state lifetime follows the mounted subtree

That makes provider-scoped stores a good fit for:

  • wizards
  • modals with complex internal state
  • embeddable widgets
  • repeated dashboard panels
  • request or test isolation

Provider Lifecycle Hooks

Sometimes you need to initialize your isolated store with data from outside (like props) before the component renders, or run a side effect right after it mounts.

The generated Provider component supports two lifecycle stages:

  • onStoreInit: Synchronous initialization during store creation.
  • onStoreReady: Post-commit side effects.
function WizardShell({ initialStep }: { initialStep: number }) {
  return (
    <WizardProvider
      onStoreInit={(store) => {
        // Initialize the store synchronously before first render
        store.setState({ step: initialStep });
      }}
      onStoreReady={(store) => {
        // Run side effects like analytics tracking after mount
        console.log("Wizard instance mounted at step", store.getState().step);
      }}
    >
      <WizardControls />
    </WizardProvider>
  );
}

That split is small, but useful:

  • onStoreInit is for deterministic setup before consumers read the store
  • onStoreReady is for effects that should happen after mount

That is a better mental model than mixing initialization and side effects in the same callback.

3. The Best of Both Worlds: createStoreToolkit

This is the layer that makes the package feel more like a toolkit and less like a single helper.

What if you have global state, but certain parts of the UI need to override it locally?

This is where createStoreToolkit becomes useful.

It creates both a global singleton store and an optional context provider. It also gives you resolved hooks such as useResolvedValue and useResolvedStoreApi.

These hooks dynamically check the React Component tree:

  1. Are we inside a Provider for this store? If yes, use the scoped context store.
  2. No Provider found? Fall back to the global singleton store.

Take a look at this Theme example:

import { createStoreToolkit } from "@okyrychenko-dev/react-zustand-toolkit";

interface ThemeStore {
  mode: 'light' | 'dark';
  setMode: (mode: 'light' | 'dark') => void;
}

// Generates both global store AND provider
const themeToolkit = createStoreToolkit<ThemeStore>((set) => ({
  mode: 'light', // Global default
  setMode: (mode) => set({ mode }),
}), { name: "Theme" });

export const { useResolvedValue: useTheme } = themeToolkit;
export const { Provider: ThemeProvider } = themeToolkit.provider;

Now consuming components do not need to care whether they are reading from the global store or a scoped provider instance:

function ThemedCard() {
  const mode = useTheme((state) => state.mode);
  return <div className={`card-${mode}`}>Smart Card</div>;
}

function App() {
  return (
    <div>
      {/* 🌍 1. Uses the global 'light' theme */}
      <ThemedCard /> 

      {/* 🏠 2. Overrides the state to 'dark' for this specific tree ONLY */}
      <ThemeProvider onStoreInit={(store) => store.getState().setMode('dark')}>
        <div className="dark-zone">
          <ThemedCard />
        </div>
      </ThemeProvider>
    </div>
  );
}

This hybrid pattern is useful for reusable UI modules, nested widgets, or apps where most of the UI can share one store, but a subtree sometimes needs an isolated instance.

Why resolved hooks are interesting

This is probably the most opinionated part of the library.

Normally, when a component can run in two modes, you end up with one of these designs:

  • separate hooks for global and scoped usage
  • props that inject the store
  • branching logic scattered across the component tree

Resolved hooks collapse that decision into one place:

  • inside the matching provider, read the scoped store
  • outside it, read the global store

That can simplify component APIs a lot, especially in shared UI packages.

A good mental model

Think of createStoreToolkit as:

  1. a normal global Zustand store
  2. plus an optional scoped override mechanism
  3. plus consumer hooks that pick the nearest valid source

That framing is more accurate than thinking of it as “magic context Zustand”.

Where to be careful

Resolved hooks are convenient, but they are also a design choice. I would avoid them when:

  • the distinction between global and local state should be explicit in the component API
  • debugging would become ambiguous because a component may silently switch data sources
  • different teams own global and scoped behavior separately

In other words, resolved hooks are best when the fallback behavior is intentional, not surprising.

4. Middleware Without Losing Types

So far the value has been architectural. This section is more about preserving the normal Zustand experience.

Zustand middleware such as Redux DevTools, Persist, or SubscribeWithSelector still belongs in the store creator.

The useful part here is that the toolkit preserves the resulting store API types, so helpers like persist.rehydrate or selector-aware subscribe remain available on useStoreApi.

import { createShallowStore } from "@okyrychenko-dev/react-zustand-toolkit";
import { devtools, persist } from "zustand/middleware";

interface CartStore {
  items: string[];
  addItem: (item: string) => void;
}

// Middleware types are preserved on the store API.
const { useStore, useStoreApi } = createShallowStore<
  CartStore,
  [["zustand/persist", CartStore], ["zustand/devtools", never]]
>(
  persist(
    devtools(
      (set) => ({
        items: [],
        addItem: (item) => set((state) => ({ items: [...state.items, item] })),
      }),
      { name: "GlobalCartStore" }
    ),
    { name: "cart-storage" }
  )
);

// Mutator APIs stay typed.
useStoreApi.persist.rehydrate();
useStoreApi.devtools.cleanUp();

This does not mean the toolkit adds a custom DevTools layer for provider stores. If you want Redux DevTools, apply Zustand middleware in the creator itself. Dynamic provider instances are not auto-connected for you.

This is a subtle point, but an important one.

The library is not trying to compete with Zustand middleware. It is trying to stay out of the way while preserving the resulting types.

That is the right design choice. Middleware remains a Zustand concern, not a toolkit-specific abstraction.

5. Ready for React 19 ⚛️

This part is useful, but it should be read with the right expectations.

React 19 introduces hooks and rendering primitives such as Transitions, Action State, and Optimistic Updates.

@okyrychenko-dev/react-zustand-toolkit includes a few small utilities around those APIs. They are wrappers, not a new state model.

Wrapping Actions in Transitions

If you have an update that may trigger expensive rendering, you can wrap the action in a transition:

import { createTransitionAction } from "@okyrychenko-dev/react-zustand-toolkit";

const incrementInTransition = createTransitionAction(() => {
  // This update runs inside React.startTransition
  counterToolkit.useStoreApi.getState().increment();
});

Action State Adapters

If you want a thin adapter over useActionState for store-related async actions:

import { useActionStateAdapter } from "@okyrychenko-dev/react-zustand-toolkit";

function SaveForm() {
  const [status, submitForm, isPending] = useActionStateAdapter(
    async (payload: FormData) => {
      await myApi.save(payload);
      myStore.getState().markSaved();
      return "saved";
    }, 
    "idle"
  );

  return (
    <form action={submitForm}>
      <button disabled={isPending}>
        {isPending ? "Saving..." : "Save"}
      </button>
      {status === 'saved' && <p>Saved successfully!</p>}
    </form>
  );
}

Optimistic UI Updates

If you want an optimistic layer on top of committed Zustand state:

import { useOptimisticReducer } from "@okyrychenko-dev/react-zustand-toolkit";

function TodoList() {
  const serverTodos = useTodos((state) => state.todos);

  const [optimisticTodos, addOptimisticTodo] = useOptimisticReducer(
    serverTodos,
    (current, nextTodo) => [...current, nextTodo]
  );

  // ... render optimisticTodos instead of serverTodos
}

I would treat these helpers as convenience utilities, not the center of the package.

They are nice because they keep React 19-oriented code close to the same toolkit, but the core value of the library is still:

  • store scoping
  • shallow-first selectors
  • resolved hooks

That is where the architectural leverage really is.

6. Which Factory Should You Reach For?

If you only remember one section from this article, make it this one.

If you are evaluating the library quickly, this is the practical decision tree:

Use createShallowStore when:

  • you want one global singleton store
  • your main annoyance is repeated useShallow usage
  • you do not need isolated instances

Use createStoreProvider when:

  • every mounted subtree should own its own store
  • the state lifetime should end when that subtree unmounts
  • store isolation should be explicit

Use createStoreToolkit when:

  • you want a global store by default
  • some subtrees should be able to override it with local instances
  • your consumers should work in both environments with the same hook API

That separation is one of the better aspects of the package. The API is not trying to force one pattern onto every use case.

Quick Comparison

Factory Best for Store lifetime Main benefit
createShallowStore One global store App-wide Shallow-first selectors with low boilerplate
createStoreProvider Isolated subtree state Per provider instance Explicit store ownership and lifecycle
createStoreToolkit Mixed global + local override scenarios Global plus optional scoped instances Shared consumer API through resolved hooks

7. When You Probably Do Not Need This Library

This section matters because a good abstraction should come with a clear boundary.

It is also worth being explicit about the non-use-cases.

You probably do not need this toolkit if:

  • your app already works well with a single global Zustand store
  • you rarely select object or array bundles
  • you do not use scoped providers at all
  • you prefer explicit store injection over fallback resolution

There is no benefit in adding an abstraction layer just because it exists.

Good Zustand architecture is still mostly about picking the right ownership model for state. This toolkit simply makes a few of those models easier to implement consistently.

Wrapping Up

The strongest part of react-zustand-toolkit is not that it reinvents Zustand. It does not.

Its value is that it packages a few repeatable patterns into a small API:

  • generated providers and hooks for isolated store instances
  • shallow-first selector hooks with explicit plain alternatives
  • resolved hooks for code that should work both inside and outside a provider
  • typed passthrough for Zustand middleware
  • a few optional React 19 wrappers

If those are problems you keep solving by hand, the library is worth a look.

If your app only needs a single global store, plain Zustand may still be enough, and that is completely fine.

But if your real problem is no longer “how do I store state?” and has become “who owns this state, how many instances of it exist, and how should components resolve it?”, then this toolkit starts to become much more interesting.

Next Steps

Install it today:

npm install @okyrychenko-dev/react-zustand-toolkit zustand

Check out the full API reference, examples, and source code in the GitHub Repository.

If you have run into the “global store everywhere, until one subtree needs isolation” problem, this is the part of Zustand architecture the toolkit is trying to simplify.