{"id":83,"date":"2026-07-17T14:00:00","date_gmt":"2026-07-17T08:30:00","guid":{"rendered":"https:\/\/softcrony.com\/blog\/?p=83"},"modified":"2026-07-17T14:00:00","modified_gmt":"2026-07-17T08:30:00","slug":"laravel-claude-api-ai-powered-feature","status":"publish","type":"post","link":"https:\/\/softcrony.com\/blog\/laravel-claude-api-ai-powered-feature\/","title":{"rendered":"Building an AI-Powered Feature in Laravel Using the Claude API"},"content":{"rendered":"<p>Adding AI capabilities to a Laravel application is simpler than most developers expect. The Claude API is well-documented, the PHP client is straightforward, and the patterns for integrating AI into Laravel&#8217;s architecture are clean.<\/p>\n<p>This guide builds a real AI-powered feature from scratch \u2014 an intelligent code review assistant that analyzes code submitted via API and returns structured feedback.<\/p>\n<h2>What We&#8217;re Building<\/h2>\n<p>An AI code review endpoint that:<\/p>\n<ul>\n<li>Accepts code via API request<\/li>\n<li>Sends it to Claude for analysis<\/li>\n<li>Returns structured feedback (issues, severity, suggestions)<\/li>\n<li>Streams the response in real-time<\/li>\n<li>Caches results for identical inputs<\/li>\n<li>Tracks usage per user<\/li>\n<\/ul>\n<h2>Prerequisites<\/h2>\n<ul>\n<li>Laravel 11 or 12<\/li>\n<li>Anthropic API key (get one at console.anthropic.com)<\/li>\n<li>PHP 8.2+<\/li>\n<li>Redis (for caching and rate limiting)<\/li>\n<\/ul>\n<h2>Step 1 \u2014 Install the Anthropic PHP SDK<\/h2>\n<pre><code>composer require anthropics\/anthropic-sdk-php<\/code><\/pre>\n<p>Add your API key to <code>.env<\/code>:<\/p>\n<pre><code>ANTHROPIC_API_KEY=sk-ant-your-key-here\r\nANTHROPIC_MODEL=claude-sonnet-4-5\r\nANTHROPIC_MAX_TOKENS=2000<\/code><\/pre>\n<p>Add to <code>config\/services.php<\/code>:<\/p>\n<pre><code>'anthropic' => [\r\n    'api_key'   => env('ANTHROPIC_API_KEY'),\r\n    'model'     => env('ANTHROPIC_MODEL', 'claude-sonnet-4-5'),\r\n    'max_tokens' => env('ANTHROPIC_MAX_TOKENS', 2000),\r\n],<\/code><\/pre>\n<h2>Step 2 \u2014 Create the Claude Service<\/h2>\n<pre><code>php artisan make:class Services\/ClaudeService<\/code><\/pre>\n<pre><code>&lt;?php\r\n\r\nnamespace App\\Services;\r\n\r\nuse Anthropic\\Client;\r\nuse Anthropic\\Anthropic;\r\nuse Illuminate\\Support\\Facades\\Cache;\r\nuse Illuminate\\Support\\Facades\\Log;\r\n\r\nclass ClaudeService\r\n{\r\n    private Client $client;\r\n    private string $model;\r\n    private int $maxTokens;\r\n\r\n    public function __construct()\r\n    {\r\n        $this->client = Anthropic::client(\r\n            config('services.anthropic.api_key')\r\n        );\r\n        $this->model      = config('services.anthropic.model');\r\n        $this->maxTokens  = config('services.anthropic.max_tokens');\r\n    }\r\n\r\n    \/**\r\n     * Send a message and get a complete response.\r\n     *\/\r\n    public function message(\r\n        string $prompt,\r\n        string $systemPrompt = '',\r\n        ?string $cacheKey = null,\r\n        int $cacheTtl = 3600\r\n    ): string {\r\n        \/\/ Return cached response if available\r\n        if ($cacheKey && Cache::has($cacheKey)) {\r\n            return Cache::get($cacheKey);\r\n        }\r\n\r\n        try {\r\n            $messages = [\r\n                ['role' => 'user', 'content' => $prompt]\r\n            ];\r\n\r\n            $params = [\r\n                'model'      => $this->model,\r\n                'max_tokens' => $this->maxTokens,\r\n                'messages'   => $messages,\r\n            ];\r\n\r\n            if ($systemPrompt) {\r\n                $params['system'] = $systemPrompt;\r\n            }\r\n\r\n            $response = $this->client->messages()->create($params);\r\n            $content  = $response->content[0]->text;\r\n\r\n            \/\/ Cache the response\r\n            if ($cacheKey) {\r\n                Cache::put($cacheKey, $content, $cacheTtl);\r\n            }\r\n\r\n            return $content;\r\n\r\n        } catch (\\Exception $e) {\r\n            Log::error('Claude API error', [\r\n                'error'  => $e->getMessage(),\r\n                'model'  => $this->model,\r\n                'prompt' => substr($prompt, 0, 200),\r\n            ]);\r\n            throw $e;\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Stream a response \u2014 yields chunks as they arrive.\r\n     *\/\r\n    public function stream(\r\n        string $prompt,\r\n        string $systemPrompt = '',\r\n        callable $onChunk = null\r\n    ): string {\r\n        $fullResponse = '';\r\n\r\n        try {\r\n            $params = [\r\n                'model'      => $this->model,\r\n                'max_tokens' => $this->maxTokens,\r\n                'messages'   => [\r\n                    ['role' => 'user', 'content' => $prompt]\r\n                ],\r\n            ];\r\n\r\n            if ($systemPrompt) {\r\n                $params['system'] = $systemPrompt;\r\n            }\r\n\r\n            $stream = $this->client->messages()->createStreaming($params);\r\n\r\n            foreach ($stream as $event) {\r\n                if ($event->type === 'content_block_delta') {\r\n                    $chunk = $event->delta->text ?? '';\r\n                    $fullResponse .= $chunk;\r\n\r\n                    if ($onChunk) {\r\n                        $onChunk($chunk);\r\n                    }\r\n                }\r\n            }\r\n\r\n        } catch (\\Exception $e) {\r\n            Log::error('Claude stream error', ['error' => $e->getMessage()]);\r\n            throw $e;\r\n        }\r\n\r\n        return $fullResponse;\r\n    }\r\n\r\n    \/**\r\n     * Send a structured prompt and parse JSON response.\r\n     *\/\r\n    public function structured(string $prompt, string $systemPrompt = ''): array\r\n    {\r\n        $jsonSystemPrompt = $systemPrompt . \"\\n\\nIMPORTANT: Respond ONLY with valid JSON. No markdown, no explanation, no code blocks. Raw JSON only.\";\r\n\r\n        $response = $this->message($prompt, $jsonSystemPrompt);\r\n\r\n        try {\r\n            return json_decode($response, true, 512, JSON_THROW_ON_ERROR);\r\n        } catch (\\JsonException $e) {\r\n            Log::error('Failed to parse Claude JSON response', [\r\n                'response' => $response,\r\n                'error'    => $e->getMessage(),\r\n            ]);\r\n            throw new \\RuntimeException('AI returned invalid JSON: ' . $e->getMessage());\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<h2>Step 3 \u2014 Create the Code Review Service<\/h2>\n<pre><code>php artisan make:class Services\/CodeReviewService<\/code><\/pre>\n<pre><code>&lt;?php\r\n\r\nnamespace App\\Services;\r\n\r\nclass CodeReviewService\r\n{\r\n    private string $systemPrompt = <<<'PROMPT'\r\nYou are an expert code reviewer specializing in PHP, Laravel, and web security.\r\nAnalyze the submitted code and identify issues across these categories:\r\n\r\n- Security vulnerabilities (SQL injection, XSS, CSRF, mass assignment, etc.)\r\n- Performance problems (N+1 queries, missing indexes, inefficient loops)\r\n- Laravel best practices violations\r\n- Code quality issues (complexity, duplication, naming)\r\n- Missing error handling\r\n- Type safety issues\r\n\r\nFor each issue found, provide:\r\n- The specific problem\r\n- The line or code block affected\r\n- The severity (critical\/high\/medium\/low)\r\n- A concrete fix or recommendation\r\n\r\nBe specific and actionable. Reference the actual code, not generic advice.\r\nPROMPT;\r\n\r\n    public function __construct(\r\n        private readonly ClaudeService $claude\r\n    ) {}\r\n\r\n    public function review(string $code, string $language = 'php'): array\r\n    {\r\n        $cacheKey = 'code_review_' . md5($code . $language);\r\n\r\n        $prompt = <<<PROMPT\r\nReview this {$language} code and return a JSON object with this exact structure:\r\n\r\n{\r\n  \"summary\": \"Brief overall assessment in 1-2 sentences\",\r\n  \"score\": 75,\r\n  \"issues\": [\r\n    {\r\n      \"title\": \"Issue title\",\r\n      \"description\": \"Detailed description of the problem\",\r\n      \"severity\": \"critical|high|medium|low\",\r\n      \"code_snippet\": \"The problematic code\",\r\n      \"fix\": \"The corrected code or recommendation\"\r\n    }\r\n  ],\r\n  \"positives\": [\"What the code does well\"],\r\n  \"recommendations\": [\"Top 3 overall recommendations\"]\r\n}\r\n\r\nCode to review:\r\n```{$language}\r\n{$code}\r\n```\r\nPROMPT;\r\n\r\n        return $this->claude->structured($prompt, $this->systemPrompt);\r\n    }\r\n}<\/code><\/pre>\n<h2>Step 4 \u2014 Create the Controller<\/h2>\n<pre><code>php artisan make:controller Api\/CodeReviewController<\/code><\/pre>\n<pre><code>&lt;?php\r\n\r\nnamespace App\\Http\\Controllers\\Api;\r\n\r\nuse App\\Http\\Controllers\\Controller;\r\nuse App\\Services\\CodeReviewService;\r\nuse App\\Models\\CodeReview;\r\nuse Illuminate\\Http\\Request;\r\nuse Illuminate\\Http\\JsonResponse;\r\n\r\nclass CodeReviewController extends Controller\r\n{\r\n    public function __construct(\r\n        private readonly CodeReviewService $reviewService\r\n    ) {}\r\n\r\n    public function review(Request $request): JsonResponse\r\n    {\r\n        $validated = $request->validate([\r\n            'code'     => 'required|string|max:10000',\r\n            'language' => 'in:php,javascript,python,typescript|nullable',\r\n        ]);\r\n\r\n        $language = $validated['language'] ?? 'php';\r\n\r\n        try {\r\n            $result = $this->reviewService->review(\r\n                $validated['code'],\r\n                $language\r\n            );\r\n\r\n            \/\/ Save review to database\r\n            $review = CodeReview::create([\r\n                'user_id'  => $request->user()->id,\r\n                'code'     => $validated['code'],\r\n                'language' => $language,\r\n                'result'   => $result,\r\n                'score'    => $result['score'] ?? null,\r\n            ]);\r\n\r\n            return response()->json([\r\n                'success'   => true,\r\n                'review_id' => $review->id,\r\n                'result'    => $result,\r\n            ]);\r\n\r\n        } catch (\\Exception $e) {\r\n            return response()->json([\r\n                'success' => false,\r\n                'message' => 'Code review failed. Please try again.',\r\n            ], 500);\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Stream the review for real-time display.\r\n     *\/\r\n    public function stream(Request $request)\r\n    {\r\n        $validated = $request->validate([\r\n            'code' => 'required|string|max:10000',\r\n        ]);\r\n\r\n        return response()->stream(function () use ($validated) {\r\n            $this->reviewService->streamReview(\r\n                $validated['code'],\r\n                function (string $chunk) {\r\n                    echo \"data: \" . json_encode(['chunk' => $chunk]) . \"\\n\\n\";\r\n                    ob_flush();\r\n                    flush();\r\n                }\r\n            );\r\n\r\n            echo \"data: [DONE]\\n\\n\";\r\n            ob_flush();\r\n            flush();\r\n        }, 200, [\r\n            'Content-Type'      => 'text\/event-stream',\r\n            'Cache-Control'     => 'no-cache',\r\n            'X-Accel-Buffering' => 'no',\r\n        ]);\r\n    }\r\n}<\/code><\/pre>\n<h2>Step 5 \u2014 Routes and Rate Limiting<\/h2>\n<pre><code>\/\/ routes\/api.php\r\nuse App\\Http\\Controllers\\Api\\CodeReviewController;\r\n\r\nRoute::middleware(['auth:sanctum', 'throttle:code_review'])->group(function () {\r\n    Route::post('\/code-review', [CodeReviewController::class, 'review']);\r\n    Route::post('\/code-review\/stream', [CodeReviewController::class, 'stream']);\r\n});<\/code><\/pre>\n<pre><code>\/\/ app\/Providers\/RouteServiceProvider.php \u2014 add the rate limiter\r\nRateLimiter::for('code_review', function (Request $request) {\r\n    return [\r\n        Limit::perMinute(5)->by($request->user()->id),\r\n        Limit::perDay(50)->by($request->user()->id),\r\n    ];\r\n});<\/code><\/pre>\n<h2>Step 6 \u2014 Test It<\/h2>\n<pre><code>POST \/api\/code-review\r\nAuthorization: Bearer YOUR_TOKEN\r\nContent-Type: application\/json\r\n\r\n{\r\n  \"code\": \"<?php\\nfunction getUser($id) {\\n  $result = mysql_query(\\\"SELECT * FROM users WHERE id = \\\" . $id);\\n  return mysql_fetch_assoc($result);\\n}\",\r\n  \"language\": \"php\"\r\n}<\/code><\/pre>\n<p>Expected response:<\/p>\n<pre><code>{\r\n  \"success\": true,\r\n  \"review_id\": 1,\r\n  \"result\": {\r\n    \"summary\": \"This code has critical security vulnerabilities and uses deprecated PHP functions that no longer exist in modern PHP.\",\r\n    \"score\": 12,\r\n    \"issues\": [\r\n      {\r\n        \"title\": \"SQL Injection Vulnerability\",\r\n        \"description\": \"The $id parameter is concatenated directly into the SQL query without sanitization.\",\r\n        \"severity\": \"critical\",\r\n        \"code_snippet\": \"\\\"SELECT * FROM users WHERE id = \\\" . $id\",\r\n        \"fix\": \"Use PDO with prepared statements: $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]);\"\r\n      },\r\n      {\r\n        \"title\": \"Deprecated mysql_ Functions\",\r\n        \"description\": \"mysql_query() and mysql_fetch_assoc() were removed in PHP 7.0.\",\r\n        \"severity\": \"critical\",\r\n        \"code_snippet\": \"mysql_query(...), mysql_fetch_assoc(...)\",\r\n        \"fix\": \"Use PDO or MySQLi instead.\"\r\n      }\r\n    ],\r\n    \"positives\": [\"Simple, readable function structure\"],\r\n    \"recommendations\": [\r\n      \"Migrate to PDO with prepared statements immediately\",\r\n      \"Add input validation and type hints\",\r\n      \"Use a framework ORM like Eloquent instead of raw SQL\"\r\n    ]\r\n  }\r\n}<\/code><\/pre>\n<h2>Production Considerations<\/h2>\n<p><strong>Cost management:<\/strong> The Claude API charges per token. A typical code review of 500 lines costs approximately $0.01\u20130.03. At 50 reviews\/day per user, that's manageable \u2014 but monitor usage and set hard limits per user per day.<\/p>\n<p><strong>Caching:<\/strong> We cache code review results by MD5 hash of the code. Identical code submitted twice returns the cached result instantly at zero cost. Cache TTL of 1 hour is usually sufficient.<\/p>\n<p><strong>Queue for heavy loads:<\/strong> For non-streaming requests, consider dispatching reviews to a queue job rather than blocking the HTTP request:<\/p>\n<pre><code>\/\/ Dispatch to queue instead of blocking\r\nDispatchCodeReview::dispatch($validated['code'], $request->user()->id);\r\nreturn response()->json(['message' => 'Review queued', 'job_id' => $jobId]);<\/code><\/pre>\n<p><strong>Error handling:<\/strong> The Anthropic API occasionally returns errors (overload, timeout). Always wrap API calls in try-catch and implement exponential backoff for retries in queue jobs.<\/p>\n<p><strong>Prompt versioning:<\/strong> Store your system prompts in the database or config files with version numbers. When you improve a prompt, you can compare results and roll back if needed.<\/p>\n<p>If you want to add AI-powered features to your Laravel application \u2014 code review, content generation, document analysis, or custom chatbots \u2014 <a href=\"https:\/\/softcrony.com\/contact\/\">our team at Softcrony can build it for you<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Adding AI capabilities to a Laravel application is simpler than most developers expect. The Claude API is well-documented, the PHP client is straightforward, and the patterns for integrating AI into Laravel&#8217;s architecture are clean. This guide builds a real AI-powered feature from scratch \u2014 an intelligent code review assistant that analyzes code submitted via API [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":85,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9],"tags":[27,87,86,85,19,30],"class_list":["post-83","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-automation","tag-ai","tag-ai-integration","tag-anthropic","tag-claude-api","tag-laravel","tag-php"],"_links":{"self":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/83","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/comments?post=83"}],"version-history":[{"count":1,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/83\/revisions"}],"predecessor-version":[{"id":84,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/83\/revisions\/84"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media\/85"}],"wp:attachment":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media?parent=83"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/categories?post=83"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/tags?post=83"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}