<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[raulnq]]></title><description><![CDATA[Somebody who likes to code]]></description><link>https://blog.raulnq.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 16 Apr 2026 14:23:01 GMT</lastBuildDate><atom:link href="https://blog.raulnq.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AWS Lambda Function: Check List]]></title><description><![CDATA[AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and read]]></description><link>https://blog.raulnq.com/aws-lambda-function-check-list</link><guid isPermaLink="true">https://blog.raulnq.com/aws-lambda-function-check-list</guid><category><![CDATA[AWS]]></category><category><![CDATA[lambda]]></category><category><![CDATA[dotnet]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Fri, 03 Apr 2026 22:03:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/625b81432f84e0d4fc33dca5/388d992e-1f84-4c4d-990c-0dbbe4cbe650.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and ready for production at scale. For .NET developers, the distance between a naive implementation and a well-engineered one is surprisingly wide — it spans everything from cold start times and memory allocation, to secret management, deployment safety, and event loss prevention.</p>
<p>This guide covers ten areas of Lambda excellence for .NET, each derived from real-world production patterns and common failure modes. It assumes you are comfortable with the basics of Lambda and the AWS .NET SDK, and focuses on the <em>why</em> behind each practice as much as the <em>how</em>.</p>
<hr />
<h2>Runtime &amp; Deployment Model</h2>
<h3>Use .NET 10 (or Latest LTS) on arm64</h3>
<p><strong>What it is:</strong> AWS Graviton2 processors power arm64 Lambda functions. They offer better performance-per-dollar for most workloads, and AWS charges approximately 20% less per GB-second compared to x86.</p>
<p><strong>Why it matters:</strong> For a high-traffic function with 500ms average duration at 512 MB memory, switching from x86 to arm64 can save over $15/month per million daily invocations with zero code changes.</p>
<pre><code class="language-yaml"># template.yaml
Globals:
  Function:
    Runtime: dotnet10
    Architectures:
      - arm64
    MemorySize: 512

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: MyFunction::MyFunction.Function::FunctionHandler
      CodeUri: src/MyFunction/
</code></pre>
<hr />
<h3>Minimize Deployment Package Size</h3>
<p><strong>What it is:</strong> The deployment package is the ZIP artifact containing your compiled application and its dependencies, uploaded to Lambda via S3. Its size directly affects how long Lambda takes to extract and load your code during a cold start.</p>
<p><strong>Why it matters:</strong> Lambda extracts and initializes your deployment package on every cold start. A 50 MB package takes significantly longer to initialize than a 5 MB one. Smaller packages also reduce S3 storage costs and deployment time.</p>
<hr />
<h3>Use Native AOT Compilation</h3>
<p><strong>What it is:</strong> Ahead-of-Time (AOT) compilation converts your .NET code directly into a native binary at build time, eliminating the Just-In-Time (JIT) compiler that normally runs when a .NET application starts. For Lambda, this means the initialization phase — the most expensive part of a cold start — is dramatically faster.</p>
<p><strong>Why it matters:</strong> Lambda bills in 1ms increments. Cold starts are fully billed duration. On a JIT-based .NET function, initialization can take 400–1200ms depending on the number of loaded assemblies and SDK clients. With Native AOT, this drops to 30–80ms.</p>
<hr />
<h3>Use Lambda SnapStart (if not using AOT)</h3>
<p><strong>What it is:</strong> SnapStart takes a snapshot of the initialized execution environment after the <code>Init</code> phase completes and caches it. Subsequent cold starts restore from the snapshot instead of re-running initialization, reducing cold start latency by up to 90%.</p>
<p><strong>Why it matters:</strong> If you have an existing JIT-based .NET function that you cannot easily migrate to AOT (e.g., it uses libraries incompatible with trimming), SnapStart delivers most of the cold start benefit without a code rewrite.</p>
<hr />
<h2>Memory &amp; CPU Sizing</h2>
<h3>Profile with AWS Lambda Power Tuning</h3>
<p><strong>What it is:</strong> The <a href="https://github.com/alexcasalboni/aws-lambda-power-tuning">AWS Lambda Power Tuning</a> tool is an open-source Step Functions state machine that runs your function at multiple memory configurations (e.g., 128 MB, 256 MB, 512 MB, 1024 MB, 1769 MB, 3008 MB) and returns a cost and performance graph showing the optimal setting.</p>
<p><strong>Why it matters:</strong> The relationship between memory and cost is not linear because CPU allocation scales with memory. A function that runs in 1000ms at 256 MB might run in 200ms at 1024 MB, making the higher-memory option cheaper despite a higher per-ms rate.</p>
<hr />
<h3>Don't Under-Allocate Memory</h3>
<p><strong>What it is:</strong> Lambda allocates CPU proportionally to memory. Setting memory too low starves your function of CPU, which can make it run slower and cost more despite the lower per-GB-second rate.</p>
<table>
<thead>
<tr>
<th>Memory</th>
<th>CPU Allocation</th>
</tr>
</thead>
<tbody><tr>
<td>128 MB</td>
<td>~5% of a vCPU</td>
</tr>
<tr>
<td>512 MB</td>
<td>~25% of a vCPU</td>
</tr>
<tr>
<td>1769 MB</td>
<td>1 full vCPU</td>
</tr>
<tr>
<td>3584 MB</td>
<td>2 full vCPUs</td>
</tr>
</tbody></table>
<p><strong>Why it matters:</strong> A .NET function with heavy computation (JSON parsing, data transformation, LINQ operations) starved of CPU at 256 MB may run 4–8x slower than at 1769 MB, making it cost more despite the lower RAM pricing.</p>
<hr />
<h3>Set Memory Based on Actual Measurements</h3>
<p><strong>What it is:</strong> Every Lambda invocation emits a <code>REPORT</code> log line containing the actual peak memory used. This measured value is the correct baseline for sizing your memory allocation, rather than guessing.</p>
<p><strong>Why it matters:</strong> Setting memory allocation to peak measured usage plus 15% headroom prevents out-of-memory errors while avoiding wasteful over-provisioning. Both extremes cost money: OOM errors cause failed invocations and retries; excessive headroom means you pay for memory you never use.</p>
<pre><code class="language-plaintext">REPORT RequestId: abc123  Duration: 245.12 ms  Billed Duration: 246 ms
Memory Size: 512 MB  Max Memory Used: 178 MB
</code></pre>
<p>Set your memory allocation to <code>peakMB * 1.15</code> (15% headroom above peak measured usage).</p>
<hr />
<h2>Cold Start Optimization</h2>
<h3>Move Heavy Initialization Outside the Handler</h3>
<p><strong>What it is:</strong> In Lambda, code that runs at class construction or in static initializers executes only once per execution environment lifecycle — during the <code>Init</code> phase — not on every invocation. By moving expensive setup (SDK client construction, config loading, connection establishment) outside the handler method, you ensure it runs once and is reused.</p>
<p><strong>Why it matters:</strong> Lambda reuses execution environments across invocations and freezes them between calls, preserving static state. Initialization that runs during the <code>Init</code> phase is billed as part of the cold start, which occurs infrequently. The same code inside the handler runs on every invocation, making every call slower and more expensive.</p>
<hr />
<h3>Use Provisioned Concurrency for Latency-Critical Paths</h3>
<p><strong>What it is:</strong> Provisioned Concurrency pre-initializes a specified number of execution environments, keeping them warm and ready to handle requests with no cold start. The environments are fully initialized — your static constructors have already run.</p>
<p><strong>Why it matters:</strong> For synchronous, user-facing functions (e.g., API endpoints powering a mobile app), cold start latency can directly degrade user experience. Provisioned Concurrency eliminates cold starts entirely for those pre-warmed environments. It should not be used for async workers, background jobs, or event-processing functions, where latency is not user-visible and the added cost is unjustified.</p>
<hr />
<h3>Lazy-Load Non-Critical Dependencies</h3>
<p><strong>What it is:</strong> For dependencies only needed in specific code paths, use <code>Lazy&lt;T&gt;</code> to defer their initialization until the first time that code path is actually executed, rather than initializing them unconditionally at startup.</p>
<p><strong>Why it matters:</strong> Eagerly initializing every dependency at startup contributes to cold start duration and memory usage, even for code paths that may never be invoked in a given execution environment. Lazy loading ensures you only pay the initialization cost for dependencies that are actually used.</p>
<hr />
<h3>Avoid Heavy DI Containers in the Hot Path</h3>
<p><strong>What it is:</strong> Reflection-based dependency injection frameworks (<code>Microsoft.Extensions.DependencyInjection</code> with assembly scanning, Autofac, etc.) perform extensive reflection during container construction. This conflicts directly with both Native AOT (which requires all types to be known at compile time) and cold start minimization goals.</p>
<p><strong>Why it matters:</strong> Reflection-based DI scanning can add hundreds of milliseconds to your <code>Init</code> phase and is incompatible with Native AOT trimming. Preferred alternatives are manual wiring (fastest, recommended for simple functions) and source-generated DI (for more complex compositions).</p>
<hr />
<h2>Execution &amp; Compute Efficiency</h2>
<h3>Use async/await Throughout — Avoid .Result and .Wait()</h3>
<p><strong>What it is:</strong> <code>async</code>/<code>await</code> enables non-blocking I/O in .NET — when a function awaits a network call, the thread is released to do other work. <code>.Result</code> and <code>.Wait()</code> are synchronous blocking calls that hold the thread until the operation completes.</p>
<p><strong>Why it matters:</strong> Lambda bills for wall-clock time, not CPU time. A thread blocked on <code>.Result</code> or <code>.Wait()</code> holds the execution environment hostage while waiting for I/O, consuming billed duration even though it is doing nothing. It can also cause deadlocks in certain synchronization contexts.</p>
<hr />
<h3>Reuse HttpClient and AWS SDK Clients Across Invocations</h3>
<p><strong>What it is:</strong> <code>HttpClient</code> and AWS SDK service clients (e.g., <code>AmazonDynamoDBClient</code>) are designed to be long-lived and thread-safe. They should be created once as static fields and reused across invocations, not constructed per-request.</p>
<p><strong>Why it matters:</strong> Each <code>new HttpClient()</code> creates a new socket pool. Creating one per invocation rapidly exhausts available ports (the default ephemeral port range is ~30,000), causing socket exhaustion under load — a notoriously difficult production bug to diagnose. AWS SDK clients hold connection pools internally; re-creating them per invocation defeats connection reuse and adds DNS resolution overhead on every call.</p>
<hr />
<h3>Set Appropriate Timeout</h3>
<p><strong>What it is:</strong> Lambda's timeout setting defines the maximum duration an invocation is allowed to run before it is forcibly terminated. It is configurable from 1 second to 15 minutes per function.</p>
<p><strong>Why it matters:</strong> Lambda's default timeout for new functions is 3 seconds — too short for most real workloads. But setting it to the maximum (15 minutes) "just in case" means a function that hangs due to a slow downstream service will bill the full 15 minutes before being terminated. Calculate your timeout based on measured performance:</p>
<pre><code class="language-plaintext">Timeout = (p99 measured duration) × 2 + network overhead margin
</code></pre>
<p>For example, if your function normally completes in 800ms and has a p99 of 1.2 seconds, set timeout to 3–5 seconds, not 15 minutes.</p>
<pre><code class="language-yaml">MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    Timeout: 10    # Seconds — not 900 (15 min)
</code></pre>
<hr />
<h3>Use System.Text.Json over Newtonsoft.Json</h3>
<p><strong>What it is:</strong> <code>System.Text.Json</code> (STJ) is the built-in .NET JSON library introduced in .NET Core 3.1. It is allocation-friendly, supports <code>Span&lt;T&gt;</code> for zero-copy parsing, and is fully compatible with Native AOT via source generation. <code>Newtonsoft.Json</code> is the older, widely used third-party library that predates STJ.</p>
<p><strong>Why it matters:</strong> <code>Newtonsoft.Json</code> relies on reflection, is incompatible with Native AOT, adds ~500 KB to your deployment package, and is measurably slower for typical Lambda payloads. STJ delivers better throughput, lower memory pressure, and AOT compatibility with no external dependency.</p>
<hr />
<h3>Avoid Boxing and Unnecessary Allocations in Hot Paths</h3>
<p><strong>What it is:</strong> Boxing occurs when a value type (e.g., <code>int</code>, <code>struct</code>) is implicitly converted to <code>object</code>, causing a heap allocation. Unnecessary allocations include creating objects, strings, arrays, or closures that are immediately discarded after a single use.</p>
<p><strong>Why it matters:</strong> Lambda functions that process thousands of events per second generate enormous GC pressure if they produce many short-lived heap objects per invocation. GC pauses directly extend billed duration and increase tail latency.</p>
<hr />
<h2>Concurrency &amp; Scaling</h2>
<h3>Set Reserved Concurrency to Avoid Noisy-Neighbor Throttling</h3>
<p><strong>What it is:</strong> Reserved concurrency is a per-function setting that both guarantees a minimum concurrency allocation for a function and caps its maximum concurrency. By default, all Lambda functions in an AWS account share a regional concurrency pool (default: 1,000 concurrent executions).</p>
<p><strong>Why it matters:</strong> Without reserved concurrency, a single function experiencing a traffic spike can consume the entire regional pool, throttling completely unrelated functions in the same account. Reserved concurrency prevents this by isolating a function's allocation from the shared pool.</p>
<hr />
<h3>Configure SQS Batch Size and Batch Window</h3>
<p><strong>What it is:</strong> When Lambda polls an SQS queue, it can retrieve and process multiple messages in a single invocation (a batch). <code>BatchSize</code> controls the maximum number of messages per invocation. <code>MaximumBatchingWindowInSeconds</code> controls how long Lambda waits to fill the batch before invoking. <code>ReportBatchItemFailures</code> tells Lambda which specific messages in a batch failed, so only those are retried.</p>
<p><strong>Why it matters:</strong> Larger batches mean fewer Lambda invocations, which directly reduces cost. Without <code>ReportBatchItemFailures</code>, if one message in a batch of 100 fails, all 100 are returned to the queue and reprocessed — a 100x amplification of failed work.</p>
<pre><code class="language-yaml">Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Events:
      SQSTrigger:
        Type: SQS
        Properties:
          Queue: !GetAtt MyQueue.Arn
          BatchSize: 100
          MaximumBatchingWindowInSeconds: 5
          FunctionResponseTypes:
            - ReportBatchItemFailures
</code></pre>
<blockquote>
<p>Message visibility timeout must be greater than max function execution time.</p>
</blockquote>
<hr />
<h3>Use Event Filtering on SQS/DynamoDB/Kinesis Triggers</h3>
<p><strong>What it is:</strong> Lambda event source filtering lets you define a filter pattern at the AWS service level. Messages that don't match the filter are discarded by the service before they ever invoke your function.</p>
<p><strong>Why it matters:</strong> You are not billed for filtered-out events, and your function doesn't need to contain logic to discard irrelevant messages. This reduces invocation count, cost, and code complexity.</p>
<pre><code class="language-yaml">Events:
  SQSTrigger:
    Type: SQS
    Properties:
      Queue: !GetAtt MyQueue.Arn
      FilterCriteria:
        Filters:
          - Pattern: '{"body": {"eventType": ["ORDER_PLACED"]}}'
</code></pre>
<hr />
<h3>Tune Maximum Concurrency on Event Source Mappings</h3>
<p><strong>What it is:</strong> The <code>ScalingConfig.MaximumConcurrency</code> property on an event source mapping caps how many concurrent Lambda executions can be processing from that specific source simultaneously, independent of the function's overall reserved concurrency.</p>
<p><strong>Why it matters:</strong> Without this limit, a large SQS queue backlog can scale Lambda to hundreds of concurrent executions simultaneously. This can overwhelm downstream databases or APIs not designed for that level of parallelism, causing cascading failures.</p>
<pre><code class="language-yaml">Events:
  SQSTrigger:
    Type: SQS
    Properties:
      Queue: !GetAtt MyQueue.Arn
      BatchSize: 10
      ScalingConfig:
        MaximumConcurrency: 10
</code></pre>
<hr />
<h2>Networking &amp; Integrations</h2>
<h3>Avoid VPC Unless Strictly Necessary</h3>
<p><strong>What it is:</strong> Placing a Lambda function inside a VPC attaches an Elastic Network Interface (ENI) to the function's execution environment, giving it access to private VPC resources. This is required for accessing services that have no public endpoint, such as RDS or ElastiCache.</p>
<p><strong>Why it matters:</strong> ENI attachment adds 100–500ms of cold start latency and introduces capacity constraints in subnets. Lambda functions outside a VPC still run in AWS-managed, isolated infrastructure with no public inbound access — the security benefit of VPC placement is often overstated. Services like DynamoDB, S3, SQS, and SNS are all accessible without a VPC (or via VPC endpoints if the function is already in one).</p>
<hr />
<h3>Use VPC Endpoints for AWS Services</h3>
<p><strong>What it is:</strong> VPC Endpoints (Gateway endpoints for S3/DynamoDB, Interface endpoints for most other services) route traffic between your Lambda function and AWS services privately within the AWS network, bypassing the public internet and NAT Gateway.</p>
<p><strong>Why it matters:</strong> Without VPC endpoints, traffic from a VPC-based Lambda to AWS services routes through a NAT Gateway, which charges $0.045 per GB of data processed. For a high-throughput function reading large S3 objects, this adds up quickly. VPC endpoints eliminate the NAT Gateway data charge (Interface endpoints have a small hourly fee, but it is offset by avoided NAT costs above roughly 10 GB/month).</p>
<hr />
<h3>Enable Connection Pooling and Keep-Alive for HTTP</h3>
<p><strong>What it is:</strong> HTTP connection pooling reuses established TCP connections across multiple requests rather than opening a new connection for each one. In Lambda, <code>SocketsHttpHandler</code> manages this pool, and it persists across invocations as long as the execution environment stays warm.</p>
<p><strong>Why it matters:</strong> Without connection reuse, each Lambda invocation (or each HTTP call within it) incurs TCP handshake and TLS negotiation overhead. Tuning pool settings prevents stale connections from causing errors while keeping connections alive long enough to benefit from reuse.</p>
<pre><code class="language-csharp">private static readonly HttpClient HttpClient = new HttpClient(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(15),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
    MaxConnectionsPerServer = 50,
    UseCookies = false
})
{
    Timeout = TimeSpan.FromSeconds(10)
};
</code></pre>
<hr />
<h3>Configure AWS SDK Retry and Timeout</h3>
<p><strong>What it is:</strong> The AWS SDK has a built-in retry policy that automatically retries failed requests with exponential backoff. The default configuration retries up to 3 times. Both the retry count and per-attempt timeout can be configured on each SDK client.</p>
<p><strong>Why it matters:</strong> For a Lambda function with a 5-second timeout calling a struggling DynamoDB table, the SDK's default aggressive retry policy may consume the entire timeout window retrying, billing you for the full duration and still returning an error. Setting explicit retry limits and per-call timeouts gives you control over this behavior.</p>
<hr />
<h2>Observability &amp; Cost Visibility</h2>
<h3>Enable Lambda Insights</h3>
<p><strong>What it is:</strong> Lambda Insights is an enhanced CloudWatch monitoring feature that captures per-invocation metrics including memory used, CPU time, init duration, and network I/O. It is enabled by attaching the managed <code>LambdaInsightsExtension</code> Lambda layer to your function.</p>
<p><strong>Why it matters:</strong> The default Lambda CloudWatch metrics (invocations, duration, errors) don't surface resource-level detail like memory pressure or CPU utilization. Lambda Insights fills that gap, making it possible to identify memory leaks, CPU-bound invocations, and slow init phases without adding custom instrumentation.</p>
<blockquote>
<p><strong>Cost note:</strong> Lambda Insights charges for custom CloudWatch metrics (~$0.30 per metric per month) and additional log data. For high-traffic functions, filter what you emit.</p>
</blockquote>
<hr />
<h3>Use Structured Logging with Log Level Filtering</h3>
<p><strong>What it is:</strong> Structured logging emits log entries as JSON objects with consistent fields (timestamp, level, message, correlation IDs, etc.) rather than plain text strings. Log level filtering suppresses verbose logs (e.g., <code>Debug</code>, <code>Trace</code>) in production while keeping <code>Warning</code> and <code>Error</code> output.</p>
<p><strong>Why it matters:</strong> Plain string logs are hard to query at scale. Structured JSON logs can be filtered and aggregated efficiently with CloudWatch Logs Insights. Log level control reduces log volume and CloudWatch ingestion costs in production.</p>
<blockquote>
<p>To accomplish this easily, use <a href="https://docs.aws.amazon.com/powertools/dotnet/">Powertools for AWS Lambda (.NET)</a>.</p>
</blockquote>
<hr />
<h3>Set CloudWatch Log Retention Policy</h3>
<p><strong>What it is:</strong> Each Lambda function automatically gets a CloudWatch Log Group. The default retention policy is "Never Expire," meaning logs accumulate indefinitely. You can configure a retention period (e.g., 14 days) via CloudFormation or the console.</p>
<p><strong>Why it matters:</strong> CloudWatch Logs storage is billed at $0.03/GB. With "Never Expire," logs from high-volume functions accumulate indefinitely, and the bill grows silently. Explicitly setting a retention period caps storage costs and keeps log groups manageable.</p>
<pre><code class="language-yaml">Resources:
  MyFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${MyFunction}"
      RetentionInDays: 14
</code></pre>
<hr />
<h3>Use X-Ray or OpenTelemetry for Distributed Tracing</h3>
<p><strong>What it is:</strong> AWS X-Ray is a distributed tracing service that instruments calls across Lambda invocations, AWS service calls (DynamoDB, S3, SQS), and outbound HTTP requests, producing service maps and per-segment latency breakdowns. OpenTelemetry is the vendor-neutral alternative that can export to X-Ray, Jaeger, or other backends.</p>
<p><strong>Why it matters:</strong> Standard CloudWatch metrics tell you that a function is slow; distributed tracing tells you <em>which downstream call</em> is responsible. This is invaluable for identifying the specific DynamoDB query or external API call causing p99 latency spikes.</p>
<pre><code class="language-yaml">Globals:
  Function:
    Tracing: Active
</code></pre>
<blockquote>
<p>To accomplish this easily, use <a href="https://docs.aws.amazon.com/powertools/dotnet/">Powertools for AWS Lambda (.NET)</a>.</p>
</blockquote>
<hr />
<h3>Tag All Lambda Functions with Cost Allocation Tags</h3>
<p><strong>What it is:</strong> AWS resource tags are key-value pairs attached to resources. Cost Allocation Tags are tags you activate in the Billing console, after which AWS Cost Explorer can group and filter Lambda spending by those tag values.</p>
<p><strong>Why it matters:</strong> Without consistent tagging, Lambda costs in a shared account appear as a single line item, making it impossible to attribute spending to specific teams, features, or environments. Tags enable per-team or per-service cost accountability.</p>
<pre><code class="language-yaml">Globals:
  Function:
    Tags:
      Team: payments
      Environment: production
      Service: order-processing
</code></pre>
<hr />
<h2>Architecture &amp; Design</h2>
<h3>Keep Functions Single-Purpose</h3>
<p><strong>What it is:</strong> A single-purpose Lambda function handles one specific operation (e.g., process an order event, resize an image, send a notification) rather than branching across multiple unrelated operations based on input type.</p>
<p><strong>Why it matters:</strong> A function that branches heavily based on input type ends up sized for its most demanding branch, wasting memory and compute for all other branches. Single-purpose functions are easier to size, tune independently, monitor, and debug.</p>
<hr />
<h3>Consider Lambda URLs vs API Gateway</h3>
<p><strong>What it is:</strong> Lambda Function URLs are built-in HTTPS endpoints directly on a Lambda function, requiring no additional AWS service. API Gateway is a fully managed service that sits in front of Lambda and adds features like request validation, authorizers, usage plans, WAF integration, and stage variables.</p>
<p><strong>Why it matters:</strong> For simple HTTP endpoints that don't need API Gateway features, Lambda Function URLs eliminate the $1.00 per million request charge that HTTP API Gateway adds, reducing cost to Lambda invocation charges only. The right choice depends on whether you need the additional API Gateway capabilities.</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>API Gateway HTTP</th>
<th>Lambda URL</th>
</tr>
</thead>
<tbody><tr>
<td>Cost per million requests</td>
<td>$1.00</td>
<td>$0 (Lambda invocation only)</td>
</tr>
<tr>
<td>CORS support</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Auth</td>
<td>IAM, Cognito, Lambda authorizer</td>
<td>IAM, none</td>
</tr>
<tr>
<td>Custom domain</td>
<td>Yes</td>
<td>No (but works behind CloudFront)</td>
</tr>
<tr>
<td>WebSocket</td>
<td>Yes</td>
<td>No</td>
</tr>
</tbody></table>
<hr />
<h3>Use Step Functions for Orchestration over Chained Lambdas</h3>
<p><strong>What it is:</strong> AWS Step Functions is a serverless orchestration service that coordinates multi-step workflows. The alternative — chaining Lambda functions by having one function synchronously invoke another and wait for its response — keeps the calling function's execution environment alive and billing during the entire wait.</p>
<p><strong>Why it matters:</strong> Synchronously chaining Lambda functions doubles or triples your Lambda bill because the caller is billed for the full wait duration. Step Functions handles orchestration at the service level; your Lambda functions execute independently and are only billed for their own compute time.</p>
<hr />
<h2>Security</h2>
<h3>Apply Least-Privilege IAM Execution Roles</h3>
<p><strong>What it is:</strong> Every Lambda function runs under an IAM execution role. This role defines exactly which AWS API calls the function is permitted to make. Many teams default to broad managed policies like <code>AmazonDynamoDBFullAccess</code> or even <code>AdministratorAccess</code> during development and never revisit them.</p>
<p><strong>Why it matters:</strong> If your function is compromised through a vulnerability in your code or a dependency, the attacker inherits every permission in that execution role. A function that can only call <code>dynamodb:GetItem</code> on one specific table does far less damage than one with write access to all tables in the account.</p>
<hr />
<h3>Never Store Secrets in Environment Variables or Source Code</h3>
<p><strong>What it is:</strong> Lambda environment variables are convenient but are stored in plaintext in the function configuration, visible to anyone with <code>lambda:GetFunctionConfiguration</code> or <code>lambda:GetFunction</code> IAM access. The correct alternative is AWS Secrets Manager or AWS Systems Manager Parameter Store (SecureString), where secrets are fetched at runtime and access is controlled via IAM.</p>
<p><strong>Why it matters:</strong> Database passwords, API keys, and signing secrets in environment variables or source code have been the root cause of numerous high-profile cloud breaches. The AWS Shared Responsibility Model makes secret storage entirely your responsibility.</p>
<hr />
<h3>Enable AWS WAF on API Gateway or CloudFront</h3>
<p><strong>What it is:</strong> AWS WAF (Web Application Firewall) inspects HTTP requests before they reach your Lambda function, blocking common attack patterns (SQL injection, XSS, path traversal), known malicious IP ranges, and volumetric abuse. It is attached to API Gateway, CloudFront, or an Application Load Balancer.</p>
<p><strong>Why it matters:</strong> Without WAF, a Lambda function exposed via API Gateway is reachable by anyone on the internet. Even a well-validated function can be overwhelmed by a flood of requests (costing money in Lambda invocations) or hit with sophisticated payloads designed to exploit dependencies.</p>
<hr />
<h3>Use Resource-Based Policies to Restrict Who Can Invoke</h3>
<p><strong>What it is:</strong> A Lambda resource policy (also called a function policy) defines which AWS principals — accounts, services, or IAM roles — are allowed to call <code>lambda:InvokeFunction</code> on your function. It is separate from the function's execution role, which controls what the function can do.</p>
<p><strong>Why it matters:</strong> Without a restrictive resource policy, an API Gateway misconfiguration or an accidental public URL can expose your function to arbitrary invocation from the internet, leading to unexpected charges and potential data exposure.</p>
<hr />
<h3>Restrict Lambda Function URL Auth</h3>
<p><strong>What it is:</strong> Lambda Function URLs support two authentication modes: <code>AuthType: NONE</code> (publicly invocable by anyone with the URL) and <code>AuthType: AWS_IAM</code> (requires a valid AWS Signature Version 4 signed request). The <code>NONE</code> mode is occasionally used for public webhooks but is a security risk for most production functions.</p>
<p><strong>Why it matters:</strong> <code>AuthType: NONE</code> means the URL is publicly accessible with no authentication. Anyone who discovers or guesses the URL can invoke your function at your expense and potentially exfiltrate or corrupt data. Use <code>AWS_IAM</code> for any function that is not intentionally public.</p>
<hr />
<h2>Operations</h2>
<h3>Configure Dead Letter Queues on Async Invocations</h3>
<p><strong>What it is:</strong> A Dead Letter Queue (DLQ) is an SQS queue or SNS topic configured to receive events that Lambda failed to process after exhausting its retry attempts. For asynchronous invocations (from SNS, S3 events, EventBridge, or direct async <code>InvokeFunction</code> calls), Lambda retries failed invocations twice by default before discarding the event.</p>
<p><strong>Why it matters:</strong> Without a DLQ, a transient downstream failure (a throttled DynamoDB table, a network timeout) can cause permanent, silent data loss with no alert and no recovery path. Silent event loss is one of the hardest production bugs to detect.</p>
<pre><code class="language-yaml">Resources:
  MyFunctionDLQ:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: my-function-dlq
      MessageRetentionPeriod: 1209600   # 14 days in seconds

  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: dotnet10
      DeadLetterQueue:
        Type: SQS
        TargetArn: !GetAtt MyFunctionDLQ.Arn
      EventInvokeConfig:
        MaximumRetryAttempts: 2
        MaximumEventAgeInSeconds: 3600
</code></pre>
<blockquote>
<p>Always alarm on DLQ depth so a backlog is immediately visible.</p>
</blockquote>
<hr />
<h3>Use Lambda Destinations for Async Success and Failure Routing</h3>
<p><strong>What it is:</strong> Lambda Destinations are a more flexible evolution of DLQs. They let you route both successful and failed async invocations to an SQS queue, SNS topic, EventBridge bus, or another Lambda function, with the full original event and function response or error details included in the routed payload.</p>
<p><strong>Why it matters:</strong> DLQs receive only the original input event on failure. Destinations receive the original event <em>plus</em> the function response or error details for both successes and failures, making post-processing, alerting, and conditional routing significantly richer. They also enable success-path routing, which DLQs cannot do.</p>
<hr />
<h3>Alarm on Errors, Throttles, and Duration Breaches</h3>
<p><strong>What it is:</strong> Lambda publishes several CloudWatch metrics out of the box: <code>Errors</code>, <code>Throttles</code>, <code>Duration</code>, <code>ConcurrentExecutions</code>, and <code>IteratorAge</code> (for stream-based triggers). CloudWatch Alarms can monitor these metrics and trigger notifications or automated actions when thresholds are breached.</p>
<p><strong>Why it matters:</strong> None of these metrics have alarms by default — you must create them explicitly. Without alarms, a function silently failing 10% of invocations, hitting account-level concurrency limits, or processing events that are hours behind will go undetected until a user or downstream system reports a problem.</p>
<pre><code class="language-yaml">ErrorRateAlarm:
  Type: AWS::CloudWatch::Alarm
  Properties:
    AlarmName: !Sub "${MyFunction}-ErrorRate"
    Metrics:
      - Id: errors
        MetricStat:
          Metric:
            Namespace: AWS/Lambda
            MetricName: Errors
            Dimensions: [{ Name: FunctionName, Value: !Ref MyFunction }]
          Period: 60
          Stat: Sum
      - Id: invocations
        MetricStat:
          Metric:
            Namespace: AWS/Lambda
            MetricName: Invocations
            Dimensions: [{ Name: FunctionName, Value: !Ref MyFunction }]
          Period: 60
          Stat: Sum
      - Id: errorRate
        Expression: "errors / invocations * 100"
        Label: ErrorRate
    ComparisonOperator: GreaterThanThreshold
    Threshold: 1
    EvaluationPeriods: 2
    TreatMissingData: notBreaching
    AlarmActions: [!Ref OpsAlertTopic]

ThrottleAlarm:
  Type: AWS::CloudWatch::Alarm
  Properties:
    AlarmName: !Sub "${MyFunction}-Throttles"
    MetricName: Throttles
    Namespace: AWS/Lambda
    Dimensions: [{ Name: FunctionName, Value: !Ref MyFunction }]
    Statistic: Sum
    Period: 300
    EvaluationPeriods: 1
    Threshold: 1
    ComparisonOperator: GreaterThanOrEqualToThreshold
    AlarmActions: [!Ref OpsAlertTopic]

DurationAlarm:
  Type: AWS::CloudWatch::Alarm
  Properties:
    AlarmName: !Sub "${MyFunction}-DurationP99"
    MetricName: Duration
    Namespace: AWS/Lambda
    Dimensions: [{ Name: FunctionName, Value: !Ref MyFunction }]
    ExtendedStatistic: p99
    Period: 300
    EvaluationPeriods: 3
    Threshold: 8000   # 8 seconds if timeout is 10 seconds
    ComparisonOperator: GreaterThanThreshold
    AlarmActions: [!Ref OpsAlertTopic]
</code></pre>
<hr />
<h3>Use Lambda Aliases and Traffic Shifting for Safe Deployments</h3>
<p><strong>What it is:</strong> Lambda aliases are named pointers to specific function versions (e.g., <code>live</code> → version 12). Traffic shifting, configured through AWS CodeDeploy, allows you to route a percentage of invocations to a new version while the majority still goes to the current stable version — a serverless canary or linear deployment.</p>
<p><strong>Why it matters:</strong> Deploying a new function version directly to 100% of traffic has no rollback path faster than re-deploying the previous version, which takes 30–90 seconds. With canary or linear deployments, CodeDeploy monitors your CloudWatch alarms and automatically rolls back to the previous version the moment an alarm fires — often within 60 seconds of a bad deployment.</p>
<hr />
<h2>Quick Reference</h2>
<blockquote>
<p><strong>Must</strong> = required for any production function regardless of workload. <strong>Optional</strong> = strongly advisable but context-dependent — the "When to apply" column clarifies when it becomes effectively mandatory.</p>
</blockquote>
<table>
<thead>
<tr>
<th>#</th>
<th>Practice</th>
<th>Priority</th>
<th>When to apply</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Runtime &amp; Deployment</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>1</td>
<td>Use latest LTS .NET on arm64</td>
<td>Optional</td>
<td>New functions or runtime upgrades; skip if library incompatibility blocks migration</td>
</tr>
<tr>
<td>2</td>
<td>Minimize deployment package size</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>3</td>
<td>Use Native AOT compilation</td>
<td>Optional</td>
<td>New functions; skip if dependencies are AOT-incompatible</td>
</tr>
<tr>
<td>4</td>
<td>Use Lambda SnapStart</td>
<td>Optional</td>
<td>JIT-based functions where cold start is a problem and AOT is not feasible</td>
</tr>
<tr>
<td><strong>Memory &amp; CPU Sizing</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>5</td>
<td>Profile with Lambda Power Tuning</td>
<td>Must</td>
<td>Before any function goes to production; re-run after significant code changes</td>
</tr>
<tr>
<td>6</td>
<td>Don't under-allocate memory</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>7</td>
<td>Set memory based on actual measurements</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td><strong>Cold Start Optimization</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>8</td>
<td>Move heavy initialization outside the handler</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>9</td>
<td>Use Provisioned Concurrency</td>
<td>Optional</td>
<td>Synchronous user-facing functions with strict latency SLAs only</td>
</tr>
<tr>
<td>10</td>
<td>Lazy-load non-critical dependencies</td>
<td>Optional</td>
<td>Functions with multiple code paths that aren't always exercised</td>
</tr>
<tr>
<td>11</td>
<td>Avoid heavy DI containers in the hot path</td>
<td>Must</td>
<td>Always; especially mandatory when using Native AOT</td>
</tr>
<tr>
<td><strong>Execution &amp; Compute Efficiency</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>12</td>
<td>Use async/await — avoid .Result and .Wait()</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>13</td>
<td>Reuse HttpClient and AWS SDK clients</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>14</td>
<td>Set appropriate timeout</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>15</td>
<td>Use System.Text.Json over Newtonsoft.Json</td>
<td>Must</td>
<td>New functions; for existing functions, mandatory when targeting Native AOT</td>
</tr>
<tr>
<td>16</td>
<td>Avoid boxing and unnecessary allocations</td>
<td>Optional</td>
<td>High-throughput functions processing thousands of events per second</td>
</tr>
<tr>
<td><strong>Concurrency &amp; Scaling</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>17</td>
<td>Set reserved concurrency</td>
<td>Must</td>
<td>Any function in a shared account with other critical functions</td>
</tr>
<tr>
<td>18</td>
<td>Configure SQS batch size and ReportBatchItemFailures</td>
<td>Must</td>
<td>All SQS-triggered functions</td>
</tr>
<tr>
<td>19</td>
<td>Use event source filtering</td>
<td>Optional</td>
<td>When the function receives a mixed event stream and only acts on a subset</td>
</tr>
<tr>
<td>20</td>
<td>Tune maximum concurrency on event source mappings</td>
<td>Must</td>
<td>When the downstream target (DB, API) has a known parallelism limit</td>
</tr>
<tr>
<td><strong>Networking &amp; Integrations</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>21</td>
<td>Avoid VPC unless strictly necessary</td>
<td>Must</td>
<td>Always — only place in VPC when there is no alternative</td>
</tr>
<tr>
<td>22</td>
<td>Use VPC endpoints for AWS services</td>
<td>Must</td>
<td>Any VPC-attached function that calls AWS services</td>
</tr>
<tr>
<td>23</td>
<td>Enable connection pooling and keep-alive</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>24</td>
<td>Configure AWS SDK retry and timeout</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td><strong>Observability &amp; Cost Visibility</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>25</td>
<td>Enable Lambda Insights</td>
<td>Optional</td>
<td>Functions where resource-level diagnostics (CPU, memory trend) are needed</td>
</tr>
<tr>
<td>26</td>
<td>Use structured logging with log level filtering</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>27</td>
<td>Set CloudWatch log retention policy</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>28</td>
<td>Use X-Ray or OpenTelemetry</td>
<td>Optional</td>
<td>Functions that call downstream services and where latency attribution matters</td>
</tr>
<tr>
<td>29</td>
<td>Tag all functions with cost allocation tags</td>
<td>Must</td>
<td>Any shared or multi-team AWS account</td>
</tr>
<tr>
<td><strong>Architecture &amp; Design</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>30</td>
<td>Keep functions single-purpose</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>31</td>
<td>Consider Lambda URLs vs API Gateway</td>
<td>Optional</td>
<td>Simple HTTP endpoints with no need for API Gateway features</td>
</tr>
<tr>
<td>32</td>
<td>Use Step Functions for orchestration</td>
<td>Must</td>
<td>Any workflow that chains Lambda calls synchronously</td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>33</td>
<td>Apply least-privilege IAM execution roles</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>34</td>
<td>Never store secrets in environment variables</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>35</td>
<td>Enable AWS WAF on API Gateway or CloudFront</td>
<td>Must</td>
<td>Any publicly exposed HTTP endpoint</td>
</tr>
<tr>
<td>36</td>
<td>Use resource-based policies to restrict invocation</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>37</td>
<td>Restrict Lambda Function URL auth</td>
<td>Must</td>
<td>Any Function URL not intentionally public</td>
</tr>
<tr>
<td><strong>Operations</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>38</td>
<td>Configure Dead Letter Queues on async invocations</td>
<td>Must</td>
<td>All async-invoked functions</td>
</tr>
<tr>
<td>39</td>
<td>Use Lambda Destinations</td>
<td>Optional</td>
<td>When you need success-path routing or richer failure payloads beyond a basic DLQ</td>
</tr>
<tr>
<td>40</td>
<td>Alarm on errors, throttles, and duration</td>
<td>Must</td>
<td>Always</td>
</tr>
<tr>
<td>41</td>
<td>Use aliases and traffic shifting for deployments</td>
<td>Must</td>
<td>Any function where a bad deploy would have immediate user or data impact</td>
</tr>
</tbody></table>
<p>The current checklist is a starting point. Adapt it to your team's context, add items that reflect your specific compliance requirements or architectural patterns, and remove items that genuinely do not apply to your workload. The goal is not a perfect score — it is a shared, team-maintained definition of what a production-ready Lambda function looks like for your organization. Thanks, and happy coding</p>
]]></content:encoded></item><item><title><![CDATA[Moto Server: Our Own Local AWS]]></title><description><![CDATA[Moto is an open-source Python library originally designed for mocking AWS services. Moto Server is its standalone HTTP server mode, which exposes a fully functional fake AWS API that any client — rega]]></description><link>https://blog.raulnq.com/moto-server-our-own-local-aws</link><guid isPermaLink="true">https://blog.raulnq.com/moto-server-our-own-local-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[moto-server]]></category><category><![CDATA[SQS]]></category><category><![CDATA[sns]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Sun, 15 Mar 2026 16:11:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/625b81432f84e0d4fc33dca5/033b8e60-c8b4-4b71-993b-19b92203b2ce.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://github.com/getmoto/moto">Moto</a> is an open-source Python library originally designed for mocking AWS services. Moto Server is its standalone HTTP server mode, which exposes a fully functional fake AWS API that any client — regardless of programming language — can talk to using the standard AWS SDK. At its core, Moto Server intercepts requests that would normally go to <code>https://&lt;service&gt;.amazonaws.com</code> and handles them locally, maintaining in-memory state that faithfully mimics real AWS behavior. Moto currently supports over 80 AWS services, including:</p>
<ul>
<li><p><strong>SNS</strong> (Simple Notification Service)</p>
</li>
<li><p><strong>SQS</strong> (Simple Queue Service)</p>
</li>
<li><p><strong>S3</strong> (Simple Storage Service)</p>
</li>
<li><p><strong>DynamoDB</strong></p>
</li>
<li><p><strong>Lambda</strong></p>
</li>
<li><p><strong>IAM</strong>, <strong>EC2</strong>, <strong>Kinesis</strong>, <strong>Secrets Manager</strong>, and many more</p>
</li>
</ul>
<p>The key insight is that Moto Server speaks the exact same HTTP protocol as real AWS. Our application code — including the AWS SDK — does not need any modification to switch between Moto Server and real AWS. <strong>The only change is the endpoint URL</strong>. Moto Server does not require real credentials, and it does not validate them. However, AWS SDKs still expect credentials to exist, so we usually must provide dummy credentials in environment variables or config files.</p>
<h2>Why Use Moto Server?</h2>
<h3>The Problem with Testing Against Real AWS</h3>
<p>Testing cloud-native applications presents a persistent challenge: real AWS services are external, stateful, and cost money. Running integration tests against live infrastructure leads to:</p>
<ul>
<li><p><strong>Slow feedback loops</strong>: Network latency adds seconds to every test case.</p>
</li>
<li><p><strong>Flaky behavior</strong>: Network timeouts, throttling, and eventual consistency make tests non-deterministic.</p>
</li>
<li><p><strong>Cloud cost</strong>: High-frequency test pipelines can generate unexpected AWS bills.</p>
</li>
<li><p><strong>Environment coupling</strong>: Developers share the same dev/staging account, causing test data collisions.</p>
</li>
<li><p><strong>No offline development</strong>: We cannot work on a plane or in an environment with no internet.</p>
</li>
</ul>
<h3>How Moto Server Solves These Problems</h3>
<ul>
<li><p><strong>Slow feedback loops</strong>: Runs locally; no network round-trips to AWS.</p>
</li>
<li><p><strong>Flaky behavior</strong>: Deterministic in-memory state; no eventual consistency.</p>
</li>
<li><p><strong>Cloud cost</strong>: Completely free; runs as a local Docker container.</p>
</li>
<li><p><strong>Environment coupling</strong>: Each developer (or CI run) spins up their own isolated instance.</p>
</li>
<li><p><strong>No offline development</strong>: Runs entirely offline.</p>
</li>
</ul>
<h3>Moto Server vs. LocalStack</h3>
<p>A natural question is: <strong>why not use LocalStack?</strong> Both tools simulate AWS locally, but they differ in philosophy and scope.</p>
<ul>
<li><p><strong>LocalStack</strong> is a commercial product (with a free tier) that runs each AWS service in a dedicated process and focuses on maximum fidelity, including networking emulation.</p>
</li>
<li><p><strong>Moto Server</strong> is a pure open-source, community-driven project. It runs all services in a single process and is particularly strong for SNS, SQS, S3, and DynamoDB.</p>
</li>
</ul>
<p>For most integration testing scenarios — especially message-broker flows with SNS and SQS — Moto Server is lightweight, zero-configuration, and entirely sufficient.</p>
<h2>Running Moto Server Locally with Docker</h2>
<p>Moto Server ships as an official Docker image. Spinning it up takes one command.</p>
<pre><code class="language-shell">docker run --rm -d --name moto-server -p 5000:5000 motoserver/moto:latest
</code></pre>
<p>What each flag does:</p>
<ul>
<li><p><code>--rm</code>: Removes the container when it stops, keeping our environment clean.</p>
</li>
<li><p><code>-d</code>: Runs in detached (background) mode.</p>
</li>
<li><p><code>--name moto-server</code>: Gives the container a predictable name for referencing later.</p>
</li>
<li><p><code>-p 5000:5000</code>: Maps port 5000 on our host machine to port 5000 inside the container. Moto Server listens on port 5000 by default.</p>
</li>
</ul>
<p>Once started, the server is immediately ready at <code>http://localhost:5000</code>. To verify the server is running navigate to <code>http://localhost:5000/moto-api/</code>, we should see a dashboard. Moto Server also exposes a reset endpoint that is useful between test runs <code>http://localhost:5000/moto-api/reset</code> (POST).</p>
<h2>Using Moto Server</h2>
<p>Let's create a couple of applications—a message producer and its consumer—to see Moto Server in action.</p>
<h3>Setting Up AWS Resources with the AWS CLI</h3>
<p>Before our applications can publish or consume messages, the AWS resources (SNS topic, SQS queue, and the subscription linking them) must exist. In a real project, we would employ Infrastructure as Code tools like Terraform, CDK, or CloudFormation. However, for local development with Moto Server, the AWS CLI offers the quickest setup, though it can also be integrated with these tools.</p>
<p>No special profile configuration is needed. Moto Server does not validate credentials — it accepts any value the CLI sends. The only flag that matters is <code>--endpoint-url</code>, which redirects the request from the real AWS endpoint to our local Moto instance. As long as the CLI has some credentials available (from environment variables, our default profile, or any other source), the request will be signed and Moto will accept it. All commands in this section use only <code>--endpoint-url http://localhost:5000</code>.</p>
<p><strong>Creating the SNS Topic</strong></p>
<pre><code class="language-shell">aws sns create-topic --name my-topic --endpoint-url http://localhost:5000
</code></pre>
<p><strong>Creating the SQS Queue</strong></p>
<pre><code class="language-shell">aws sqs create-queue --queue-name my-queue --endpoint-url http://localhost:5000
</code></pre>
<p><strong>Subscribing the SQS Queue to the SNS Topic</strong></p>
<p>The SNS subscription requires the SQS queue's ARN, not its URL. Retrieve it with:</p>
<pre><code class="language-shell">aws sqs get-queue-attributes --queue-url &lt;QUEUE_URL&gt; --attribute-names QueueArn --endpoint-url http://localhost:5000
</code></pre>
<p>This is the step that wires the two services together. When a message is published to the SNS topic, SNS will automatically deliver it to all subscribers — in this case, the SQS queue.</p>
<pre><code class="language-shell">aws sns subscribe --topic-arn &lt;TOPIC_ARN&gt; --protocol sqs --notification-endpoint &lt;QUEUE_ARN&gt; --endpoint-url http://localhost:5000
</code></pre>
<h3>Building the SNS Producer</h3>
<p>The producer application is a minimal .NET Web API that accepts an HTTP request and publishes an event to the SNS topic.</p>
<pre><code class="language-shell">dotnet new webapi -n SnsProducer
cd SnsProducer
dotnet add package AWSSDK.SimpleNotificationService
dotnet add package AWSSDK.Extensions.NETCore.Setup
</code></pre>
<p>Why these packages:</p>
<ul>
<li><p><code>AWSSDK.SimpleNotificationService</code>: The AWS SDK for the SNS service.</p>
</li>
<li><p><code>AWSSDK.Extensions.NETCore.Setup</code>: Provides <code>GetAWSOptions()</code> and <code>AddAWSService&lt;T&gt;()</code>, the idiomatic integration between the AWS SDK and .NET's dependency injection container.</p>
</li>
</ul>
<p>The SDK's <code>GetAWSOptions()</code> extension reads from the reserved <code>AWS</code> section of <code>appsettings.json</code>. This is the only place that needs to change between environments — the application code is identical across all of them.</p>
<pre><code class="language-json">{
  "AWS": {
    "Region": "us-east-1",
    "ServiceURL": "http://localhost:5000",
  }
}
</code></pre>
<p>Why two keys in the <code>AWS</code> section?</p>
<ul>
<li><p><code>Region</code>: the logical AWS region. The SDK uses this to construct ARNs and to select the correct real AWS endpoint under normal circumstances.</p>
</li>
<li><p><code>ServiceURL</code>: overrides the endpoint entirely, redirecting all requests to Moto Server instead of real AWS. When this is set, <code>Region</code> no longer drives endpoint selection — it is used purely for ARN construction.</p>
</li>
</ul>
<p>In rare cases we could need to setup the <code>AuthenticationRegion</code> key. This key tells the SDK which region to embed in the <a href="https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">AWS Signature Version 4</a> request signature. Both <code>Region</code> and <code>AuthenticationRegion</code> must match the region used when creating our AWS resources. Open the <code>Program.cs</code> file and replace the content with:</p>
<pre><code class="language-csharp">// Program.cs
using System.Text.Json;
using Amazon.SimpleNotificationService;
using Amazon.SimpleNotificationService.Model;

var builder = WebApplication.CreateBuilder(args);

// GetAWSOptions() reads the "AWS" section from appsettings.json.
// Credentials are resolved from environment variables by the SDK automatically.
builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions());

// Registers a fully configured, singleton IAmazonSimpleNotificationService.
builder.Services.AddAWSService&lt;IAmazonSimpleNotificationService&gt;();

var app = builder.Build();

const string TopicArn = "&lt;TOPIC_ARN&gt;";

var jsonOptions = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = false
};

app.MapPost("/api/events", async (
    IAmazonSimpleNotificationService sns,
    ILogger&lt;Program&gt; logger,
    CancellationToken cancellationToken) =&gt;
{
    var eventId = Guid.NewGuid();

    var orderEvent = new MyEvent(
        EventId: eventId,
        Message: $"New event {eventId}",
        CreatedAt: DateTimeOffset.UtcNow
    );

    // Serialize the event to JSON. The message body is always a string in SNS.
    var messageBody = JsonSerializer.Serialize(orderEvent, jsonOptions);

    var publishRequest = new PublishRequest
    {
        TopicArn = TopicArn,
        Message = messageBody,
        // MessageGroupId and MessageDeduplicationId are required for FIFO topics.
        // For standard topics, Message Attributes are useful for filtering.
        MessageAttributes = new Dictionary&lt;string, MessageAttributeValue&gt;
        {
            ["EventType"] = new MessageAttributeValue
            {
                DataType = "String",
                StringValue = nameof(MyEvent)
            },
            ["Version"] = new MessageAttributeValue
            {
                DataType = "String",
                StringValue = "1.0"
            }
        }
    };

    logger.LogInformation(
        "Publishing {EventType} for order {EventId} to topic {TopicArn}",
        nameof(MyEvent), eventId, TopicArn);

    try
    {
        var response = await sns.PublishAsync(publishRequest, cancellationToken);

        logger.LogInformation(
            "Published {EventType} successfully. MessageId: {MessageId}",
            nameof(MyEvent), response.MessageId);
    }
    catch (Exception ex)
    {
        logger.LogError(ex,
            "Failed to publish {EventType} for order {EventId}",
            nameof(MyEvent), eventId);
        throw;
    }

    return Results.Ok();
});

app.Run();

record MyEvent(
    Guid EventId,
    string Message,
    DateTimeOffset CreatedAt
);
</code></pre>
<h3>Building the SQS Consumer</h3>
<p>The consumer is a .NET Worker Service — a long-running background process that continuously polls the SQS queue and processes incoming messages.</p>
<pre><code class="language-csharp">dotnet new worker -n SqsConsumer
cd SqsConsumer
dotnet add package AWSSDK.SQS
dotnet add package AWSSDK.Extensions.NETCore.Setup
</code></pre>
<p>Just like the producer, the entire consumer lives in a single file. Open the <code>Program.cs</code> file and replace the content with:</p>
<pre><code class="language-csharp">// Program.cs
using System.Text.Json;
using System.Text.Json.Serialization;
using Amazon.SQS;
using Amazon.SQS.Model;

var builder = Host.CreateApplicationBuilder(args);

// GetAWSOptions() reads the "AWS" section from appsettings.json.
// Credentials are resolved from environment variables by the SDK automatically.
builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions());

// Registers a fully configured, singleton IAmazonSQS.
builder.Services.AddAWSService&lt;IAmazonSQS&gt;();

builder.Services.AddHostedService&lt;SqsPollingWorker&gt;();

var host = builder.Build();
host.Run();

public class SqsPollingWorker(IAmazonSQS sqs, ILogger&lt;SqsPollingWorker&gt; logger)
    : BackgroundService
{
    // QueueUrl is hardcoded here for simplicity in this example.
    // In a production codebase, read it from configuration or AWS Parameter Store.
    private const string QueueUrl =
        "&lt;QUEUE_URL&gt;";

    // Long polling: holds the connection open for up to 20 seconds.
    // This drastically reduces empty responses and cuts API call costs.
    private const int WaitTimeSeconds = 20;

    // Retrieve up to 10 messages per poll request (the SQS maximum).
    private const int MaxNumberOfMessages = 10;

    // VisibilityTimeout hides the message from other consumers while processing.
    // If processing fails and the message is not deleted, it reappears after this timeout.
    // Set it comfortably above the maximum expected processing time.
    private const int VisibilityTimeoutSeconds = 30;

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("SQS consumer started. Polling queue: {QueueUrl}", QueueUrl);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await PollAndProcessAsync(stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Expected when the host is shutting down. Break cleanly.
                break;
            }
            catch (Exception ex)
            {
                // Log unexpected errors but keep the loop running.
                // Without this, a single transient error would kill the worker.
                logger.LogError(ex, "Unexpected error in SQS polling loop. Retrying in 5 seconds.");
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }

        logger.LogInformation("SQS consumer stopped.");
    }

    private async Task PollAndProcessAsync(CancellationToken cancellationToken)
    {
        var receiveRequest = new ReceiveMessageRequest
        {
            QueueUrl              = QueueUrl,
            WaitTimeSeconds       = WaitTimeSeconds,
            MaxNumberOfMessages   = MaxNumberOfMessages,
            VisibilityTimeout     = VisibilityTimeoutSeconds,
            MessageSystemAttributeNames        = ["All"],
            MessageAttributeNames = ["All"]
        };

        var response = await sqs.ReceiveMessageAsync(receiveRequest, cancellationToken);

        if (response.Messages == null)
            return; // No messages — long poll timed out. Loop again immediately.

        if (response.Messages.Count == 0)
            return; // No messages — long poll timed out. Loop again immediately.

        logger.LogInformation("Received {Count} message(s) from SQS.", response.Messages.Count);

        // Process each message independently so a failure on one does not
        // prevent the others from being handled.
        await Task.WhenAll(response.Messages
            .Select(msg =&gt; ProcessMessageAsync(msg, cancellationToken)));
    }

    private async Task ProcessMessageAsync(Message sqsMessage, CancellationToken cancellationToken)
    {
        try
        {
            // Step 1: Unwrap the SNS envelope.
            // When SNS delivers to SQS it wraps the payload in a notification envelope.
            // The actual event lives inside envelope.Message as a JSON string.
            var envelope = JsonSerializer.Deserialize&lt;SnsEnvelope&gt;(sqsMessage.Body, JsonOptions);

            if (envelope is null || envelope.Type != "Notification")
            {
                logger.LogWarning(
                    "Received non-notification message type '{Type}'. Deleting.",
                    envelope?.Type ?? "unknown");
                // Delete malformed or unexpected messages to avoid infinite retries.
                await DeleteMessageAsync(sqsMessage, cancellationToken);
                return;
            }

            var eventType = envelope.MessageAttributes?.GetValueOrDefault("EventType")?.Value;

            logger.LogDebug(
                "Processing SNS notification. MessageId={MessageId}, EventType={EventType}",
                envelope.MessageId, eventType ?? "unknown");

            // Step 2: Deserialize the inner payload.
            var myEvent = JsonSerializer.Deserialize&lt;MyEvent&gt;(envelope.Message, JsonOptions);

            if (myEvent is null)
            {
                logger.LogError(
                    "Failed to deserialize MyEvent from message {MessageId}. Body: {Body}",
                    sqsMessage.MessageId, envelope.Message);
                // Do NOT delete — let the visibility timeout expire so the message
                // can be retried or moved to a dead-letter queue.
                return;
            }

            // Step 3: Process the event.
            logger.LogInformation(
                "Processing MyEvent: EventId={EventId}, Message={Message}",
                myEvent.EventId, myEvent.Message);

            // Simulate async processing work (e.g., writing to a database,
            // calling a downstream service, triggering a workflow).
            await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);

            logger.LogInformation(
                "Successfully processed MyEvent for EventId={EventId}", myEvent.EventId);

            // Step 4: Delete the message ONLY after successful processing.
            // This is the at-least-once delivery guarantee — if we crash before
            // deleting, SQS will redeliver the message.
            await DeleteMessageAsync(sqsMessage, cancellationToken);
        }
        catch (JsonException ex)
        {
            logger.LogError(ex,
                "JSON deserialization failed for message {MessageId}. Deleting to avoid poison-pill loop.",
                sqsMessage.MessageId);
            await DeleteMessageAsync(sqsMessage, cancellationToken);
        }
        catch (Exception ex)
        {
            logger.LogError(ex,
                "Failed to process message {MessageId}. It will reappear after the visibility timeout.",
                sqsMessage.MessageId);
            // Do NOT delete — let SQS retry (and eventually the DLQ) handle it.
        }
    }

    private async Task DeleteMessageAsync(Message sqsMessage, CancellationToken cancellationToken)
    {
        try
        {
            await sqs.DeleteMessageAsync(QueueUrl, sqsMessage.ReceiptHandle, cancellationToken);
            logger.LogDebug("Deleted message {MessageId} from queue.", sqsMessage.MessageId);
        }
        catch (Exception ex)
        {
            // Deletion failure is non-fatal — the message will reappear after the
            // visibility timeout and may be processed again (idempotency matters here).
            logger.LogWarning(ex,
                "Failed to delete message {MessageId}. It may be processed again.",
                sqsMessage.MessageId);
        }
    }
}

// Represents the SNS notification envelope that wraps every message
// delivered to SQS via an SNS subscription. The actual event payload
// lives inside the Message field as a JSON-encoded string.
record SnsEnvelope(
    [property: JsonPropertyName("Type")]      string Type,
    [property: JsonPropertyName("MessageId")] string MessageId,
    [property: JsonPropertyName("TopicArn")]  string TopicArn,
    [property: JsonPropertyName("Message")]   string Message,
    [property: JsonPropertyName("Timestamp")] string Timestamp,
    [property: JsonPropertyName("MessageAttributes")]
        Dictionary&lt;string, SnsMessageAttribute&gt;? MessageAttributes
);

record SnsMessageAttribute(
    [property: JsonPropertyName("Type")]  string Type,
    [property: JsonPropertyName("Value")] string Value
);

// Must match the shape serialized by the producer.
record MyEvent(
    Guid EventId,
    string Message,
    DateTimeOffset CreatedAt
);
</code></pre>
<h3>Full End-to-End Flow</h3>
<p>Go to the SnsProducer folder and run the command <code>dotnet run</code>, then go to the SqsConsumer folder and run the command <code>dotnet run</code> and trigger and event by sending a request to <code>http://localhost:5050/api/events</code> (POST).</p>
<p>You can find all the code <a href="https://github.com/raulnq/moto-server">here</a>. Thanks, and happy coding</p>
]]></content:encoded></item><item><title><![CDATA[OpenAPI + Hono: Building Type-Safe REST APIs with Automatic Documentation]]></title><description><![CDATA[Modern API development requires balancing multiple concerns: runtime validation, compile-time type safety, and comprehensive documentation. Traditionally, these have been separate concerns, leading to documentation drift and type mismatches between s...]]></description><link>https://blog.raulnq.com/openapi-hono-building-type-safe-rest-apis-with-automatic-documentation</link><guid isPermaLink="true">https://blog.raulnq.com/openapi-hono-building-type-safe-rest-apis-with-automatic-documentation</guid><category><![CDATA[hono]]></category><category><![CDATA[OpenApi]]></category><category><![CDATA[scalar]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Wed, 28 Jan 2026 22:35:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769611224739/1fed2602-e74c-4d0c-9bdf-fd55df0bcaf4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Modern API development requires balancing multiple concerns: runtime validation, compile-time type safety, and comprehensive documentation. Traditionally, these have been separate concerns, leading to documentation drift and type mismatches between specification and implementation.</p>
<p>This article demonstrates how to build a production-ready REST API using <a target="_blank" href="https://hono.dev/">Hono</a> with the <a target="_blank" href="https://github.com/honojs/middleware/tree/main/packages/zod-openapi"><code>@hono/zod-openapi</code></a> library, which bridges the gap between Zod validation schemas and OpenAPI specifications, enabling a single source of truth for types, validation, and documentation.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>In traditional API development, you typically maintain three separate artifacts:</p>
<ol>
<li><p><strong>TypeScript types</strong> for compile-time safety</p>
</li>
<li><p><strong>Runtime validators</strong> (like Joi, Yup, <a target="_blank" href="https://zod.dev/">Zod</a>, or manual validation)</p>
</li>
<li><p><strong>OpenAPI/Swagger specification</strong> for documentation</p>
</li>
</ol>
<p>This creates several problems:</p>
<ul>
<li><p><strong>Duplication</strong>: Same structure defined three times</p>
</li>
<li><p><strong>Drift</strong>: Changes in one place don't automatically update others</p>
</li>
<li><p><strong>Maintenance burden</strong>: Keeping all three in sync is error-prone</p>
</li>
<li><p><strong>Source of bugs</strong>: Mismatches between types and validation</p>
</li>
</ul>
<p><code>@hono/zod-openapi</code> solves this by:</p>
<ul>
<li><p>Using Zod schemas as the single source of truth</p>
</li>
<li><p>Automatically generating TypeScript types from schemas</p>
</li>
<li><p>Automatically generating OpenAPI specifications from schemas</p>
</li>
<li><p>Providing type-safe route handlers based on schema definitions</p>
</li>
</ul>
<h2 id="heading-understanding-core-concepts">Understanding Core Concepts</h2>
<p>Before diving into implementation, let's understand the key components of <code>@hono/zod-openapi</code>.</p>
<h3 id="heading-openapihono-the-enhanced-hono-instance">OpenAPIHono - The Enhanced Hono Instance</h3>
<p><code>OpenAPIHono</code> is an extended version of Hono's base class that adds OpenAPI-specific functionality.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { OpenAPIHono } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> OpenAPIHono();
</code></pre>
<p><strong>What OpenAPIHono Adds:</strong></p>
<ul>
<li><p><code>.openapi()</code> method: Registers routes with OpenAPI configuration</p>
</li>
<li><p><code>.doc()</code> method: Generates OpenAPI JSON specification endpoint</p>
</li>
<li><p><strong>Built-in validation</strong>: Automatically validates requests using Zod schemas</p>
</li>
<li><p><strong>Type inference</strong>: Derives TypeScript types from route configurations</p>
</li>
</ul>
<p><strong>Constructor Options:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> OpenAPIHono({
  strict: <span class="hljs-built_in">boolean</span>,        <span class="hljs-comment">// URL path matching strictness</span>
  defaultHook: Hook,      <span class="hljs-comment">// Global validation error handler</span>
});
</code></pre>
<ul>
<li><p><code>strict</code>: When <code>false</code>, allows flexible path matching (e.g., trailing slashes)</p>
</li>
<li><p><code>defaultHook</code>: Function called when validation fails, customizing error responses</p>
</li>
</ul>
<p><strong>Comparison with Standard Hono:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Standard Hono - no validation or OpenAPI</span>
<span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono();
app.post(<span class="hljs-string">'/todos'</span>, <span class="hljs-keyword">async</span> (c) =&gt; {
  <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> c.req.json(); <span class="hljs-comment">// ❌ No validation, any type</span>
  <span class="hljs-comment">// Manual validation required</span>
});

<span class="hljs-comment">// OpenAPIHono - validation and OpenAPI included</span>
<span class="hljs-keyword">import</span> { OpenAPIHono } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> OpenAPIHono();
app.openapi(config, <span class="hljs-keyword">async</span> (c) =&gt; {
  <span class="hljs-keyword">const</span> body = c.req.valid(<span class="hljs-string">'json'</span>); <span class="hljs-comment">// ✅ Validated and typed</span>
  <span class="hljs-comment">// Type: { title: string }</span>
});
</code></pre>
<h3 id="heading-createroute-route-configuration-factory">createRoute - Route Configuration Factory</h3>
<p>The <code>createRoute</code> function in <code>@hono/zod-openapi</code> takes a route configuration object and returns an enhanced route object with a path conversion method.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;

<span class="hljs-keyword">const</span> route = createRoute({
  method: <span class="hljs-string">'post'</span>,
  path: <span class="hljs-string">'/todos'</span>,
  request: { <span class="hljs-comment">/* ... */</span> },
  responses: { <span class="hljs-comment">/* ... */</span> },
});
</code></pre>
<p><code>createRoute</code> accepts one parameter, which includes:</p>
<ul>
<li><p><code>method</code>: HTTP method (e.g., 'get', 'post')</p>
</li>
<li><p><code>path</code>: OpenAPI path pattern (e.g., '/users/{id}'). Path parameters use curly braces: <code>{paramName}</code>. These must match parameter names in the request schema.</p>
</li>
<li><p><code>request</code>: Optional request validation schemas containing:</p>
<ul>
<li><p><code>params</code>: Zod schema for path parameters</p>
</li>
<li><p><code>query</code>: Zod schema for query parameters</p>
</li>
<li><p><code>headers</code>: Zod schema or array of schemas for request headers</p>
</li>
<li><p><code>cookies</code>: Zod schema for cookies</p>
</li>
<li><p><code>body</code>: Request body definition with:</p>
<ul>
<li><p><code>content</code>: Media type to schema mapping</p>
</li>
<li><p><code>description</code>: Body description</p>
</li>
<li><p><code>required</code>: Boolean indicating if body is required</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>responses</code>: Response definitions mapping status codes to response objects with content and descriptions</p>
</li>
<li><p><code>hide</code>: Optional boolean to hide from documentation</p>
</li>
<li><p><code>middleware</code>: Optional middleware handlers. Allows us to apply Hono middleware handlers specifically to individual routes, enabling per-route functionality like authentication, logging, caching, or custom request processing</p>
</li>
<li><p><code>summary</code>: A short summary of what the operation does</p>
</li>
<li><p><code>description</code>: A verbose explanation of the operation behavior</p>
</li>
<li><p><code>operationId</code>: Unique string used to identify the operation</p>
</li>
<li><p><code>tags</code>: A list of tags for API documentation control</p>
</li>
<li><p><code>security</code>: Declaration of which security schemes can be used</p>
</li>
</ul>
<h3 id="heading-routehandler-type-safe-request-handler">RouteHandler - Type-Safe Request Handler</h3>
<p><code>RouteHandler</code> is a generic type that infers request/response types from route configuration. <code>RouteHandler</code> takes four generic parameters:</p>
<ul>
<li><p><code>R extends RouteConfig</code>: The route configuration type</p>
</li>
<li><p><code>E extends Env = RouteConfigToEnv&lt;R&gt;</code>: Environment type, inferred from route middleware</p>
</li>
<li><p><code>I extends Input = ...</code>: Combined input types from params, query, headers, cookies, form, and JSON</p>
</li>
<li><p><code>P extends string = ConvertPathType&lt;R['path']&gt;</code>: Path type converted from OpenAPI to Hono syntax</p>
</li>
</ul>
<p><strong>How Type Inference Works:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> config = createRoute({
  method: <span class="hljs-string">'post'</span>,
  path: <span class="hljs-string">'/todos'</span>,
  request: {
    body: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: z.object({
            title: z.string(),
          }),
        },
      },
    },
  },
  responses: {
    <span class="hljs-number">201</span>: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: z.object({
            id: z.string(),
            title: z.string(),
          }),
        },
      },
      description: <span class="hljs-string">'Created'</span>,
    },
  },
});

<span class="hljs-comment">// Extract config type</span>
<span class="hljs-keyword">type</span> Config = <span class="hljs-keyword">typeof</span> config;

<span class="hljs-comment">// Handler infers types from Config</span>
<span class="hljs-keyword">const</span> handler: RouteHandler&lt;Config&gt; = <span class="hljs-keyword">async</span> (c) =&gt; {
  <span class="hljs-comment">// c.req.valid('json') returns { title: string }</span>
  <span class="hljs-keyword">const</span> { title } = c.req.valid(<span class="hljs-string">'json'</span>);

  <span class="hljs-comment">// Return type must match response schema</span>
  <span class="hljs-keyword">return</span> c.json(
    {
      id: <span class="hljs-string">'123'</span>,
      title: title,
    },
    <span class="hljs-number">201</span>
  );
};
</code></pre>
<p><strong>The Magic:</strong> <code>c.req.valid()</code></p>
<p>The <code>valid()</code> method is the key to type-safe validation:</p>
<pre><code class="lang-typescript">c.req.valid(<span class="hljs-string">'json'</span>)   <span class="hljs-comment">// Returns validated body (from request.body.schema)</span>
c.req.valid(<span class="hljs-string">'query'</span>)  <span class="hljs-comment">// Returns validated query params (from request.query)</span>
c.req.valid(<span class="hljs-string">'param'</span>)  <span class="hljs-comment">// Returns validated path params (from request.params)</span>
c.req.valid(<span class="hljs-string">'header'</span>) <span class="hljs-comment">// Returns validated headers (from request.header)</span>
</code></pre>
<p><strong>What valid() Does:</strong></p>
<ol>
<li><p><strong>Validates</strong> the incoming data using the Zod schema</p>
</li>
<li><p><strong>Transforms</strong> the data (e.g., <code>z.coerce.number()</code> converts strings)</p>
</li>
<li><p><strong>Returns</strong> typed data (TypeScript knows the exact type)</p>
</li>
<li><p><strong>Calls defaultHook</strong> if validation fails (never reaches your handler)</p>
</li>
</ol>
<p><strong>Type Safety Example:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> config = createRoute({
  request: {
    body: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: z.object({
            title: z.string(),
            priority: z.number(),
          }),
        },
      },
    },
  },
  <span class="hljs-comment">// ...</span>
});

<span class="hljs-keyword">type</span> Config = <span class="hljs-keyword">typeof</span> config;
<span class="hljs-keyword">const</span> handler: RouteHandler&lt;Config&gt; = <span class="hljs-keyword">async</span> (c) =&gt; {
  <span class="hljs-keyword">const</span> data = c.req.valid(<span class="hljs-string">'json'</span>);

  <span class="hljs-comment">// ✅ TypeScript knows these properties exist</span>
  <span class="hljs-built_in">console</span>.log(data.title);     <span class="hljs-comment">// string</span>
  <span class="hljs-built_in">console</span>.log(data.priority);  <span class="hljs-comment">// number</span>

  <span class="hljs-comment">// ❌ TypeScript error: Property doesn't exist</span>
  <span class="hljs-built_in">console</span>.log(data.description);
};
</code></pre>
<h3 id="heading-the-hook-system-validation-error-handling">The Hook System - Validation Error Handling</h3>
<p>Hooks intercept validation failures before they reach your handler. The <code>Hook</code> type takes four generic parameters:</p>
<ul>
<li><p><code>T</code>: The validated data type</p>
</li>
<li><p><code>E extends Env</code>: Environment type</p>
</li>
<li><p><code>P extends string</code>: Path type</p>
</li>
<li><p><code>R</code>: Return type</p>
</li>
</ul>
<p><strong>And two parameters:</strong></p>
<ul>
<li><p><code>result</code>: An object containing:</p>
<ul>
<li><p><code>target</code>: The validation target (keyof ValidationTargets)</p>
</li>
<li><p>Either success data (<code>{ success: true, data: T }</code>) or error information (<code>{ success: false, error: ZodError }</code>)</p>
</li>
</ul>
</li>
</ul>
<ol>
<li><code>c</code>: The Hono context object (same as in handlers)</li>
</ol>
<p><strong>Example Hook Implementation:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Hook } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { BAD_REQUEST } <span class="hljs-keyword">from</span> <span class="hljs-string">'./http-status-codes.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> defaultHook: Hook&lt;<span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>&gt; = <span class="hljs-function">(<span class="hljs-params">result, c</span>) =&gt;</span> {
  <span class="hljs-comment">// Only runs when validation fails</span>
  <span class="hljs-keyword">if</span> (!result.success) {
    <span class="hljs-keyword">return</span> c.json(
      {
        success: <span class="hljs-literal">false</span>,
        error: {
          issues: result.error.issues.map(<span class="hljs-function"><span class="hljs-params">issue</span> =&gt;</span> ({
            path: issue.path.join(<span class="hljs-string">'.'</span>),
            message: issue.message,
            code: issue.code,
          })),
        },
      },
      BAD_REQUEST
    );
  }
  <span class="hljs-comment">// If validation succeeds, return nothing (continue to handler)</span>
};
</code></pre>
<p><strong>How Hooks Are Applied:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Option 1: Per-route hook</span>
<span class="hljs-keyword">new</span> OpenAPIHono().openapi(config, handler, hook);

<span class="hljs-comment">// Option 2: Global default hook</span>
<span class="hljs-keyword">new</span> OpenAPIHono({ defaultHook }).openapi(config, handler);
</code></pre>
<p><strong>When the Hook Runs:</strong></p>
<pre><code class="lang-javascript"><span class="hljs-number">1.</span> Request arrives
<span class="hljs-number">2.</span> @hono/zod-openapi validates request against schema
<span class="hljs-number">3.</span> ❌ Validation fails
   → Hook is called
   → Returns error response
   → Handler never executes
<span class="hljs-number">4.</span> ✅ Validation succeeds
   → Hook is not called
   → Handler executes <span class="hljs-keyword">with</span> validated data
</code></pre>
<h3 id="heading-zod-openapi-extensions">Zod OpenAPI Extensions</h3>
<p><code>@hono/zod-openapi</code> extends Zod with <code>.openapi()</code> method for OpenAPI-specific metadata.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;

<span class="hljs-keyword">const</span> schema = z.string().openapi({
  example: <span class="hljs-string">'example value'</span>,
  description: <span class="hljs-string">'Field description'</span>,
  <span class="hljs-comment">// ... other OpenAPI properties</span>
});
</code></pre>
<p><strong>Why Import from @hono/zod-openapi?</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ❌ Wrong - missing .openapi() extension</span>
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-comment">// ✅ Correct - includes .openapi() extension</span>
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
</code></pre>
<p>The <code>@hono/zod-openapi</code> package re-exports Zod with extensions. Always import from this package when defining schemas for OpenAPI routes. The <code>.openapi()</code> method added to Zod schemas accepts different parameter forms depending on how you want to enhance the schema for OpenAPI documentation.</p>
<p><strong>Metadata Object Form:</strong> Takes an object with OpenAPI-specific properties:</p>
<ul>
<li><p><code>example</code>: Example value for the schema</p>
</li>
<li><p><code>description</code>: Schema description</p>
</li>
<li><p><code>deprecated</code>: Boolean to mark as deprecated</p>
</li>
<li><p><code>readOnly</code>: Boolean for read-only properties</p>
</li>
<li><p><code>writeOnly</code>: Boolean for write-only properties</p>
</li>
<li><p><code>param</code>: Parameter metadata for path/query parameters</p>
<ul>
<li><p><code>name</code>: Parameter name</p>
</li>
<li><p><code>in</code>: Parameter location ('path', 'query', 'header', 'cookie')</p>
</li>
<li><p><code>required</code></p>
</li>
</ul>
</li>
</ul>
<p><strong>Schema Registration Form:</strong> Takes a string to register the schema as a referenced component in the OpenAPI document.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Without name - inlined in OpenAPI spec</span>
<span class="hljs-keyword">const</span> todoSchema = z.object({
  title: z.string(),
});

<span class="hljs-comment">// With name - creates reusable component</span>
<span class="hljs-keyword">const</span> todoSchema = z.object({
  title: z.string(),
}).openapi(<span class="hljs-string">'Todo'</span>);
</code></pre>
<p><strong>Generated OpenAPI (without name):</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"requestBody"</span>: {
    <span class="hljs-attr">"content"</span>: {
      <span class="hljs-attr">"application/json"</span>: {
        <span class="hljs-attr">"schema"</span>: {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
          <span class="hljs-attr">"properties"</span>: {
            <span class="hljs-attr">"title"</span>: { <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span> }
          }
        }
      }
    }
  }
}
</code></pre>
<p><strong>Generated OpenAPI (with name):</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"requestBody"</span>: {
    <span class="hljs-attr">"content"</span>: {
      <span class="hljs-attr">"application/json"</span>: {
        <span class="hljs-attr">"schema"</span>: {
          <span class="hljs-attr">"$ref"</span>: <span class="hljs-string">"#/components/schemas/Todo"</span>
        }
      }
    }
  },
  <span class="hljs-attr">"components"</span>: {
    <span class="hljs-attr">"schemas"</span>: {
      <span class="hljs-attr">"Todo"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
        <span class="hljs-attr">"properties"</span>: {
          <span class="hljs-attr">"title"</span>: { <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span> }
        }
      }
    }
  }
}
</code></pre>
<p><strong>Benefits of Named Schemas:</strong></p>
<ul>
<li><p>Smaller OpenAPI spec (no duplication)</p>
</li>
<li><p>Better generated client SDKs</p>
</li>
<li><p>Easier to reference in documentation</p>
</li>
</ul>
<h2 id="heading-building-the-todo-api-step-by-step">Building the Todo API - Step by Step</h2>
<p>Now that we understand the <code>@hono/zod-openapi</code> fundamentals, let's build a complete TODO API. The project initial setup was explaned in the <a target="_blank" href="https://blog.raulnq.com/hono-setting-up-the-development-environment"><strong>Hono: Setting up the development environment</strong></a> <strong>article.</strong></p>
<p>Install dependencies:</p>
<pre><code class="lang-bash">npm install @hono/zod-openapi uuid @scalar/hono-api-reference http-problem-details
</code></pre>
<p><strong>Dependency Breakdown:</strong></p>
<ul>
<li><p><code>@hono/zod-openapi</code>: The star of the show - provides OpenAPI integration</p>
</li>
<li><p><code>@scalar/hono-api-reference</code>: Interactive API documentation UI</p>
</li>
<li><p><code>http-problem-details</code>: RFC 7807 error response formatting</p>
</li>
<li><p><code>uuid</code>: For generating UUIDv7 identifiers</p>
</li>
</ul>
<h3 id="heading-step-1-domain-model">Step 1: Domain Model</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/features/todos/todo.ts</span>
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> todoSchema = z
  .object({
    todoId: z.uuidv7().openapi({
      example: <span class="hljs-string">'019af0ad-4ac8-7052-a609-24a539d353cd'</span>,
    }),
    title: z.string().min(<span class="hljs-number">1</span>).openapi({
      example: <span class="hljs-string">'Buy groceries'</span>,
    }),
    completed: z.boolean().default(<span class="hljs-literal">false</span>).openapi({
      example: <span class="hljs-literal">false</span>,
    }),
  })
  .openapi(<span class="hljs-string">'Todo'</span>);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Todo = z.infer&lt;<span class="hljs-keyword">typeof</span> todoSchema&gt;;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> todos: Todo[] = [];

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> tags = [<span class="hljs-string">'Tasks'</span>];
</code></pre>
<p><strong>Schema Composition:</strong></p>
<p><code>todoSchema</code> serves as the base. We'll derive other schemas from it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Create schema - omit server-managed fields</span>
<span class="hljs-keyword">const</span> createSchema = todoSchema.omit({
  todoId: <span class="hljs-literal">true</span>,
  completed: <span class="hljs-literal">true</span>
});

<span class="hljs-comment">// Update schema - all fields optional except ID</span>
<span class="hljs-keyword">const</span> updateSchema = todoSchema.partial().omit({
  todoId: <span class="hljs-literal">true</span>
});

<span class="hljs-comment">// Query schema - for filtering</span>
<span class="hljs-keyword">const</span> querySchema = todoSchema.pick({
  completed: <span class="hljs-literal">true</span>
});
</code></pre>
<h3 id="heading-step-2-error-handling-schema">Step 2: Error Handling Schema</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/schemas/problemDocument.ts</span>
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;

<span class="hljs-keyword">const</span> errorSchema = z.object({
  path: z.string().openapi({
    example: <span class="hljs-string">'title'</span>,
    description: <span class="hljs-string">'The path to the field that failed validation'</span>,
  }),
  message: z.string().openapi({
    example: <span class="hljs-string">'Too small: expected string to have &gt;=1 characters'</span>,
    description: <span class="hljs-string">'The validation error message'</span>,
  }),
  code: z.string().openapi({
    example: <span class="hljs-string">'too_small'</span>,
    description: <span class="hljs-string">'The validation error code'</span>,
  }),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> problemDocumentSchema = z
  .object({
    <span class="hljs-keyword">type</span>: z.string().optional().openapi({
      example: <span class="hljs-string">'/problems/resource-not-found'</span>,
      description: <span class="hljs-string">'A URI reference that identifies the problem type'</span>,
    }),
    title: z.string().openapi({
      example: <span class="hljs-string">'Resource not found'</span>,
      description: <span class="hljs-string">'A short, human-readable summary of the problem type'</span>,
    }),
    status: z.number().openapi({
      example: <span class="hljs-number">404</span>,
      description: <span class="hljs-string">'The HTTP status code'</span>,
    }),
    detail: z.string().optional().openapi({
      example: <span class="hljs-string">'The requested todo was not found'</span>,
      description: <span class="hljs-string">'A human-readable explanation specific to this occurrence'</span>,
    }),
    instance: z.string().optional().openapi({
      example: <span class="hljs-string">'/todos/019af0ad-4ac8-7052-a609-24a539d353cd'</span>,
      description: <span class="hljs-string">'A URI reference that identifies the specific occurrence'</span>,
    }),
    errors: z.array(errorSchema).optional().openapi({
      description: <span class="hljs-string">'Array of validation errors'</span>,
    }),
  })
  .openapi(<span class="hljs-string">'ProblemDocument'</span>);
</code></pre>
<p><strong>RFC 7807 Problem Details:</strong></p>
<p>This standardized error format provides:</p>
<ul>
<li><p><strong>Machine-readable</strong> error types</p>
</li>
<li><p><strong>Human-readable</strong> messages</p>
</li>
<li><p><strong>Traceable</strong> request instances</p>
</li>
<li><p><strong>Extensible</strong> with custom fields</p>
</li>
</ul>
<h3 id="heading-step-3-validation-hook">Step 3: Validation Hook</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/hooks.ts</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Hook } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { BAD_REQUEST } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/http-status-codes.js'</span>;
<span class="hljs-keyword">import</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> defaultHook: Hook&lt;<span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>&gt; = <span class="hljs-function">(<span class="hljs-params">result, c</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (!result.success) {
    <span class="hljs-keyword">return</span> c.json(
      <span class="hljs-keyword">new</span> ProblemDocument(
        {
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'/problems/validation-error'</span>,
          title: <span class="hljs-string">'Validation Error'</span>,
          status: BAD_REQUEST,
          detail: <span class="hljs-string">'The request contains invalid data'</span>,
          instance: c.req.path,
        },
        {
          errors: result.error.issues.map(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> ({
            path: err.path.join(<span class="hljs-string">'.'</span>),
            message: err.message,
            code: err.code,
          })),
        }
      ),
      BAD_REQUEST
    );
  }
};
</code></pre>
<p><strong>Hook Execution Flow:</strong></p>
<pre><code class="lang-javascript">Client Request
    ↓
@hono/zod-openapi validates against schema
    ↓
  ❌ Validation Fails
    ↓
defaultHook is called <span class="hljs-keyword">with</span> ZodError
    ↓
Hook transforms error to Problem Document
    ↓
Returns <span class="hljs-number">400</span> response
    ↓
Handler NEVER executes
</code></pre>
<p><strong>Zod Error Structure:</strong></p>
<pre><code class="lang-typescript">result.error.issues = [
  {
    code: <span class="hljs-string">'too_small'</span>,
    minimum: <span class="hljs-number">1</span>,
    <span class="hljs-keyword">type</span>: <span class="hljs-string">'string'</span>,
    inclusive: <span class="hljs-literal">true</span>,
    exact: <span class="hljs-literal">false</span>,
    message: <span class="hljs-string">'Too small: expected string to have &gt;=1 characters'</span>,
    path: [<span class="hljs-string">'title'</span>],
  }
]
</code></pre>
<h3 id="heading-step-4-create-operation">Step 4: CREATE Operation</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/features/todos/addTodo.ts</span>
<span class="hljs-keyword">import</span> { createRoute, OpenAPIHono, <span class="hljs-keyword">type</span> RouteHandler } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { v7 <span class="hljs-keyword">as</span> uuidv7 } <span class="hljs-keyword">from</span> <span class="hljs-string">'uuid'</span>;
<span class="hljs-keyword">import</span> { todoSchema, todos, tags } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todo.js'</span>;
<span class="hljs-keyword">import</span> { CREATED, BAD_REQUEST } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/http-status-codes.js'</span>;
<span class="hljs-keyword">import</span> { defaultHook } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks.js'</span>;
<span class="hljs-keyword">import</span> { problemDocumentSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/schemas/problemDocument.js'</span>;

<span class="hljs-comment">// Derive create schema from base schema</span>
<span class="hljs-keyword">const</span> addTodoSchema = todoSchema
  .omit({ todoId: <span class="hljs-literal">true</span>, completed: <span class="hljs-literal">true</span> })
  .openapi(<span class="hljs-string">'CreateTodo'</span>);

<span class="hljs-comment">// Define route configuration</span>
<span class="hljs-keyword">const</span> config = createRoute({
  method: <span class="hljs-string">'post'</span>,
  path: <span class="hljs-string">'/todos'</span>,
  tags: tags,
  request: {
    body: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: addTodoSchema,
        },
      },
      description: <span class="hljs-string">'Todo to create'</span>,
      required: <span class="hljs-literal">true</span>,
    },
  },
  responses: {
    [CREATED]: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: todoSchema,
        },
      },
      description: <span class="hljs-string">'Create a new todo'</span>,
    },
    [BAD_REQUEST]: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: problemDocumentSchema,
        },
      },
      description: <span class="hljs-string">'Invalid request data'</span>,
    },
  },
});

<span class="hljs-comment">// Extract route type</span>
<span class="hljs-keyword">type</span> Config = <span class="hljs-keyword">typeof</span> config;

<span class="hljs-comment">// Type-safe handler</span>
<span class="hljs-keyword">const</span> handler: RouteHandler&lt;Config&gt; = <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-comment">// c.req.valid('json') returns { title: string }</span>
  <span class="hljs-keyword">const</span> { title } = c.req.valid(<span class="hljs-string">'json'</span>);

  <span class="hljs-keyword">const</span> todo = {
    todoId: uuidv7(),
    title,
    completed: <span class="hljs-literal">false</span>,
  };

  todos.push(todo);

  <span class="hljs-keyword">return</span> c.json(todo, CREATED);
};

<span class="hljs-comment">// Export as OpenAPIHono instance</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addRoute = <span class="hljs-keyword">new</span> OpenAPIHono({
  strict: <span class="hljs-literal">false</span>,
  defaultHook,
}).openapi(config, handler);
</code></pre>
<p><strong>Breaking Down the Implementation:</strong></p>
<ol>
<li><p><strong>Schema Derivation:</strong></p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> addTodoSchema = todoSchema.omit({
   todoId: <span class="hljs-literal">true</span>,    <span class="hljs-comment">// Server generates</span>
   completed: <span class="hljs-literal">true</span>   <span class="hljs-comment">// Server sets default</span>
 });
</code></pre>
<p> Clients provide only <code>title</code>. Server manages <code>todoId</code> and <code>completed</code>.</p>
</li>
<li><p><strong>Route Configuration:</strong></p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> config = createRoute({
   method: <span class="hljs-string">'post'</span>,
   path: <span class="hljs-string">'/todos'</span>,
   <span class="hljs-comment">// ...</span>
 });
</code></pre>
<p> This generates the OpenAPI specification for <code>POST /todos</code>.</p>
</li>
<li><p><strong>Type Extraction:</strong></p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">type</span> Config = <span class="hljs-keyword">typeof</span> config;
</code></pre>
<p> Captures the full type of the configuration, used by <code>RouteHandler</code>.</p>
</li>
<li><p><strong>Type-Safe Handler:</strong></p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> handler: RouteHandler&lt;Config&gt; = <span class="hljs-keyword">async</span> c =&gt; {
   <span class="hljs-keyword">const</span> { title } = c.req.valid(<span class="hljs-string">'json'</span>);
   <span class="hljs-comment">// TypeScript knows: { title: string }</span>
 };
</code></pre>
<p> <code>RouteHandler&lt;Config&gt;</code> infers parameter types from <code>config</code>.</p>
</li>
<li><p><strong>Validation Flow:</strong></p>
<pre><code class="lang-javascript"> Request: POST /todos
 <span class="hljs-attr">Body</span>: { <span class="hljs-string">"title"</span>: <span class="hljs-string">""</span> }
     ↓
 @hono/zod-openapi validates against addTodoSchema
     ↓
 z.string().min(<span class="hljs-number">1</span>) fails (empty string)
     ↓
 defaultHook called
     ↓
 <span class="hljs-attr">Returns</span>: <span class="hljs-number">400</span> Bad Request <span class="hljs-keyword">with</span> Problem Document
</code></pre>
</li>
<li><p><strong>Route Export:</strong></p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addRoute = <span class="hljs-keyword">new</span> OpenAPIHono({
   strict: <span class="hljs-literal">false</span>,
   defaultHook,
 }).openapi(config, handler);
</code></pre>
<p> Creates a standalone Hono instance for this route, enabling modular composition.</p>
</li>
</ol>
<h3 id="heading-step-5-read-operation-list-with-pagination">Step 5: READ Operation - List with Pagination</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/schemas/pagination.ts</span>
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;

<span class="hljs-keyword">const</span> DEFAULT_PAGE_NUMBER = <span class="hljs-number">1</span>;
<span class="hljs-keyword">const</span> DEFAULT_PAGE_SIZE = <span class="hljs-number">10</span>;
<span class="hljs-keyword">const</span> MAX_PAGE_SIZE = <span class="hljs-number">100</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> paginationParametersSchema = z.object({
  pageNumber: z.coerce.number().min(<span class="hljs-number">1</span>).optional().default(DEFAULT_PAGE_NUMBER),
  pageSize: z.coerce
    .number()
    .min(<span class="hljs-number">1</span>)
    .max(MAX_PAGE_SIZE)
    .optional()
    .default(DEFAULT_PAGE_SIZE),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createPageSchema = &lt;T&gt;<span class="hljs-function">(<span class="hljs-params">itemSchema: z.ZodSchema&lt;T&gt;</span>) =&gt;</span>
  z.object({
    items: z.array(itemSchema),
    pageNumber: z.number().min(<span class="hljs-number">1</span>),
    pageSize: z.number().min(<span class="hljs-number">1</span>).max(MAX_PAGE_SIZE),
    totalPages: z.number().min(<span class="hljs-number">0</span>),
    totalCount: z.number().min(<span class="hljs-number">0</span>),
  });
</code></pre>
<p><strong>Generic Schema Factory:</strong></p>
<pre><code class="lang-typescript">createPageSchema&lt;T&gt;(itemSchema: z.ZodSchema&lt;T&gt;)
</code></pre>
<p>Creates a paginated response schema for any item type:</p>
<ul>
<li><p><code>createPageSchema(todoSchema)</code> → <code>Page&lt;Todo&gt;</code></p>
</li>
<li><p><code>createPageSchema(userSchema)</code> → <code>Page&lt;User&gt;</code></p>
</li>
</ul>
<p><strong>Query Parameter Coercion:</strong></p>
<pre><code class="lang-typescript">pageNumber: z.coerce.number()
</code></pre>
<p>Query parameters arrive as strings:</p>
<pre><code class="lang-javascript">GET /todos?pageNumber=<span class="hljs-number">2</span>&amp;pageSize=<span class="hljs-number">20</span>
          ↓
{ <span class="hljs-attr">pageNumber</span>: <span class="hljs-string">"2"</span>, <span class="hljs-attr">pageSize</span>: <span class="hljs-string">"20"</span> }  <span class="hljs-comment">// Strings</span>
          ↓
z.coerce.number() converts
          ↓
{ <span class="hljs-attr">pageNumber</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">pageSize</span>: <span class="hljs-number">20</span> }      <span class="hljs-comment">// Numbers</span>
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/features/todos/listTodos.ts</span>
<span class="hljs-keyword">import</span> { createRoute, OpenAPIHono, <span class="hljs-keyword">type</span> RouteHandler } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { todoSchema, todos, tags } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todo.js'</span>;
<span class="hljs-keyword">import</span> {
  paginationParametersSchema,
  createPageSchema,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/schemas/pagination.js'</span>;
<span class="hljs-keyword">import</span> { OK } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/http-status-codes.js'</span>;
<span class="hljs-keyword">import</span> { defaultHook } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks.js'</span>;

<span class="hljs-keyword">const</span> config = createRoute({
  path: <span class="hljs-string">'/todos'</span>,
  method: <span class="hljs-string">'get'</span>,
  tags: tags,
  request: {
    query: paginationParametersSchema,
  },
  responses: {
    [OK]: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: createPageSchema(todoSchema),
        },
      },
      description: <span class="hljs-string">'List all todos'</span>,
    },
  },
});

<span class="hljs-keyword">type</span> Config = <span class="hljs-keyword">typeof</span> config;

<span class="hljs-keyword">const</span> handler: RouteHandler&lt;Config&gt; = <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-comment">// c.req.valid('query') returns { pageNumber: number, pageSize: number }</span>
  <span class="hljs-keyword">const</span> { pageNumber, pageSize } = c.req.valid(<span class="hljs-string">'query'</span>);

  <span class="hljs-keyword">const</span> startIndex = (pageNumber - <span class="hljs-number">1</span>) * pageSize;
  <span class="hljs-keyword">const</span> endIndex = startIndex + pageSize;
  <span class="hljs-keyword">const</span> paginatedTodos = todos.slice(startIndex, endIndex);

  <span class="hljs-keyword">return</span> c.json(
    {
      items: paginatedTodos,
      pageNumber,
      pageSize,
      totalPages: <span class="hljs-built_in">Math</span>.ceil(todos.length / pageSize),
      totalCount: todos.length,
    },
    OK
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> listRoute = <span class="hljs-keyword">new</span> OpenAPIHono({
  strict: <span class="hljs-literal">false</span>,
  defaultHook,
}).openapi(config, handler);
</code></pre>
<p><strong>Query Parameter Validation:</strong></p>
<pre><code class="lang-typescript">request: {
  query: paginationParametersSchema,
}
</code></pre>
<p>This validates query parameters:</p>
<ul>
<li><p><code>pageNumber</code>: Must be a number ≥ 1 (defaults to 1)</p>
</li>
<li><p><code>pageSize</code>: Must be between 1 and 100 (defaults to 10)</p>
</li>
</ul>
<p><strong>Type-Safe Query Access:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { pageNumber, pageSize } = c.req.valid(<span class="hljs-string">'query'</span>);
<span class="hljs-comment">// TypeScript knows both are numbers, not string | undefined</span>
</code></pre>
<h3 id="heading-step6-read-operation-find-by-id">Step6: READ Operation - Find by ID</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/features/todos/findTodo.ts</span>
<span class="hljs-keyword">import</span> {
  createRoute,
  OpenAPIHono,
  z,
  <span class="hljs-keyword">type</span> RouteHandler,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { todoSchema, todos, tags } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todo.js'</span>;
<span class="hljs-keyword">import</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details'</span>;
<span class="hljs-keyword">import</span> { problemDocumentSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/schemas/problemDocument.js'</span>;
<span class="hljs-keyword">import</span> { OK, NOT_FOUND } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/http-status-codes.js'</span>;
<span class="hljs-keyword">import</span> { defaultHook } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks.js'</span>;

<span class="hljs-keyword">const</span> config = createRoute({
  path: <span class="hljs-string">'/todos/{todoId}'</span>,
  method: <span class="hljs-string">'get'</span>,
  tags: tags,
  request: {
    params: z.object({
      todoId: z.uuidv7().openapi({
        param: {
          name: <span class="hljs-string">'todoId'</span>,
          <span class="hljs-keyword">in</span>: <span class="hljs-string">'path'</span>,
          required: <span class="hljs-literal">true</span>,
        },
        example: <span class="hljs-string">'019af0ad-4ac8-7052-a609-24a539d353cd'</span>,
      }),
    }),
  },
  responses: {
    [OK]: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: todoSchema,
        },
      },
      description: <span class="hljs-string">'Get a todo by ID'</span>,
    },
    [NOT_FOUND]: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: problemDocumentSchema,
        },
      },
      description: <span class="hljs-string">'Todo not found'</span>,
    },
  },
});

<span class="hljs-keyword">type</span> Config = <span class="hljs-keyword">typeof</span> config;

<span class="hljs-keyword">const</span> handler: RouteHandler&lt;Config&gt; = <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-comment">// c.req.valid('param') returns { todoId: string }</span>
  <span class="hljs-keyword">const</span> { todoId } = c.req.valid(<span class="hljs-string">'param'</span>);
  <span class="hljs-keyword">const</span> todo = todos.find(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> t.todoId === todoId);

  <span class="hljs-keyword">if</span> (!todo) {
    <span class="hljs-keyword">return</span> c.json(
      <span class="hljs-keyword">new</span> ProblemDocument({
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'/problems/resource-not-found'</span>,
        title: <span class="hljs-string">'Resource not found'</span>,
        status: NOT_FOUND,
        detail: <span class="hljs-string">`Todo with id <span class="hljs-subst">${todoId}</span> not found`</span>,
        instance: c.req.path,
      }),
      NOT_FOUND
    );
  }

  <span class="hljs-keyword">return</span> c.json(todo, OK);
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> findRoute = <span class="hljs-keyword">new</span> OpenAPIHono({
  strict: <span class="hljs-literal">false</span>,
  defaultHook,
}).openapi(config, handler);
</code></pre>
<p><strong>Path Parameter Configuration:</strong></p>
<pre><code class="lang-typescript">params: z.object({
  todoId: z.uuidv7().openapi({
    param: {
      name: <span class="hljs-string">'todoId'</span>,     <span class="hljs-comment">// Must match {todoId} in path</span>
      <span class="hljs-keyword">in</span>: <span class="hljs-string">'path'</span>,         <span class="hljs-comment">// Parameter location</span>
      required: <span class="hljs-literal">true</span>,     <span class="hljs-comment">// Path params always required</span>
    },
  }),
})
</code></pre>
<p><strong>Critical: Name Must Match Path Placeholder:</strong></p>
<pre><code class="lang-typescript">path: <span class="hljs-string">'/todos/{todoId}'</span>,
params: z.object({
  todoId: z.uuidv7(),  <span class="hljs-comment">// ✅ Matches {todoId}</span>
}),

path: <span class="hljs-string">'/todos/{id}'</span>,
params: z.object({
  todoId: z.uuidv7(),  <span class="hljs-comment">// ❌ Doesn't match {id}</span>
}),
</code></pre>
<p><strong>Validation Flow:</strong></p>
<pre><code class="lang-javascript">Request: GET /todos/invalid-uuid
     ↓
@hono/zod-openapi validates <span class="hljs-string">'invalid-uuid'</span>
     ↓
z.uuidv7() fails
     ↓
defaultHook called
     ↓
<span class="hljs-attr">Returns</span>: <span class="hljs-number">400</span> Bad Request

<span class="hljs-attr">Request</span>: GET /todos/<span class="hljs-number">019</span>af0ad<span class="hljs-number">-4</span>ac8<span class="hljs-number">-7052</span>-a609<span class="hljs-number">-24</span>a539d353cd
     ↓
Validation succeeds
     ↓
Handler executes
     ↓
Todo not <span class="hljs-keyword">in</span> array
     ↓
<span class="hljs-attr">Returns</span>: <span class="hljs-number">404</span> Not Found <span class="hljs-keyword">with</span> Problem Document
</code></pre>
<h3 id="heading-step-7-update-operation-mark-complete">Step 7: UPDATE Operation - Mark Complete</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/features/todos/checkTodo.ts</span>
<span class="hljs-keyword">import</span> { createRoute, OpenAPIHono, <span class="hljs-keyword">type</span> RouteHandler } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { todoSchema, todos, tags } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todo.js'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details'</span>;
<span class="hljs-keyword">import</span> { problemDocumentSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/schemas/problemDocument.js'</span>;
<span class="hljs-keyword">import</span> { OK, NOT_FOUND } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/http-status-codes.js'</span>;
<span class="hljs-keyword">import</span> { defaultHook } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks.js'</span>;

<span class="hljs-keyword">const</span> config = createRoute({
  path: <span class="hljs-string">'/todos/{todoId}/check'</span>,
  method: <span class="hljs-string">'put'</span>,
  tags: tags,
  request: {
    params: z.object({
      todoId: z.uuidv7().openapi({
        param: {
          name: <span class="hljs-string">'todoId'</span>,
          <span class="hljs-keyword">in</span>: <span class="hljs-string">'path'</span>,
          required: <span class="hljs-literal">true</span>,
        },
        example: <span class="hljs-string">'019af0ad-4ac8-7052-a609-24a539d353cd'</span>,
      }),
    }),
  },
  responses: {
    [OK]: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: todoSchema,
        },
      },
      description: <span class="hljs-string">'Check a todo as completed'</span>,
    },
    [NOT_FOUND]: {
      content: {
        <span class="hljs-string">'application/json'</span>: {
          schema: problemDocumentSchema,
        },
      },
      description: <span class="hljs-string">'Todo not found'</span>,
    },
  },
});

<span class="hljs-keyword">type</span> Config = <span class="hljs-keyword">typeof</span> config;

<span class="hljs-keyword">const</span> handler: RouteHandler&lt;Config&gt; = <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-keyword">const</span> { todoId } = c.req.valid(<span class="hljs-string">'param'</span>);
  <span class="hljs-keyword">const</span> todo = todos.find(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> t.todoId === todoId);

  <span class="hljs-keyword">if</span> (!todo) {
    <span class="hljs-keyword">return</span> c.json(
      <span class="hljs-keyword">new</span> ProblemDocument({
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'/problems/resource-not-found'</span>,
        title: <span class="hljs-string">'Resource not found'</span>,
        status: NOT_FOUND,
        detail: <span class="hljs-string">`Todo with id <span class="hljs-subst">${todoId}</span> not found`</span>,
        instance: c.req.path,
      }),
      NOT_FOUND
    );
  }

  todo.completed = <span class="hljs-literal">true</span>;
  <span class="hljs-keyword">return</span> c.json(todo, OK);
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> checkRoute = <span class="hljs-keyword">new</span> OpenAPIHono({
  strict: <span class="hljs-literal">false</span>,
  defaultHook,
}).openapi(config, handler);
</code></pre>
<p><strong>Action Endpoint Pattern:</strong></p>
<pre><code class="lang-typescript">path: <span class="hljs-string">'/todos/{todoId}/check'</span>,
method: <span class="hljs-string">'put'</span>,
</code></pre>
<p>This is an "action" endpoint:</p>
<ul>
<li><p>No request body needed</p>
</li>
<li><p>Performs a specific action (checking todo)</p>
</li>
<li><p>Idempotent (calling multiple times same effect)</p>
</li>
</ul>
<h3 id="heading-step-8-assembling-the-application">Step 8: Assembling the Application</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/index.ts</span>
<span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>;
<span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/env.js'</span>;
<span class="hljs-keyword">import</span> { OpenAPIHono } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-openapi'</span>;
<span class="hljs-keyword">import</span> { addRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/todos/addTodo.js'</span>;
<span class="hljs-keyword">import</span> { Scalar } <span class="hljs-keyword">from</span> <span class="hljs-string">'@scalar/hono-api-reference'</span>;
<span class="hljs-keyword">import</span> { listRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/todos/listTodos.js'</span>;
<span class="hljs-keyword">import</span> { findRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/todos/findTodo.js'</span>;
<span class="hljs-keyword">import</span> { checkRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/todos/checkTodo.js'</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> OpenAPIHono()
  .doc(<span class="hljs-string">'/doc'</span>, {
    openapi: <span class="hljs-string">'3.0.0'</span>,
    info: {
      version: <span class="hljs-string">'1.0.0'</span>,
      title: <span class="hljs-string">'Todo API'</span>,
    },
  })
  .get(
    <span class="hljs-string">'/reference'</span>,
    Scalar({
      url: <span class="hljs-string">'/doc'</span>,
      theme: <span class="hljs-string">'kepler'</span>,
      layout: <span class="hljs-string">'classic'</span>,
      darkMode: <span class="hljs-literal">true</span>,
    })
  )
  .route(<span class="hljs-string">'/'</span>, addRoute)
  .route(<span class="hljs-string">'/'</span>, listRoute)
  .route(<span class="hljs-string">'/'</span>, findRoute)
  .route(<span class="hljs-string">'/'</span>, checkRoute);

serve(
  {
    fetch: app.fetch,
    port: ENV.PORT,
  },
  <span class="hljs-function"><span class="hljs-params">info</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(
      <span class="hljs-string">`Server(<span class="hljs-subst">${ENV.NODE_ENV}</span>) is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>
    );
  }
);
</code></pre>
<p><strong>The</strong> <code>.doc()</code> Method:</p>
<pre><code class="lang-typescript">.doc(<span class="hljs-string">'/doc'</span>, {
  openapi: <span class="hljs-string">'3.0.0'</span>,
  info: {
    version: <span class="hljs-string">'1.0.0'</span>,
    title: <span class="hljs-string">'Todo API'</span>,
  },
})
</code></pre>
<p>This registers <code>GET /doc</code> endpoint that returns the OpenAPI JSON specification. The spec is automatically generated from all registered routes.</p>
<p><strong>What Gets Generated:</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"openapi"</span>: <span class="hljs-string">"3.0.0"</span>,
  <span class="hljs-attr">"info"</span>: {
    <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
    <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Todo API"</span>
  },
  <span class="hljs-attr">"paths"</span>: {
    <span class="hljs-attr">"/todos"</span>: {
      <span class="hljs-attr">"get"</span>: { <span class="hljs-comment">/* listRoute config */</span> },
      <span class="hljs-attr">"post"</span>: { <span class="hljs-comment">/* addRoute config */</span> }
    },
    <span class="hljs-attr">"/todos/{todoId}"</span>: {
      <span class="hljs-attr">"get"</span>: { <span class="hljs-comment">/* findRoute config */</span> }
    },
    <span class="hljs-attr">"/todos/{todoId}/check"</span>: {
      <span class="hljs-attr">"put"</span>: { <span class="hljs-comment">/* checkRoute config */</span> }
    }
  },
  <span class="hljs-attr">"components"</span>: {
    <span class="hljs-attr">"schemas"</span>: {
      <span class="hljs-attr">"Todo"</span>: { <span class="hljs-comment">/* todoSchema */</span> },
      <span class="hljs-attr">"CreateTodo"</span>: { <span class="hljs-comment">/* addTodoSchema */</span> },
      <span class="hljs-attr">"ProblemDocument"</span>: { <span class="hljs-comment">/* problemDocumentSchema */</span> }
    }
  }
}
</code></pre>
<p><a target="_blank" href="https://github.com/scalar/scalar"><strong>Scalar</strong></a> <strong>Documentation UI:</strong></p>
<pre><code class="lang-typescript">.get(
  <span class="hljs-string">'/reference'</span>,
  Scalar({
    url: <span class="hljs-string">'/doc'</span>,           <span class="hljs-comment">// Points to OpenAPI spec endpoint</span>
    theme: <span class="hljs-string">'kepler'</span>,       <span class="hljs-comment">// Visual theme</span>
    layout: <span class="hljs-string">'classic'</span>,     <span class="hljs-comment">// Layout style</span>
    darkMode: <span class="hljs-literal">true</span>,        <span class="hljs-comment">// Enable dark mode</span>
  })
)
</code></pre>
<p>Access at <a target="_blank" href="http://localhost:3000/reference"><code>http://localhost:3000/reference</code></a> for interactive API documentation.</p>
<p><strong>Mounting Routes:</strong></p>
<pre><code class="lang-typescript">.route(<span class="hljs-string">'/'</span>, addRoute)
.route(<span class="hljs-string">'/'</span>, listRoute)
.route(<span class="hljs-string">'/'</span>, findRoute)
.route(<span class="hljs-string">'/'</span>, checkRoute)
</code></pre>
<p>Each route is a complete <code>OpenAPIHono</code> instance. <code>.route()</code> mounts them on the main app at the specified base path.</p>
<p><strong>Why This Pattern?</strong></p>
<ul>
<li><p><strong>Modularity</strong>: Each route is self-contained</p>
</li>
<li><p><strong>Testing</strong>: Test routes independently</p>
</li>
<li><p><strong>Organization</strong>: Related code stays together</p>
</li>
<li><p><strong>Reusability</strong>: Routes can be mounted on different apps</p>
</li>
</ul>
<h2 id="heading-advanced-patterns">Advanced Patterns</h2>
<h3 id="heading-middleware-integration">Middleware Integration</h3>
<p><code>OpenAPIHono</code> is fully compatible with Hono middleware:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { logger } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/logger'</span>;
<span class="hljs-keyword">import</span> { cors } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/cors'</span>;
<span class="hljs-keyword">import</span> { bearerAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/bearer-auth'</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> OpenAPIHono()
  .use(<span class="hljs-string">'*'</span>, logger())
  .use(<span class="hljs-string">'/api/*'</span>, cors())
  .use(<span class="hljs-string">'/api/admin/*'</span>, bearerAuth({ token: <span class="hljs-string">'secret'</span> }));
</code></pre>
<p><strong>Documenting Authentication:</strong></p>
<pre><code class="lang-typescript">.doc(<span class="hljs-string">'/doc'</span>, {
  openapi: <span class="hljs-string">'3.0.0'</span>,
  info: { <span class="hljs-comment">/* ... */</span> },
  components: {
    securitySchemes: {
      bearerAuth: {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'http'</span>,
        scheme: <span class="hljs-string">'bearer'</span>,
        bearerFormat: <span class="hljs-string">'JWT'</span>,
      },
    },
  },
  security: [{ bearerAuth: [] }],
})
</code></pre>
<h3 id="heading-custom-validation-hooks-per-route">Custom Validation Hooks Per Route</h3>
<p>Override the default hook for specific routes:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> customHook: Hook&lt;<span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>&gt; = <span class="hljs-function">(<span class="hljs-params">result, c</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (!result.success) {
    <span class="hljs-comment">// Custom error handling for this specific route</span>
    <span class="hljs-keyword">return</span> c.json(
      {
        error: <span class="hljs-string">'Custom error format'</span>,
        details: result.error.issues,
      },
      <span class="hljs-number">400</span>
    );
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> specialRoute = <span class="hljs-keyword">new</span> OpenAPIHono()
  .openapi(config, handler, customHook);  <span class="hljs-comment">// Route-specific hook</span>
</code></pre>
<h3 id="heading-type-safe-response-helpers">Type-Safe Response Helpers</h3>
<p>Create helpers for common responses:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> createSuccessResponse = &lt;T <span class="hljs-keyword">extends</span> z.ZodSchema&gt;<span class="hljs-function">(<span class="hljs-params">schema: T</span>) =&gt;</span> ({
  [OK]: {
    content: {
      <span class="hljs-string">'application/json'</span>: {
        schema,
      },
    },
    description: <span class="hljs-string">'Success'</span>,
  },
});

<span class="hljs-keyword">const</span> createErrorResponses = <span class="hljs-function">() =&gt;</span> ({
  [BAD_REQUEST]: {
    content: {
      <span class="hljs-string">'application/json'</span>: {
        schema: problemDocumentSchema,
      },
    },
    description: <span class="hljs-string">'Validation error'</span>,
  },
  [NOT_FOUND]: {
    content: {
      <span class="hljs-string">'application/json'</span>: {
        schema: problemDocumentSchema,
      },
    },
    description: <span class="hljs-string">'Resource not found'</span>,
  },
});

<span class="hljs-comment">// Usage</span>
<span class="hljs-keyword">const</span> config = createRoute({
  method: <span class="hljs-string">'get'</span>,
  path: <span class="hljs-string">'/todos'</span>,
  responses: {
    ...createSuccessResponse(createPageSchema(todoSchema)),
    ...createErrorResponses(),
  },
});
</code></pre>
<h2 id="heading-testing">Testing</h2>
<h3 id="heading-running-the-application">Running the Application</h3>
<pre><code class="lang-bash">npm run dev
</code></pre>
<p>Access:</p>
<ul>
<li><p><strong>API</strong>: <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a></p>
</li>
<li><p><strong>Documentation</strong>: <a target="_blank" href="http://localhost:3000/reference"><code>http://localhost:3000/reference</code></a></p>
</li>
<li><p><strong>OpenAPI Spec</strong>: <a target="_blank" href="http://localhost:3000/doc"><code>http://localhost:3000/doc</code></a></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769636645840/a4c07406-87fe-4b04-a27d-91a8156eb72a.png" alt class="image--center mx-auto" /></p>
<p>The <code>@hono/zod-openapi</code> library eliminates the traditional pain points of API development by deriving types, validation, and documentation from a single schema definition. This approach scales from simple APIs to complex enterprise systems while maintaining type safety and developer experience. You can find all the code <a target="_blank" href="https://github.com/raulnq/zod-open-api-hono">here</a>. Thanks, and happy coding</p>
]]></content:encoded></item><item><title><![CDATA[Building a Full-Stack TypeScript Monorepo with React and Hono]]></title><description><![CDATA[This article guides you through creating a full-stack TypeScript monorepo from scratch. By the end, you'll have a React frontend and Hono API backend sharing the same repository with unified tooling for linting, formatting, and commit conventions.
Pr...]]></description><link>https://blog.raulnq.com/building-a-full-stack-typescript-monorepo-with-react-and-hono</link><guid isPermaLink="true">https://blog.raulnq.com/building-a-full-stack-typescript-monorepo-with-react-and-hono</guid><category><![CDATA[Node.js]]></category><category><![CDATA[React]]></category><category><![CDATA[vite]]></category><category><![CDATA[monorepo]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Mon, 19 Jan 2026 16:31:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768837610386/07b553d0-8dfe-4382-94a7-867b229563b1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article guides you through creating a full-stack TypeScript monorepo from scratch. By the end, you'll have a <a target="_blank" href="https://react.dev/">React</a> frontend and <a target="_blank" href="https://hono.dev/">Hono</a> API backend sharing the same repository with unified tooling for linting, formatting, and commit conventions.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, ensure you have installed:</p>
<ul>
<li><p>Node.js 20 or higher</p>
</li>
<li><p>npm 10 or higher</p>
</li>
<li><p>Git</p>
</li>
</ul>
<h2 id="heading-what-is-a-monorepo">What is a Monorepo?</h2>
<p>A <a target="_blank" href="https://monorepo.tools/">monorepo</a> (monolithic repository) is a software development strategy where multiple projects are stored in a single repository. Instead of having separate repositories for your frontend, backend, and shared libraries, everything lives together under one roof.</p>
<h3 id="heading-monorepo-vs-polyrepo">Monorepo vs. Polyrepo</h3>
<p>To understand monorepos, let's compare them with the traditional polyrepo approach:</p>
<p><strong>Polyrepo (Multiple Repositories):</strong></p>
<pre><code class="lang-powershell">github.com/your<span class="hljs-literal">-org</span>/frontend    → React application
github.com/your<span class="hljs-literal">-org</span>/backend     → Hono API
github.com/your<span class="hljs-literal">-org</span>/shared<span class="hljs-literal">-ui</span>   → Component library
github.com/your<span class="hljs-literal">-org</span>/utils       → Shared utilities
</code></pre>
<p><strong>Monorepo (Single Repository):</strong></p>
<pre><code class="lang-powershell">github.com/your<span class="hljs-literal">-org</span>/platform
├── apps/
│   ├── frontend/    → React application
│   └── backend/     → Hono API
└── packages/
    ├── shared<span class="hljs-literal">-ui</span>/   → Component library
    └── utils/       → Shared utilities
</code></pre>
<h3 id="heading-how-monorepos-work">How Monorepos Work</h3>
<p>In a JavaScript/TypeScript monorepo, package managers like npm, Yarn, or pnpm provide workspaces functionality. Workspaces allow you to:</p>
<ol>
<li><p><strong>Link packages locally</strong>: Instead of publishing <code>@your-org/utils</code> to npm and installing it in your frontend, the package manager creates symlinks between workspace packages. Changes are immediately available without publishing.</p>
</li>
<li><p><strong>Hoist shared dependencies</strong>: Common dependencies (like TypeScript or React) are installed once at the root level and shared across all packages, reducing disk space and ensuring version consistency.</p>
</li>
<li><p><strong>Run scripts across packages</strong>: Execute commands like <code>npm run build</code> across all packages or target specific workspaces with flags like <code>-w @your-org/frontend</code>.</p>
</li>
</ol>
<h3 id="heading-monorepo-tools">Monorepo Tools</h3>
<p>While npm workspaces (which we'll use in this guide) provide basic monorepo functionality, specialized tools offer additional features:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Tool</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://www.npmjs.com/"><strong>npm</strong></a><strong>/</strong><a target="_blank" href="https://yarnpkg.com/"><strong>yarn</strong></a><strong>/</strong><a target="_blank" href="https://pnpm.io/es/"><strong>pnpm</strong></a> <strong>workspaces</strong></td><td>Built-in workspace support in package managers</td></tr>
<tr>
<td><a target="_blank" href="https://turborepo.dev/"><strong>Turborepo</strong></a></td><td>A high-performance build system focused on speed</td></tr>
</tbody>
</table>
</div><p>For this guide, we'll use <strong>npm workspaces</strong> as it requires no additional dependencies and covers the essential functionality needed for most projects.</p>
<h2 id="heading-why-a-monorepo">Why a Monorepo?</h2>
<p>Before we begin, let's understand the benefits of a monorepo architecture:</p>
<ul>
<li><p><strong>Shared tooling</strong>: Configure ESLint, Prettier, and TypeScript once for all packages</p>
</li>
<li><p><strong>Atomic commits</strong>: Changes spanning the frontend and backend can be committed together</p>
</li>
<li><p><strong>Simplified dependency management</strong>: Shared dependencies are hoisted to the root</p>
</li>
<li><p><strong>Cross-package imports</strong>: Frontend can import types directly from backend</p>
</li>
<li><p><strong>Unified CI/CD</strong>: A single pipeline handles all packages</p>
</li>
</ul>
<h2 id="heading-step-1-initialize-the-project">Step 1: Initialize the Project</h2>
<p>Start by creating your project directory and initializing git:</p>
<pre><code class="lang-bash">mkdir node-monorepo
<span class="hljs-built_in">cd</span> node-monorepo
git init
</code></pre>
<p>Create the folder structure for our monorepo:</p>
<pre><code class="lang-bash">mkdir -p apps/backend/src
mkdir -p apps/frontend/src
mkdir packages
</code></pre>
<p>We use the <code>apps/packages</code> convention, which is widely adopted in the JavaScript ecosystem:</p>
<ul>
<li><p><code>apps/</code> contains deployable applications (our backend and frontend)</p>
</li>
<li><p><code>packages/</code> is reserved for shared libraries (we'll leave it empty for now)</p>
</li>
</ul>
<h2 id="heading-step-2-create-the-root-package-configuration">Step 2: Create the Root Package Configuration</h2>
<h3 id="heading-21-initialize-packagejson">2.1 Initialize package.json</h3>
<p>Initialize the root package using npm:</p>
<pre><code class="lang-bash">npm init -y
</code></pre>
<p>Now open <code>package.json</code> and replace its contents with:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"node-monorepo"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"private"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"workspaces"</span>: [
    <span class="hljs-string">"apps/*"</span>,
    <span class="hljs-string">"packages/*"</span>
  ],
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"concurrently \"npm:dev:backend\" \"npm:dev:frontend\""</span>,
    <span class="hljs-attr">"dev:backend"</span>: <span class="hljs-string">"npm run dev -w @node-monorepo/backend"</span>,
    <span class="hljs-attr">"dev:frontend"</span>: <span class="hljs-string">"npm run dev -w @node-monorepo/frontend"</span>,
    <span class="hljs-attr">"build:backend"</span>: <span class="hljs-string">"npm run build -w @node-monorepo/backend"</span>,
    <span class="hljs-attr">"build:frontend"</span>: <span class="hljs-string">"npm run build -w @node-monorepo/frontend"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"npm run build:backend &amp;&amp; npm run build:frontend"</span>,
    <span class="hljs-attr">"start:backend"</span>: <span class="hljs-string">"npm run start -w @node-monorepo/backend"</span>,
    <span class="hljs-attr">"preview:frontend"</span>: <span class="hljs-string">"npm run preview -w @node-monorepo/frontend"</span>,
    <span class="hljs-attr">"format"</span>: <span class="hljs-string">"prettier --write ."</span>,
    <span class="hljs-attr">"format:check"</span>: <span class="hljs-string">"prettier --check ."</span>,
    <span class="hljs-attr">"lint"</span>: <span class="hljs-string">"eslint ."</span>,
    <span class="hljs-attr">"lint:fix"</span>: <span class="hljs-string">"eslint . --fix"</span>,
    <span class="hljs-attr">"lint:format"</span>: <span class="hljs-string">"npm run lint:fix &amp;&amp; npm run format"</span>,
    <span class="hljs-attr">"prepare"</span>: <span class="hljs-string">"husky || true"</span>,
    <span class="hljs-attr">"commit"</span>: <span class="hljs-string">"commit"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@commitlint/cli"</span>: <span class="hljs-string">"^20.3.1"</span>,
    <span class="hljs-attr">"@commitlint/config-conventional"</span>: <span class="hljs-string">"^20.3.1"</span>,
    <span class="hljs-attr">"@commitlint/prompt-cli"</span>: <span class="hljs-string">"^20.3.1"</span>,
    <span class="hljs-attr">"@eslint/js"</span>: <span class="hljs-string">"^9.18.0"</span>,
    <span class="hljs-attr">"concurrently"</span>: <span class="hljs-string">"^9.1.2"</span>,
    <span class="hljs-attr">"eslint"</span>: <span class="hljs-string">"^9.18.0"</span>,
    <span class="hljs-attr">"eslint-config-prettier"</span>: <span class="hljs-string">"^10.0.1"</span>,
    <span class="hljs-attr">"eslint-plugin-react-hooks"</span>: <span class="hljs-string">"^7.0.1"</span>,
    <span class="hljs-attr">"eslint-plugin-react-refresh"</span>: <span class="hljs-string">"^0.4.18"</span>,
    <span class="hljs-attr">"globals"</span>: <span class="hljs-string">"^17.0.0"</span>,
    <span class="hljs-attr">"husky"</span>: <span class="hljs-string">"^9.1.7"</span>,
    <span class="hljs-attr">"prettier"</span>: <span class="hljs-string">"^3.4.2"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^5.7.3"</span>,
    <span class="hljs-attr">"typescript-eslint"</span>: <span class="hljs-string">"^8.21.0"</span>
  }
}
</code></pre>
<p>Let's understand the key properties:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Property</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><code>private: true</code></td><td>Prevents accidental publishing to npm</td></tr>
<tr>
<td><code>type: "module"</code></td><td>Enables ES modules throughout the project</td></tr>
<tr>
<td><code>workspaces</code></td><td>Defines npm workspaces—npm will link packages and hoist shared dependencies</td></tr>
</tbody>
</table>
</div><p><strong>About the scripts:</strong></p>
<ul>
<li><p><code>dev</code>: Runs both servers in parallel using <code>concurrently</code>. We can't use <code>npm run dev --workspaces</code> because that runs scripts sequentially, not in parallel.</p>
</li>
<li><p><code>-w @node-monorepo/backend</code>: The <code>-w</code> flag targets a specific workspace by name.</p>
</li>
<li><p><code>prepare</code>: Runs automatically after <code>npm install</code> to set up Husky. The <code>|| true</code> prevents failures in CI environments where git might not be initialized.</p>
</li>
</ul>
<p><strong>About devDependencies:</strong></p>
<p>All shared tooling (<a target="_blank" href="https://eslint.org/">ESLint</a>, <a target="_blank" href="https://prettier.io/">Prettier</a>, TypeScript, <a target="_blank" href="https://typicode.github.io/husky/">Husky</a>, <a target="_blank" href="https://commitlint.js.org/">Commitlint</a>) lives at the root. This ensures:</p>
<ul>
<li><p>Consistent versions across all packages</p>
</li>
<li><p>Single source of truth for configuration</p>
</li>
<li><p>Reduced duplication in <code>node_modules</code></p>
</li>
</ul>
<h3 id="heading-22-create-tsconfigbasejson">2.2 Create tsconfig.base.json</h3>
<p>In a monorepo, TypeScript configurations often share many options. Instead of duplicating them in every workspace, we create a base configuration that all others extend from.</p>
<p>Create <code>tsconfig.base.json</code> in the project root:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ES2023"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"ESNext"</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"verbatimModuleSyntax"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"forceConsistentCasingInFileNames"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noUnusedLocals"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noUnusedParameters"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noFallthroughCasesInSwitch"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noUncheckedSideEffectImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"allowUnreachableCode"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"noErrorTruncation"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noPropertyAccessFromIndexSignature"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"resolveJsonModule"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>
  }
}
</code></pre>
<p><strong>Key options explained:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Option</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><code>strict</code></td><td>Enables all strict type-checking options</td></tr>
<tr>
<td><code>verbatimModuleSyntax</code></td><td>Enforces explicit <code>type</code> imports for better tree-shaking</td></tr>
<tr>
<td><code>noUnusedLocals</code> / <code>noUnusedParameters</code></td><td>Catches unused variables and parameters</td></tr>
<tr>
<td><code>noFallthroughCasesInSwitch</code></td><td>Prevents accidental fallthrough in switch statements</td></tr>
<tr>
<td><code>noPropertyAccessFromIndexSignature</code></td><td>Forces bracket notation for index signatures, making dynamic access explicit</td></tr>
<tr>
<td><code>forceConsistentCasingInFileNames</code></td><td>Prevents issues on case-sensitive file systems</td></tr>
</tbody>
</table>
</div><h3 id="heading-23-create-root-tsconfigjson">2.3 Create Root tsconfig.json</h3>
<p>Now, create <code>tsconfig.json</code> that extends the base configuration. This config is specifically for the root-level config files (ESLint, Prettier, Commitlint):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"extends"</span>: <span class="hljs-string">"./tsconfig.base.json"</span>,
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2023"</span>],
    <span class="hljs-attr">"types"</span>: [<span class="hljs-string">"node"</span>],
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"bundler"</span>,
    <span class="hljs-attr">"allowImportingTsExtensions"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"moduleDetection"</span>: <span class="hljs-string">"force"</span>,
    <span class="hljs-attr">"noEmit"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"eslint.config.ts"</span>, <span class="hljs-string">"commitlint.config.ts"</span>, <span class="hljs-string">"prettier.config.ts"</span>]
}
</code></pre>
<p>By using <code>"extends": "./tsconfig.base.json"</code>, we inherit all the strict options from the base config and only specify what's unique to this context:</p>
<ul>
<li><p><code>lib: ["ES2023"]</code>: Standard library types (no DOM since these run in Node.js)</p>
</li>
<li><p><code>types: ["node"]</code>: Node.js type definitions</p>
</li>
<li><p><code>noEmit: true</code>: We're only type-checking, not compiling</p>
</li>
<li><p><code>strict: true</code>: Enables all strict type-checking options</p>
</li>
</ul>
<h2 id="heading-step-3-create-the-backend-hono-api">Step 3: Create the Backend (Hono API)</h2>
<h3 id="heading-31-initialize-backend-packagejson">3.1 Initialize Backend package.json</h3>
<p>Navigate to the backend directory and initialize the package:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> apps/backend
npm init -y
</code></pre>
<p>Open <code>apps/backend/package.json</code> and replace its contents with:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@node-monorepo/backend"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"private"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"tsx watch src/index.ts"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"tsc"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node dist/index.js"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@hono/node-server"</span>: <span class="hljs-string">"^1.13.8"</span>,
    <span class="hljs-attr">"hono"</span>: <span class="hljs-string">"^4.6.18"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/node"</span>: <span class="hljs-string">"^25.0.9"</span>,
    <span class="hljs-attr">"tsx"</span>: <span class="hljs-string">"^4.19.4"</span>
  }
}
</code></pre>
<p><strong>Key decisions:</strong></p>
<ul>
<li><p><strong>Scoped name</strong> <code>@node-monorepo/backend</code>: Follows <a target="_blank" href="https://docs.npmjs.com/about-scopes">npm's scoped package</a> convention. This prevents naming conflicts and makes workspace references clearer.</p>
</li>
<li><p><code>tsx</code> for development: A TypeScript execution engine that provides fast compilation via esbuild and includes watch mode for automatic reloading.</p>
</li>
<li><p><strong>Hono +</strong> <code>@hono/node-server</code>: Hono is a lightweight, fast web framework. The <code>@hono/node-server</code> adapter allows it to run on Node.js.</p>
</li>
</ul>
<h3 id="heading-32-create-backend-tsconfigjson">3.2 Create Backend tsconfig.json</h3>
<p>Create <code>apps/backend/tsconfig.json</code> that extends the base configuration:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"extends"</span>: <span class="hljs-string">"../../tsconfig.base.json"</span>,
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ESNext"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"NodeNext"</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"NodeNext"</span>,
    <span class="hljs-attr">"sourceMap"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"types"</span>: [<span class="hljs-string">"node"</span>],
    <span class="hljs-attr">"jsx"</span>: <span class="hljs-string">"react-jsx"</span>,
    <span class="hljs-attr">"jsxImportSource"</span>: <span class="hljs-string">"hono/jsx"</span>,
    <span class="hljs-attr">"removeComments"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"./dist"</span>,
    <span class="hljs-attr">"rootDir"</span>: <span class="hljs-string">"./src"</span>,
    <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">"."</span>,
    <span class="hljs-attr">"paths"</span>: {
      <span class="hljs-attr">"#/*"</span>: [<span class="hljs-string">"./src/*"</span>]
    }
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/**/*"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules"</span>, <span class="hljs-string">"dist"</span>]
}
</code></pre>
<p>Notice how much shorter this is compared to a standalone config. By extending <code>../../tsconfig.base.json</code>, we inherit all the strict options and only specify what's unique to the backend.</p>
<p><strong>Important options explained:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Option</td><td>Value</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><code>module</code></td><td><code>"NodeNext"</code></td><td>Proper Node.js ES module support</td></tr>
<tr>
<td><code>moduleResolution</code></td><td><code>"NodeNext"</code></td><td>Matches the module system for correct resolution</td></tr>
<tr>
<td><code>jsx</code></td><td><code>"react-jsx"</code></td><td>Enables JSX support (Hono has its own JSX runtime)</td></tr>
<tr>
<td><code>jsxImportSource</code></td><td><code>"hono/jsx"</code></td><td>Uses Hono's JSX runtime instead of React</td></tr>
<tr>
<td><code>sourceMap</code></td><td><code>true</code></td><td>Enables debugging in VS Code</td></tr>
<tr>
<td><code>paths</code></td><td><code>{"#/*": ["./src/*"]}</code></td><td>Path alias for clean imports</td></tr>
</tbody>
</table>
</div><p><strong>Why</strong> <code>#/</code> for the path alias? We use <code>#/</code> for backend and <code>@/</code> for frontend to create a clear visual distinction. In the backend code, you can write:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { someUtil } <span class="hljs-keyword">from</span> <span class="hljs-string">'#/utils/helper'</span>;
<span class="hljs-comment">// Instead of: import { someUtil } from '../../../utils/helper';</span>
</code></pre>
<h3 id="heading-33-create-the-backend-api">3.3 Create the Backend API</h3>
<p>Create the main entry point at <code>apps/backend/src/index.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { cors } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/cors'</span>;
<span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono();

app.use(
  <span class="hljs-string">'/api/*'</span>,
  cors({
    origin: <span class="hljs-string">'http://localhost:5173'</span>,
  })
);

app.get(<span class="hljs-string">'/api/hello'</span>, <span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span> {
  <span class="hljs-keyword">return</span> c.json({ message: <span class="hljs-string">'Hello World from Hono!'</span> });
});

<span class="hljs-keyword">const</span> port = <span class="hljs-number">3000</span>;
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${port}</span>`</span>);

serve({
  fetch: app.fetch,
  port,
});
</code></pre>
<p>Let's break down this code:</p>
<ol>
<li><p><strong>CORS middleware</strong>: The frontend runs on port 5173 (Vite's default), so we explicitly allow that origin. Without this, the browser would block requests from the frontend to the backend due to the same-origin policy.</p>
</li>
<li><p><code>/api/*</code> prefix: Prefixing all API routes with <code>/api</code> is a common convention that:</p>
<ul>
<li><p>Makes it easy to distinguish API calls from static assets</p>
</li>
<li><p>Simplifies reverse proxy configuration in production</p>
</li>
<li><p>Allows CORS to be applied only to API routes</p>
</li>
</ul>
</li>
<li><p><code>/api/hello</code> endpoint: A simple GET endpoint that returns a JSON message. The <code>c</code> parameter is Hono's context object, which provides request/response utilities.</p>
</li>
<li><p><code>serve()</code> function: The <code>@hono/node-server</code> adapter that starts the HTTP server.</p>
</li>
</ol>
<h2 id="heading-step-4-create-the-frontend-react-vite">Step 4: Create the Frontend (React + Vite)</h2>
<h3 id="heading-41-initialize-frontend-packagejson">4.1 Initialize Frontend package.json</h3>
<p>Navigate to the frontend directory and initialize the package:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> apps/frontend
npm init -y
</code></pre>
<p>Open <code>apps/frontend/package.json</code> and replace its contents with:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@node-monorepo/frontend"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"private"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"vite"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"tsc -b &amp;&amp; vite build"</span>,
    <span class="hljs-attr">"preview"</span>: <span class="hljs-string">"vite preview"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"react"</span>: <span class="hljs-string">"^19.0.0"</span>,
    <span class="hljs-attr">"react-dom"</span>: <span class="hljs-string">"^19.0.0"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/react"</span>: <span class="hljs-string">"^19.0.7"</span>,
    <span class="hljs-attr">"@types/react-dom"</span>: <span class="hljs-string">"^19.0.3"</span>,
    <span class="hljs-attr">"@vitejs/plugin-react"</span>: <span class="hljs-string">"^5.1.2"</span>,
    <span class="hljs-attr">"vite"</span>: <span class="hljs-string">"^7.3.1"</span>
  }
}
</code></pre>
<p><strong>Why separate dependencies from the root?</strong> Runtime dependencies (react, react-dom) are placed in each workspace because:</p>
<ul>
<li><p>They are specific to that application</p>
</li>
<li><p>They will be bundled into the final build</p>
</li>
<li><p>Different apps might need different versions</p>
</li>
</ul>
<h3 id="heading-42-create-frontend-typescript-configuration">4.2 Create Frontend TypeScript Configuration</h3>
<p>The frontend uses a multi-file TypeScript configuration pattern recommended by Vite. This allows separate settings for browser code and Node.js code (like <code>vite.config.ts</code>).</p>
<p>Create <code>apps/frontend/tsconfig.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"files"</span>: [],
  <span class="hljs-attr">"references"</span>: [
    { <span class="hljs-attr">"path"</span>: <span class="hljs-string">"./tsconfig.app.json"</span> },
    { <span class="hljs-attr">"path"</span>: <span class="hljs-string">"./tsconfig.node.json"</span> }
  ]
}
</code></pre>
<p>This file doesn't compile anything—it orchestrates the other configs using <strong>project references</strong>. This enables:</p>
<ul>
<li><p>Faster incremental builds</p>
</li>
<li><p>Separate configurations for browser and Node.js code</p>
</li>
<li><p>Better IDE support</p>
</li>
</ul>
<p>Create <code>apps/frontend/tsconfig.app.json</code> for browser code:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"extends"</span>: <span class="hljs-string">"../../tsconfig.base.json"</span>,
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ES2022"</span>,
    <span class="hljs-attr">"useDefineForClassFields"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2022"</span>, <span class="hljs-string">"DOM"</span>, <span class="hljs-string">"DOM.Iterable"</span>],
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"bundler"</span>,
    <span class="hljs-attr">"allowImportingTsExtensions"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"moduleDetection"</span>: <span class="hljs-string">"force"</span>,
    <span class="hljs-attr">"noEmit"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"jsx"</span>: <span class="hljs-string">"react-jsx"</span>,
    <span class="hljs-attr">"paths"</span>: {
      <span class="hljs-attr">"@/*"</span>: [<span class="hljs-string">"./src/*"</span>],
      <span class="hljs-attr">"#/*"</span>: [<span class="hljs-string">"../backend/src/*"</span>]
    }
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src"</span>]
}
</code></pre>
<p><strong>Key points:</strong></p>
<ul>
<li><p><code>extends</code>: Inherits strict options from the base config</p>
</li>
<li><p><code>lib: ["ES2022", "DOM", "DOM.Iterable"]</code>: Includes browser APIs (DOM)</p>
</li>
<li><p><code>moduleResolution: "bundler"</code>: Optimized for bundlers like Vite</p>
</li>
<li><p><code>paths</code>: Defines two aliases:</p>
<ul>
<li><p><code>@/*</code> for internal frontend imports</p>
</li>
<li><p><code>#/*</code> for importing backend types (cross-workspace)</p>
</li>
</ul>
</li>
</ul>
<p><strong>Cross-workspace type imports:</strong> The <code>#/*</code> path pointing to <code>../backend/src/*</code> allows the frontend to import types directly from the backend:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// In frontend, you could import backend types like this:</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { SomeApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'#/types'</span>;
</code></pre>
<p>Create <code>apps/frontend/tsconfig.node.json</code> for Vite config:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"extends"</span>: <span class="hljs-string">"../../tsconfig.base.json"</span>,
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2023"</span>],
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"bundler"</span>,
    <span class="hljs-attr">"allowImportingTsExtensions"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"moduleDetection"</span>: <span class="hljs-string">"force"</span>,
    <span class="hljs-attr">"noEmit"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"vite.config.ts"</span>]
}
</code></pre>
<p>This config is much smaller because it extends the base. It exists separately because <code>vite.config.ts</code> runs in Node.js, not the browser—notice there's no <code>DOM</code> in the <code>lib</code> array.</p>
<h3 id="heading-43-create-vite-configuration">4.3 Create Vite Configuration</h3>
<p>Create <code>apps/frontend/vite.config.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'vite'</span>;
<span class="hljs-keyword">import</span> react <span class="hljs-keyword">from</span> <span class="hljs-string">'@vitejs/plugin-react'</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      <span class="hljs-string">'@'</span>: path.resolve(__dirname, <span class="hljs-string">'./src'</span>),
      <span class="hljs-string">'#'</span>: path.resolve(__dirname, <span class="hljs-string">'../backend/src'</span>),
    },
  },
});
</code></pre>
<p><strong>Important:</strong> Path aliases must be defined in <strong>both</strong> <code>tsconfig.app.json</code> (for TypeScript type checking) and <code>vite.config.ts</code> (for the bundler's module resolution). TypeScript handles type checking, while Vite handles the actual import resolution during development and build.</p>
<h3 id="heading-44-create-html-entry-point">4.4 Create HTML Entry Point</h3>
<p>Create <code>apps/frontend/index.html</code>:</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!doctype <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Node Monorepo<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"root"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/src/main.tsx"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Vite uses <code>index.html</code> as the entry point. The <code>&lt;script type="module"&gt;</code> tag points to our main TypeScript file.</p>
<h3 id="heading-45-create-react-application">4.5 Create React Application</h3>
<p>Create the main entry file at <code>apps/frontend/src/main.tsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { StrictMode } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-dom/client'</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.tsx'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./index.css'</span>;

createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'root'</span>)!).render(
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">StrictMode</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">App</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">StrictMode</span>&gt;</span></span>
);
</code></pre>
<p><code>StrictMode</code> helps identify potential problems by activating additional checks during development.</p>
<p>Create the App component at <code>apps/frontend/src/App.tsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [message, setMessage] = useState&lt;string&gt;(<span class="hljs-string">'Loading...'</span>);
  <span class="hljs-keyword">const</span> [error, setError] = useState&lt;string | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetch(<span class="hljs-string">'http://localhost:3000/api/hello'</span>)
      .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> {
        <span class="hljs-keyword">if</span> (!response.ok) {
          <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to fetch'</span>);
        }
        <span class="hljs-keyword">return</span> response.json();
      })
      .then(<span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> {
        setMessage(data.message);
      })
      .catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
        setError(err.message);
      });
  }, []);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Node Monorepo<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      {error ? (
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"error"</span>&gt;</span>Error: {error}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      ) : (
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"message"</span>&gt;</span>{message}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      )}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>This component:</p>
<ol>
<li><p>Uses <code>useState</code> to manage the message and error state</p>
</li>
<li><p>Uses <code>useEffect</code> to fetch data from the backend when the component mounts</p>
</li>
<li><p>Displays either the message or an error</p>
</li>
</ol>
<p>Create the styles at <code>apps/frontend/src/index.css</code>:</p>
<pre><code class="lang-css">* {
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">box-sizing</span>: border-box;
}

<span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">font-family</span>:
    -apple-system, BlinkMacSystemFont, <span class="hljs-string">'Segoe UI'</span>, Roboto, Oxygen, Ubuntu,
    sans-serif;
  <span class="hljs-attribute">min-height</span>: <span class="hljs-number">100vh</span>;
  <span class="hljs-attribute">display</span>: flex;
  <span class="hljs-attribute">align-items</span>: center;
  <span class="hljs-attribute">justify-content</span>: center;
  <span class="hljs-attribute">background</span>: <span class="hljs-built_in">linear-gradient</span>(<span class="hljs-number">135deg</span>, #<span class="hljs-number">667</span>eea <span class="hljs-number">0%</span>, #<span class="hljs-number">764</span>ba2 <span class="hljs-number">100%</span>);
}

<span class="hljs-selector-class">.container</span> {
  <span class="hljs-attribute">text-align</span>: center;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">2rem</span>;
  <span class="hljs-attribute">background</span>: white;
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">1rem</span>;
  <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">10px</span> <span class="hljs-number">40px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.2</span>);
}

<span class="hljs-selector-tag">h1</span> {
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#333</span>;
  <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">1rem</span>;
}

<span class="hljs-selector-class">.message</span> {
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.25rem</span>;
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#667eea</span>;
}

<span class="hljs-selector-class">.error</span> {
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#e53e3e</span>;
}
</code></pre>
<h2 id="heading-step-5-configure-eslint">Step 5: Configure ESLint</h2>
<p>ESLint 9 introduced the flat config format, replacing the legacy <code>.eslintrc</code> files. Create <code>eslint.config.ts</code> at the project root:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> jseslint <span class="hljs-keyword">from</span> <span class="hljs-string">'@eslint/js'</span>;
<span class="hljs-keyword">import</span> tseslint <span class="hljs-keyword">from</span> <span class="hljs-string">'typescript-eslint'</span>;
<span class="hljs-keyword">import</span> { defineConfig, globalIgnores } <span class="hljs-keyword">from</span> <span class="hljs-string">'eslint/config'</span>;
<span class="hljs-keyword">import</span> prettierConfig <span class="hljs-keyword">from</span> <span class="hljs-string">'eslint-config-prettier'</span>;
<span class="hljs-keyword">import</span> reactHooks <span class="hljs-keyword">from</span> <span class="hljs-string">'eslint-plugin-react-hooks'</span>;
<span class="hljs-keyword">import</span> reactRefresh <span class="hljs-keyword">from</span> <span class="hljs-string">'eslint-plugin-react-refresh'</span>;
<span class="hljs-keyword">import</span> globals <span class="hljs-keyword">from</span> <span class="hljs-string">'globals'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig(
  globalIgnores([<span class="hljs-string">'**/dist/**/*'</span>, <span class="hljs-string">'**/node_modules/**/*'</span>, <span class="hljs-string">'**/*.tsbuildinfo'</span>]),
  jseslint.configs.recommended,
  tseslint.configs.recommended,
  prettierConfig,
  {
    files: [<span class="hljs-string">'eslint.config.ts'</span>, <span class="hljs-string">'commitlint.config.ts'</span>, <span class="hljs-string">'prettier.config.ts'</span>],
    languageOptions: {
      globals: globals.node,
      parserOptions: {
        tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
        project: <span class="hljs-string">'./tsconfig.json'</span>,
      },
    },
  },
  {
    files: [<span class="hljs-string">'apps/backend/**/*.{ts,tsx}'</span>],
    languageOptions: {
      globals: globals.node,
      parserOptions: {
        tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
        project: <span class="hljs-string">'./apps/backend/tsconfig.json'</span>,
      },
    },
  },
  {
    files: [<span class="hljs-string">'apps/frontend/src/**/*.{ts,tsx}'</span>],
    <span class="hljs-keyword">extends</span>: [reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
    languageOptions: {
      globals: globals.browser,
      parserOptions: {
        tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
        project: <span class="hljs-string">'./apps/frontend/tsconfig.app.json'</span>,
      },
    },
  },
  {
    files: [<span class="hljs-string">'apps/frontend/vite.config.ts'</span>],
    languageOptions: {
      globals: globals.node,
      parserOptions: {
        tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
        project: <span class="hljs-string">'./apps/frontend/tsconfig.node.json'</span>,
      },
    },
  }
);
</code></pre>
<p>Let's understand each part:</p>
<h3 id="heading-51-global-ignores">5.1 Global Ignores</h3>
<pre><code class="lang-typescript">globalIgnores([<span class="hljs-string">'**/dist/**/*'</span>, <span class="hljs-string">'**/node_modules/**/*'</span>, <span class="hljs-string">'**/*.tsbuildinfo'</span>]),
</code></pre>
<p>This replaces the old <code>.eslintignore</code> file. We ignore:</p>
<ul>
<li><p><code>dist/</code>: Build output directories</p>
</li>
<li><p><code>node_modules/</code>: Dependencies</p>
</li>
<li><p><code>*.tsbuildinfo</code>: TypeScript incremental compilation cache</p>
</li>
</ul>
<h3 id="heading-52-base-configurations">5.2 Base Configurations</h3>
<pre><code class="lang-typescript">jseslint.configs.recommended,
tseslint.configs.recommended,
prettierConfig,
</code></pre>
<p>These apply to all files:</p>
<ul>
<li><p><code>jseslint.configs.recommended</code>: ESLint's recommended JavaScript rules</p>
</li>
<li><p><code>tseslint.configs.recommended</code>: TypeScript-specific rules</p>
</li>
<li><p><code>prettierConfig</code>: <strong>Must come after</strong> other configs to disable rules that conflict with Prettier</p>
</li>
</ul>
<h3 id="heading-53-file-specific-configurations">5.3 File-Specific Configurations</h3>
<p>Each block targets specific files with appropriate settings:</p>
<p><strong>Root config files:</strong></p>
<pre><code class="lang-typescript">{
  files: [<span class="hljs-string">'eslint.config.ts'</span>, <span class="hljs-string">'commitlint.config.ts'</span>, <span class="hljs-string">'prettier.config.ts'</span>],
  languageOptions: {
    globals: globals.node,  <span class="hljs-comment">// Node.js globals (process, __dirname, etc.)</span>
    parserOptions: {
      tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
      project: <span class="hljs-string">'./tsconfig.json'</span>,  <span class="hljs-comment">// Points to root tsconfig</span>
    },
  },
},
</code></pre>
<p><strong>Backend files:</strong></p>
<pre><code class="lang-typescript">{
  files: [<span class="hljs-string">'apps/backend/**/*.{ts,tsx}'</span>],
  languageOptions: {
    globals: globals.node,
    parserOptions: {
      tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
      project: <span class="hljs-string">'./apps/backend/tsconfig.json'</span>,
    },
  },
},
</code></pre>
<p><strong>Frontend source files:</strong></p>
<pre><code class="lang-typescript">{
  files: [<span class="hljs-string">'apps/frontend/src/**/*.{ts,tsx}'</span>],
  <span class="hljs-keyword">extends</span>: [reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
  languageOptions: {
    globals: globals.browser,  <span class="hljs-comment">// Browser globals (window, document, etc.)</span>
    parserOptions: {
      tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
      project: <span class="hljs-string">'./apps/frontend/tsconfig.app.json'</span>,
    },
  },
},
</code></pre>
<p>Note the React-specific plugins:</p>
<ul>
<li><p><code>reactHooks.configs.flat.recommended</code>: Enforces Rules of Hooks</p>
</li>
<li><p><code>reactRefresh.configs.vite</code>: Ensures components are compatible with hot module replacement</p>
</li>
</ul>
<p><strong>Vite config file:</strong></p>
<pre><code class="lang-typescript">{
  files: [<span class="hljs-string">'apps/frontend/vite.config.ts'</span>],
  languageOptions: {
    globals: globals.node,  <span class="hljs-comment">// Vite config runs in Node.js</span>
    parserOptions: {
      tsconfigRootDir: <span class="hljs-keyword">import</span>.meta.dirname,
      project: <span class="hljs-string">'./apps/frontend/tsconfig.node.json'</span>,
    },
  },
},
</code></pre>
<p><strong>Why specify a</strong> <code>project</code> for each file pattern? TypeScript-ESLint can provide type-aware linting when it knows which <code>tsconfig.json</code> applies to each file. This enables catching more errors like unused variables that TypeScript alone might not flag.</p>
<p><strong>Important:</strong> You must use <code>eslint-plugin-react-hooks</code> version 7 or higher. Earlier versions don't export a flat config (<code>configs.flat.recommended</code> is only available in v7+).</p>
<h2 id="heading-step-6-configure-prettier">Step 6: Configure Prettier</h2>
<p>Create <code>prettier.config.ts</code> at the project root:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Config } <span class="hljs-keyword">from</span> <span class="hljs-string">'prettier'</span>;

<span class="hljs-keyword">const</span> config: Config = {
  trailingComma: <span class="hljs-string">'es5'</span>,
  singleQuote: <span class="hljs-literal">true</span>,
  arrowParens: <span class="hljs-string">'avoid'</span>,
  endOfLine: <span class="hljs-string">'crlf'</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> config;
</code></pre>
<p><strong>Options explained:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Option</td><td>Value</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><code>trailingComma</code></td><td><code>'es5'</code></td><td>Adds trailing commas in objects and arrays. Creates cleaner git diffs when adding items.</td></tr>
<tr>
<td><code>singleQuote</code></td><td><code>true</code></td><td>Uses single quotes for strings (common JavaScript convention)</td></tr>
<tr>
<td><code>arrowParens</code></td><td><code>'avoid'</code></td><td>Omits parentheses for single-parameter arrow functions: <code>x =&gt; x</code> instead of <code>(x) =&gt; x</code></td></tr>
<tr>
<td><code>endOfLine</code></td><td><code>'crlf'</code></td><td>Windows line endings. Use <code>'lf'</code> for Unix/macOS teams.</td></tr>
</tbody>
</table>
</div><p>Create <code>.prettierignore</code> at the project root to exclude generated files:</p>
<pre><code class="lang-powershell">**/dist/
**/*.tsbuildinfo
**/package<span class="hljs-literal">-lock</span>.json
</code></pre>
<p><strong>Why ignore these?</strong></p>
<ul>
<li><p><code>dist/</code>: Generated build output—formatting would be overwritten on next build</p>
</li>
<li><p><code>*.tsbuildinfo</code>: Binary cache files</p>
</li>
<li><p><code>package-lock.json</code>: Auto-generated by npm, formatting changes create noise in git history</p>
</li>
</ul>
<h2 id="heading-step-7-configure-commitlint-and-husky">Step 7: Configure Commitlint and Husky</h2>
<h3 id="heading-71-create-commitlint-configuration">7.1 Create Commitlint Configuration</h3>
<p>Commitlint ensures all commit messages follow a consistent format. Create <code>commitlint.config.ts</code> at the project root:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { UserConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'@commitlint/types'</span>;

<span class="hljs-keyword">const</span> config: UserConfig = {
  <span class="hljs-keyword">extends</span>: [<span class="hljs-string">'@commitlint/config-conventional'</span>],
  rules: {
    <span class="hljs-string">'scope-enum'</span>: [<span class="hljs-number">2</span>, <span class="hljs-string">'always'</span>, [<span class="hljs-string">'backend'</span>, <span class="hljs-string">'frontend'</span>, <span class="hljs-string">'repo'</span>]],
    <span class="hljs-string">'subject-case'</span>: [<span class="hljs-number">2</span>, <span class="hljs-string">'always'</span>, [<span class="hljs-string">'sentence-case'</span>, <span class="hljs-string">'lower-case'</span>]],
  },
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> config;
</code></pre>
<p><strong>Understanding rule format:</strong> <code>[level, applicable, value]</code></p>
<ul>
<li><p><strong>Level</strong>: <code>0</code> = disabled, <code>1</code> = warning, <code>2</code> = error</p>
</li>
<li><p><strong>Applicable</strong>: <code>'always'</code> (must match) or <code>'never'</code> (must not match)</p>
</li>
<li><p><strong>Value</strong>: The rule configuration</p>
</li>
</ul>
<p><strong>Our rules:</strong></p>
<p><code>scope-enum</code>: Restricts commit scopes to predefined values:</p>
<pre><code class="lang-powershell">feat(backend): Add user authentication  ✓
fix(frontend): Resolve button styling   ✓
chore(repo): Update dependencies        ✓
feat(api): Add endpoint                 ✗ (<span class="hljs-string">'api'</span> not <span class="hljs-keyword">in</span> allowed scopes)
</code></pre>
<p><code>subject-case</code>: Allows either sentence case or lowercase:</p>
<pre><code class="lang-powershell">feat: Add new feature     ✓
feat: add new feature     ✓
feat: ADD NEW FEATURE     ✗
</code></pre>
<h3 id="heading-72-conventional-commit-format">7.2 Conventional Commit Format</h3>
<p>The conventional commit format is:</p>
<pre><code class="lang-powershell">&lt;<span class="hljs-built_in">type</span>&gt;(&lt;scope&gt;): &lt;subject&gt;

[<span class="hljs-type">optional</span> <span class="hljs-type">body</span>]

[<span class="hljs-type">optional</span> <span class="hljs-type">footer</span>]
</code></pre>
<p><strong>Common types:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Type</td><td>When to use</td></tr>
</thead>
<tbody>
<tr>
<td><code>feat</code></td><td>New feature</td></tr>
<tr>
<td><code>fix</code></td><td>Bug fix</td></tr>
<tr>
<td><code>docs</code></td><td>Documentation changes</td></tr>
<tr>
<td><code>style</code></td><td>Code style changes (formatting, semicolons)</td></tr>
<tr>
<td><code>refactor</code></td><td>Code changes that neither fix bugs nor add features</td></tr>
<tr>
<td><code>test</code></td><td>Adding or modifying tests</td></tr>
<tr>
<td><code>chore</code></td><td>Maintenance tasks (dependencies, build config)</td></tr>
</tbody>
</table>
</div><h3 id="heading-73-set-up-husky">7.3 Set Up Husky</h3>
<p>Create the <code>.husky</code> directory and hook files:</p>
<pre><code class="lang-bash">mkdir -p .husky
</code></pre>
<p>Create <code>.husky/pre-commit</code> with the following content:</p>
<pre><code class="lang-bash">npm run lint
npm run format:check
</code></pre>
<p>This runs ESLint and checks Prettier formatting before each commit. If either fails, the commit is aborted, ensuring only properly formatted and linted code enters the repository.</p>
<p>Create <code>.husky/commit-msg</code> with the following content:</p>
<pre><code class="lang-bash">npx commitlint --edit <span class="hljs-variable">$1</span>
</code></pre>
<p>This validates the commit message against your commitlint rules. The <code>$1</code> argument is the path to the temporary file containing the commit message.</p>
<p>Make the hooks executable (Unix/macOS):</p>
<pre><code class="lang-bash">chmod +x .husky/pre-commit
chmod +x .husky/commit-msg
</code></pre>
<h2 id="heading-step-8-install-dependencies-and-test">Step 8: Install Dependencies and Test</h2>
<h3 id="heading-81-install-all-dependencies">8.1 Install All Dependencies</h3>
<p>From the project root, run:</p>
<pre><code class="lang-bash">npm install
</code></pre>
<p>npm workspaces will:</p>
<ol>
<li><p>Install root devDependencies</p>
</li>
<li><p>Install each workspace's dependencies</p>
</li>
<li><p>Hoist shared dependencies to the root <code>node_modules</code></p>
</li>
<li><p>Create symlinks for workspace packages</p>
</li>
<li><p>Run the <code>prepare</code> script, initializing Husky</p>
</li>
</ol>
<h3 id="heading-82-verify-the-setup">8.2 Verify the Setup</h3>
<p>Run ESLint to check for errors:</p>
<pre><code class="lang-bash">npm run lint
</code></pre>
<p>Check Prettier formatting:</p>
<pre><code class="lang-bash">npm run format:check
</code></pre>
<p>If there are formatting issues, fix them:</p>
<pre><code class="lang-bash">npm run format
</code></pre>
<h3 id="heading-83-start-the-development-servers">8.3 Start the Development Servers</h3>
<pre><code class="lang-bash">npm run dev
</code></pre>
<p>This runs both servers concurrently:</p>
<ul>
<li><p>Backend at <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a></p>
</li>
<li><p>Frontend at <a target="_blank" href="http://localhost:5173"><code>http://localhost:5173</code></a></p>
</li>
</ul>
<p>Open your browser to <a target="_blank" href="http://localhost:5173"><code>http://localhost:5173</code></a>. You should see the "Node Monorepo" heading with the message "Hello World from Hono!" fetched from the backend.</p>
<h3 id="heading-84-test-the-build">8.4 Test the Build</h3>
<pre><code class="lang-bash">npm run build
</code></pre>
<p>This builds both the backend (TypeScript compilation) and frontend (Vite production build).</p>
<h3 id="heading-85-test-commit-hooks">8.5 Test Commit Hooks</h3>
<p>Try making a commit to verify the hooks work:</p>
<pre><code class="lang-bash">git add .
git commit -m <span class="hljs-string">"feat(repo): Initial monorepo setup"</span>
</code></pre>
<p>The pre-commit hook will run lint and format checks. The commit-msg hook will validate your commit message format.</p>
<h2 id="heading-final-project-structure">Final Project Structure</h2>
<pre><code class="lang-powershell">node<span class="hljs-literal">-monorepo</span>/
├── apps/
│   ├── backend/
│   │   ├── src/
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json          <span class="hljs-comment"># Extends tsconfig.base.json</span>
│   └── frontend/
│       ├── src/
│       │   ├── App.tsx
│       │   ├── main.tsx
│       │   └── index.css
│       ├── index.html
│       ├── package.json
│       ├── tsconfig.json          <span class="hljs-comment"># Project references</span>
│       ├── tsconfig.app.json      <span class="hljs-comment"># Extends tsconfig.base.json</span>
│       ├── tsconfig.node.json     <span class="hljs-comment"># Extends tsconfig.base.json</span>
│       └── vite.config.ts
├── packages/
├── .husky/
│   ├── pre<span class="hljs-literal">-commit</span>
│   └── commit<span class="hljs-literal">-msg</span>
├── package.json
├── tsconfig.base.json             <span class="hljs-comment"># Shared TypeScript options</span>
├── tsconfig.json                  <span class="hljs-comment"># Extends tsconfig.base.json</span>
├── eslint.config.ts
├── prettier.config.ts
├── commitlint.config.ts
└── .prettierignore
</code></pre>
<p>This template provides a solid foundation for building full-stack TypeScript applications with modern tooling and best practices. You can find all the code <a target="_blank" href="https://github.com/raulnq/node-monorepo">here</a>. Thanks, and happy coding</p>
]]></content:encoded></item><item><title><![CDATA[Hono: Testing]]></title><description><![CDATA[Testing is a critical aspect of building reliable APIs. When working with Hono, a lightweight and fast web framework, we can leverage its built-in testing utilities alongside Node.js's native test runner to create comprehensive integration tests. Thi...]]></description><link>https://blog.raulnq.com/hono-testing</link><guid isPermaLink="true">https://blog.raulnq.com/hono-testing</guid><category><![CDATA[hono]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Tue, 06 Jan 2026 14:00:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767065534104/d81143a5-b89a-4f99-a6e8-a5ee1480e3d5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Testing is a critical aspect of building reliable APIs. When working with Hono, a lightweight and fast web framework, we can leverage its built-in testing utilities alongside Node.js's native test runner to create comprehensive integration tests. This article walks us through building a robust test suite for a REST API using a Domain-Specific Language (DSL) approach that makes tests readable, maintainable, and expressive.</p>
<p>We'll use a price tracker application as our example, a system that manages stores, products, and price histories. The starting code is available at <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/drizzle">https://github.com/raulnq/price-tracker/tree/drizzle</a>.</p>
<p>By the end of this guide, we'll have built:</p>
<ul>
<li><p>A complete test infrastructure with setup and teardown.</p>
</li>
<li><p>Reusable DSL functions for all API operations.</p>
</li>
<li><p>Custom assertion helpers for fluent testing.</p>
</li>
<li><p>Comprehensive test suites for stores.</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, ensure we have:</p>
<ul>
<li><p>Node.js 24+ installed.</p>
</li>
<li><p>The price-tracker project cloned from the repository.</p>
</li>
<li><p>Docker Desktop installed.</p>
</li>
<li><p>A PostgreSQL database running (use <code>npm run database: up</code>)</p>
</li>
<li><p>Dependencies installed (<code>npm install</code>).</p>
</li>
</ul>
<h2 id="heading-step-1-install-testing-dependencies">Step 1: Install Testing Dependencies</h2>
<p>First, let's add the testing library we need. We'll use <code>@faker-js/faker</code> for generating unique test data:</p>
<pre><code class="lang-bash">npm install --save-dev @faker-js/faker
</code></pre>
<p><a target="_blank" href="https://github.com/faker-js/faker">Faker</a> is a JavaScript library that generates massive amounts of fake but realistic data. It's essential for testing because:</p>
<ul>
<li><p><strong>Unique identifiers</strong>: Prevents test collisions when tests share a database.</p>
</li>
<li><p><strong>Realistic data</strong>: Makes tests more representative of real-world scenarios.</p>
</li>
<li><p><strong>Random variations</strong>: Helps uncover edge cases through varied input.</p>
</li>
</ul>
<p>Common methods we'll use:</p>
<pre><code class="lang-typescript">faker.string.uuid()     <span class="hljs-comment">// '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'</span>
faker.number.float({ min: <span class="hljs-number">1</span>, max: <span class="hljs-number">1000</span>, fractionDigits: <span class="hljs-number">2</span> })  <span class="hljs-comment">// 123.45</span>
</code></pre>
<h2 id="heading-step-2-configure-the-test-script">Step 2: Configure the Test Script</h2>
<p>Update <code>package.json</code> to add the test script:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"cross-env NODE_ENV=test tsx --import ./tests/setup.ts --test ./tests/**/*.test.ts"</span>
  }
}
</code></pre>
<p>This configuration:</p>
<ul>
<li><p><code>cross-env NODE_ENV=test</code>: Sets the environment to test mode for different configurations (like a test database).</p>
</li>
<li><p><code>tsx</code>: Enables TypeScript execution directly without pre-compilation.</p>
</li>
<li><p><code>--import ./tests/setup.ts</code>: Imports the setup file before running tests.</p>
</li>
<li><p><code>--test ./tests/**/*.test.ts</code>: Uses Node.js's native test runner with glob patterns.</p>
</li>
</ul>
<h2 id="heading-step-3-create-the-test-directory-structure">Step 3: Create the Test Directory Structure</h2>
<p>Create the following folder structure:</p>
<pre><code class="lang-powershell">tests/
├── setup.ts
├── utils.ts
├── errors.ts
├── assertions.ts
├── stores/
│   └── stores<span class="hljs-literal">-dsl</span>.ts
└── products/
    └── products<span class="hljs-literal">-dsl</span>.ts
</code></pre>
<h2 id="heading-step-4-create-the-global-test-setup">Step 4: Create the Global Test Setup</h2>
<p>The setup file handles global test lifecycle management. Create <code>tests/setup.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { after } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> { client } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/database/client.js'</span>;

after(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> client.$client.end();
});
</code></pre>
<p>This ensures that after all tests complete, the database connection pool is properly closed. The <code>after</code> hook from Node's test runner executes once after all tests finish, preventing hanging connections.</p>
<h2 id="heading-step-5-create-utility-functions">Step 5: Create Utility Functions</h2>
<p>Create <code>tests/utils.ts</code> with a helper for handling JSON date serialization:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> parseDatesFromJSON = &lt;T&gt;(
  <span class="hljs-comment">// eslint-disable-next-line @typescript-eslint/no-explicit-any</span>
  json: <span class="hljs-built_in">any</span>,
  dateFields: (keyof T)[]
): <span class="hljs-function"><span class="hljs-params">T</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> result = { ...json };
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> field <span class="hljs-keyword">of</span> dateFields) {
    <span class="hljs-keyword">if</span> (result[field]) {
      result[field] = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(result[field]);
    }
  }
  <span class="hljs-keyword">return</span> result <span class="hljs-keyword">as</span> T;
};
</code></pre>
<p>When JSON responses come from an API, dates are serialized as ISO strings. This utility converts specified fields back to JavaScript <code>Date</code> objects, enabling proper date comparisons in assertions.</p>
<h2 id="heading-step-6-create-error-helper-functions">Step 6: Create Error Helper Functions</h2>
<p>Create <code>tests/errors.ts</code> with factory functions for creating expected error responses:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> emptyText = <span class="hljs-string">''</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> bigText = (length: <span class="hljs-built_in">number</span> = <span class="hljs-number">256</span>): <span class="hljs-function"><span class="hljs-params">string</span> =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-string">'a'</span>.repeat(length);
};

<span class="hljs-keyword">const</span> tooBigString = (maxLength: <span class="hljs-built_in">number</span>): <span class="hljs-function"><span class="hljs-params">string</span> =&gt;</span>
  <span class="hljs-string">`Too big: expected string to have &lt;=<span class="hljs-subst">${maxLength}</span> characters`</span>;

<span class="hljs-keyword">const</span> tooSmallString = (minLength: <span class="hljs-built_in">number</span>): <span class="hljs-function"><span class="hljs-params">string</span> =&gt;</span>
  <span class="hljs-string">`Too small: expected string to have &gt;=<span class="hljs-subst">${minLength}</span> characters`</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> ValidationError = {
  path: <span class="hljs-built_in">string</span>;
  message: <span class="hljs-built_in">string</span>;
  code: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createValidationError = (
  errors: ValidationError[]
): <span class="hljs-function"><span class="hljs-params">ProblemDocument</span> =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ProblemDocument(
    {
      detail: <span class="hljs-string">'The request contains invalid data'</span>,
      status: StatusCodes.BAD_REQUEST,
    },
    { errors }
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> validationError = {
  tooSmall: (path: <span class="hljs-built_in">string</span>, minLength: <span class="hljs-built_in">number</span>): <span class="hljs-function"><span class="hljs-params">ValidationError</span> =&gt;</span> ({
    path,
    message: tooSmallString(minLength),
    code: <span class="hljs-string">'too_small'</span>,
  }),
  tooBig: (path: <span class="hljs-built_in">string</span>, maxLength: <span class="hljs-built_in">number</span>): <span class="hljs-function"><span class="hljs-params">ValidationError</span> =&gt;</span> ({
    path,
    message: tooBigString(maxLength),
    code: <span class="hljs-string">'too_big'</span>,
  }),
  requiredString: (path: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">ValidationError</span> =&gt;</span> ({
    path,
    message: <span class="hljs-string">'Invalid input: expected string, received undefined'</span>,
    code: <span class="hljs-string">'invalid_type'</span>,
  }),
  invalidUrl: (path: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">ValidationError</span> =&gt;</span> ({
    path,
    message: <span class="hljs-string">'Invalid URL'</span>,
    code: <span class="hljs-string">'invalid_format'</span>,
  }),
  invalidUuid: (path: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">ValidationError</span> =&gt;</span> ({
    path,
    message: <span class="hljs-string">'Invalid UUID'</span>,
    code: <span class="hljs-string">'invalid_format'</span>,
  }),
  notPositive: (path: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">ValidationError</span> =&gt;</span> ({
    path,
    message: <span class="hljs-string">'Too small: expected number to be &gt;0'</span>,
    code: <span class="hljs-string">'too_small'</span>,
  }),
  requiredNumber: (path: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">ValidationError</span> =&gt;</span> ({
    path,
    message: <span class="hljs-string">'Invalid input: expected number, received undefined'</span>,
    code: <span class="hljs-string">'invalid_type'</span>,
  }),
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createNotFoundError = (detail: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">ProblemDocument</span> =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ProblemDocument({
    detail,
    status: StatusCodes.NOT_FOUND,
  });
};
</code></pre>
<p>Let's break down what each part does:</p>
<ul>
<li><p><code>emptyText</code> and <code>bigText()</code>: Generate test data for boundary validation (empty strings and strings exceeding maximum length).</p>
</li>
<li><p><code>ValidationError</code> type: Matches the structure returned by Zod validation errors.</p>
</li>
<li><p><code>createValidationError()</code>: Constructs a <code>ProblemDocument</code> (RFC 7807 standard for HTTP API errors) with validation errors.</p>
</li>
<li><p><code>validationError</code> object: Factory methods for each validation error type, ensuring test expectations match actual API messages.</p>
</li>
<li><p><code>createNotFoundError()</code>: Creates expected 404 responses for <code>resource-not-found</code> scenarios.</p>
</li>
</ul>
<h2 id="heading-step-7-create-custom-assertion-helpers">Step 7: Create Custom Assertion Helpers</h2>
<p>Create <code>tests/assertions.ts</code> with fluent assertion builders:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> assert <span class="hljs-keyword">from</span> <span class="hljs-string">'node:assert'</span>;
<span class="hljs-keyword">import</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Page } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/pagination.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> assertPage = &lt;TResult&gt;<span class="hljs-function">(<span class="hljs-params">page: Page&lt;TResult&gt;</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> {
    hasItemsCountAtLeast(expected: <span class="hljs-built_in">number</span>) {
      assert.ok(page);
      assert.ok(
        page.items.length &gt;= expected,
        <span class="hljs-string">`Expected at least <span class="hljs-subst">${expected}</span> items, got <span class="hljs-subst">${page.items.length}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>;
    },
    hasItemsCount(expected: <span class="hljs-built_in">number</span>) {
      assert.ok(page);
      assert.strictEqual(
        page.items.length,
        expected,
        <span class="hljs-string">`Expected <span class="hljs-subst">${expected}</span> items, got <span class="hljs-subst">${page.items.length}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>;
    },
    hasTotalCount(expected: <span class="hljs-built_in">number</span>) {
      assert.ok(page);
      assert.strictEqual(
        page.totalCount,
        expected,
        <span class="hljs-string">`Expected totalCount to be <span class="hljs-subst">${expected}</span>, got <span class="hljs-subst">${page.totalCount}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>;
    },
    hasTotalPages(expected: <span class="hljs-built_in">number</span>) {
      assert.ok(page);
      assert.strictEqual(
        page.totalPages,
        expected,
        <span class="hljs-string">`Expected totalPages to be <span class="hljs-subst">${expected}</span>, got <span class="hljs-subst">${page.totalPages}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>;
    },
    hasEmptyResult() {
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.hasItemsCount(<span class="hljs-number">0</span>).hasTotalCount(<span class="hljs-number">0</span>).hasTotalPages(<span class="hljs-number">0</span>);
    },
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> assertStrictEqualProblemDocument = (
  actual: ProblemDocument,
  expected: ProblemDocument
): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
  assert.strictEqual(actual.status, expected.status);
  assert.strictEqual(actual.detail, expected.detail);
  <span class="hljs-keyword">if</span> (<span class="hljs-string">'errors'</span> <span class="hljs-keyword">in</span> actual &amp;&amp; <span class="hljs-string">'errors'</span> <span class="hljs-keyword">in</span> expected) {
    assert.deepStrictEqual(actual[<span class="hljs-string">'errors'</span>], expected[<span class="hljs-string">'errors'</span>]);
  }
};
</code></pre>
<p>The fluent interface pattern enables chainable assertions that read like natural language:</p>
<pre><code class="lang-typescript">assertPage(page)
  .hasItemsCount(<span class="hljs-number">10</span>)
  .hasTotalCount(<span class="hljs-number">50</span>)
  .hasTotalPages(<span class="hljs-number">5</span>);
</code></pre>
<p><strong>Key Benefits</strong>:</p>
<ul>
<li><p><strong>Readability</strong>: Assertions read like specifications</p>
</li>
<li><p><strong>Chainability</strong>: Multiple assertions in one statement</p>
</li>
<li><p><strong>Custom messages</strong>: Clear failure messages aid debugging</p>
</li>
</ul>
<h2 id="heading-step-8-understanding-honos-testclient">Step 8: Understanding Hono's testClient</h2>
<p>Before building the DSL, let's understand the core testing utility we'll use. <a target="_blank" href="https://hono.dev/docs/helpers/testing">Hono</a> provides a built-in <code>testClient</code> function from <code>hono/testing</code> that wraps our routes and provides a type-safe interface to make requests without spinning up an actual HTTP server.</p>
<h3 id="heading-key-features">Key Features</h3>
<ol>
<li><p><strong>No Server Required</strong>: Executes requests in-memory, making tests faster and more reliable</p>
</li>
<li><p><strong>Full Type Safety</strong>: The client is automatically typed based on our route definitions:</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// TypeScript knows exactly what parameters this endpoint accepts</span>
 <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.stores.$post({
   json: { name: <span class="hljs-string">'Walmart'</span>, url: <span class="hljs-string">'https://walmart.com'</span> }
 });
</code></pre>
</li>
<li><p><strong>Route-Aware API</strong>: The client mirrors our route structure:</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// GET /stores/:storeId</span>
 <span class="hljs-keyword">await</span> client.stores[<span class="hljs-string">':storeId'</span>].$get({
   param: { storeId: <span class="hljs-string">'123'</span> }
 });

 <span class="hljs-comment">// POST /products/:productId/prices</span>
 <span class="hljs-keyword">await</span> client.products[<span class="hljs-string">':productId'</span>].prices.$post({
   param: { productId: <span class="hljs-string">'456'</span> },
   json: { price: <span class="hljs-number">99.99</span> }
 });
</code></pre>
</li>
<li><p><strong>Query Parameter Support</strong>:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">await</span> client.stores.$get({
   query: { pageNumber: <span class="hljs-string">'1'</span>, pageSize: <span class="hljs-string">'10'</span>, name: <span class="hljs-string">'walmart'</span> }
 });
</code></pre>
</li>
</ol>
<h3 id="heading-why-testclient-over-supertest-or-fetch">Why testClient Over Supertest or Fetch?</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td>testClient</td><td>Supertest/Fetch</td></tr>
</thead>
<tbody>
<tr>
<td>Requires running server</td><td>No</td><td>Yes</td></tr>
<tr>
<td>Type-safe requests</td><td>Yes</td><td>No</td></tr>
<tr>
<td>Route autocompletion</td><td>Yes</td><td>No</td></tr>
<tr>
<td>Network overhead</td><td>None</td><td>Present</td></tr>
</tbody>
</table>
</div><h2 id="heading-step-9-build-the-stores-dsl">Step 9: Build the Stores DSL</h2>
<p>Now let's build the Domain-Specific Language for store operations. Create <code>tests/stores/stores-dsl.ts</code>:</p>
<h3 id="heading-part-1-imports-and-test-data-factories">Part 1: Imports and Test Data Factories</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { testClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/testing'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> AddStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/add-store.js'</span>;
<span class="hljs-keyword">import</span> { storeRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/index.js'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details/dist/ProblemDocument.js'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/store.js'</span>;
<span class="hljs-keyword">import</span> { faker } <span class="hljs-keyword">from</span> <span class="hljs-string">'@faker-js/faker'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> assert <span class="hljs-keyword">from</span> <span class="hljs-string">'node:assert'</span>;
<span class="hljs-keyword">import</span> { assertStrictEqualProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'../assertions.js'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { EditStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/edit-store.js'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ListStores } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/list-stores.js'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Page } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/pagination.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> wallmart = (overrides?: Partial&lt;AddStore&gt;): <span class="hljs-function"><span class="hljs-params">AddStore</span> =&gt;</span> {
  <span class="hljs-keyword">return</span> {
    name: <span class="hljs-string">`wallmart <span class="hljs-subst">${faker.<span class="hljs-built_in">string</span>.uuid()}</span>`</span>,
    url: <span class="hljs-string">'https://www.walmart.com'</span>,
    ...overrides,
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> nike = (overrides?: Partial&lt;AddStore&gt;): <span class="hljs-function"><span class="hljs-params">AddStore</span> =&gt;</span> {
  <span class="hljs-keyword">return</span> {
    name: <span class="hljs-string">`nike <span class="hljs-subst">${faker.<span class="hljs-built_in">string</span>.uuid()}</span>`</span>,
    url: <span class="hljs-string">'https://www.nike.com'</span>,
    ...overrides,
  };
};
</code></pre>
<p>Factory functions create test data with:</p>
<ul>
<li><p><strong>Unique names</strong>: Using <code>faker.string.uuid()</code> prevents collision between tests.</p>
</li>
<li><p><strong>Sensible defaults</strong>: Valid data by default.</p>
</li>
<li><p><strong>Override capability</strong>: Spread operator allows partial customization for specific test scenarios.</p>
</li>
</ul>
<h3 id="heading-part-2-add-store-operation">Part 2: Add Store Operation</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addStore</span>(<span class="hljs-params">input: AddStore</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Store</span>&gt;</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addStore</span>(<span class="hljs-params">
  input: AddStore,
  expectedProblemDocument: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">ProblemDocument</span>&gt;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addStore</span>(<span class="hljs-params">
  input: AddStore,
  expectedProblemDocument?: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Store</span> | <span class="hljs-title">ProblemDocument</span>&gt; </span>{
  <span class="hljs-keyword">const</span> client = testClient(storeRoute);
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.stores.$post({
    json: input,
  });

  <span class="hljs-keyword">if</span> (response.status === StatusCodes.CREATED) {
    assert.ok(
      !expectedProblemDocument,
      <span class="hljs-string">'Expected a problem document but received CREATED status'</span>
    );
    <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> response.json();
    assert.ok(store);
    <span class="hljs-keyword">return</span> store;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">const</span> problemDocument = <span class="hljs-keyword">await</span> response.json();
    assert.ok(problemDocument);
    assert.ok(
      expectedProblemDocument,
      <span class="hljs-string">`Expected CREATED status but received <span class="hljs-subst">${response.status}</span>`</span>
    );
    assertStrictEqualProblemDocument(problemDocument, expectedProblemDocument);
    <span class="hljs-keyword">return</span> problemDocument;
  }
}
</code></pre>
<p><strong>Key Design Decisions</strong>:</p>
<ol>
<li><p><strong>TypeScript Overloads</strong>: Two signatures provide type safety:</p>
<ul>
<li><p>Without error expectation → returns <code>Store</code></p>
</li>
<li><p>With error expectation → returns <code>ProblemDocument</code></p>
</li>
</ul>
</li>
<li><p><strong>Dual-mode behavior</strong>: The function both executes the request AND validates expectations, reducing boilerplate in tests</p>
</li>
<li><p><strong>Clear assertions</strong>: If we expect success but get an error (or vice versa), the test fails with a descriptive message</p>
</li>
</ol>
<h3 id="heading-part-3-edit-store-operation">Part 3: Edit Store Operation</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">editStore</span>(<span class="hljs-params">
  storeId: <span class="hljs-built_in">string</span>,
  input: AddStore
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Store</span>&gt;</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">editStore</span>(<span class="hljs-params">
  storeId: <span class="hljs-built_in">string</span>,
  input: AddStore,
  expectedProblemDocument: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">ProblemDocument</span>&gt;</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">editStore</span>(<span class="hljs-params">
  storeId: <span class="hljs-built_in">string</span>,
  input: EditStore,
  expectedProblemDocument?: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Store</span> | <span class="hljs-title">ProblemDocument</span>&gt; </span>{
  <span class="hljs-keyword">const</span> client = testClient(storeRoute);
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.stores[<span class="hljs-string">':storeId'</span>].$put({
    param: { storeId },
    json: input,
  });

  <span class="hljs-keyword">if</span> (response.status === StatusCodes.OK) {
    <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> response.json();
    assert.ok(store);
    <span class="hljs-keyword">return</span> store;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">const</span> problemDocument = <span class="hljs-keyword">await</span> response.json();
    assert.ok(problemDocument);
    <span class="hljs-keyword">if</span> (expectedProblemDocument) {
      assertStrictEqualProblemDocument(
        problemDocument,
        expectedProblemDocument
      );
    }
    <span class="hljs-keyword">return</span> problemDocument;
  }
}
</code></pre>
<p>Note the route path syntax: <code>client.stores[':storeId'].$put()</code> mirrors the Hono route definition <code>/stores/:storeId</code>.</p>
<h3 id="heading-part-4-get-store-operation">Part 4: Get Store Operation</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getStore</span>(<span class="hljs-params">storeId: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Store</span>&gt;</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getStore</span>(<span class="hljs-params">
  storeId: <span class="hljs-built_in">string</span>,
  expectedProblemDocument: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">ProblemDocument</span>&gt;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getStore</span>(<span class="hljs-params">
  storeId: <span class="hljs-built_in">string</span>,
  expectedProblemDocument?: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Store</span> | <span class="hljs-title">ProblemDocument</span>&gt; </span>{
  <span class="hljs-keyword">const</span> client = testClient(storeRoute);
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.stores[<span class="hljs-string">':storeId'</span>].$get({
    param: { storeId },
  });

  <span class="hljs-keyword">if</span> (response.status === StatusCodes.OK) {
    <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> response.json();
    assert.ok(store);
    <span class="hljs-keyword">return</span> store;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">const</span> problemDocument = <span class="hljs-keyword">await</span> response.json();
    assert.ok(problemDocument);
    <span class="hljs-keyword">if</span> (expectedProblemDocument) {
      assertStrictEqualProblemDocument(
        problemDocument,
        expectedProblemDocument
      );
    }
    <span class="hljs-keyword">return</span> problemDocument;
  }
}
</code></pre>
<h3 id="heading-part-5-list-stores-operation">Part 5: List Stores Operation</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">listStores</span>(<span class="hljs-params">params: ListStores</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Page</span>&lt;<span class="hljs-title">Store</span>&gt;&gt;</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">listStores</span>(<span class="hljs-params">
  params: ListStores,
  expectedProblemDocument: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">ProblemDocument</span>&gt;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">listStores</span>(<span class="hljs-params">
  params: ListStores,
  expectedProblemDocument?: ProblemDocument
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Page</span>&lt;<span class="hljs-title">Store</span>&gt; | <span class="hljs-title">ProblemDocument</span>&gt; </span>{
  <span class="hljs-keyword">const</span> client = testClient(storeRoute);
  <span class="hljs-keyword">const</span> queryParams = {
    pageNumber: params.pageNumber?.toString(),
    pageSize: params.pageSize?.toString(),
    name: params.name,
  };
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.stores.$get({
    query: queryParams,
  });

  <span class="hljs-keyword">if</span> (response.status === StatusCodes.OK) {
    <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> response.json();
    assert.ok(page);
    <span class="hljs-keyword">return</span> page;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">const</span> problemDocument = <span class="hljs-keyword">await</span> response.json();
    assert.ok(problemDocument);
    <span class="hljs-keyword">if</span> (expectedProblemDocument) {
      assertStrictEqualProblemDocument(
        problemDocument,
        expectedProblemDocument
      );
    }
    <span class="hljs-keyword">return</span> problemDocument;
  }
}
</code></pre>
<p>Note how <code>listStores</code> converts numeric parameters to strings, query parameters are always strings in HTTP.</p>
<h3 id="heading-part-6-store-assertions">Part 6: Store Assertions</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> assertStore = <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> {
    hasName(expected: <span class="hljs-built_in">string</span>) {
      assert.strictEqual(
        store.name,
        expected,
        <span class="hljs-string">`Expected name to be <span class="hljs-subst">${expected}</span>, got <span class="hljs-subst">${store.name}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>;
    },
    hasUrl(expected: <span class="hljs-built_in">string</span>) {
      assert.strictEqual(
        store.url,
        expected,
        <span class="hljs-string">`Expected url to be <span class="hljs-subst">${expected}</span>, got <span class="hljs-subst">${store.url}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>;
    },
    hasStoreId(expected: <span class="hljs-built_in">string</span>) {
      assert.strictEqual(
        store.storeId,
        expected,
        <span class="hljs-string">`Expected storeId to be <span class="hljs-subst">${expected}</span>, got <span class="hljs-subst">${store.storeId}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>;
    },
    isTheSameOf(expected: Store) {
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.hasStoreId(expected.storeId)
        .hasName(expected.name)
        .hasUrl(expected.url);
    },
  };
};
</code></pre>
<p>The <code>isTheSameOf</code> method is particularly useful for verifying that retrieved entities match created ones.</p>
<h2 id="heading-step-10-write-store-tests">Step 10: Write Store Tests</h2>
<p>Now let's create the test files using our DSL.</p>
<h3 id="heading-add-store-tests">Add Store Tests</h3>
<p>Create <code>tests/stores/add-store.test.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> { addStore, assertStore, wallmart } <span class="hljs-keyword">from</span> <span class="hljs-string">'./stores-dsl.js'</span>;
<span class="hljs-keyword">import</span> {
  emptyText,
  bigText,
  createValidationError,
  validationError,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'../errors.js'</span>;

describe(<span class="hljs-string">'Add Store Endpoint'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should create a new store with valid data'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> input = wallmart();
    <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> addStore(input);
    assertStore(store).hasName(input.name).hasUrl(input.url);
  });

  describe(<span class="hljs-string">'Property validations'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> testCases = [
      {
        name: <span class="hljs-string">'should reject empty store name'</span>,
        input: wallmart({ name: emptyText }),
        expectedError: createValidationError([
          validationError.tooSmall(<span class="hljs-string">'name'</span>, <span class="hljs-number">1</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject store name longer than 1024 characters'</span>,
        input: wallmart({ name: bigText(<span class="hljs-number">1025</span>) }),
        expectedError: createValidationError([
          validationError.tooBig(<span class="hljs-string">'name'</span>, <span class="hljs-number">1024</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject missing store name'</span>,
        input: wallmart({ name: <span class="hljs-literal">undefined</span> }),
        expectedError: createValidationError([
          validationError.requiredString(<span class="hljs-string">'name'</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject empty store URL'</span>,
        input: wallmart({ url: emptyText }),
        expectedError: createValidationError([
          validationError.invalidUrl(<span class="hljs-string">'url'</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject invalid URL format'</span>,
        input: wallmart({ url: <span class="hljs-string">'not-a-valid-url'</span> }),
        expectedError: createValidationError([
          validationError.invalidUrl(<span class="hljs-string">'url'</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject URL longer than 2048 characters'</span>,
        input: wallmart({
          url: <span class="hljs-string">`https://www.walmart.com/<span class="hljs-subst">${bigText(<span class="hljs-number">2048</span>)}</span>`</span>,
        }),
        expectedError: createValidationError([
          validationError.tooBig(<span class="hljs-string">'url'</span>, <span class="hljs-number">2048</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject missing URL'</span>,
        input: wallmart({ url: <span class="hljs-literal">undefined</span> }),
        expectedError: createValidationError([
          validationError.requiredString(<span class="hljs-string">'url'</span>),
        ]),
      },
    ];

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> { name, input, expectedError } <span class="hljs-keyword">of</span> testCases) {
      test(name, <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">await</span> addStore(input, expectedError);
      });
    }
  });
});
</code></pre>
<p><strong>Key Patterns</strong>:</p>
<ul>
<li><p><strong>Data-driven tests</strong>: The <code>testCases</code> array with loop pattern avoids repetitive test code.</p>
</li>
<li><p><strong>Descriptive names</strong>: Each test case has a clear <code>name</code> describing what it validates.</p>
</li>
<li><p><strong>Clean DSL usage</strong>: <code>addStore(input, expectedError)</code> reads naturally.</p>
</li>
</ul>
<h3 id="heading-edit-store-tests">Edit Store Tests</h3>
<p>Create <code>tests/stores/edit-store.test.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> {
  addStore,
  editStore,
  wallmart,
  nike,
  assertStore,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'./stores-dsl.js'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/store.js'</span>;
<span class="hljs-keyword">import</span> {
  emptyText,
  bigText,
  createValidationError,
  validationError,
  createNotFoundError,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'../errors.js'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { EditStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/edit-store.js'</span>;

describe(<span class="hljs-string">'Edit Store Endpoint'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should edit an existing store with valid data'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> addStore(wallmart());
    <span class="hljs-keyword">const</span> input = nike();
    <span class="hljs-keyword">const</span> newStore = <span class="hljs-keyword">await</span> editStore(store.storeId, input);
    assertStore(newStore).hasName(input.name).hasUrl(input.url);
  });

  describe(<span class="hljs-string">'Property validations'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> testCases = [
      {
        name: <span class="hljs-string">'should reject empty store name'</span>,
        input: <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> ({ name: emptyText, url: store.url }),
        expectedError: createValidationError([
          validationError.tooSmall(<span class="hljs-string">'name'</span>, <span class="hljs-number">1</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject store name longer than 1024 characters'</span>,
        input: <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> ({ name: bigText(<span class="hljs-number">1025</span>), url: store.url }),
        expectedError: createValidationError([
          validationError.tooBig(<span class="hljs-string">'name'</span>, <span class="hljs-number">1024</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject missing store name'</span>,
        input: <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> ({ url: store.url }) <span class="hljs-keyword">as</span> EditStore,
        expectedError: createValidationError([
          validationError.requiredString(<span class="hljs-string">'name'</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject empty store URL'</span>,
        input: <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> ({ name: store.name, url: emptyText }),
        expectedError: createValidationError([
          validationError.invalidUrl(<span class="hljs-string">'url'</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject invalid URL format'</span>,
        input: <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> ({ name: store.name, url: <span class="hljs-string">'not-a-valid-url'</span> }),
        expectedError: createValidationError([
          validationError.invalidUrl(<span class="hljs-string">'url'</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject URL longer than 2048 characters'</span>,
        input: <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> ({
          name: store.name,
          url: <span class="hljs-string">`https://www.walmart.com/<span class="hljs-subst">${bigText(<span class="hljs-number">2048</span>)}</span>`</span>,
        }),
        expectedError: createValidationError([
          validationError.tooBig(<span class="hljs-string">'url'</span>, <span class="hljs-number">2048</span>),
        ]),
      },
      {
        name: <span class="hljs-string">'should reject missing URL'</span>,
        input: <span class="hljs-function">(<span class="hljs-params">store: Store</span>) =&gt;</span> ({ name: store.name }) <span class="hljs-keyword">as</span> EditStore,
        expectedError: createValidationError([
          validationError.requiredString(<span class="hljs-string">'url'</span>),
        ]),
      },
    ];

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> { name, input, expectedError } <span class="hljs-keyword">of</span> testCases) {
      test(name, <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> addStore(wallmart());
        <span class="hljs-keyword">await</span> editStore(store.storeId, input(store), expectedError);
      });
    }
  });

  test(<span class="hljs-string">'should return 404 for non-existent store'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> nonExistentId = <span class="hljs-string">'01940b6d-1234-7890-abcd-ef1234567890'</span>;
    <span class="hljs-keyword">await</span> editStore(
      nonExistentId,
      nike(),
      createNotFoundError(<span class="hljs-string">`Store <span class="hljs-subst">${nonExistentId}</span> not found`</span>)
    );
  });

  test(<span class="hljs-string">'should reject invalid UUID format'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> editStore(
      <span class="hljs-string">'invalid-uuid'</span>,
      nike(),
      createValidationError([validationError.invalidUuid(<span class="hljs-string">'storeId'</span>)])
    );
  });
});
</code></pre>
<p><strong>Notable Details</strong>:</p>
<ul>
<li><p>Test cases use a function <code>(store: Store) =&gt; {...}</code> to construct input based on an existing store.</p>
</li>
<li><p>Each validation test creates fresh data to ensure test isolation.</p>
</li>
<li><p>Edge cases (404, invalid UUID) are tested explicitly.</p>
</li>
</ul>
<h3 id="heading-get-store-tests">Get Store Tests</h3>
<p>Create <code>tests/stores/get-store.test.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> { addStore, assertStore, getStore, wallmart } <span class="hljs-keyword">from</span> <span class="hljs-string">'./stores-dsl.js'</span>;
<span class="hljs-keyword">import</span> {
  createNotFoundError,
  createValidationError,
  validationError,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'../errors.js'</span>;

describe(<span class="hljs-string">'Get Store Endpoint'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should get an existing store by ID'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> createdStore = <span class="hljs-keyword">await</span> addStore(wallmart());
    <span class="hljs-keyword">const</span> retrievedStore = <span class="hljs-keyword">await</span> getStore(createdStore.storeId);
    assertStore(retrievedStore).isTheSameOf(createdStore);
  });

  test(<span class="hljs-string">'should return 404 for non-existent store'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> nonExistentId = <span class="hljs-string">'01940b6d-1234-7890-abcd-ef1234567890'</span>;
    <span class="hljs-keyword">await</span> getStore(
      nonExistentId,
      createNotFoundError(<span class="hljs-string">`Store <span class="hljs-subst">${nonExistentId}</span> not found`</span>)
    );
  });

  test(<span class="hljs-string">'should reject invalid UUID format'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> getStore(
      <span class="hljs-string">'invalid-uuid'</span>,
      createValidationError([validationError.invalidUuid(<span class="hljs-string">'storeId'</span>)])
    );
  });

  test(<span class="hljs-string">'should reject empty storeId'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> getStore(
      <span class="hljs-string">''</span>,
      createValidationError([validationError.invalidUuid(<span class="hljs-string">'storeId'</span>)])
    );
  });
});
</code></pre>
<p>The <code>isTheSameOf</code> assertion makes the retrieval test extremely readable.</p>
<h3 id="heading-list-stores-tests">List Stores Tests</h3>
<p>Create <code>tests/stores/list-stores.test.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> { addStore, assertStore, listStores, wallmart } <span class="hljs-keyword">from</span> <span class="hljs-string">'./stores-dsl.js'</span>;
<span class="hljs-keyword">import</span> { assertPage } <span class="hljs-keyword">from</span> <span class="hljs-string">'../assertions.js'</span>;

describe(<span class="hljs-string">'List Stores Endpoint'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should filter stores by name'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> addStore(wallmart());

    <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> listStores({
      name: store.name,
      pageSize: <span class="hljs-number">10</span>,
      pageNumber: <span class="hljs-number">1</span>,
    });

    assertPage(page).hasItemsCount(<span class="hljs-number">1</span>);
    assertStore(page.items[<span class="hljs-number">0</span>]).isTheSameOf(store);
  });

  test(<span class="hljs-string">'should return empty items when no stores match filter'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> listStores({
      name: <span class="hljs-string">'nonexistent-store-xyz'</span>,
      pageSize: <span class="hljs-number">10</span>,
      pageNumber: <span class="hljs-number">1</span>,
    });

    assertPage(page).hasEmptyResult();
  });
});
</code></pre>
<p>The unique store names generated by <code>faker.string.uuid()</code> ensure that filtering by name returns exactly the expected store.</p>
<h2 id="heading-step-11-run-the-tests">Step 11: Run the Tests</h2>
<p>With everything in place, run the test suite:</p>
<pre><code class="lang-bash">npm <span class="hljs-built_in">test</span>
</code></pre>
<p>We should see output similar to:</p>
<pre><code class="lang-powershell">▶ Add Store Endpoint
  ✔ should create a new store with valid <span class="hljs-keyword">data</span>
  ▶ Property validations
    ✔ should reject empty store name
    ✔ should reject store name longer than <span class="hljs-number">1024</span> characters
    ...
▶ Edit Store Endpoint
  ✔ should edit an existing store with valid <span class="hljs-keyword">data</span>
  ...
</code></pre>
<h2 id="heading-best-practices-summary">Best Practices Summary</h2>
<ol>
<li><p><strong>Use Hono's</strong> <code>testClient</code>: It provides type-safe testing without running a server, making tests fast and reliable.</p>
</li>
<li><p><strong>Generate unique test data with Faker</strong>: Use <code>@faker-js/faker</code> to create unique identifiers and realistic data that prevents test collisions.</p>
</li>
<li><p><strong>Create a DSL for our domain</strong>: Encapsulate API interactions in reusable functions that handle both success and error paths.</p>
</li>
<li><p><strong>Use TypeScript overloads</strong>: Provide different return types based on whether an error is expected, improving type safety in tests.</p>
</li>
<li><p><strong>Use fluent assertions</strong>: They improve readability and make failures easier to diagnose.</p>
</li>
<li><p><strong>Data-driven validation tests</strong>: Loop through test cases to avoid repetitive test code.</p>
</li>
<li><p><strong>Test edge cases explicitly</strong>: Invalid UUIDs, empty strings, non-existent resources, and boundary conditions should all have tests.</p>
</li>
<li><p><strong>Verify side effects</strong>: When an operation affects related entities (like price history affecting product's current price), verify those changes.</p>
</li>
<li><p><strong>Clean up resources</strong>: Use global teardown to close database connections.</p>
</li>
<li><p><strong>Structure tests logically</strong>: Group by endpoint/feature, with happy path first, then validation, then edge cases.</p>
</li>
</ol>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<ol>
<li><p><strong>Not parsing dates from JSON</strong>: Leads to string/Date comparison failures.</p>
</li>
<li><p><strong>Using static test data</strong>: Causes flaky tests in shared databases.</p>
</li>
<li><p><strong>Forgetting to await async operations</strong>: Results in tests passing incorrectly.</p>
</li>
<li><p><strong>Over-mocking</strong>: Integration tests should use real database operations when possible.</p>
</li>
<li><p><strong>Not testing error responses thoroughly</strong>: Validation errors should verify status, message, and error details.</p>
</li>
<li><p><strong>Starting an HTTP server for tests</strong>: Use <code>testClient</code> instead for faster, more reliable tests.</p>
</li>
</ol>
<p>You can find all the code <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/testing">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Hono and Drizzle ORM]]></title><description><![CDATA[As modern web applications demand increasingly robust data access patterns, the combination of Hono's lightweight framework with Drizzle ORM's type-safe database operations creates a powerful stack for building performant APIs.
We'll build upon an ex...]]></description><link>https://blog.raulnq.com/hono-and-drizzle-orm</link><guid isPermaLink="true">https://blog.raulnq.com/hono-and-drizzle-orm</guid><category><![CDATA[Node.js]]></category><category><![CDATA[hono]]></category><category><![CDATA[DrizzleORM]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Tue, 30 Dec 2025 14:00:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765486648262/70d15b1c-0158-44df-bc93-1b9a75217dfa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As modern web applications demand increasingly robust data access patterns, the combination of Hono's lightweight framework with Drizzle ORM's type-safe database operations creates a powerful stack for building performant APIs.</p>
<p>We'll build upon an existing Hono <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/error-handling-security">application</a>, implementing a complete data layer using Drizzle ORM with PostgreSQL. By the end, we will understand how to leverage Drizzle's type-safe query builder and schema management to create production-ready APIs.</p>
<h2 id="heading-what-is-drizzle">What is Drizzle?</h2>
<p><a target="_blank" href="https://orm.drizzle.team/docs/overview">Drizzle</a> is a TypeScript-first Object-Relational Mapping library designed with performance and developer experience as primary concerns. Unlike traditional ORMs that abstract SQL away entirely, Drizzle takes a different approach: it provides a thin, type-safe layer over SQL that feels natural to developers who understand relational databases. Drizzle follows several core design principles that distinguish it from other ORMs:</p>
<ul>
<li><p><strong>SQL-like API</strong>: Query builders mirror SQL syntax closely rather than hiding it.</p>
</li>
<li><p><strong>Type Safety</strong>: Full TypeScript type inference from schema to query results.</p>
</li>
<li><p><strong>Zero Dependencies</strong>: Core package has no runtime dependencies.</p>
</li>
<li><p><strong>Database Agnostic Drivers</strong>: All database drivers are optional peer dependencies.</p>
</li>
<li><p><strong>Thin Abstraction Layer</strong>: Minimal runtime overhead, direct SQL generation.</p>
</li>
</ul>
<h2 id="heading-why-drizzle-for-hono">Why Drizzle for Hono?</h2>
<p>Hono is designed to be lightweight and fast, making it perfect for edge computing. Drizzle shares this philosophy, providing a minimal footprint while maintaining full type safety. Together, they create an optimal stack for modern API development.</p>
<h2 id="heading-creating-a-postgresql-database">Creating a PostgreSQL Database</h2>
<p>We will use <a target="_blank" href="https://www.docker.com/">Docker</a> to create our local database. Create the <code>docker-compose.yml</code> file with the following content:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">database:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'5432:5432'</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">mydb</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">myuser</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">mypassword</span>
</code></pre>
<p>Then, add the following scripts to the <code>package.json</code> file:</p>
<pre><code class="lang-yaml">{
  <span class="hljs-string">...</span>
  <span class="hljs-attr">"scripts":</span> {
    <span class="hljs-string">...</span>
    <span class="hljs-attr">"database:up":</span> <span class="hljs-string">"docker-compose up database -d"</span>,
    <span class="hljs-attr">"database:down":</span> <span class="hljs-string">"docker-compose down database"</span>,
    <span class="hljs-attr">"database:stop":</span> <span class="hljs-string">"docker-compose stop database"</span>,
  },
<span class="hljs-string">...</span>
}
</code></pre>
<p>Execute <code>npm run database:up</code> to start up the database.</p>
<h2 id="heading-creating-a-database-connection">Creating a Database Connection</h2>
<p>Install the following packages:</p>
<pre><code class="lang-powershell">npm install drizzle<span class="hljs-literal">-orm</span> 
npm install pg
npm install drizzle<span class="hljs-literal">-kit</span>
</code></pre>
<p>First, we need to configure our database connection string. In the <code>env.ts</code> file, we've already defined the environment schema:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { config } <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
<span class="hljs-keyword">import</span> { expand } <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv-expand'</span>;
<span class="hljs-keyword">import</span> { ZodError, z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">const</span> ENVSchema = z.object({
  NODE_ENV: z
    .enum([<span class="hljs-string">'development'</span>, <span class="hljs-string">'production'</span>, <span class="hljs-string">'test'</span>])
    .default(<span class="hljs-string">'development'</span>),
  PORT: z.coerce.number().default(<span class="hljs-number">3000</span>),
  DATABASE_URL: z.string(),
  TOKEN: z.string().optional(),
});

expand(config());

<span class="hljs-keyword">try</span> {
  ENVSchema.parse(process.env);
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-keyword">if</span> (error <span class="hljs-keyword">instanceof</span> ZodError) {
    <span class="hljs-keyword">const</span> e = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(
      <span class="hljs-string">`Environment validation failed:\n <span class="hljs-subst">${z.treeifyError(error)}</span>`</span>
    );
    e.stack = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">throw</span> e;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Unexpected error during environment validation:'</span>, error);
    <span class="hljs-keyword">throw</span> error;
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ENV = ENVSchema.parse(process.env);
</code></pre>
<p>This validates that <code>DATABASE_URL</code> is present at startup, preventing runtime errors from missing configuration. The database <a target="_blank" href="https://orm.drizzle.team/docs/connect-overview">client</a> is initialized in the <code>database/client.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { drizzle } <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-orm/node-postgres'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> schema <span class="hljs-keyword">from</span> <span class="hljs-string">'./schemas.js'</span>;
<span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/env.js'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> client = drizzle(ENV.DATABASE_URL, {
  schema,
  logger: ENV.NODE_ENV === <span class="hljs-string">'development'</span>,
});
</code></pre>
<ul>
<li><p><strong>Connection String</strong>: We use <a target="_blank" href="https://orm.drizzle.team/docs/get-started-postgresql#node-postgres"><code>drizzle-orm/node-postgres</code></a>, which accepts a connection string directly.</p>
</li>
<li><p><strong>Schema Import</strong>: All schemas are passed to enable type-safe queries with relations.</p>
</li>
<li><p><strong>Query Logging</strong>: Enabled in development to see generated SQL queries.</p>
</li>
<li><p><strong>Single Instance</strong>: This client is exported and reused throughout the application.</p>
</li>
</ul>
<h2 id="heading-schema-definition">Schema Definition</h2>
<p>Drizzle <a target="_blank" href="https://orm.drizzle.team/docs/sql-schema-declaration">schemas</a> define our database structure using TypeScript. They serve as both the source of truth for our database and the foundation for type inference. The Drizzle stores schema is defined in the <code>features/stores/store.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> { varchar, pgSchema, uuid } <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-orm/pg-core'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> storeSchema = z.object({
  storeId: z.uuidv7(),
  name: z.string().min(<span class="hljs-number">1</span>).max(<span class="hljs-number">1024</span>),
  url: z.url().max(<span class="hljs-number">2048</span>),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Store = z.infer&lt;<span class="hljs-keyword">typeof</span> storeSchema&gt;;

<span class="hljs-keyword">const</span> dbSchema = pgSchema(<span class="hljs-string">'price_tracker'</span>);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> stores = dbSchema.table(<span class="hljs-string">'stores'</span>, {
  storeId: uuid(<span class="hljs-string">'storeid'</span>).primaryKey(),
  name: varchar(<span class="hljs-string">'name'</span>, { length: <span class="hljs-number">1024</span> }).notNull(),
  url: varchar(<span class="hljs-string">'url'</span>, { length: <span class="hljs-number">2048</span> }).notNull(),
});
</code></pre>
<p>Drizzle core features:</p>
<ul>
<li><p><a target="_blank" href="https://orm.drizzle.team/docs/schemas"><strong>pgSchema</strong></a>: Creates a PostgreSQL schema to organize tables.</p>
</li>
<li><p><strong>table</strong>: Defines a table within the schema.</p>
</li>
<li><p><a target="_blank" href="https://orm.drizzle.team/docs/column-types/pg"><strong>Column Types</strong></a>: Type-safe column definitions (uuid, varchar, numeric, timestamp).</p>
</li>
<li><p><strong>Constraints</strong>: <a target="_blank" href="https://orm.drizzle.team/docs/indexes-constraints#primary-key">Primary keys</a>, <a target="_blank" href="https://orm.drizzle.team/docs/indexes-constraints#not-null">not null</a>, <a target="_blank" href="https://orm.drizzle.team/docs/indexes-constraints#default">default values</a>.</p>
</li>
</ul>
<p>The Drizzle products schema in <code>features/stores/product.ts</code> demonstrates more complex features:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> {
  varchar,
  pgSchema,
  uuid,
  numeric,
  timestamp,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-orm/pg-core'</span>;
<span class="hljs-keyword">import</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/store.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> productSchema = z.object({
  storeId: z.uuidv7(),
  productId: z.uuidv7(),
  name: z.string().min(<span class="hljs-number">1</span>).max(<span class="hljs-number">1024</span>),
  url: z.url().max(<span class="hljs-number">2048</span>),
  currentPrice: z.number().positive().nullable(),
  priceChangePercentage: z.number().nullable(),
  lastUpdated: z.coerce.date().nullable(),
  currency: z.string().length(<span class="hljs-number">3</span>),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Product = z.infer&lt;<span class="hljs-keyword">typeof</span> productSchema&gt;;

<span class="hljs-keyword">const</span> dbSchema = pgSchema(<span class="hljs-string">'price_tracker'</span>);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> products = dbSchema.table(<span class="hljs-string">'products'</span>, {
  productId: uuid(<span class="hljs-string">'productid'</span>).primaryKey(),
  storeId: uuid(<span class="hljs-string">'storeid'</span>)
    .notNull()
    .references(<span class="hljs-function">() =&gt;</span> stores.storeId),
  name: varchar(<span class="hljs-string">'name'</span>, { length: <span class="hljs-number">1024</span> }).notNull(),
  url: varchar(<span class="hljs-string">'url'</span>, { length: <span class="hljs-number">2048</span> }).notNull(),
  currentPrice: numeric(<span class="hljs-string">'currentprice'</span>, {
    precision: <span class="hljs-number">10</span>,
    scale: <span class="hljs-number">2</span>,
    mode: <span class="hljs-string">'number'</span>,
  }),
  priceChangePercentage: numeric(<span class="hljs-string">'pricechangepercentage'</span>, {
    precision: <span class="hljs-number">5</span>,
    scale: <span class="hljs-number">2</span>,
    mode: <span class="hljs-string">'number'</span>,
  }),
  lastUpdated: timestamp(<span class="hljs-string">'lastupdated'</span>, { mode: <span class="hljs-string">'date'</span> }),
  currency: varchar(<span class="hljs-string">'currency'</span>, { length: <span class="hljs-number">3</span> }).notNull(),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> priceHistorySchema = z.object({
  productId: z.uuidv7(),
  priceHistoryId: z.uuidv7(),
  timestamp: z.coerce.date(),
  price: z.number().positive(),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> PriceHistory = z.infer&lt;<span class="hljs-keyword">typeof</span> priceHistorySchema&gt;;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> priceHistories = dbSchema.table(<span class="hljs-string">'price_histories'</span>, {
  priceHistoryId: uuid(<span class="hljs-string">'pricehistoryid'</span>).primaryKey(),
  productId: uuid(<span class="hljs-string">'productid'</span>)
    .notNull()
    .references(<span class="hljs-function">() =&gt;</span> products.productId),
  timestamp: timestamp(<span class="hljs-string">'timestamp'</span>, { mode: <span class="hljs-string">'date'</span> }).notNull(),
  price: numeric(<span class="hljs-string">'price'</span>, {
    precision: <span class="hljs-number">10</span>,
    scale: <span class="hljs-number">2</span>,
    mode: <span class="hljs-string">'number'</span>,
  }).notNull(),
});
</code></pre>
<p>Drizzle advanced features:</p>
<ul>
<li><p><a target="_blank" href="https://orm.drizzle.team/docs/indexes-constraints#foreign-key"><strong>Foreign Keys</strong></a>: <code>.references(() =&gt; stores.storeId)</code> creates a relationship.</p>
</li>
<li><p><strong>Numeric Types</strong>: <code>precision</code> and <code>scale</code> define decimal precision.</p>
</li>
<li><p><strong>Timestamp Modes</strong>: <code>mode: 'date'</code> returns JavaScript Date objects.</p>
</li>
<li><p><strong>Number Modes</strong>: <code>mode: 'number'</code> returns a number.</p>
</li>
<li><p><strong>Nullable Fields</strong>: Fields without <code>.notNull()</code> are nullable by default.</p>
</li>
</ul>
<p>All schemas are exported from the <code>database/schemas.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/stores/store.js'</span>;
<span class="hljs-keyword">export</span> { products, priceHistories } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/features/products/product.js'</span>;
</code></pre>
<p>This centralized export is imported by the database client, enabling Drizzle to understand relationships between tables.</p>
<h2 id="heading-migrations">Migrations</h2>
<p><a target="_blank" href="https://orm.drizzle.team/docs/migrations">Drizzle Kit</a> generates and manages database migrations automatically from our schema definitions. The migration configuration is defined in the <code>drizzle.config.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">'./src/env.js'</span>;
<span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-kit'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  out: <span class="hljs-string">'./src/database/migrations'</span>,
  schema: <span class="hljs-string">'./src/database/schemas.ts'</span>,
  dialect: <span class="hljs-string">'postgresql'</span>,
  dbCredentials: {
    url: ENV.DATABASE_URL,
  },
  migrations: {
    schema: <span class="hljs-string">'price_tracker'</span>,
  },
});
</code></pre>
<p>The configuration options are:</p>
<ul>
<li><p><code>out</code>: Directory where migration files are generated.</p>
</li>
<li><p><code>schema</code>: Path to our schema definitions.</p>
</li>
<li><p><code>dialect</code>: Database type (postgresql, mysql, sqlite).</p>
</li>
<li><p><code>dbCredentials</code>: Connection information.</p>
</li>
<li><p><code>migrations.schema</code>: PostgreSQL schema name for migrations table.</p>
</li>
</ul>
<p>The <code>package.json</code> file includes scripts for migration management:</p>
<pre><code class="lang-json">{
  ...
  <span class="hljs-attr">"scripts"</span>: {
    ...
    <span class="hljs-attr">"database:generate"</span>: <span class="hljs-string">"tsx ./node_modules/drizzle-kit/bin.cjs generate"</span>,
    <span class="hljs-attr">"database:migrate"</span>: <span class="hljs-string">"tsx ./node_modules/drizzle-kit/bin.cjs migrate"</span>,
    <span class="hljs-attr">"database:studio"</span>: <span class="hljs-string">"tsx ./node_modules/drizzle-kit/bin.cjs studio"</span>
  },
...
}
</code></pre>
<blockquote>
<p>Drizzle Kit cannot properly handle path aliases or file extensions (in certain TypeScript configurations) because it uses <code>esbuild-register</code>. This library has limited module resolution capabilities compared to the full TypeScript compiler. Due to this issue, we use tsx to run Drizzle-kit instead of the regular command <a target="_blank" href="https://orm.drizzle.team/docs/drizzle-kit-generate"><code>drizzle-kit generate</code></a> and <a target="_blank" href="https://orm.drizzle.team/docs/drizzle-kit-migrate"><code>drizzle-kit migrate</code></a>.</p>
</blockquote>
<p><strong>The workflow to use Drizzle-kit could be something like</strong>:</p>
<ul>
<li><p>Define or modify schemas in our TypeScript files.</p>
</li>
<li><p>Generate migration: <code>npm run database:generate</code>.</p>
</li>
<li><p>Review the SQL in the generated migration file.</p>
</li>
<li><p>Apply migration: <code>npm run database:migrate</code>.</p>
</li>
</ul>
<blockquote>
<p><code>drizzle-kit studio</code> command spins up a server for <a target="_blank" href="https://orm.drizzle.team/drizzle-studio/overview">Drizzle Studio</a>(SQL database explorer) hosted on https://local.drizzle.studio</p>
</blockquote>
<p>Generated migrations are stored in the <code>database/migrations</code> folders. Drizzle Kit also maintains metadata in the <code>database/migrations/meta</code> folder to track migration history and generate incremental changes.</p>
<h2 id="heading-querying-with-drizzle">Querying with Drizzle</h2>
<p>In Drizzle ORM, we have two query styles.</p>
<h3 id="heading-sql-like-queries">SQL-like queries</h3>
<p>These are explicit, low-level, SQL-shaped queries. They map very closely to real SQL and give us full control over joins, filters, grouping, and projections.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> db
  .select({
    userId: users.id,
    userName: users.name,
    postTitle: posts.title,
  })
  .from(users)
  .leftJoin(posts, eq(posts.userId, users.id))
  .where(eq(users.active, <span class="hljs-literal">true</span>));
</code></pre>
<blockquote>
<p>I want to write SQL, but safely.</p>
</blockquote>
<h3 id="heading-relational-queries">Relational queries</h3>
<p>Relational queries are higher-level and schema-aware. We define relations once in our schema, and Drizzle automatically figures out what to do.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> db.query.users.findMany({
  where: <span class="hljs-function">(<span class="hljs-params">users, { eq }</span>) =&gt;</span> eq(users.active, <span class="hljs-literal">true</span>),
  <span class="hljs-keyword">with</span>: {
    posts: {
      columns: {
        id: <span class="hljs-literal">true</span>,
        title: <span class="hljs-literal">true</span>,
      },
    },
  },
});
</code></pre>
<blockquote>
<p>I want objects. Drizzle figures out the joins.</p>
</blockquote>
<p>Let's see how to integrate Drizzle in our application using the low-level API.</p>
<h3 id="heading-insert-statement">Insert Statement</h3>
<p>Update the file <code>features/stores/add-store.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { v7 } <span class="hljs-keyword">from</span> <span class="hljs-string">'uuid'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/validation.js'</span>;
<span class="hljs-keyword">import</span> { client } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/database/client.js'</span>;

<span class="hljs-keyword">const</span> schema = storeSchema.omit({ storeId: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addRoute = <span class="hljs-keyword">new</span> Hono().post(
  <span class="hljs-string">'/'</span>,
  zValidator(<span class="hljs-string">'json'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> data = c.req.valid(<span class="hljs-string">'json'</span>);
    <span class="hljs-keyword">const</span> [store] = <span class="hljs-keyword">await</span> client
      .insert(stores)
      .values({ ...data, storeId: v7() })
      .returning();
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.CREATED);
  }
);
</code></pre>
<ul>
<li><p><a target="_blank" href="https://orm.drizzle.team/docs/insert"><strong>Insert Operation</strong></a>: The <code>.insert(stores)</code> method initiates an insert query on the stores table.</p>
</li>
<li><p><strong>Values Method</strong>: <code>.values()</code> accepts an object matching the table schema, providing type safety.</p>
</li>
<li><p><strong>UUID Generation</strong>: Uses UUID v7 for time-sortable unique identifiers.</p>
</li>
<li><p><strong>Returning Clause</strong>: <code>.returning()</code> is a PostgreSQL feature that returns the inserted record, eliminating the need for a separate <code>SELECT</code> query.</p>
</li>
<li><p><strong>Type Safety</strong>: TypeScript ensures that the data object contains all required fields with correct types.</p>
</li>
</ul>
<h3 id="heading-select-statement">Select Statement</h3>
<p>Update the file <code>feature/stores/get-store.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/validation.js'</span>;
<span class="hljs-keyword">import</span> { createResourceNotFoundPD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/problem-document.js'</span>;
<span class="hljs-keyword">import</span> { client } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/database/client.js'</span>;
<span class="hljs-keyword">import</span> { eq } <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-orm'</span>;

<span class="hljs-keyword">const</span> schema = storeSchema.pick({ storeId: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getRoute = <span class="hljs-keyword">new</span> Hono().get(
  <span class="hljs-string">'/:storeId'</span>,
  zValidator(<span class="hljs-string">'param'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { storeId } = c.req.valid(<span class="hljs-string">'param'</span>);
    <span class="hljs-keyword">const</span> [store] = <span class="hljs-keyword">await</span> client
      .select()
      .from(stores)
      .where(eq(stores.storeId, storeId))
      .limit(<span class="hljs-number">1</span>);
    <span class="hljs-keyword">if</span> (!store) {
      <span class="hljs-keyword">return</span> c.json(
        createResourceNotFoundPD(c.req.path, <span class="hljs-string">`Store <span class="hljs-subst">${storeId}</span> not found`</span>),
        StatusCodes.NOT_FOUND
      );
    }
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.OK);
  }
);
</code></pre>
<ul>
<li><p><a target="_blank" href="https://orm.drizzle.team/docs/select"><strong>Selective Operation</strong></a>: <code>.select()</code> without arguments retrieves all columns, but Drizzle supports selective column retrieval.</p>
</li>
<li><p><strong>Primary Key Lookup</strong>: Using <code>eq()</code> on the primary key ensures an index is used, providing O(1) lookup performance.</p>
</li>
<li><p><strong>Limit Clause</strong>: <code>.limit(1)</code> tells the database to stop scanning after finding the first match.</p>
</li>
<li><p><strong>Null Handling</strong>: The query returns an array; checking <code>if (!store)</code> handles the not-found case gracefully.</p>
</li>
</ul>
<p>Update the <code>features/stores/list-stores.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { paginationSchema, createPage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/pagination.js'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/validation.js'</span>;
<span class="hljs-keyword">import</span> { client } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/database/client.js'</span>;
<span class="hljs-keyword">import</span> { like, count, SQL, and } <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-orm'</span>;

<span class="hljs-keyword">const</span> schema = paginationSchema.extend({
  name: z.string().optional(),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> listRoute = <span class="hljs-keyword">new</span> Hono().get(
  <span class="hljs-string">'/'</span>,
  zValidator(<span class="hljs-string">'query'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { pageNumber, pageSize, name } = c.req.valid(<span class="hljs-string">'query'</span>);
    <span class="hljs-keyword">const</span> filters: SQL[] = [];
    <span class="hljs-keyword">const</span> offset = (pageNumber - <span class="hljs-number">1</span>) * pageSize;
    <span class="hljs-keyword">if</span> (name) filters.push(like(stores.name, <span class="hljs-string">`%<span class="hljs-subst">${name}</span>%`</span>));

    <span class="hljs-keyword">const</span> [{ totalCount }] = <span class="hljs-keyword">await</span> client
      .select({ totalCount: count() })
      .from(stores)
      .where(and(...filters));

    <span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> client
      .select()
      .from(stores)
      .where(and(...filters))
      .limit(pageSize)
      .offset(offset);
    <span class="hljs-keyword">return</span> c.json(
      createPage(items, totalCount, pageNumber, pageSize),
      StatusCodes.OK
    );
  }
);
</code></pre>
<ul>
<li><p><strong>Aggregate Functions</strong>: <code>count()</code> provides SQL COUNT functionality with type safety.</p>
</li>
<li><p><strong>LIKE Operator</strong>: The <code>like()</code> function enables pattern matching for text search.</p>
</li>
<li><p><a target="_blank" href="https://orm.drizzle.team/docs/operators"><strong>Filtering</strong></a>: Building a filter array allows conditional query construction.</p>
</li>
<li><p><strong>Logical Operators</strong>: <code>and(...filters)</code> combines multiple conditions with AND logic.</p>
</li>
<li><p><strong>Pagination</strong>: <code>.limit()</code> and <code>.offset()</code> implement cursor-based pagination.</p>
</li>
<li><p><strong>Two-Query Pattern</strong>: Separate queries for count and data retrieval ensure accurate pagination metadata.</p>
</li>
</ul>
<h3 id="heading-update-statement">Update Statement</h3>
<p>Navigate to the <code>features/stores/edit-store.ts</code> file and update the content with:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/validation.js'</span>;
<span class="hljs-keyword">import</span> { createResourceNotFoundPD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/problem-document.js'</span>;
<span class="hljs-keyword">import</span> { client } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/database/client.js'</span>;
<span class="hljs-keyword">import</span> { eq } <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-orm'</span>;

<span class="hljs-keyword">const</span> paramSchema = storeSchema.pick({ storeId: <span class="hljs-literal">true</span> });
<span class="hljs-keyword">const</span> bodySchema = storeSchema.pick({ name: <span class="hljs-literal">true</span>, url: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> editRoute = <span class="hljs-keyword">new</span> Hono().put(
  <span class="hljs-string">'/:storeId'</span>,
  zValidator(<span class="hljs-string">'param'</span>, paramSchema),
  zValidator(<span class="hljs-string">'json'</span>, bodySchema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { storeId } = c.req.valid(<span class="hljs-string">'param'</span>);
    <span class="hljs-keyword">const</span> data = c.req.valid(<span class="hljs-string">'json'</span>);
    <span class="hljs-keyword">const</span> existing = <span class="hljs-keyword">await</span> client
      .select()
      .from(stores)
      .where(eq(stores.storeId, storeId))
      .limit(<span class="hljs-number">1</span>);

    <span class="hljs-keyword">if</span> (existing.length === <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">return</span> c.json(
        createResourceNotFoundPD(c.req.path, <span class="hljs-string">`Store <span class="hljs-subst">${storeId}</span> not found`</span>),
        StatusCodes.NOT_FOUND
      );
    }
    <span class="hljs-keyword">const</span> [store] = <span class="hljs-keyword">await</span> client
      .update(stores)
      .set(data)
      .where(eq(stores.storeId, storeId))
      .returning();
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.OK);
  }
);
</code></pre>
<ul>
<li><p><strong>Select Query</strong>: <code>.select().from(stores)</code> creates a SELECT statement with full type inference.</p>
</li>
<li><p><strong>Where Clauses</strong>: The <code>eq()</code> operator provides type-safe equality comparisons.</p>
</li>
<li><p><strong>Existence Check</strong>: Queries the database to verify the resource exists before attempting updates.</p>
</li>
<li><p><a target="_blank" href="https://orm.drizzle.team/docs/update"><strong>Update Operation</strong></a>: <code>.update(stores).set(data)</code> initiates an update query.</p>
</li>
<li><p><strong>Conditional Updates</strong>: The <code>.where()</code> clause ensures only the specific record is modified.</p>
</li>
</ul>
<p>This endpoint follows a check-then-update pattern. While this involves two database round-trips, it provides better error handling and user feedback.</p>
<p>Drizzle ORM provides a type-safe, lightweight solution for database operations in Hono applications. Its SQL-like API, excellent TypeScript integration, and minimal runtime overhead make it ideal for modern web applications. You can find all the code <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/drizzle">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Hono: Error Handling and Security]]></title><description><![CDATA[Building production-ready APIs requires robust error handling and security mechanisms. This article explores how to implement comprehensive error handling using the RFC 7807 Problem Details standard and secure our Hono application using built-in midd...]]></description><link>https://blog.raulnq.com/hono-error-handling-and-security</link><guid isPermaLink="true">https://blog.raulnq.com/hono-error-handling-and-security</guid><category><![CDATA[hono]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Security]]></category><category><![CDATA[error handling]]></category><category><![CDATA[honojs]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Tue, 23 Dec 2025 14:00:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765409243978/c3b83b2e-e9a2-4bac-94b0-d2521b9e2b5d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building production-ready APIs requires robust error handling and security mechanisms. This article explores how to implement comprehensive error handling using the RFC 7807 Problem Details standard and secure our Hono application using built-in middleware. We'll modify our <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/validation">application</a> to cover structured error responses, custom error handlers, authentication, security headers, and Node.js process-level error handling.</p>
<h2 id="heading-rfc-7807-problem-details-for-http-apis">RFC 7807: Problem Details for HTTP APIs</h2>
<p>The <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7807">RFC 7807</a> standard defines a problem detail format for HTTP API errors. This specification provides a consistent, machine-readable way to express error information.</p>
<ul>
<li><p><strong>type</strong>: A URI reference identifying the problem type.</p>
</li>
<li><p><strong>title</strong>: A short, human-readable summary.</p>
</li>
<li><p><strong>status</strong>: The HTTP status code.</p>
</li>
<li><p><strong>detail</strong>: A human-readable explanation specific to this occurrence.</p>
</li>
<li><p><strong>instance</strong>: A URI reference identifying the occurrence.</p>
</li>
</ul>
<p>Let’s start the implementation of the standard by installing the following package:</p>
<pre><code class="lang-powershell">npm install http<span class="hljs-literal">-problem</span><span class="hljs-literal">-details</span>
</code></pre>
<p>Create the <code>utils/problem-document.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/env.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createResourceNotFoundPD = <span class="hljs-function">(<span class="hljs-params">path: <span class="hljs-built_in">string</span>, detail: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ProblemDocument({
    <span class="hljs-keyword">type</span>: <span class="hljs-string">'/problems/resource-not-found'</span>,
    title: <span class="hljs-string">'Resource not found'</span>,
    status: StatusCodes.NOT_FOUND,
    detail: detail,
    instance: path,
  });
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createInternalServerErrorPD = <span class="hljs-function">(<span class="hljs-params">path: <span class="hljs-built_in">string</span>, error?: <span class="hljs-built_in">Error</span></span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> extensions =
    ENV.NODE_ENV === <span class="hljs-string">'development'</span> &amp;&amp; error?.stack
      ? { stack: error.stack }
      : <span class="hljs-literal">undefined</span>;

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ProblemDocument(
    {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">'/problems/internal-server-error'</span>,
      title: <span class="hljs-string">'Internal Server Error'</span>,
      status: StatusCodes.INTERNAL_SERVER_ERROR,
      detail: <span class="hljs-string">'An unexpected error occurred'</span>,
      instance: path,
    },
    extensions
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createValidationErrorPD = <span class="hljs-function">(<span class="hljs-params">
  path: <span class="hljs-built_in">string</span>,
  issues: z.core.$ZodIssue[]
</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ProblemDocument(
    {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">'/problems/validation-error'</span>,
      title: <span class="hljs-string">'Validation Error'</span>,
      status: StatusCodes.BAD_REQUEST,
      detail: <span class="hljs-string">'The request contains invalid data'</span>,
      instance: path,
    },
    {
      errors: issues.map(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> ({
        path: err.path.join(<span class="hljs-string">'.'</span>),
        message: err.message,
        code: err.code,
      })),
    }
  );
};
</code></pre>
<p>The methods above help us create the <code>ProblemDocument</code> objects that we will use across our project. The method <code>createResourceNotFoundPD</code> is used within route handlers to indicate specific resources were not found. For example, the <code>features/stores/get-store.ts</code> file has the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/validation.js'</span>;
<span class="hljs-keyword">import</span> { createResourceNotFoundPD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/problem-document.js'</span>;

<span class="hljs-keyword">const</span> schema = storeSchema.pick({ storeId: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getRoute = <span class="hljs-keyword">new</span> Hono().get(
  <span class="hljs-string">'/:storeId'</span>,
  zValidator(<span class="hljs-string">'param'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { storeId } = c.req.valid(<span class="hljs-string">'param'</span>);
    <span class="hljs-keyword">const</span> store = stores.find(<span class="hljs-function"><span class="hljs-params">s</span> =&gt;</span> s.storeId === storeId);
    <span class="hljs-keyword">if</span> (!store) {
      <span class="hljs-keyword">return</span> c.json(
        createResourceNotFoundPD(c.req.path, <span class="hljs-string">`Store <span class="hljs-subst">${storeId}</span> not found`</span>),
        StatusCodes.NOT_FOUND
      );
    }
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.OK);
  }
);
</code></pre>
<p>This ensures consistency between route-level <code>404</code>s. For validation failures, the <code>createValidationErrorPD</code> implementation extends the <code>ProblemDetails</code> object with additional error context from Zod. To complete the integration, the <code>zValidator</code> method provides a hook to customize error responses. Create the <code>utils/validation.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> ZodSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ValidationTargets } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { zValidator <span class="hljs-keyword">as</span> zv } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { createValidationErrorPD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/problem-document.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> zValidator = &lt;
  T <span class="hljs-keyword">extends</span> ZodSchema,
  Target <span class="hljs-keyword">extends</span> keyof ValidationTargets,
&gt;<span class="hljs-function">(<span class="hljs-params">
  target: Target,
  schema: T
</span>) =&gt;</span>
  zv(target, schema, <span class="hljs-function">(<span class="hljs-params">result, _c</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (!result.success) {
      <span class="hljs-keyword">return</span> _c.json(
        createValidationErrorPD(_c.req.path, result.error.issues),
        StatusCodes.BAD_REQUEST
      );
    }
  });
</code></pre>
<p>The validator is used throughout the application, such as in the <code>features/stores/add-store.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { v7 } <span class="hljs-keyword">from</span> <span class="hljs-string">'uuid'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/validation.js'</span>;

<span class="hljs-keyword">const</span> schema = storeSchema.omit({ storeId: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addRoute = <span class="hljs-keyword">new</span> Hono().post(
  <span class="hljs-string">'/'</span>,
  zValidator(<span class="hljs-string">'json'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> data = c.req.valid(<span class="hljs-string">'json'</span>);
    <span class="hljs-keyword">const</span> store = { ...data, storeId: v7() };
    stores.push(store);
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.CREATED);
  }
);
</code></pre>
<p>All the references were updated from <code>@hono/zod-validator</code> to <code>@/utils/validation.js</code>.</p>
<h2 id="heading-error-handlers-in-hono"><strong>Error Handlers in Hono</strong></h2>
<p>Hono provides two types of error handlers. These are special handlers that are called directly in error conditions, not part of the middleware chain. The <a target="_blank" href="https://hono.dev/docs/api/hono#error-handling"><code>ErrorHandler</code></a> intercepts all unhandled errors thrown during request processing. Create the <code>middlewares/on-error.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createInternalServerErrorPD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/problem-document.js'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ErrorHandler } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> onError: ErrorHandler = <span class="hljs-function">(<span class="hljs-params">_err, c</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> c.json(
    createInternalServerErrorPD(c.req.path),
    StatusCodes.INTERNAL_SERVER_ERROR
  );
};
</code></pre>
<p>The <a target="_blank" href="https://hono.dev/docs/api/hono#not-found"><code>NotFoundHandler</code></a> is in charge of handling requests to undefined routes. Create the <code>middlewares/on-not-found.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NotFoundHandler } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { createResourceNotFoundPD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/utils/problem-document.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> onNotFound: NotFoundHandler = <span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span> {
  <span class="hljs-keyword">return</span> c.json(
    createResourceNotFoundPD(c.req.path, <span class="hljs-string">'Resource not found'</span>),
    StatusCodes.NOT_FOUND
  );
};
</code></pre>
<p>Both handlers are registered in the <code>app.ts</code> file.</p>
<pre><code class="lang-typescript">app.notFound(onNotFound);
app.onError(onError);
</code></pre>
<h2 id="heading-security-middlewares">Security Middlewares</h2>
<p>Hono provides several built-in security middlewares to protect our application from common web vulnerabilities:</p>
<ul>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/basic-auth">Basic Auth</a>: Implements HTTP Basic Authentication.</p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/bearer-auth">Bearer Auth</a><strong>:</strong> Validates Bearer tokens.</p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/jwt">JWT Auth</a><strong>:</strong> Verifies JWT tokens.</p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/jwk">JWK Auth</a>: Authenticates using JSON Web Keys with dynamic key fetching.</p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/cors">CORS</a>: Controls cross-origin requests with configurable policies.</p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/csrf">CSRF Protection</a>: Protects against Cross-Site Request Forgery attacks.</p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/secure-headers">Secure Headers</a>: Sets comprehensive security headers including CSP, HSTS, and XSS protection.</p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/middleware/builtin/ip-restriction">IP Restriction</a>: limits access to resources based on the IP address of the user.</p>
</li>
</ul>
<p>Apart from them, there are other third-party middleware we can check <a target="_blank" href="https://hono.dev/docs/middleware/third-party">here</a>. The project will implement token-based authentication and security-related HTTP headers. Update the <code>app.ts</code> file as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { storeRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/stores/index.js'</span>;
<span class="hljs-keyword">import</span> { productRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/products/index.js'</span>;
<span class="hljs-keyword">import</span> { onError } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/middlewares/on-error.js'</span>;
<span class="hljs-keyword">import</span> { onNotFound } <span class="hljs-keyword">from</span> <span class="hljs-string">'./middlewares/on-not-found.js'</span>;
<span class="hljs-keyword">import</span> { bearerAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/bearer-auth'</span>;
<span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">'./env.js'</span>;
<span class="hljs-keyword">import</span> { secureHeaders } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/secure-headers'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono({ strict: <span class="hljs-literal">false</span> });

app.use(secureHeaders());
<span class="hljs-keyword">if</span> (ENV.TOKEN) {
  app.use(<span class="hljs-string">'/api/*'</span>, bearerAuth({ token: ENV.TOKEN }));
}

app.route(<span class="hljs-string">'/api'</span>, storeRoute);
app.route(<span class="hljs-string">'/api'</span>, productRoute);

app.get(<span class="hljs-string">'/live'</span>, <span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span>
  c.json({
    status: <span class="hljs-string">'healthy'</span>,
    uptime: process.uptime(),
    timestamp: <span class="hljs-built_in">Date</span>.now(),
  })
);

app.notFound(onNotFound);
app.onError(onError);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> App = <span class="hljs-keyword">typeof</span> app;
</code></pre>
<ul>
<li><p><strong>Conditional protection</strong>: Authentication is only applied if a token is configured in the environment.</p>
</li>
<li><p><strong>Path-based protection</strong>: Only <code>/api/*</code> routes require authentication. The <code>/live</code> health check endpoint remains public.</p>
</li>
<li><p><strong>Wildcard patterns</strong>: Use path patterns like <code>/api/*</code> to protect entire route groups.</p>
</li>
<li><p><strong>Headers automatically added (among others):</strong></p>
<ul>
<li><p><code>X-Frame-Options</code>: Prevents clickjacking attacks by controlling iframe embedding.</p>
</li>
<li><p><code>X-Content-Type-Options</code>: Prevents MIME-sniffing attacks.</p>
</li>
<li><p><code>Referrer-Policy</code>: Controls referrer information leakage.</p>
</li>
<li><p><code>Strict-Transport-Security</code>: Enforces HTTPS connections (HSTS).</p>
</li>
<li><p><code>X-XSS-Protection</code>: Enables browser XSS filters (legacy support).</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-process-level-error-handling"><strong>Process-Level Error Handling</strong></h2>
<p>Node.js provides two critical process-level error events that must be handled to prevent silent failures and undefined application states.</p>
<h3 id="heading-uncaught-exceptions"><strong>Uncaught Exceptions</strong></h3>
<p>Uncaught exceptions are errors that happen during synchronous code execution and are not caught by any error-handling mechanism.</p>
<h3 id="heading-unhandled-rejections"><strong>Unhandled Rejections</strong></h3>
<p>Unhandled rejections happen when a promise is rejected (asynchronous code), but there is no <code>.catch()</code> handler or <code>try-catch</code> block to manage the rejection.</p>
<p>Update the <code>index.ts</code> file as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>;
<span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/env.js'</span>;
<span class="hljs-keyword">import</span> { app } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.js'</span>;

process.on(<span class="hljs-string">'uncaughtException'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  process.exit(<span class="hljs-number">1</span>);
});

<span class="hljs-keyword">const</span> server = serve(
  {
    fetch: app.fetch,
    port: ENV.PORT,
  },
  <span class="hljs-function"><span class="hljs-params">info</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(
      <span class="hljs-string">`Server(<span class="hljs-subst">${ENV.NODE_ENV}</span>) is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>
    );
  }
);

process.on(<span class="hljs-string">'unhandledRejection'</span>, <span class="hljs-function">(<span class="hljs-params">err: <span class="hljs-built_in">Error</span></span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  server.close(<span class="hljs-function">() =&gt;</span> {
    process.exit(<span class="hljs-number">1</span>);
  });
});
</code></pre>
<p>Building secure, resilient APIs is an ongoing process. Stay informed about emerging threats, keep dependencies updated, and continuously refine error handling and security strategies. The patterns and practices covered in this article provide only a foundation for Hono applications. You can find all the code <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/error-handling-security">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Hono: Validation]]></title><description><![CDATA[Validation is a critical aspect of building robust APIs. It ensures that incoming data meets expected criteria before processing, preventing bugs, security vulnerabilities, and unexpected behavior. In this article, we'll explore how to implement vali...]]></description><link>https://blog.raulnq.com/hono-validation</link><guid isPermaLink="true">https://blog.raulnq.com/hono-validation</guid><category><![CDATA[hono]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[zod]]></category><category><![CDATA[Validation]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Tue, 16 Dec 2025 14:00:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765373437742/98194eb2-37b5-45a0-afc3-2d8ee0298752.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Validation is a critical aspect of building robust APIs. It ensures that incoming data meets expected criteria before processing, preventing bugs, security vulnerabilities, and unexpected behavior. In this article, we'll explore how to implement validation in the application built in our previous <a target="_blank" href="https://blog.raulnq.com/hono-routing">post</a> using Zod.</p>
<h2 id="heading-hono-middlewares">Hono Middlewares</h2>
<p>Before diving into validatio<a target="_blank" href="https://blog.raulnq.com/hono-routing">n, i</a>t's essential to understand how Hono's middleware system works, as validators are implemented as middleware. Middleware functions are handlers that execute during the request-response cycle.</p>
<h3 id="heading-what-is-hono-middleware">What is Hono Middleware?</h3>
<p>A middleware is a function that accepts two parameters:</p>
<ul>
<li><p><strong>Context</strong> (<code>c</code>) - Provides access to request data, environment bindings, and response utilities.</p>
</li>
<li><p><strong>Next</strong> (<code>next</code>) - An async function that executes the next middleware in the chain.</p>
</li>
</ul>
<p>The middleware handler interface is defined as:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> MiddlewareHandler = <span class="hljs-function">(<span class="hljs-params">c: Context, next: Next</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;Response | <span class="hljs-built_in">void</span>&gt;
</code></pre>
<h3 id="heading-how-does-middleware-work">How does Middleware Work?</h3>
<p>The execution flow is:</p>
<ul>
<li><p>Each middleware is called in order.</p>
</li>
<li><p>When <code>await next()</code> is called, the next middleware is executed.</p>
</li>
<li><p>After the <code>next()</code> resolves, middleware can perform post-processing.</p>
</li>
<li><p>The chain continues until a response is generated.</p>
</li>
</ul>
<p>Each middleware can:</p>
<ul>
<li><p>Process the incoming request.</p>
</li>
<li><p>Pass control to the next middleware using <code>await next()</code>.</p>
</li>
<li><p>Short-circuit the chain by returning a response.</p>
</li>
<li><p>Modify the context object shared across all handlers.</p>
</li>
</ul>
<h3 id="heading-what-is-middleware-for">What is Middleware For?</h3>
<p>Middleware enables separation of concerns by handling cross-cutting functionality like:</p>
<ul>
<li><p>Authentication</p>
<ul>
<li><p><strong>Basic Auth</strong>: Validates username/password credentials.</p>
</li>
<li><p><strong>Bearer Auth</strong>: Validates bearer tokens.</p>
</li>
<li><p><strong>JWT</strong>: Verifies JSON Web Tokens and stores payload in context.</p>
</li>
</ul>
</li>
<li><p>HTTP Utilities</p>
<ul>
<li><p><strong>CORS</strong>: Handles Cross-Origin Resource Sharing headers.</p>
</li>
<li><p><strong>ETag</strong>: Generates ETags and returns 304 responses for unchanged content.</p>
</li>
<li><p><strong>Cache</strong>: Implements HTTP caching using the Cache API.</p>
</li>
</ul>
</li>
<li><p>Security</p>
<ul>
<li><strong>CSRF</strong>: Protects against Cross-Site Request Forgery attacks.</li>
</ul>
</li>
<li><p>Request Processing</p>
<ul>
<li><strong>Validation</strong>: Validates request data (JSON, form, query, params).</li>
</ul>
</li>
</ul>
<h3 id="heading-how-to-use-middlewares">How to Use <strong>Middlewares?</strong></h3>
<p>A middleware can be applied globally to all routes, to specific path patterns, directly to individual routes, chained together, or through sub-app routing. Each method provides different levels of scope and control over request processing.</p>
<p><strong>Global Application</strong></p>
<p>Apply middleware to all routes using the wildcard pattern:</p>
<pre><code class="lang-typescript">app.use(<span class="hljs-string">'*'</span>, <span class="hljs-keyword">async</span> (c, next) =&gt; {  
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`<span class="hljs-subst">${c.req.method}</span> : <span class="hljs-subst">${c.req.url}</span>`</span>)  
  <span class="hljs-keyword">await</span> next()  
})
</code></pre>
<p><strong>Path-Specific Application</strong></p>
<p>Apply middleware to routes matching a path pattern:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Apply to all routes under /api  </span>
app.use(<span class="hljs-string">'/api/*'</span>, cors())  
<span class="hljs-comment">// Apply to specific route  </span>
app.use(<span class="hljs-string">'/hello'</span>, <span class="hljs-keyword">async</span> (c, next) =&gt; {  
  <span class="hljs-keyword">await</span> next()  
  c.res.headers.append(<span class="hljs-string">'x-message'</span>, <span class="hljs-string">'custom-header'</span>)  
})
</code></pre>
<p><strong>Route-Level Application</strong></p>
<p>Pass middleware directly as arguments to route methods:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Single middleware with handler  </span>
app.get(<span class="hljs-string">'/protected'</span>, authMiddleware, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {  
  <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">'Authorized'</span>)  
})  
<span class="hljs-comment">// Multiple middleware chained  </span>
app.get(<span class="hljs-string">'/api/data'</span>,   
  middleware1,  
  middleware2,  
  <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.json({ data: <span class="hljs-string">'success'</span> })  
)
</code></pre>
<p><strong>Chained Application</strong></p>
<p>Chain multiple middleware calls for the same path:</p>
<pre><code class="lang-typescript">app  
  .use(<span class="hljs-string">'/chained/*'</span>, <span class="hljs-keyword">async</span> (c, next) =&gt; {  
    c.req.raw.headers.append(<span class="hljs-string">'x-before'</span>, <span class="hljs-string">'abc'</span>)  
    <span class="hljs-keyword">await</span> next()  
  })  
  .use(<span class="hljs-keyword">async</span> (c, next) =&gt; {  
    <span class="hljs-keyword">await</span> next()  
    c.header(<span class="hljs-string">'x-after'</span>, c.req.header(<span class="hljs-string">'x-before'</span>))  
  })  
  .get(<span class="hljs-string">'/chained/abc'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {  
    <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">'GET chained'</span>)  
  })
</code></pre>
<p><strong>Sub-Application Routing</strong></p>
<p>Apply middleware through sub-apps using <code>app.route()</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> api = <span class="hljs-keyword">new</span> Hono()  
api.use(<span class="hljs-string">'*'</span>, <span class="hljs-keyword">async</span> (c, next) =&gt; {  
  <span class="hljs-keyword">await</span> next()  
  c.res.headers.append(<span class="hljs-string">'x-custom-a'</span>, <span class="hljs-string">'a'</span>)  
})  

<span class="hljs-keyword">const</span> middleware = <span class="hljs-keyword">new</span> Hono()  
middleware.use(<span class="hljs-string">'*'</span>, <span class="hljs-keyword">async</span> (c, next) =&gt; {  
  <span class="hljs-keyword">await</span> next()  
  c.res.headers.append(<span class="hljs-string">'x-custom-b'</span>, <span class="hljs-string">'b'</span>)  
})  

app.route(<span class="hljs-string">'/api'</span>, middleware)  
app.route(<span class="hljs-string">'/api'</span>, api)
</code></pre>
<h2 id="heading-hono-validation-system">Hono Validation System</h2>
<p>Hono's validation system is built on middleware that intercepts requests, validates specific parts (body, query parameters, headers, path parameters), and either allows the request to proceed or returns validation errors.</p>
<h3 id="heading-honovalidator"><strong>hono/validator</strong></h3>
<p><code>hono/validator</code> is a middleware that validates incoming request data across different targets (JSON body, form data, query parameters, path parameters, headers, and cookies) with full TypeScript type safety.</p>
<p>The <code>validator</code> function creates middleware that:</p>
<ul>
<li><p>Extracts data from a specific request target. The supported targets are:</p>
<ul>
<li><p><strong>Body Validation</strong> (<code>json</code>): Validates JSON request bodies.</p>
</li>
<li><p><strong>Query Parameters</strong> (<code>query</code>): Validates URL query strings.</p>
</li>
<li><p><strong>Path Parameters</strong> (<code>param</code>): Validates route parameters.</p>
</li>
<li><p><strong>Headers</strong> (<code>header</code>): Validates HTTP headers.</p>
</li>
<li><p><strong>Form Parameters</strong> (<code>form</code>): Validates form data from the request body.</p>
</li>
<li><p><strong>Cookies</strong> (<code>cookie</code>): Validates HTTP cookies.</p>
</li>
</ul>
</li>
<li><p>Runs a validation function on the extracted data.</p>
</li>
<li><p>Either returns an error response (short-circuiting) or stores validated data.</p>
</li>
<li><p>Makes validated data accessible via <code>c.req.valid(target)</code>.</p>
</li>
</ul>
<pre><code class="lang-typescript">app.get(<span class="hljs-string">'/search'</span>,     
  validator(<span class="hljs-string">'query'</span>, <span class="hljs-function">(<span class="hljs-params">value, c</span>) =&gt;</span> {    
    <span class="hljs-keyword">if</span> (!value.q) {    
      <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">'Parameter not found'</span>, <span class="hljs-number">400</span>)    
    }    
    <span class="hljs-keyword">return</span> value <span class="hljs-keyword">as</span> { q: <span class="hljs-built_in">string</span> }    
  }),    
  <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {    
    <span class="hljs-keyword">const</span> { q } = c.req.valid(<span class="hljs-string">'query'</span>)
    <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">`Searching <span class="hljs-subst">${q}</span>!!!`</span>, <span class="hljs-number">200</span>)    
  }    
)
</code></pre>
<p>The <code>validator</code> function receives:</p>
<ul>
<li><p><code>value</code>: The extracted data from the target.</p>
</li>
<li><p><code>c</code>: The <code>Context</code> object.</p>
</li>
</ul>
<p>It can return:</p>
<ul>
<li><p><strong>Validated data</strong>: Stored and accessible via <code>c.req.valid()</code>.</p>
</li>
<li><p><strong>A Response object</strong>: Short-circuits the middleware chain with an error response.</p>
</li>
<li><p><strong>Throw an error</strong>: Propagates through the error handler.</p>
</li>
</ul>
<p>Based on this validation system, the ecosystem provides adapters for multiple validation libraries:</p>
<ul>
<li><p><code>@hono/zod-validator</code>: Uses <a target="_blank" href="https://zod.dev/">Zod</a>, supports v3/v4.</p>
</li>
<li><p><code>@hono/valibot-validator</code>: Uses <a target="_blank" href="https://valibot.dev/guides/quick-start/">Valibot</a>, a modular validation approach.</p>
</li>
<li><p><code>@hono/typebox-validator</code>: Uses <a target="_blank" href="https://github.com/sinclairzx81/typebox">TypeBox</a>, JSON Schema compliance.</p>
</li>
<li><p><code>@hono/standard-validator</code><strong>:</strong> Uses <a target="_blank" href="https://standardschema.dev/">Standard Schema V1</a> specification.</p>
</li>
</ul>
<h2 id="heading-implementing-zod-validations"><strong>Implementing Zod Validations</strong></h2>
<p>Let's start by installing the following package:</p>
<pre><code class="lang-powershell">npm install @hono/zod<span class="hljs-literal">-validator</span>
</code></pre>
<p>The validation starts with defining schemas. In the <code>features/stores/store.ts</code> file, we define the store data structure:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> storeSchema = z.object({
  storeId: z.uuidv7(),
  name: z.string().min(<span class="hljs-number">1</span>).max(<span class="hljs-number">1024</span>),
  url: z.url().min(<span class="hljs-number">1</span>).max(<span class="hljs-number">2048</span>),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Store = z.infer&lt;<span class="hljs-keyword">typeof</span> storeSchema&gt;;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> stores: Store[] = [];
</code></pre>
<p>This schema enforces several validation rules:</p>
<ul>
<li><p><code>storeId</code> must be a valid UUIDv7.</p>
</li>
<li><p><code>name</code> must be a non-empty string with a maximum length of 1024 characters.</p>
</li>
<li><p><code>url</code> must be a valid URL with a maximum length of 2048 characters.</p>
</li>
</ul>
<p>The <code>Store</code> type is automatically inferred from the schema, ensuring TypeScript types and runtime validation stay synchronized. The pagination schema in the <code>types/pagination.ts</code> file demonstrates Zod's advanced features:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">const</span> DEFAULT_PAGE_NUMBER = <span class="hljs-number">1</span>;
<span class="hljs-keyword">const</span> DEFAULT_PAGE_SIZE = <span class="hljs-number">10</span>;
<span class="hljs-keyword">const</span> MAX_PAGE_SIZE = <span class="hljs-number">100</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> paginationSchema = z.object({
  pageNumber: z.coerce.number().min(<span class="hljs-number">1</span>).optional().default(DEFAULT_PAGE_NUMBER),
  pageSize: z.coerce
    .number()
    .min(<span class="hljs-number">1</span>)
    .max(MAX_PAGE_SIZE)
    .optional()
    .default(DEFAULT_PAGE_SIZE),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createPage = &lt;TResult&gt;<span class="hljs-function">(<span class="hljs-params">
  items: TResult[],
  totalCount: <span class="hljs-built_in">number</span>,
  pageNumber: <span class="hljs-built_in">number</span>,
  pageSize: <span class="hljs-built_in">number</span>
</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> totalPages = <span class="hljs-built_in">Math</span>.ceil(totalCount / pageSize);
  <span class="hljs-keyword">return</span> {
    items,
    pageNumber,
    pageSize,
    totalPages,
    totalCount,
  };
};
</code></pre>
<p>The <code>z.coerce.number()</code> method automatically converts string query parameters to numbers, which is essential since HTTP query parameters are always strings. The <code>features/stores/add-store.ts</code> file demonstrates body validation:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { v7 } <span class="hljs-keyword">from</span> <span class="hljs-string">'uuid'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>;

<span class="hljs-keyword">const</span> schema = storeSchema.omit({ storeId: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addRoute = <span class="hljs-keyword">new</span> Hono().post(
  <span class="hljs-string">'/'</span>,
  zValidator(<span class="hljs-string">'json'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { name, url } = c.req.valid(<span class="hljs-string">'json'</span>);
    <span class="hljs-keyword">const</span> store = { name, url, storeId: v7() };
    stores.push(store);
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.CREATED);
  }
);
</code></pre>
<ul>
<li><p>Uses <code>storeSchema.omit({ storeId: true })</code> to generate a new schema excluding the <code>storeId</code> property since it's generated server-side.</p>
</li>
<li><p>Validates the JSON request body with the <code>zValidator('json', schema)</code> middleware.</p>
</li>
<li><p>Provides type-safe access to <code>name</code> and <code>url</code> from the validated body.</p>
</li>
</ul>
<p>For retrieving a specific store in the <code>features/stores/get-store.ts</code> file, we validate the path parameter:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>;

<span class="hljs-keyword">const</span> schema = storeSchema.pick({ storeId: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getRoute = <span class="hljs-keyword">new</span> Hono().get(
  <span class="hljs-string">'/:storeId'</span>,
  zValidator(<span class="hljs-string">'param'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { storeId } = c.req.valid(<span class="hljs-string">'param'</span>);
    <span class="hljs-keyword">const</span> store = stores.find(<span class="hljs-function"><span class="hljs-params">s</span> =&gt;</span> s.storeId === storeId);
    <span class="hljs-keyword">if</span> (!store) {
      <span class="hljs-keyword">return</span> c.json(
        { message: <span class="hljs-string">`Store <span class="hljs-subst">${storeId}</span> not found`</span> },
        StatusCodes.NOT_FOUND
      );
    }
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.OK);
  }
);
</code></pre>
<ul>
<li><p><code>storeSchema.pick({ storeId: true })</code> creates a new schema with only the <code>storeId</code> field.</p>
</li>
<li><p>This ensures the URL parameter is a valid UUIDv7.</p>
</li>
<li><p>The validated <code>storeId</code> is type-safe and guaranteed to match the schema.</p>
</li>
</ul>
<p>In the <code>features/stores/list-stores.ts</code> file, we validate query parameters for listing stores:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { paginationSchema, createPage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/pagination.js'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>;

<span class="hljs-keyword">const</span> schema = paginationSchema.extend({
  name: z.string().optional(),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> listRoute = <span class="hljs-keyword">new</span> Hono().get(
  <span class="hljs-string">'/'</span>,
  zValidator(<span class="hljs-string">'query'</span>, schema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { pageNumber, pageSize, name } = c.req.valid(<span class="hljs-string">'query'</span>);
    <span class="hljs-keyword">const</span> pn = pageNumber;
    <span class="hljs-keyword">const</span> pz = pageSize;
    <span class="hljs-keyword">let</span> filteredStores = stores;

    <span class="hljs-keyword">if</span> (name) {
      filteredStores = stores.filter(<span class="hljs-function"><span class="hljs-params">store</span> =&gt;</span>
        store.name.toLowerCase().includes(name.toLowerCase())
      );
    }
    <span class="hljs-keyword">const</span> totalCount = filteredStores.length;
    <span class="hljs-keyword">const</span> startIndex = (pn - <span class="hljs-number">1</span>) * pz;
    <span class="hljs-keyword">const</span> endIndex = startIndex + pz;
    <span class="hljs-keyword">const</span> page = filteredStores.slice(startIndex, endIndex);
    <span class="hljs-keyword">return</span> c.json(createPage(page, totalCount, pn, pz), StatusCodes.OK);
  }
);
</code></pre>
<ul>
<li><p>We extend the paginationSchema with an optional <code>name</code> filter.</p>
</li>
<li><p>The <code>zValidator('query', schema)</code> middleware validates query parameters before the handler executes.</p>
</li>
<li><p><code>c.req.valid('query')</code> provides type-safe access to validated query parameters.</p>
</li>
<li><p>If validation fails, a <code>400</code> error is returned automatically.</p>
</li>
</ul>
<p>The edit store endpoint in the <code>features/stores/edit-store.ts</code> file shows how to validate request parts:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { stores, storeSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>;

<span class="hljs-keyword">const</span> paramSchema = storeSchema.pick({ storeId: <span class="hljs-literal">true</span> });
<span class="hljs-keyword">const</span> bodySchema = storeSchema.pick({ name: <span class="hljs-literal">true</span>, url: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> editRoute = <span class="hljs-keyword">new</span> Hono().put(
  <span class="hljs-string">'/:storeId'</span>,
  zValidator(<span class="hljs-string">'param'</span>, paramSchema),
  zValidator(<span class="hljs-string">'json'</span>, bodySchema),
  <span class="hljs-keyword">async</span> c =&gt; {
    <span class="hljs-keyword">const</span> { storeId } = c.req.valid(<span class="hljs-string">'param'</span>);
    <span class="hljs-keyword">const</span> { name, url } = c.req.valid(<span class="hljs-string">'json'</span>);
    <span class="hljs-keyword">const</span> store = stores.find(<span class="hljs-function"><span class="hljs-params">s</span> =&gt;</span> s.storeId === storeId);
    <span class="hljs-keyword">if</span> (!store) {
      <span class="hljs-keyword">return</span> c.json(
        { message: <span class="hljs-string">`Store <span class="hljs-subst">${storeId}</span> not found`</span> },
        StatusCodes.NOT_FOUND
      );
    }
    store.name = name;
    store.url = url;
    <span class="hljs-keyword">return</span> c.json(store, StatusCodes.OK);
  }
);
</code></pre>
<p>Multiple validators can be chained, each validating a different part of the request. The order matters: validators execute in the order they're declared.</p>
<p>The combination of Hono's middleware system and Zod's type-safe validation provides a powerful foundation for building robust APIs with minimal boilerplate. By mastering validation patterns, we'll build APIs that are more secure, maintainable, and easier to work with for both our team and API consumers. You can find all the code <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/validation">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Hono: Routing]]></title><description><![CDATA[Hono is a fast web framework built on Web Standards that provides a powerful routing system with excellent TypeScript support. Its routing system provides an intuitive API for building RESTful applications while maintaining exceptional performance. T...]]></description><link>https://blog.raulnq.com/hono-routing</link><guid isPermaLink="true">https://blog.raulnq.com/hono-routing</guid><category><![CDATA[hono]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[routing]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Tue, 09 Dec 2025 22:52:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765293514543/4709fd68-4aab-4447-93e1-9947531ba95b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://hono.dev/">Hono</a> is a fast web framework built on Web Standards that provides a powerful routing system with excellent TypeScript support. Its routing system provides an intuitive API for building RESTful applications while maintaining exceptional performance. This article explores Hono's routing mechanisms, from the core application objects to request handling, ending with a practical implementation.</p>
<h2 id="heading-the-hono-application-object">The Hono Application Object</h2>
<p>The <a target="_blank" href="https://hono.dev/docs/api/hono"><code>Hono</code></a> class serves as the foundation of every Hono application. It encapsulates the application's routing table, middleware stack, and request handling logic.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>  
<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono()
</code></pre>
<p>The <code>Hono</code> constructor accepts a single optional parameter, including the following properties:</p>
<ul>
<li><p><code>strict</code><strong>:</strong> Controls strict mode for path matching (distinguishes <code>/path</code> from <code>/path/</code>). The default value is <code>true</code>.</p>
</li>
<li><p><code>router</code><strong>:</strong> Specifies which router to use. The default router is <code>SmartRouter</code>.</p>
</li>
<li><p><code>getPath</code><strong>:</strong> Custom function to extract path from request.</p>
</li>
</ul>
<p>The <code>Hono</code> class also has three generic type parameters:</p>
<ul>
<li><p><code>E</code>: Defines the shape of environment-specific types for our application. Uses <code>BlankEnv</code> by default. The environment type extends from the <code>Env</code> type and defines two properties:</p>
<ul>
<li><p><code>Bindings</code>: Access platform-specific resources, with full type safety, via the <code>c.env</code> variable.</p>
</li>
<li><p><code>Variables</code>: Share typed data between middleware and handlers via <code>c.set()</code> and <code>c.get()</code>.</p>
</li>
</ul>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> MyEnv = {  
  Variables: {  
    foo: <span class="hljs-built_in">string</span>  
  }  
  Bindings: {  
    flag: <span class="hljs-built_in">boolean</span>  
  }  
}  
<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono&lt;MyEnv&gt;()  

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {  
  <span class="hljs-keyword">const</span> foo = c.get(<span class="hljs-string">'foo'</span>) 
  <span class="hljs-keyword">const</span> flag = c.env.flag  
  ...
})
</code></pre>
<ul>
<li><code>S</code>: Enables automatic type inference for API validation and type-safe client generation. Uses <code>BlankSchema</code> by default. The schema type extends from the <code>Schema</code> type. When we add routes, Hono's type system automatically constructs the schema using the <code>ToSchema</code> type. Therefore, defining a schema type is not common.</li>
</ul>
<ul>
<li><code>BasePath</code>: Enforces type safety for base path routing. Uses <code>/</code> by default.</li>
</ul>
<p>These generics parameters enable Hono's compile-time type-safety and automatic client generation.</p>
<h2 id="heading-hono-routing-system">Hono Routing System</h2>
<p>Hono's <a target="_blank" href="https://hono.dev/docs/api/routing">routing</a> system matches incoming HTTP requests to registered handlers based on the request method and URL path. The router supports static paths, dynamic parameters, and wildcard patterns.</p>
<h3 id="heading-route-registration">Route Registration</h3>
<p>In Hono, we can register routes using several methods that are implemented in the <code>Hono</code> class:</p>
<p><strong>HTTP Method Handlers</strong></p>
<p>The most common way is using HTTP method handlers (<code>get</code>, <code>post</code>, <code>put</code>, <code>delete</code>, <code>options</code>, <code>patch</code>, <code>all</code>).</p>
<pre><code class="lang-typescript">app.get(<span class="hljs-string">'/api'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.text(<span class="hljs-string">'Hello'</span>))
</code></pre>
<p><strong>Custom Methods with</strong> <code>on()</code></p>
<p>Register handlers for custom or multiple HTTP methods.</p>
<pre><code class="lang-typescript">app.on([<span class="hljs-string">'GET'</span>, <span class="hljs-string">'POST'</span>], <span class="hljs-string">'/api'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.text(<span class="hljs-string">'Hello'</span>))
</code></pre>
<p><strong>Middleware with</strong> <code>use()</code></p>
<p>Register middleware that runs before route handlers.</p>
<pre><code class="lang-typescript">app.use(<span class="hljs-string">'/api'</span>, middleware)
</code></pre>
<p><strong>Mounting Sub-applications with</strong> <code>route()</code></p>
<p>Mount another Hono instance under a specific path.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> userApp = <span class="hljs-keyword">new</span> Hono()  
userApp.get(<span class="hljs-string">'/users'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.text(<span class="hljs-string">'Hello'</span>))
<span class="hljs-comment">// Mount under /api/v1  </span>
app.route(<span class="hljs-string">'/api/v1'</span>, userApp)
</code></pre>
<p><strong>Base Path with</strong> <code>basePath()</code></p>
<p>Create a new Hono instance with a base path prefix.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> api = <span class="hljs-keyword">new</span> Hono().basePath(<span class="hljs-string">'/api'</span>)  
api.get(<span class="hljs-string">'/users'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.text(<span class="hljs-string">'Hello'</span>))
<span class="hljs-comment">// Results in: /api/users</span>
</code></pre>
<h3 id="heading-route-matching">Route Matching</h3>
<p>Routes are matched using the router's <code>match()</code> method. Hono supports:</p>
<ul>
<li><p>Static routes: <code>/users</code></p>
</li>
<li><p>Parameter routes: <code>/users/:id</code></p>
</li>
<li><p>Wildcard routes: <code>/api/*</code></p>
</li>
<li><p>Optional parameters: <code>/posts/:id?</code></p>
</li>
<li><p>Custom regex patterns: <code>/files/:name{.*}</code></p>
</li>
</ul>
<h3 id="heading-route-matching-priority">Route Matching Priority</h3>
<p>Hono evaluates routes in the order they are defined. More specific routes should be registered before generic ones.</p>
<h3 id="heading-request-processing">Request Processing</h3>
<p>When a request hits our application:</p>
<ol>
<li><p><strong>Path Extraction</strong>: Hono extracts the path from the request URL.</p>
</li>
<li><p><strong>Route Matching</strong>: The router finds matching handlers based on method and path.</p>
</li>
<li><p><strong>Context Creation</strong>: A <code>Context</code> object is created with request data.</p>
</li>
<li><p><strong>Handler Execution</strong>: Our code runs with access to the <code>Context</code> object.</p>
</li>
</ol>
<h2 id="heading-the-context-object">The Context Object</h2>
<p>The <a target="_blank" href="https://hono.dev/docs/api/context"><code>Context</code></a> class is the central hub for handling individual requests, providing access to request data, response helpers, and variable storage.</p>
<h3 id="heading-properties">Properties</h3>
<ul>
<li><p><code>req</code>: Provides access to the <code>HonoRequest</code> object containing request data.</p>
</li>
<li><p><code>env</code>: Provide access to the environment bindings properties.</p>
</li>
<li><p><code>error</code>: Error object if the handler threw an error.</p>
</li>
<li><p><code>res</code><strong>:</strong> Provides access to the Response object.</p>
</li>
</ul>
<h3 id="heading-methods">Methods</h3>
<ul>
<li>The <code>Context</code> class provides type-safe response helpers:</li>
</ul>
<pre><code class="lang-typescript">app.get(<span class="hljs-string">'/text'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.text(<span class="hljs-string">'Hello'</span>, <span class="hljs-number">200</span>))  
app.get(<span class="hljs-string">'/json'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.json({ message: <span class="hljs-string">'Hello'</span> }, <span class="hljs-number">201</span>))  
app.get(<span class="hljs-string">'/html'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.html(<span class="hljs-string">'&lt;h1&gt;Hello&lt;/h1&gt;'</span>))  
app.get(<span class="hljs-string">'/redirect'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.redirect(<span class="hljs-string">'/target'</span>))
</code></pre>
<ul>
<li><p><code>status</code><strong>:</strong> Sets the response HTTP status code.</p>
</li>
<li><p><code>header</code>: Sets HTTP headers for the response.</p>
</li>
</ul>
<h3 id="heading-variable-storage">Variable Storage</h3>
<p>The <code>Context</code> class provides type-safe variable storage through <code>set()</code> and <code>get()</code> methods:</p>
<pre><code class="lang-typescript">app.use(<span class="hljs-string">'*'</span>, <span class="hljs-keyword">async</span> (c, next) =&gt; {  
  c.set(<span class="hljs-string">'startTime'</span>, <span class="hljs-built_in">Date</span>.now())  
  <span class="hljs-keyword">await</span> next()  
  <span class="hljs-keyword">const</span> duration = <span class="hljs-built_in">Date</span>.now() - c.get(<span class="hljs-string">'startTime'</span>)  
  c.header(<span class="hljs-string">'X-Duration'</span>, duration.toString())  
})
</code></pre>
<p>We can also access the value of a variable with <code>c.var</code>.</p>
<h2 id="heading-the-honorequest-object">The HonoRequest Object</h2>
<p>The <a target="_blank" href="https://hono.dev/docs/api/request"><code>HonoRequest</code></a> class wraps the raw <code>Request</code> object, providing convenient accessors for request data.</p>
<ul>
<li><p><code>path</code><strong>:</strong> The pathname of the request (without query string).</p>
</li>
<li><p><code>method</code><strong>:</strong> HTTP method of the request.</p>
</li>
<li><p><code>url</code>: The full request URL.</p>
</li>
<li><p><code>raw</code>: The raw <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Request"><code>Request</code></a> object.</p>
</li>
</ul>
<h3 id="heading-parameter-access">Parameter Access</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// Single parameter  </span>
<span class="hljs-keyword">const</span> id = c.req.param(<span class="hljs-string">'id'</span>)  
<span class="hljs-comment">// All parameters as object  </span>
<span class="hljs-keyword">const</span> { id, name } = c.req.param()
</code></pre>
<h3 id="heading-query-string-handling">Query String Handling</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// Single query value  </span>
<span class="hljs-keyword">const</span> page = c.req.query(<span class="hljs-string">'page'</span>)  
<span class="hljs-comment">// All queries as object  </span>
<span class="hljs-keyword">const</span> { page, limit } = c.req.query()    
<span class="hljs-comment">// Multiple values for same key  </span>
<span class="hljs-keyword">const</span> tags = c.req.queries(<span class="hljs-string">'tags'</span>) <span class="hljs-comment">// string[]</span>
</code></pre>
<h3 id="heading-body-parsing">Body Parsing</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// JSON body  </span>
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> c.req.json&lt;T&gt;()  
<span class="hljs-comment">// Text body  </span>
<span class="hljs-keyword">const</span> text = <span class="hljs-keyword">await</span> c.req.text()  
<span class="hljs-comment">// Form data (multipart or urlencoded)  </span>
<span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">await</span> c.req.parseBody()  
<span class="hljs-comment">// Raw body methods  </span>
<span class="hljs-keyword">const</span> arrayBuffer = <span class="hljs-keyword">await</span> c.req.arrayBuffer()  
<span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">await</span> c.req.blob()  
<span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">await</span> c.req.formData()
</code></pre>
<h3 id="heading-headers">Headers</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// Single header  </span>
<span class="hljs-keyword">const</span> userAgent = c.req.header(<span class="hljs-string">'User-Agent'</span>)  
<span class="hljs-comment">// All headers as object  </span>
<span class="hljs-keyword">const</span> headers = c.req.header()
</code></pre>
<h2 id="heading-building-an-application">Building an Application</h2>
<p>We'll implement a complete API for tracking price changes. This demonstrates practical routing patterns and context usage. This application will evolve as we write new articles about Hono. The starting point will be the project setup we built in the post <a target="_blank" href="https://blog.raulnq.com/hono-setting-up-the-development-environment">Hono: Setting up the development environment</a>. Let’s start by instaling the following packages:</p>
<pre><code class="lang-powershell">npm install uuid
npm install http<span class="hljs-literal">-status</span><span class="hljs-literal">-codes</span>
</code></pre>
<p>Create the file <code>features/stores/store.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Store = {
  storeId: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  url: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> stores: Store[] = [];
</code></pre>
<ul>
<li><p><code>Store</code> defines the TypeScript type for store objects.</p>
</li>
<li><p><code>stores</code> is a module-level array serving as in-memory storage.</p>
</li>
</ul>
<p>Create the file <code>features/stores/add-stores.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { v7 } <span class="hljs-keyword">from</span> <span class="hljs-string">'uuid'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;
<span class="hljs-keyword">import</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addRoute = <span class="hljs-keyword">new</span> Hono().post(<span class="hljs-string">'/'</span>, <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-keyword">const</span> { name, url } = <span class="hljs-keyword">await</span> c.req.json();
  <span class="hljs-keyword">const</span> store = { name, url, storeId: v7() };
  stores.push(store);
  <span class="hljs-keyword">return</span> c.json(store, StatusCodes.CREATED);
});
</code></pre>
<p>The <code>addRoute</code> handler in <code>add-store.ts</code> accepts JSON payloads and creates new stores.</p>
<ul>
<li><p><code>c.req.json()</code> parses the request body as JSON.</p>
</li>
<li><p><code>v7()</code> from the <code>uuid</code> package generates a UUID v7 identifier.</p>
</li>
<li><p><code>stores.push(store)</code> adds the new store to the in-memory <code>stores</code> array.</p>
</li>
<li><p><code>c.json(store, StatusCodes.CREATED)</code> returns the created store with a <code>201</code> status code.</p>
</li>
</ul>
<p>Create the file <code>features/stores/get-store.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getRoute = <span class="hljs-keyword">new</span> Hono().get(<span class="hljs-string">'/:storeId'</span>, <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-keyword">const</span> storeId = c.req.param(<span class="hljs-string">'storeId'</span>);
  <span class="hljs-keyword">const</span> store = stores.find(<span class="hljs-function"><span class="hljs-params">s</span> =&gt;</span> s.storeId === storeId);
  <span class="hljs-keyword">if</span> (!store) {
    <span class="hljs-keyword">return</span> c.json(
      { message: <span class="hljs-string">`Store <span class="hljs-subst">${storeId}</span> not found`</span> },
      StatusCodes.NOT_FOUND
    );
  }
  <span class="hljs-keyword">return</span> c.json(store, StatusCodes.OK);
});
</code></pre>
<p>The getRoute handler in <code>get-store.ts</code> fetches stores by ID.</p>
<ul>
<li>Returns <code>StatusCodes.NOT_FOUND</code> (<code>404</code>) when the store doesn't exist.</li>
</ul>
<p>Create the file <code>features/stores/list-stores.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> listRoute = <span class="hljs-keyword">new</span> Hono().get(<span class="hljs-string">'/'</span>, <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-keyword">const</span> { pageNumber, pageSize, name } = c.req.query();
  <span class="hljs-keyword">const</span> pn = <span class="hljs-built_in">parseInt</span>(pageNumber || <span class="hljs-string">'1'</span>, <span class="hljs-number">10</span>);
  <span class="hljs-keyword">const</span> pz = <span class="hljs-built_in">parseInt</span>(pageSize || <span class="hljs-string">'10'</span>, <span class="hljs-number">10</span>);
  <span class="hljs-keyword">let</span> filteredStores = stores;
  <span class="hljs-keyword">if</span> (name) {
    filteredStores = stores.filter(<span class="hljs-function"><span class="hljs-params">store</span> =&gt;</span>
      store.name.toLowerCase().includes(name.toLowerCase())
    );
  }
  <span class="hljs-keyword">const</span> totalCount = filteredStores.length;
  <span class="hljs-keyword">const</span> startIndex = (pn - <span class="hljs-number">1</span>) * pz;
  <span class="hljs-keyword">const</span> endIndex = startIndex + pz;
  <span class="hljs-keyword">const</span> page = filteredStores.slice(startIndex, endIndex);
  <span class="hljs-keyword">return</span> c.json(
    {
      items: page,
      pageNumber: pn,
      pageSize: pz,
      totalPages: <span class="hljs-built_in">Math</span>.ceil(totalCount / pz),
      totalCount,
    },
    StatusCodes.OK
  );
});
</code></pre>
<p>The listRoute handler in <code>list-stores.ts</code> supports pagination and filtering.</p>
<ul>
<li><p><code>c.req.query()</code> retrieves all query parameters as an object.</p>
</li>
<li><p>The handler implements pagination with <code>pageNumber</code> and <code>pageSize</code> parameters.</p>
</li>
<li><p>Case-insensitive filtering is applied when the <code>name</code> parameter is provided.</p>
</li>
<li><p>Returns pagination metadata for client consumption.</p>
</li>
</ul>
<p>Create the file <code>features/stores/edit-store.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { stores } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>;
<span class="hljs-keyword">import</span> { StatusCodes } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-status-codes'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> editRoute = <span class="hljs-keyword">new</span> Hono().put(<span class="hljs-string">'/:storeId'</span>, <span class="hljs-keyword">async</span> c =&gt; {
  <span class="hljs-keyword">const</span> storeId = c.req.param(<span class="hljs-string">'storeId'</span>);
  <span class="hljs-keyword">const</span> { name, url } = <span class="hljs-keyword">await</span> c.req.json();
  <span class="hljs-keyword">const</span> store = stores.find(<span class="hljs-function"><span class="hljs-params">s</span> =&gt;</span> s.storeId === storeId);
  <span class="hljs-keyword">if</span> (!store) {
    <span class="hljs-keyword">return</span> c.json(
      { message: <span class="hljs-string">`Store <span class="hljs-subst">${storeId}</span> not found`</span> },
      StatusCodes.NOT_FOUND
    );
  }
  store.name = name;
  store.url = url;
  <span class="hljs-keyword">return</span> c.json(store, StatusCodes.OK);
});
</code></pre>
<ul>
<li><p>Combines route parameters (<code>storeId</code>) and request body (<code>name</code>, <code>url</code>).</p>
</li>
<li><p>Mutates the existing <code>store</code> object directly in the <code>stores</code> array.</p>
</li>
<li><p>Returns <code>404</code> if the store doesn't exist.</p>
</li>
</ul>
<p>As we mentioned, Hono supports composing applications from smaller, feature-focused sub-applications. Create the file <code>features/stores/index.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { listRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./list-stores.js'</span>;
<span class="hljs-keyword">import</span> { addRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./add-store.js'</span>;
<span class="hljs-keyword">import</span> { getRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./get-store.js'</span>;
<span class="hljs-keyword">import</span> { editRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./edit-store.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> storeRoute = <span class="hljs-keyword">new</span> Hono()
  .basePath(<span class="hljs-string">'/stores'</span>)
  .route(<span class="hljs-string">'/'</span>, listRoute)
  .route(<span class="hljs-string">'/'</span>, addRoute)
  .route(<span class="hljs-string">'/'</span>, getRoute)
  .route(<span class="hljs-string">'/'</span>, editRoute);
</code></pre>
<ul>
<li><p>Each feature (<code>list</code>, <code>add</code>, <code>get</code>, <code>edit</code>) has its own Hono instance.</p>
</li>
<li><p><code>route('/', subRoute)</code> mounts multiple sub-routes at the same base path.</p>
</li>
<li><p>Hono automatically merges routes based on HTTP methods and path patterns.</p>
</li>
</ul>
<p>Create the <code>app.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { storeRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/stores/index.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono({ strict: <span class="hljs-literal">false</span> });

app.route(<span class="hljs-string">'/api'</span>, storeRoute);

app.get(<span class="hljs-string">'/live'</span>, <span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span>
  c.json({
    status: <span class="hljs-string">'healthy'</span>,
    uptime: process.uptime(),
    timestamp: <span class="hljs-built_in">Date</span>.now(),
  })
);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> App = <span class="hljs-keyword">typeof</span> app;
</code></pre>
<ul>
<li><p><code>new Hono({ strict: false })</code> creates a Hono instance with non-strict routing, allowing trailing slashes in URLs to be ignored.</p>
</li>
<li><p><code>app.route()</code> mounts sub-applications at specific base paths, enabling modular route organization.</p>
</li>
<li><p><code>app.get()</code> registers a simple health check endpoint.</p>
</li>
</ul>
<p>Update the <code>index.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>;
<span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/env.js'</span>;
<span class="hljs-keyword">import</span> { app } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.js'</span>;

serve(
  {
    fetch: app.fetch,
    port: ENV.PORT,
  },
  <span class="hljs-function"><span class="hljs-params">info</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(
      <span class="hljs-string">`Server(<span class="hljs-subst">${ENV.NODE_ENV}</span>) is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>
    );
  }
);
</code></pre>
<ul>
<li><p><code>@hono/node-server</code> provides the Node.js adapter for Hono.</p>
</li>
<li><p><code>app.fetch</code> is Hono's standard request handler compatible with the Fetch API.</p>
</li>
</ul>
<p>The API demonstrated in this article showcases Hono's core routing capabilities, including:</p>
<ul>
<li><p>Route definition and parameter extraction</p>
</li>
<li><p>Request body parsing and response formatting</p>
</li>
<li><p>Modular route organization with sub-applications</p>
</li>
</ul>
<p>You can find all the code <a target="_blank" href="https://github.com/raulnq/price-tracker/tree/routing">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[All You Need to Know About Axios and Interceptors]]></title><description><![CDATA[Axios is one of the most popular HTTP client libraries in the JavaScript ecosystem. While the native Fetch API has become more powerful, Axios continues to offer a rich feature set that simplifies common HTTP tasks. One of its most powerful features ...]]></description><link>https://blog.raulnq.com/all-you-need-to-know-about-axios-and-interceptors</link><guid isPermaLink="true">https://blog.raulnq.com/all-you-need-to-know-about-axios-and-interceptors</guid><category><![CDATA[axios]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[interceptors]]></category><category><![CDATA[axios-interceptor]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Mon, 01 Dec 2025 01:13:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764507471078/d8f3666b-d446-4a14-9193-8b92bc33b656.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Axios is one of the most popular HTTP client libraries in the JavaScript ecosystem. While the native Fetch API has become more powerful, Axios continues to offer a rich feature set that simplifies common HTTP tasks. One of its most powerful features is interceptors, which allow us to intercept and modify requests and responses before they reach our application code.</p>
<p>This article explores Axios interceptors in depth, from basic concepts to advanced implementations using popular interceptor libraries. By the end, we'll understand how to leverage interceptors to add logging, automatic retries, authentication, and caching to our HTTP layer.</p>
<h2 id="heading-express-api-server">Express API Server</h2>
<p>First, let's create a realistic API server that simulates various scenarios to test our interceptors. Run the command <code>npm init</code> and create the <code>server.js</code> file with the following content:</p>
<pre><code class="lang-javascript">app.get(<span class="hljs-string">'/api/success'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.json({ 
    <span class="hljs-attr">message</span>: <span class="hljs-string">'OK'</span>,
    <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()
  });
});

app.get(<span class="hljs-string">'/api/fail'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'NO OK'</span> });
});

app.get(<span class="hljs-string">'/api/delay'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> seconds = <span class="hljs-built_in">parseInt</span>(req.query.seconds) || <span class="hljs-number">1</span>;
  <span class="hljs-keyword">const</span> delay = seconds * <span class="hljs-number">1000</span>;

  <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, delay));

  res.json({ 
    <span class="hljs-attr">message</span>: <span class="hljs-string">'OK'</span>
  });
});

app.get(<span class="hljs-string">'/api/random-fail'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> failureRate = <span class="hljs-built_in">parseInt</span>(req.query.percentage) || <span class="hljs-number">50</span>;
  <span class="hljs-keyword">const</span> randomValue = <span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">100</span>;
  <span class="hljs-keyword">if</span> (randomValue &lt; failureRate) {
    res.status(<span class="hljs-number">500</span>).json({ 
      <span class="hljs-attr">error</span>: <span class="hljs-string">'NO OK'</span>,
    });
  } <span class="hljs-keyword">else</span> {
    res.json({ 
      <span class="hljs-attr">message</span>: <span class="hljs-string">'OK'</span>,
      <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()
    });
  }
});

app.get(<span class="hljs-string">'/api/protected'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> header = req.headers.authorization;

  <span class="hljs-keyword">if</span> (!header) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Unauthorized'</span> });
  }

  <span class="hljs-keyword">const</span> token = header.replace(<span class="hljs-string">'Bearer '</span>, <span class="hljs-string">''</span>);

  <span class="hljs-keyword">if</span> (token !== <span class="hljs-string">'ABC'</span>) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Unauthorized'</span> });
  }

  res.json({ 
    <span class="hljs-attr">message</span>: <span class="hljs-string">'OK'</span>,
    <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()
  });
});
</code></pre>
<p>We can start the server at any time by running the command <code>node server.js</code>.</p>
<h2 id="heading-what-is-axios">What is Axios?</h2>
<p><a target="_blank" href="https://axios-http.com/docs/intro">Axios</a> is a promise-based HTTP client for the browser and Node.js. It provides a simple and intuitive API for making HTTP requests with built-in support for features that would require additional code with native solutions. Axios provides features like:</p>
<ul>
<li><p><strong>Promise-based API</strong>: Clean async/await syntax support</p>
</li>
<li><p><strong>Automatic JSON transformation</strong>: Requests and responses are automatically converted</p>
</li>
<li><p><strong>Request/Response interceptors</strong>: Modify requests or responses globally</p>
</li>
<li><p><strong>Request cancellation</strong>: Built-in support for aborting requests</p>
</li>
<li><p><strong>Timeout configuration</strong>: Set time limits for requests</p>
</li>
<li><p><strong>And more…</strong></p>
</li>
</ul>
<p>Install Axios by running the command:</p>
<pre><code class="lang-powershell">npm install axios
</code></pre>
<p>Here's a simple example that demonstrates the basics of Axios:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/success'</span>);
    <span class="hljs-built_in">console</span>.log(response.data);
}

run();
</code></pre>
<p>Axios offers several configuration options, which we can find <a target="_blank" href="https://axios-http.com/docs/req_config">here</a>. The most commonly used Axios parameters include:</p>
<ul>
<li><p><code>url</code>: The server URL (required for all requests).</p>
</li>
<li><p><code>method</code>: HTTP method (defaults to <code>get</code>).</p>
</li>
<li><p><code>baseURL</code>: Base URL for relative URLs (commonly set for API clients).</p>
</li>
<li><p><code>data</code>: Request body data for <code>POST</code>, <code>PUT</code>, and <code>PATCH</code>.</p>
</li>
<li><p><code>params</code>: URL query parameters.</p>
</li>
<li><p><code>headers</code>: Custom headers (frequently used for auth, content-type).</p>
</li>
<li><p><code>auth</code>: HTTP Basic authentication.</p>
</li>
<li><p><code>timeout</code>: Request timeout in milliseconds.</p>
</li>
<li><p><code>responseType</code>: Expected response format (<code>json</code>, <code>text</code>, etc.).</p>
</li>
<li><p><code>validateStatus</code>: Custom status code validation.</p>
</li>
</ul>
<h2 id="heading-axios-vs-fetch-api">Axios vs Fetch API</h2>
<p>Understanding the differences between Axios and the native Fetch API helps us make informed decisions about which tool to use.</p>
<h3 id="heading-fetch-api">Fetch API</h3>
<p>The <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a> is the native JavaScript solution for making HTTP requests, available in modern browsers and Node.js (v18+).</p>
<p><strong>Pros:</strong></p>
<ul>
<li><p><strong>No dependencies</strong>: Built into the platform, no installation required.</p>
</li>
<li><p><strong>Smaller bundle size</strong>: No additional library weight.</p>
</li>
<li><p><strong>Modern standard</strong>: Part of the web platform standard.</p>
</li>
<li><p><strong>Stream support</strong>: Native support for readable streams.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p><strong>No timeout support</strong>: Requires manual implementation with AbortController.</p>
</li>
<li><p><strong>Manual JSON parsing</strong>: Must call <code>.json()</code> on responses.</p>
</li>
<li><p><strong>No automatic error handling</strong>: Network errors only; HTTP errors (4xx, 5xx) don't reject.</p>
</li>
<li><p><strong>Verbose error checking</strong>: Must check the <code>response.ok</code> manually.</p>
</li>
<li><p><strong>No interceptors</strong>: Requires wrapper functions for global behavior.</p>
</li>
<li><p><strong>No upload progress</strong>: Cannot track upload progress easily.</p>
</li>
</ul>
<h3 id="heading-axios">Axios</h3>
<p><strong>Pros:</strong></p>
<ul>
<li><p><strong>Built-in timeout</strong>: Simple timeout configuration.</p>
</li>
<li><p><strong>Automatic JSON handling</strong>: Automatic request/response transformation.</p>
</li>
<li><p><strong>Better error handling</strong>: HTTP errors automatically reject promises.</p>
</li>
<li><p><strong>Interceptors</strong>: Powerful request/response modification.</p>
</li>
<li><p><strong>Request cancellation</strong>: Clean API for aborting requests.</p>
</li>
<li><p><strong>Progress tracking</strong>: Built-in upload/download progress events.</p>
</li>
<li><p><strong>Backward compatibility</strong>: Works in older browsers with polyfills.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p><strong>External dependency</strong>: Adds ~13KB to our bundle (minified).</p>
</li>
<li><p><strong>Additional maintenance</strong>: Depends on third-party library updates.</p>
</li>
<li><p><strong>Learning curve</strong>: Additional API concepts to understand.</p>
</li>
</ul>
<h3 id="heading-common-use-cases"><strong>Common Use Cases</strong></h3>
<ul>
<li><p><strong>Choose Axios when:</strong> We need interceptors, automatic error handling, timeout support, or are building a complex application with consistent HTTP patterns.</p>
</li>
<li><p><strong>Choose Fetch when:</strong> We want zero dependencies, need advanced streaming capabilities, or are building a simple application with minimal HTTP requirements.</p>
</li>
</ul>
<h2 id="heading-interceptors">Interceptors</h2>
<p><a target="_blank" href="https://axios-http.com/docs/interceptors">Interceptors</a> are functions that Axios calls for every request or response. They allow us to modify requests before they're sent or process responses before they reach our application code. Think of them as middleware for our HTTP client.</p>
<h3 id="heading-types-of-interceptors">Types of Interceptors</h3>
<p><strong>Request Interceptors</strong>: Execute before a request is sent to the server. Common uses include:</p>
<ul>
<li><p>Adding authentication tokens</p>
</li>
<li><p>Logging requests</p>
</li>
<li><p>Modifying headers</p>
</li>
<li><p>Transforming request data</p>
</li>
</ul>
<p><strong>Response Interceptors</strong>: Execute after a response is received but before it reaches our application. Common uses include:</p>
<ul>
<li><p>Handling errors globally</p>
</li>
<li><p>Logging responses</p>
</li>
<li><p>Caching responses</p>
</li>
</ul>
<p>Here's a basic example demonstrating both request and response interceptors:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

client.interceptors.request.use(
  <span class="hljs-function">(<span class="hljs-params">config</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request:'</span>, config.method.toUpperCase(), config.url);
    config.metadata = { <span class="hljs-attr">startTime</span>: <span class="hljs-built_in">Date</span>.now() };   
    <span class="hljs-keyword">return</span> config;
  },
  <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Request error:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error);
  }
);

client.interceptors.response.use(
  <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> duration = <span class="hljs-built_in">Date</span>.now() - response.config.metadata.startTime;
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response:'</span>, response.status, <span class="hljs-string">`(<span class="hljs-subst">${duration}</span>ms)`</span>);
    <span class="hljs-keyword">return</span> response;
  },
  <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (error.response) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Response error:'</span>, error.response.status);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error.request) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'No response received'</span>);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Request setup error:'</span>, error.message);
    }
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error);
  }
);

run();
</code></pre>
<p>In this example, the request interceptor logs outgoing requests and adds metadata for timing. The response interceptor calculates request duration and handles errors consistently across the application.</p>
<h3 id="heading-interceptor-execution-order">Interceptor Execution Order</h3>
<p>Understanding how interceptors execute is crucial for implementing complex behaviors. Axios processes interceptors in a specific order that resembles a middleware chain.</p>
<p><strong>Request Interceptor Order</strong></p>
<p>Request interceptors execute in <strong>reverse order</strong> of registration. The last registered interceptor runs first. This reverse order means that interceptors registered later have priority and can modify the configuration before earlier interceptors see it. This is useful when we want to override default behaviors.</p>
<p><strong>Response Interceptor Order</strong></p>
<p>Response interceptors are executed in the order of registration. The first registered interceptor runs first.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

client.interceptors.request.use(<span class="hljs-function">(<span class="hljs-params">config</span>)=&gt;</span>{
  <span class="hljs-built_in">console</span>.info(<span class="hljs-string">'logging interceptor'</span>);
  <span class="hljs-keyword">return</span> config;
}, <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>.reject(error));

client.interceptors.request.use(<span class="hljs-function">(<span class="hljs-params">config</span>)=&gt;</span>{
  <span class="hljs-built_in">console</span>.info(<span class="hljs-string">'Request interceptor 2'</span>);
  <span class="hljs-keyword">return</span> config;
}, <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>.reject(error));

client.interceptors.request.use(<span class="hljs-function">(<span class="hljs-params">config</span>)=&gt;</span>{
  <span class="hljs-built_in">console</span>.info(<span class="hljs-string">'Request interceptor 3'</span>);
  <span class="hljs-keyword">return</span> config;
}, <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>.reject(error));

client.interceptors.response.use(<span class="hljs-function">(<span class="hljs-params">response</span>)=&gt;</span>{
  <span class="hljs-built_in">console</span>.info(<span class="hljs-string">'Response interceptor 1'</span>);
  <span class="hljs-keyword">return</span> response;
}, <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>.reject(error));

client.interceptors.response.use(<span class="hljs-function">(<span class="hljs-params">response</span>)=&gt;</span>{
  <span class="hljs-built_in">console</span>.info(<span class="hljs-string">'Response interceptor 2'</span>);
  <span class="hljs-keyword">return</span> response;
}, <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>.reject(error));

client.interceptors.response.use(<span class="hljs-function">(<span class="hljs-params">response</span>)=&gt;</span>{
  <span class="hljs-built_in">console</span>.info(<span class="hljs-string">'Response interceptor 3'</span>);
  <span class="hljs-keyword">return</span> response;
}, <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>.reject(error));


<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/success'</span>);
    <span class="hljs-built_in">console</span>.log(response.data);
}

run();
</code></pre>
<p>In the example above, the request/response cycle follows this pattern:</p>
<pre><code class="lang-javascript">Request Interceptor <span class="hljs-number">3</span> (last registered)
↓
Request Interceptor <span class="hljs-number">2</span>
↓
Request Interceptor <span class="hljs-number">1</span> (first registered)
↓
HTTP Request sent to server
↓
HTTP Response received <span class="hljs-keyword">from</span> server
↓
Response Interceptor <span class="hljs-number">1</span> (first registered)
↓
Response Interceptor <span class="hljs-number">2</span>
↓
Response Interceptor <span class="hljs-number">3</span> (last registered)
↓
Response returned to application
</code></pre>
<p>Understanding this order is essential when combining multiple interceptors:</p>
<ol>
<li><p><strong>Authentication should be added early in the request chain</strong>: Register auth interceptors(request) last so they run first.</p>
</li>
<li><p><strong>Retry logic needs an early position in the response chain</strong>: Register retry interceptors(response) first, so they handle errors before other error handlers.</p>
</li>
<li><p><strong>Caching should happen late in the response chain</strong>: Register cache interceptors(response) last so they receive fully processed responses.</p>
</li>
<li><p><strong>Logging should generally ocurr at both ends</strong>: Register loggers first for requests (so they run last) and first for responses (so they run first) to capture the final state.</p>
</li>
</ol>
<h2 id="heading-popular-interceptor-libraries">Popular Interceptor Libraries</h2>
<h3 id="heading-logging-axios-logger">Logging: <code>axios-logger</code></h3>
<p>The <a target="_blank" href="https://www.npmjs.com/package/axios-logger"><code>axios-logger</code></a> library automatically logs information about every HTTP request and response. When we add it to our Axios instance, it intercepts both requests and responses to provide detailed debugging information. Use it only in development. Install it by running the command <code>npm install axios-logger</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> AxiosLogger <span class="hljs-keyword">from</span> <span class="hljs-string">'axios-logger'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

client.interceptors.request.use(
  <span class="hljs-function">(<span class="hljs-params">request</span>) =&gt;</span> AxiosLogger.requestLogger(request, {
    <span class="hljs-attr">prefixText</span>: <span class="hljs-string">'API'</span>,
  }),
  <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> AxiosLogger.errorLogger(error, {
    <span class="hljs-attr">prefixText</span>: <span class="hljs-string">'API'</span>,
    <span class="hljs-attr">logger</span>: <span class="hljs-built_in">console</span>.error
  })
);  

client.interceptors.response.use(
  <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> AxiosLogger.responseLogger(response, {
    <span class="hljs-attr">prefixText</span>: <span class="hljs-string">'API'</span>,
  }),
  <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> AxiosLogger.errorLogger(error, {
    <span class="hljs-attr">prefixText</span>: <span class="hljs-string">'API'</span>,
    <span class="hljs-attr">logger</span>: <span class="hljs-built_in">console</span>.error
  })
);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/success'</span>);
  <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/fail'</span>);
  } <span class="hljs-keyword">catch</span> (error) {
  }
}

run();
</code></pre>
<p>We can configure the following parameters in <code>axios-logger</code> to control what information is logged and how it's formatted:</p>
<ul>
<li><p><code>method</code>: Include HTTP method. The default value is <code>true</code>.</p>
</li>
<li><p><code>url</code>: Include request URL. The default value is <code>true</code>.</p>
</li>
<li><p><code>params</code>: Include URL query parameters. The default value is <code>false</code>.</p>
</li>
<li><p><code>data</code>: Include request/response body data. The default value is <code>true</code>.</p>
</li>
<li><p><code>status</code>: Include HTTP status code. The default value is <code>true</code>.</p>
</li>
<li><p><code>statusText</code>: Include HTTP status text. The default value is <code>true</code>.</p>
</li>
<li><p><code>headers</code>: Include HTTP headers. The default value is <code>false</code>.</p>
</li>
<li><p><code>prefixText</code>: Custom prefix text or <code>false</code> to disable. The default value is <code>Axios</code>.</p>
</li>
<li><p><code>dateFormat</code>: Timestamp format or <code>false</code> to disable. The default value is <code>false</code>.</p>
</li>
<li><p><code>logger</code>: Custom logger function. The default value is <code>console.log</code>.</p>
</li>
</ul>
<h3 id="heading-security-axios-token-interceptor">Security: <code>axios-token-interceptor</code></h3>
<p>The <a target="_blank" href="https://www.npmjs.com/package/axios-token-interceptor"><code>axios-token-interceptor</code></a> library simplifies adding authentication tokens to requests. It handles token storage, cache handling, and automatic injection. Install it by running the command <code>npm install axios-token-interceptor</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> tokenProvider <span class="hljs-keyword">from</span> <span class="hljs-string">'axios-token-interceptor'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

<span class="hljs-keyword">const</span> cache = tokenProvider.tokenCache(  
  <span class="hljs-function">()  =&gt;</span> <span class="hljs-built_in">Promise</span>.resolve(<span class="hljs-string">"ABC"</span>),  
  { <span class="hljs-attr">maxAge</span>: <span class="hljs-number">3600000</span> } 
);  

client.interceptors.request.use(
  tokenProvider({
    <span class="hljs-attr">getToken</span>: cache
  })
);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/protected'</span>);
  <span class="hljs-built_in">console</span>.log(response.data);
}

run();
</code></pre>
<p>We can configure parameters for two main functions in the <code>axios-token-interceptor</code> library:</p>
<p><strong>Token Provider Parameters</strong></p>
<ul>
<li><p><code>token</code>: Static token string for all requests.</p>
</li>
<li><p><code>getToken</code><strong>:</strong> Function that returns a token (sync or async).</p>
</li>
<li><p><code>header</code>: HTTP header name for token injection. The default value is <code>Authorization</code>.</p>
</li>
<li><p><code>headerFormatter</code>: Function to format the header value. The default value is <code>(token) =&gt; 'Bearer ${token}'</code>.</p>
</li>
</ul>
<p>Either <code>token</code> or <code>getToken</code> must be provided.</p>
<p><strong>Token Cache Parameters</strong></p>
<ul>
<li><p><code>maxAge</code><strong>:</strong> Fixed cache duration in milliseconds.</p>
</li>
<li><p><code>getMaxAge</code><strong>:</strong> Function to compute cache duration from token.</p>
</li>
</ul>
<p>Either <code>maxAge</code> or <code>getMaxAge</code> should be provided for effective caching.</p>
<h3 id="heading-caching-axios-cache-interceptor">Caching: <code>axios-cache-interceptor</code></h3>
<p>The <a target="_blank" href="https://www.npmjs.com/package/axios-cache-interceptor"><code>axios-cache-interceptor</code></a> library adds sophisticated HTTP caching to Axios, dramatically reducing redundant network requests and improving application performance. Install it by running the command <code>npm install axios-cache-interceptor</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> { setupCache } <span class="hljs-keyword">from</span> <span class="hljs-string">'axios-cache-interceptor'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

<span class="hljs-keyword">const</span> clientWithCache = setupCache(client, {
  <span class="hljs-attr">ttl</span>: <span class="hljs-number">15</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>
});

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> clientWithCache.get(<span class="hljs-string">'/api/success'</span>);
    <span class="hljs-built_in">console</span>.log(response.data);

    <span class="hljs-keyword">const</span> response2 = <span class="hljs-keyword">await</span> clientWithCache.get(<span class="hljs-string">'/api/success'</span>);
    <span class="hljs-built_in">console</span>.log(response2.data);
}

run();
</code></pre>
<p>The <a target="_blank" href="https://axios-cache-interceptor.js.org/guide"><code>axios-cache-interceptor</code></a> uses both a request interceptor and a response interceptor internally:</p>
<ol>
<li><p>The request interceptor runs before requests are sent to the network and is responsible for:</p>
<ul>
<li><p>Generating cache keys for requests.</p>
</li>
<li><p>Checking if valid cached responses exist.</p>
</li>
<li><p>Serving cached responses when available.</p>
</li>
<li><p>Handling concurrent requests for the same resource.</p>
</li>
<li><p>Forwarding requests to the network when the cache is missing or stale.</p>
</li>
</ul>
</li>
<li><p>The response interceptor (<code>defaultResponseInterceptor</code>) runs after responses are received from the network and handles:</p>
<ul>
<li><p>Determining if responses should be cached.</p>
</li>
<li><p>Interpreting HTTP cache headers for TTL calculation.</p>
</li>
<li><p>Storing valid responses in cache.</p>
</li>
<li><p>Resolving pending concurrent requests.</p>
</li>
<li><p>Handling errors with stale cache fallback.</p>
</li>
</ul>
</li>
</ol>
<p>We can configure both <a target="_blank" href="https://axios-cache-interceptor.js.org/config">global options</a> when setting up the cache interceptor and <a target="_blank" href="https://axios-cache-interceptor.js.org/config/request-specifics">per-request options</a> for individual HTTP requests. Remember, we can set all per-request options as global defaults in <code>setupCache()</code>.</p>
<h3 id="heading-resilience-axios-retry">Resilience: <code>axios-retry</code></h3>
<p>The <a target="_blank" href="https://www.npmjs.com/package/axios-retry"><code>axios-retry</code></a> package adds intelligent retry logic to our Axios instance, automatically retrying failed requests based on configurable conditions. Install it by running the command <code>npm install axios-retry</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> axiosRetry <span class="hljs-keyword">from</span> <span class="hljs-string">'axios-retry'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

client.interceptors.request.use(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">config</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request interceptor registered before'</span>);
    <span class="hljs-keyword">return</span> config;
  }, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">error</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request error interceptor registered before'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error);
  });

client.interceptors.response.use(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response interceptor registered before'</span>);
    <span class="hljs-keyword">return</span> response;
  }, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">error</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response error interceptor registered before'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error);
  });


axiosRetry(client, {
  <span class="hljs-attr">retries</span>: <span class="hljs-number">3</span>,
  <span class="hljs-attr">retryDelay</span>: axiosRetry.exponentialDelay,
  <span class="hljs-attr">retryCondition</span>: <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> axiosRetry.isNetworkOrIdempotentRequestError(error);
  },
  <span class="hljs-attr">onRetry</span>: <span class="hljs-function">(<span class="hljs-params">retryCount, error, requestConfig</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Retry attempt #<span class="hljs-subst">${retryCount}</span>`</span>);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Error: <span class="hljs-subst">${error.message}</span>`</span>);
  }
});

client.interceptors.request.use(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">config</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request interceptor registered after'</span>);
    <span class="hljs-keyword">return</span> config;
  }, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">error</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request error interceptor registered after'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error);
  });

client.interceptors.response.use(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response interceptor registered after'</span>);
    <span class="hljs-keyword">return</span> response;
  }, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">error</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response error interceptor registered after'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error);
  });


<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/random-fail?percentage=90'</span>);
      <span class="hljs-built_in">console</span>.log(response.data);
  } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Error: <span class="hljs-subst">${error.message}</span>`</span>);
  }
}

run();
</code></pre>
<p>The <code>axios-retry</code> function installs two interceptors into the Axios instance:</p>
<ol>
<li><p>The request interceptor initializes the retry state and configures response validation.</p>
</li>
<li><p>The response interceptor handles error processing and retry logic execution.</p>
</li>
</ol>
<p>When <code>axios-retry</code> performs a retry, the request passes through the entire Axios interceptor chain again. So, understanding the execution order is especially important when we combine it with other interceptors. In the example above, the output will look something like this (when all retries fail):</p>
<pre><code class="lang-javascript">Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Retry attempt #<span class="hljs-number">1</span>
<span class="hljs-attr">Error</span>: Request failed <span class="hljs-keyword">with</span> status code <span class="hljs-number">500</span>
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Retry attempt #<span class="hljs-number">2</span>
<span class="hljs-attr">Error</span>: Request failed <span class="hljs-keyword">with</span> status code <span class="hljs-number">500</span>
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Retry attempt #<span class="hljs-number">3</span>
<span class="hljs-attr">Error</span>: Request failed <span class="hljs-keyword">with</span> status code <span class="hljs-number">500</span>
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Response error interceptor registered after
Response error interceptor registered after
Response error interceptor registered after
Response error interceptor registered after
<span class="hljs-attr">Error</span>: Request failed <span class="hljs-keyword">with</span> status code <span class="hljs-number">500</span>
</code></pre>
<p>Each retry follows this sequence:</p>
<pre><code class="lang-javascript">Request interceptor registered after  
Request interceptor registered before    
Response error interceptor registered before  
Retry attempt #N  
<span class="hljs-attr">Error</span>: Request failed <span class="hljs-keyword">with</span> status code <span class="hljs-number">500</span>
</code></pre>
<p>As we mentioned, request interceptors are executed in reverse registration order. Then, the response interceptors are executed in order. Notice that the last interceptor registered never appears during retry attempts; it only shows up after all retries fail. When a request fails, axios-retry's response interceptor catches the error and decides whether to retry.</p>
<ul>
<li><p>If retryable, it creates a new request, so the error never reaches subsequent interceptors.</p>
</li>
<li><p>If not retryable, the error is rejected and continues through the interceptor chain.</p>
</li>
</ul>
<p>After 3 failed retries, the error flows through all remaining response error interceptors:</p>
<pre><code class="lang-javascript">Response error interceptor registered after (x4)  
<span class="hljs-attr">Error</span>: Request failed <span class="hljs-keyword">with</span> status code <span class="hljs-number">500</span>
</code></pre>
<p>The "after" interceptor runs 4 times because:</p>
<ul>
<li><p>1 time for the original request failure.</p>
</li>
<li><p>3 times for each retry failure (when errors are finally rejected).</p>
</li>
</ul>
<p>We can configure the following parameters:</p>
<ul>
<li><p><code>retries</code>: Number of retry attempts before failing. The default value is <code>3</code>.</p>
</li>
<li><p><code>retryCondition</code>: Callback to determine if the request should be retried.</p>
</li>
<li><p><code>shouldResetTimeout</code>: Whether timeout resets between retries. The default value is <code>false</code>.</p>
</li>
<li><p><code>retryDelay</code>: Delay function between retries (in ms).</p>
</li>
<li><p><code>onRetry</code>: Callback executed before each retry attempt.</p>
</li>
<li><p><code>onMaxRetryTimesExceeded</code>: Callback when all retries are exhausted.</p>
</li>
<li><p><code>validateResponse</code>: Callback to determine if response should be resolved/rejected.</p>
</li>
</ul>
<p>Additionally, <code>axios-retry</code> provides several built-in functions to help with parameter configuration, organized into the following categories:</p>
<p><strong>Error Classification Functions (for</strong> <code>retryCondition</code><strong>)</strong></p>
<ul>
<li><p><code>isNetworkError</code>: Detects network connectivity issues.</p>
</li>
<li><p><code>isRetryableError</code>: Checks for HTTP errors worth retrying (429, 5xx).</p>
</li>
<li><p><code>isSafeRequestError</code>: Combines the <code>isRetryableError</code> check with safe HTTP methods (<code>GET</code>, <code>HEAD</code>, <code>OPTIONS</code>).</p>
</li>
<li><p><code>isIdempotentRequestError</code>: Combines the <code>isRetryableError</code> check with idempotent methods (<code>GET</code>, <code>HEAD</code>, <code>OPTIONS</code>, <code>PUT</code>, <code>DELETE</code>).</p>
</li>
<li><p><code>isNetworkOrIdempotentRequestError</code>: <code>isNetworkError</code> or <code>isIdempotentRequestError</code>. Default value for the <code>retryCondition</code> parameter.</p>
</li>
</ul>
<p><strong>Delay Functions (for</strong> <code>retryDelay</code>)</p>
<ul>
<li><p><code>noDelay</code><strong>:</strong> No delay between retries. Default value for the <code>retryDelay</code> parameter.</p>
</li>
<li><p><code>exponentialDelay</code><strong>:</strong> Exponential backoff with 20% randomization.</p>
</li>
<li><p><code>linearDelay</code>: Linear delay progression, accepts custom delay factor.</p>
</li>
</ul>
<p>All delay functions automatically respect the <code>Retry-After</code> header when present.</p>
<h3 id="heading-testing-axios-mock-adapter">Testing: <code>axios-mock-adapter</code></h3>
<p>When writing tests, we should not hit real APIs. The <a target="_blank" href="https://www.npmjs.com/package/axios-mock-adapter"><code>axios-mock-adapter</code></a> package intercepts requests at the adapter level to return mock data. Install it by running the command <code>npm install axios-mock-adapter --save-dev</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> AxiosMockAdapter <span class="hljs-keyword">from</span> <span class="hljs-string">"axios-mock-adapter"</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>
});

<span class="hljs-keyword">const</span> mock = <span class="hljs-keyword">new</span> AxiosMockAdapter(client);

mock.onGet(<span class="hljs-string">"/api/success"</span>).reply(<span class="hljs-number">200</span>, {
  <span class="hljs-attr">users</span>: [{ <span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">"John Smith"</span> }],
});

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/success'</span>);
    <span class="hljs-built_in">console</span>.log(response.data);
}

run();
</code></pre>
<p>The <code>axios-mock-adapter</code> package intercepts requests by replacing the Axios adapter, not by adding interceptors. This approach allows it to mock requests before they reach the network while preserving the normal interceptor flow. During the <code>AxiosMockAdapter</code> creation, we can configure:</p>
<ul>
<li><p><code>delayResponse</code><strong>:</strong> delay for all responses in milliseconds.</p>
</li>
<li><p><code>onNoMatch</code><strong>:</strong> Behavior when no handler matches. Values are <code>passthrough</code> or <code>throwException</code></p>
</li>
</ul>
<p>For each HTTP method handler, we can configure the URL matching:</p>
<ul>
<li><p><code>String</code>: Exact URL match.</p>
</li>
<li><p><code>RegExp</code>: Pattern matching.</p>
</li>
<li><p><code>Undefined</code>: Match any URL.</p>
</li>
</ul>
<p>Besides that, we can also set up matching by parameters, headers, and even the request body data. Each handler supports these response methods:</p>
<ul>
<li><p><code>reply</code>: Returns a static(status, data, and headers) or dynamic(a function that returns a tuple, status, data, and headers) response.</p>
</li>
<li><p><code>replyOnce</code>: One-time response</p>
</li>
<li><p><code>withDelayInMs</code>: Delay per request.</p>
</li>
<li><p><code>passThrough</code>: Forward the request to the real server.</p>
</li>
<li><p><code>networkError</code>: Simulate network error.</p>
</li>
<li><p><code>timeout</code>: Simulate timeout.</p>
</li>
<li><p><code>abortRequest</code>: Simulate aborted request.</p>
</li>
</ul>
<h2 id="heading-combining-multiple-interceptors">Combining Multiple Interceptors</h2>
<p>Real-world applications often need multiple interceptor functionalities working together. Understanding how to combine them effectively is crucial for building robust HTTP clients. The following example combines three interceptors to create a production-ready API client with logging, automatic retries, and token-based authentication.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> AxiosLogger <span class="hljs-keyword">from</span> <span class="hljs-string">'axios-logger'</span>;
<span class="hljs-keyword">import</span> axiosRetry <span class="hljs-keyword">from</span> <span class="hljs-string">'axios-retry'</span>;
<span class="hljs-keyword">import</span> tokenProvider <span class="hljs-keyword">from</span> <span class="hljs-string">'axios-token-interceptor'</span>;

<span class="hljs-keyword">const</span> client = axios.create({
  <span class="hljs-attr">baseURL</span>: <span class="hljs-string">'http://localhost:3000'</span>,
});

client.interceptors.response.use(
  <span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> duration = <span class="hljs-built_in">Date</span>.now() - response.config.metadata.startTime;
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request duration: <span class="hljs-subst">${duration}</span>ms`</span>);
    <span class="hljs-keyword">return</span> AxiosLogger.responseLogger(response);
  },
  <span class="hljs-function"><span class="hljs-params">error</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> retryState = error.config[<span class="hljs-string">'axios-retry'</span>]; 
    <span class="hljs-keyword">const</span> isLastAttempt = retryState?.retryCount === retryState?.retries;
    <span class="hljs-keyword">if</span> (isLastAttempt) {
        <span class="hljs-keyword">if</span> (error.config?.metadata?.startTime) {
          <span class="hljs-keyword">const</span> duration = <span class="hljs-built_in">Date</span>.now() - error.config.metadata.startTime;
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request duration (error): <span class="hljs-subst">${duration}</span>ms`</span>);
        }
      <span class="hljs-keyword">return</span> AxiosLogger.errorLogger(error);
    }
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(error);
  }
);

axiosRetry(client, {
  <span class="hljs-attr">retries</span>: <span class="hljs-number">3</span>,
  <span class="hljs-attr">retryDelay</span>: axiosRetry.exponentialDelay,
  <span class="hljs-attr">retryCondition</span>: <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> axiosRetry.isNetworkOrIdempotentRequestError(error);
  },
  <span class="hljs-attr">onRetry</span>: <span class="hljs-function">(<span class="hljs-params">retryCount, error, requestConfig</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Retry attempt #<span class="hljs-subst">${retryCount}</span>`</span>);
  }
});

client.interceptors.request.use(
  tokenProvider({
    <span class="hljs-attr">getToken</span>: <span class="hljs-function">() =&gt;</span> { 
        <span class="hljs-keyword">return</span> <span class="hljs-string">"ABC"</span>;
      }
  })
);

client.interceptors.request.use(
  <span class="hljs-function"><span class="hljs-params">config</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> retryState = config[<span class="hljs-string">'axios-retry'</span>]; 
    <span class="hljs-keyword">if</span> (!retryState) {
      config.metadata = { <span class="hljs-attr">startTime</span>: <span class="hljs-built_in">Date</span>.now() };
      <span class="hljs-keyword">return</span> AxiosLogger.requestLogger(config);
    }
    <span class="hljs-keyword">return</span> config;
  }
);


<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.get(<span class="hljs-string">'/api/random-fail?percentage=50'</span>);
      <span class="hljs-built_in">console</span>.log(response.data);
  } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Error: <span class="hljs-subst">${error.message}</span>`</span>);
  }
}

run();
</code></pre>
<p>The <code>client</code> instance has the following pipeline for every request:</p>
<ul>
<li><p>Starts a timer and calls <code>AxiosLogger.requestLogger</code> (only once, not on retries).</p>
</li>
<li><p><code>axios-token-interceptor</code> injects the token.</p>
</li>
<li><p><code>axios-retry</code> initializes the retry state.</p>
</li>
<li><p>Request is executed.</p>
</li>
<li><p><code>axios-retry</code> handles retry logic execution.</p>
</li>
<li><p>On error, logs duration and error using <code>AxiosLogger.responseLogger</code> only for the final retry attempt.</p>
</li>
<li><p>On success, logs duration and response using <code>AxiosLogger.responseLogger</code>.</p>
</li>
</ul>
<p>Axios interceptors provide a powerful mechanism for implementing cross-cutting concerns in your HTTP layer. By understanding and properly utilizing interceptors, we can create robust, maintainable, and feature-rich API clients. Thanks, and happy coding. You can find all the code <a target="_blank" href="https://github.com/raulnq/axios">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Keep an Eye on the Costs]]></title><description><![CDATA[Modern software architecture decisions often emphasize scalability, reliability, and performance, but cost is frequently overlooked until the bill arrives. For distributed systems, especially those involving cloud services, cost should be treated as ...]]></description><link>https://blog.raulnq.com/keep-an-eye-on-the-costs</link><guid isPermaLink="true">https://blog.raulnq.com/keep-an-eye-on-the-costs</guid><category><![CDATA[cost-optimisation]]></category><category><![CDATA[CostSavings]]></category><category><![CDATA[cost]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Splunk]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Sun, 23 Nov 2025 04:23:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763845280972/c4917545-d401-41d4-a110-7f6198e55898.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Modern software architecture decisions often emphasize scalability, reliability, and performance, but cost is frequently overlooked until the bill arrives. For distributed systems, especially those involving cloud services, cost should be treated as a first-class technical requirement.</p>
<p>In this article, we analyze a real-world problem from a cost-first perspective and walk through three alternative implementations step by step. The goal is to help software developers internalize a repeatable method for cost analysis and provide practical techniques to prevent unexpected cloud bills.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>We have a public Single-Page Application (SPA) that stores logs locally in <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a>. A <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Worker</a> collects and sends logs every 5 minutes, with approximately:</p>
<ul>
<li><p><strong>Request size</strong>: ~20 log entries per request.</p>
</li>
<li><p><strong>Request weight</strong>: ~40 KB per request.</p>
</li>
<li><p><strong>Scale</strong>: 2,000 instances running 12 hours daily.</p>
</li>
</ul>
<p>This results in:</p>
<ul>
<li><p><strong>Requests per instance per day</strong>: 12 hours × (60 / 5) = 144 requests.</p>
</li>
<li><p><strong>Total requests per day</strong>: 144 × 2000 = 288,000 requests.</p>
</li>
<li><p><strong>Total requests per month</strong>: 288,000 × 30 = 8,640,000 requests.</p>
</li>
<li><p><strong>Daily log volume</strong>: 288,000 × 40 KB = 11.52 GB.</p>
</li>
</ul>
<p>Logs must ultimately reach <a target="_blank" href="https://www.splunk.com/">Splunk</a>, which is only accessible privately. We are evaluating three architectural destinations to receive logs from the SPA.</p>
<h2 id="heading-option-1-aws-lambda-256mb">Option 1: AWS Lambda (256MB)</h2>
<p>An AWS Lambda function receives the request, processes it, and forwards it to Splunk over the private network.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li><p>Zero infrastructure management.</p>
</li>
<li><p>Auto-scaling without configuration.</p>
</li>
<li><p>Built-in monitoring and logging.</p>
</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li><p>API Gateway adds latency.</p>
</li>
<li><p>Cold starts can affect P99 latency.</p>
</li>
</ul>
<h3 id="heading-cost-analysis">Cost Analysis</h3>
<ul>
<li><p><strong>AWS Lambda Invocations:</strong></p>
<ul>
<li><p><a target="_blank" href="https://aws.amazon.com/lambda/pricing/"><strong>Cost</strong></a>: $0.20 per 1 million requests.</p>
</li>
<li><p><strong>Total Invocation Cost</strong>: 8,640,000 requests * $0.20/1,000,000 requests = $1.73</p>
</li>
</ul>
</li>
<li><p><strong>AWS Lambda Compute Time:</strong></p>
<ul>
<li><p><strong>Average Duration</strong>: 150 ms per request.</p>
</li>
<li><p><strong>Total Compute (seconds)</strong>: 8,640,000 requests * 150 ms/request = 1,296,000,000 ms = 1,296,000 seconds.</p>
</li>
<li><p><strong>Total Compute (GB-seconds)</strong>: 1,296,000 seconds * (256/1024) GB = 324,000 GB-seconds.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/lambda/pricing/"><strong>Cost</strong></a>: $0.0000166667 per GB-second.</p>
</li>
<li><p><strong>Total Compute Cost</strong>: 324,000 GB-seconds * $0.0000166667/GB-second = $5.40</p>
</li>
</ul>
</li>
<li><p><strong>AWS API Gateway (HTTP API)</strong></p>
<ul>
<li><p><a target="_blank" href="https://aws.amazon.com/api-gateway/pricing/?nc1=h_ls">Cost</a>: $1.00 per million requests.</p>
</li>
<li><p><strong>Total Requests Cost</strong>: 8,640,000 requests × $1/1,000,000 requests = $8.64</p>
</li>
</ul>
</li>
<li><p><strong>Total Cost</strong>: $1.73 + $5.40 + $8.64 = $15.77</p>
</li>
</ul>
<h3 id="heading-option-2-kubernetes-pod-256mi-256m-cpu">Option 2: Kubernetes Pod (256Mi, 256m CPU)</h3>
<p>A lightweight API deployed as a Pod in an existing Kubernetes cluster.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>Lower per-request cost when using an existing cluster.</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li><p>Requires cluster management expertise.</p>
</li>
<li><p>More complex deployment pipeline.</p>
</li>
</ul>
<h3 id="heading-cost-analysis-1">Cost Analysis</h3>
<blockquote>
<p><strong>EKS Control Plane</strong> and ALB costs will not be included.</p>
</blockquote>
<ul>
<li><p><strong>Pod resource allocation</strong>:</p>
<ul>
<li><p><strong>Node Type</strong>: <a target="_blank" href="https://instances.vantage.sh/aws/ec2/c7i-flex.xlarge?currency=USD"><code>c7i-flex.xlarge</code></a> (4 vCPU, 8 GB RAM).</p>
</li>
<li><p><a target="_blank" href="https://instances.vantage.sh/aws/ec2/c7i-flex.xlarge?currency=USD&amp;duration=hourly"><strong>Cost</strong></a><strong>(On-Demand)</strong>: $0.17/hour = $122.4/month.</p>
</li>
<li><p><strong>CPU Pod Utilization</strong>: 0.256 vCPU / 4 vCPU = 6.4%</p>
</li>
<li><p><strong>Memory Pod Utilization</strong>: 256 MB / 8192 MB = 3.1%</p>
</li>
<li><p><strong>Allocated Cost by CPU</strong>: $122.4 × 0.064 = $7.83</p>
</li>
<li><p><strong>Allocated Cost by Memory</strong>: $122.4 × 0.031 = $3.79</p>
</li>
<li><p><strong>Total Allocated Cost</strong>: max($7.83, $3.79) = $7.83</p>
</li>
</ul>
</li>
<li><p><strong>Total Cost</strong>: $7.83</p>
</li>
</ul>
<h3 id="heading-option-3-upload-logs-to-s3">Option 3: Upload Logs to S3</h3>
<p>The service worker uploads directly to S3 using pre-signed URLs. A message is sent to an SQS queue, which Splunk listens to. Splunk reads from S3, and the logs expire after one day.</p>
<p><strong>Advantages:</strong></p>
<ul>
<li>No compute resources needed.</li>
</ul>
<p><strong>Disadvantages:</strong></p>
<ul>
<li><p>Splunk must be configured to read from S3.</p>
</li>
<li><p>Less real-time than push models.</p>
</li>
</ul>
<h3 id="heading-cost-analysis-2">Cost Analysis</h3>
<ul>
<li><p><strong>Upload Costs (PUT requests)</strong>:</p>
<ul>
<li><p><a target="_blank" href="https://aws.amazon.com/s3/pricing/"><strong>Cost</strong></a><strong>:</strong> $0.005 per 1,000 requests.</p>
</li>
<li><p><strong>Total Upload Cost:</strong> 8,640,000 requests * $0.005/1,000 requests = $43.20</p>
</li>
</ul>
</li>
<li><p><strong>Storage Costs:</strong></p>
<ul>
<li><p>Since files are deleted after one day, the average daily storage is just the amount uploaded daily.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/s3/pricing/"><strong>Cost</strong></a><strong>(S3 Standard Storage):</strong> $0.023 per GB.</p>
</li>
<li><p><strong>Total Storage Cost:</strong> 11.52 GB * $0.023/GB = $0.26</p>
</li>
</ul>
</li>
<li><p><strong>Amazon SQS Costs:</strong></p>
<ul>
<li><p><strong>Messages Sent from S3 to SQS:</strong> 8,640,000 <code>SendMessage</code> requests.</p>
</li>
<li><p><strong>Polling by Splunk (every 10 seconds):</strong> 8,640 polls/day * 30 days = 2,592,000 <code>ReceiveMessage</code> requests.</p>
</li>
<li><p><strong>Messages Deleted by Splunk:</strong> 8,640,000 <code>DeleteMessage</code> requests.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/sqs/pricing/"><strong>Cost</strong></a><strong>:</strong> $0.40 per 1 million requests.</p>
</li>
<li><p><strong>Total SQS Cost:</strong> 19,872,000 requests * $0.40/1,000,000 requests = $7.94</p>
</li>
</ul>
</li>
<li><p><strong>Download Costs (GET requests):</strong></p>
<ul>
<li><p><a target="_blank" href="https://aws.amazon.com/s3/pricing/"><strong>Cost</strong></a>: $0.0004 per 1,000 requests.</p>
</li>
<li><p>Total Download Cost: 8,640,000 requests * $0.0004/1000 requests = $3.45</p>
</li>
</ul>
</li>
<li><p><strong>Total Cost</strong>: $43.20 + $0.26 +$7.94 + $3.45 = $54.85</p>
</li>
</ul>
<h2 id="heading-conclusions">Conclusions</h2>
<p><strong>The Kubernetes Pod is the lowest-cost solution</strong> at <strong>$7.83/month</strong>, assuming the organization already maintains an EKS cluster. The operational cost is low only because cluster and ALB expenses are excluded; otherwise, the economics change significantly. This option is best suited for teams with Kubernetes expertise and existing cluster capacity.</p>
<p><strong>AWS Lambda offers the best balance between low cost and zero operational overhead.</strong> At <strong>$15.77/month</strong>, it is inexpensive, scales transparently, and requires no infrastructure management. For most teams, this is the most practical and maintainable trade-off.</p>
<p><strong>S3 ingestion is the most expensive (and perhaps the most elegant) option</strong> at <strong>$54.85/month</strong>, primarily because PUT requests scale linearly with the number of batches. While it eliminates compute concerns, it adds complexity (S3 → SQS → Splunk) and is less real-time. This model only makes sense if Splunk’s S3 ingestion pipeline is strategically preferred or if we increase the batch frequency to at least 60 minutes.</p>
<p>If minimizing cloud spend is the absolute priority and Kubernetes capacity already exists, the second option provides the lowest cost. Otherwise, Lambda gives the best cost-to-operational-simplicity ratio.</p>
<p>Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Forward Client-Side Logs to Splunk]]></title><description><![CDATA[Client-side applications, such as React SPAs, generate valuable logs on their side: UI errors, unexpected flows, browser-specific issues, performance signals, and metrics about user interactions. However, centralizing these logs in enterprise monitor...]]></description><link>https://blog.raulnq.com/forward-client-side-logs-to-splunk</link><guid isPermaLink="true">https://blog.raulnq.com/forward-client-side-logs-to-splunk</guid><category><![CDATA[dotnet]]></category><category><![CDATA[Splunk]]></category><category><![CDATA[spa]]></category><category><![CDATA[serilog]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Fri, 14 Nov 2025 20:50:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763143643625/11509597-4316-44a9-99d9-0c3771fe403d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Client-side applications, such as React SPAs, generate valuable logs on their side: UI errors, unexpected flows, browser-specific issues, performance signals, and metrics about user interactions. However, centralizing these logs in enterprise monitoring systems like Splunk presents unique challenges:</p>
<ul>
<li><p><strong>Security constraints</strong>: Client-side applications typically cannot send logs directly to Splunk due to security policies, credential exposure risks, and CORS restrictions.</p>
</li>
<li><p><strong>Format compatibility</strong>: When our backend uses <a target="_blank" href="https://serilog.net/">Serilog</a> with <a target="_blank" href="https://github.com/serilog-contrib/serilog-sinks-splunk">Serilog.Sinks.Splunk</a>, maintaining a consistent log format across frontend and backend becomes critical for unified querying and alerting.</p>
</li>
<li><p><strong>Network efficiency</strong>: Batching, retry logic, and error handling require careful implementation to avoid losing logs or impacting application performance.</p>
</li>
</ul>
<p>This article presents a practical solution: a TypeScript logging library that mirrors Serilog's format, combined with a .NET proxy that forwards logs to Splunk. We'll examine the implementation from the <a target="_blank" href="https://github.com/raulnq/ui-logger-to-proxy">ui-logger-to-proxy</a> repository, explaining both the architectural decisions and the technical details.</p>
<h2 id="heading-the-architecture">The Architecture</h2>
<p>The solution consists of three components:</p>
<ul>
<li><p><strong>Client-side logger</strong> (TypeScript): Captures logs in Serilog-compatible format.</p>
</li>
<li><p><strong>.NET proxy API</strong>: Receives logs from clients and forwards them to Splunk.</p>
</li>
<li><p><strong>Splunk</strong>: The final destination for centralized log storage and analysis.</p>
</li>
</ul>
<h2 id="heading-understanding-the-serilog-log-format">Understanding the Serilog Log Format</h2>
<p>Before implementing the client-side logger, we need to understand the target format. Serilog.Sinks.Splunk produces JSON logs with this structure:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Level"</span>: <span class="hljs-string">"Information"</span>,
  <span class="hljs-attr">"RenderedMessage"</span>: <span class="hljs-string">"User logged in successfully"</span>,
  <span class="hljs-attr">"MessageTemplate"</span>: <span class="hljs-string">"User {UserName} logged in successfully"</span>,
  <span class="hljs-attr">"Properties"</span>: {
    <span class="hljs-attr">"UserName"</span>: <span class="hljs-string">"johndoe@gmail.com"</span>,
    <span class="hljs-attr">"SessionId"</span>: <span class="hljs-string">"abc123"</span>
  },
  <span class="hljs-attr">"ReleaseVersion"</span>: <span class="hljs-string">"10.0.0"</span>,
  <span class="hljs-attr">"Timestamp"</span>: <span class="hljs-string">"2024-11-14T10:30:00.000Z"</span>
}
</code></pre>
<p>Key fields explained:</p>
<ul>
<li><p><strong>Level</strong>: Log severity (Verbose, Debug, Information, Warning, Error, Fatal).</p>
</li>
<li><p><strong>MessageTemplate</strong>: The template string with placeholders (e.g., <code>{UserName}</code>).</p>
</li>
<li><p><strong>RenderedMessage</strong>: The final message with placeholders replaced.</p>
</li>
<li><p><strong>Properties</strong>: Structured data extracted from the message template or added as a context.</p>
</li>
<li><p><strong>And Other Custom Fields</strong>.</p>
</li>
</ul>
<p>The distinction between <code>MessageTemplate</code> and <code>RenderedMessage</code> is crucial. Splunk can index and query based on the template pattern, enabling queries like "show all login failures" regardless of the specific username.</p>
<h2 id="heading-client-side-implementation">Client-Side Implementation</h2>
<p>The TypeScript <code>BatchLogger</code> class provides the foundation for structured logging with Serilog compatibility:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> LogLevel =
  | <span class="hljs-string">"Verbose"</span>
  | <span class="hljs-string">"Debug"</span>
  | <span class="hljs-string">"Information"</span>
  | <span class="hljs-string">"Warning"</span>
  | <span class="hljs-string">"Error"</span>
  | <span class="hljs-string">"Fatal"</span>;

<span class="hljs-keyword">type</span> LogEntry = {
  time: <span class="hljs-built_in">number</span>;
  host: <span class="hljs-built_in">string</span>;
  source: <span class="hljs-built_in">string</span>;
  sourcetype: <span class="hljs-built_in">string</span>;
  index: <span class="hljs-built_in">string</span>;
  event: {
    Level: LogLevel;
    RenderedMessage: <span class="hljs-built_in">string</span>;
    MessageTemplate: <span class="hljs-built_in">string</span>;
    Properties: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
    Exception?: <span class="hljs-built_in">string</span>;
    [key: <span class="hljs-built_in">string</span>]: <span class="hljs-built_in">any</span>;
  };
};
</code></pre>
<p>This structure mirrors Serilog's internal format, ensuring seamless integration with existing Splunk configurations.</p>
<h3 id="heading-configuration"><strong>Configuration</strong></h3>
<p>The <code>BatchLogger</code> constructor accepts a comprehensive configuration object that controls both behavior and performance characteristics:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> LoggerConfig = {
  source: <span class="hljs-built_in">string</span>;
  sourcetype: <span class="hljs-built_in">string</span>;
  index: <span class="hljs-built_in">string</span>;
  host: <span class="hljs-built_in">string</span>;
  endpoint: <span class="hljs-built_in">string</span>;
  batchSize: <span class="hljs-built_in">number</span>;
  flushInterval: <span class="hljs-built_in">number</span>;
  maxRetries: <span class="hljs-built_in">number</span>;
  minimumLogLevel?: LogLevel;
  enrichment?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
};
</code></pre>
<h4 id="heading-configuration-parameters"><strong>Configuration Parameters</strong></h4>
<p><strong>Splunk Metadata:</strong></p>
<ul>
<li><p><code>source</code>: Identifies the application generating logs (e.g., "my-app").</p>
</li>
<li><p><code>sourcetype</code>: Categorizes the log format (e.g., "ui", "json").</p>
</li>
<li><p><code>index</code>: Specifies the Splunk index for storage (e.g., "my-index").</p>
</li>
<li><p><code>host</code>: Logical hostname for the logs (e.g., "web-client").</p>
</li>
</ul>
<p><strong>Network Configuration:</strong></p>
<ul>
<li><code>endpoint</code>: URL of the .NET proxy server (e.g., "<a target="_blank" href="http://localhost:5244/collector">http://localhost:5244/collector")</a>.</li>
</ul>
<p><strong>Performance Tuning:</strong></p>
<ul>
<li><p><code>batchSize</code>: Number of log entries per batch (default: 10).</p>
</li>
<li><p><code>flushInterval</code>: Maximum time to hold logs before sending in milliseconds (default: 5000).</p>
</li>
<li><p><code>maxRetries</code>: Number of retry attempts for failed requests (default: 3).</p>
</li>
</ul>
<p><strong>Log Level Filtering:</strong></p>
<ul>
<li><p><code>minimumLogLevel</code>: Minimum log level to process (default: "Information").</p>
<ul>
<li><p>Only logs at or above this level will be processed and sent.</p>
</li>
<li><p>Hierarchy: Verbose(0) &lt; Debug(1) &lt; Information(2) &lt; Warning(3) &lt; Error(4) &lt; Fatal(5).</p>
</li>
</ul>
</li>
</ul>
<p><strong>Global Enrichment:</strong></p>
<ul>
<li><code>enrichment</code>: Key-value pairs added to all log entries (e.g., version, environment).</li>
</ul>
<h4 id="heading-considerations"><strong>Considerations</strong></h4>
<p><strong>Batch Size:</strong></p>
<ul>
<li><p><strong>Small batches (5-10)</strong>: Better for real-time monitoring, higher network overhead.</p>
</li>
<li><p><strong>Large batches (20-50)</strong>: More efficient network usage, potential memory pressure.</p>
</li>
<li><p><strong>Very large batches (100+)</strong>: Risk of losing many logs on failures.</p>
</li>
</ul>
<p><strong>Flush Interval:</strong></p>
<ul>
<li><p><strong>Short intervals (1-3 seconds)</strong>: Near real-time delivery, more network requests.</p>
</li>
<li><p><strong>Medium intervals (5-10 seconds)</strong>: Balanced performance and timeliness.</p>
</li>
<li><p><strong>Long intervals (30+ seconds)</strong>: Risk of log loss on page navigation.</p>
</li>
</ul>
<p><strong>Retry Strategy:</strong></p>
<ul>
<li><p><strong>Few retries (1-2)</strong>: Fast failure detection, potential log loss.</p>
</li>
<li><p><strong>Moderate retries (3-5)</strong>: Good balance for temporary network issues.</p>
</li>
<li><p><strong>Many retries (10+)</strong>: Risk of blocking the logging queue.</p>
</li>
</ul>
<p><strong>Log Level Strategy:</strong></p>
<ul>
<li><p><strong>Verbose/Debug</strong>: Development and troubleshooting scenarios only.</p>
</li>
<li><p><strong>Information</strong>: General application flow and user actions (production default).</p>
</li>
<li><p><strong>Warning</strong>: Potentially problematic situations that don't break functionality.</p>
</li>
<li><p><strong>Error/Fatal</strong>: Production environments focusing on actionable issues.</p>
</li>
</ul>
<h4 id="heading-example"><strong>Example</strong></h4>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> logger = <span class="hljs-keyword">new</span> BatchLogger({
  source: <span class="hljs-string">"my-app"</span>,
  sourcetype: <span class="hljs-string">"ui"</span>,
  index: <span class="hljs-string">"my-index"</span>,
  host: <span class="hljs-string">"127.0.0.1"</span>,
  endpoint: <span class="hljs-string">"http://localhost:5244/collector"</span>,
  batchSize: <span class="hljs-number">10</span>,
  flushInterval: <span class="hljs-number">5000</span>,
  maxRetries: <span class="hljs-number">3</span>,
  minimumLogLevel: <span class="hljs-string">"Information"</span>,
  enrichment: {
    ReleaseVersion: <span class="hljs-string">"10.0.0"</span>,
    Environment: <span class="hljs-string">"Development"</span>
  },
});
</code></pre>
<h3 id="heading-template-rendering-engine"><strong>Template Rendering Engine</strong></h3>
<p>The logger implements a simple but effective template rendering system:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">private</span> renderMessage(template: <span class="hljs-built_in">string</span>, properties: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;): <span class="hljs-built_in">string</span> {
  <span class="hljs-keyword">let</span> message = template;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> [key, value] <span class="hljs-keyword">of</span> <span class="hljs-built_in">Object</span>.entries(properties)) {
    <span class="hljs-keyword">const</span> placeholder = <span class="hljs-string">`{<span class="hljs-subst">${key}</span>}`</span>;
    message = message.replace(placeholder, <span class="hljs-built_in">String</span>(value));
  }
  <span class="hljs-keyword">return</span> message;
}
</code></pre>
<p>This approach maintains compatibility with Serilog's message template format, allowing developers to write familiar logging statements:</p>
<pre><code class="lang-typescript">logger.information(<span class="hljs-string">"Processed {Count} items in {Duration}ms"</span>, {
  Count: <span class="hljs-number">150</span>,
  Duration: <span class="hljs-number">2340</span>,
});
</code></pre>
<h3 id="heading-log-level-filtering">Log Level Filtering</h3>
<p>The logger implements efficient log level filtering to reduce noise and improve performance:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> LogLevelValues: Record&lt;LogLevel, <span class="hljs-built_in">number</span>&gt; = {
  Verbose: <span class="hljs-number">0</span>,
  Debug: <span class="hljs-number">1</span>,
  Information: <span class="hljs-number">2</span>,
  Warning: <span class="hljs-number">3</span>,
  <span class="hljs-built_in">Error</span>: <span class="hljs-number">4</span>,
  Fatal: <span class="hljs-number">5</span>,
};

<span class="hljs-keyword">private</span> isEnabled(level: LogLevel): <span class="hljs-built_in">boolean</span> {
  <span class="hljs-keyword">const</span> minimumLogLevel = <span class="hljs-built_in">this</span>.config.minimumLogLevel || <span class="hljs-string">'Information'</span>;
  <span class="hljs-keyword">return</span> LogLevelValues[level] &gt;= LogLevelValues[minimumLogLevel];
}

<span class="hljs-keyword">private</span> log(level: LogLevel, messageTemplate: <span class="hljs-built_in">string</span>, properties?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;, error?: <span class="hljs-built_in">Error</span>): <span class="hljs-built_in">void</span> {
  <span class="hljs-comment">// Early exit if log level is below minimum threshold</span>
  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.isEnabled(level)) <span class="hljs-keyword">return</span>;

  <span class="hljs-comment">// ... continue with log processing</span>
}
</code></pre>
<p><strong>Key Benefits:</strong></p>
<ul>
<li><p><strong>Performance optimization</strong>: Prevents unnecessary object creation and processing for filtered logs.</p>
</li>
<li><p><strong>Centralized filtering</strong>: Single point of control for all log level decisions.</p>
</li>
<li><p><strong>Early exit</strong>: Returns immediately without any overhead for filtered logs.</p>
</li>
</ul>
<h3 id="heading-batching-strategy"><strong>Batching Strategy</strong></h3>
<p>Performance optimization is achieved through intelligent batching:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">private</span> log(
  level: LogLevel,
  messageTemplate: <span class="hljs-built_in">string</span>,
  properties?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;,
  error?: <span class="hljs-built_in">Error</span>
): <span class="hljs-built_in">void</span> {
  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.isEnabled(level)) <span class="hljs-keyword">return</span>;

  <span class="hljs-keyword">const</span> props = properties || {};
  <span class="hljs-keyword">const</span> renderedMessage = <span class="hljs-built_in">this</span>.renderMessage(messageTemplate, props);
  <span class="hljs-keyword">const</span> mergedProperties = {
    ...this.contextProperties,
    ...props,
  };
  <span class="hljs-keyword">const</span> logEntry: LogEntry = {
    time: <span class="hljs-built_in">Date</span>.now(),
    host: <span class="hljs-built_in">this</span>.config.host,
    source: <span class="hljs-built_in">this</span>.config.source,
    sourcetype: <span class="hljs-built_in">this</span>.config.sourcetype,
    index: <span class="hljs-built_in">this</span>.config.index,
    event: {
      Level: level,
      RenderedMessage: renderedMessage,
      MessageTemplate: messageTemplate,
      Properties: mergedProperties,
      ...this.config.enrichment,
    },
  };

  <span class="hljs-keyword">if</span> (error) {
    logEntry.event.Exception = error.stack;
  }

  <span class="hljs-built_in">this</span>.logQueue.push(logEntry);

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.logQueue.length &gt;= <span class="hljs-built_in">this</span>.config.batchSize) {
    <span class="hljs-built_in">this</span>.flush();
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.flushTimer) {
    <span class="hljs-built_in">this</span>.flushTimer = <span class="hljs-built_in">window</span>.setTimeout(
      <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">this</span>.flush(),
      <span class="hljs-built_in">this</span>.config.flushInterval
    );
  }
}
</code></pre>
<p>The batching mechanism:</p>
<ul>
<li><p><strong>Size-based flushing</strong>: Triggers when batch size is reached.</p>
</li>
<li><p><strong>Time-based flushing</strong>: Ensures logs aren't held indefinitely.</p>
</li>
<li><p><strong>Event-based flushing</strong>: Flushes on page unload and visibility changes.</p>
</li>
</ul>
<h3 id="heading-reliability-features"><strong>Reliability Features</strong></h3>
<p>The logger includes robust error handling and retry logic:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> flush(): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.logQueue.length === <span class="hljs-number">0</span> || <span class="hljs-built_in">this</span>.isFlushing) <span class="hljs-keyword">return</span>;

    <span class="hljs-built_in">this</span>.isFlushing = <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">const</span> logsToSend = [...this.logQueue];
    <span class="hljs-built_in">this</span>.logQueue = [];

    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.flushTimer) {
      <span class="hljs-built_in">clearTimeout</span>(<span class="hljs-built_in">this</span>.flushTimer);
      <span class="hljs-built_in">this</span>.flushTimer = <span class="hljs-literal">null</span>;
    }

    <span class="hljs-keyword">let</span> retries = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">let</span> success = <span class="hljs-literal">false</span>;

    <span class="hljs-keyword">while</span> (retries &lt; <span class="hljs-built_in">this</span>.config.maxRetries &amp;&amp; !success) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> AbortController();
        <span class="hljs-keyword">const</span> timeoutId = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> controller.abort(), <span class="hljs-number">10000</span>);

        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-built_in">this</span>.config.endpoint, {
          method: <span class="hljs-string">'POST'</span>,
          headers: {
            <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
          },
          body: <span class="hljs-built_in">JSON</span>.stringify(logsToSend),
          signal: controller.signal,
        });

        <span class="hljs-built_in">clearTimeout</span>(timeoutId);

        <span class="hljs-keyword">if</span> (response.ok) {
          success = <span class="hljs-literal">true</span>;
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-keyword">const</span> shouldRetry = <span class="hljs-built_in">this</span>.shouldRetryError(response.status);
          <span class="hljs-keyword">if</span> (!shouldRetry) {
            <span class="hljs-built_in">console</span>.error(
              <span class="hljs-string">`Non-retryable error <span class="hljs-subst">${response.status}</span>: <span class="hljs-subst">${response.statusText}</span>`</span>
            );
            <span class="hljs-keyword">break</span>;
          }
          <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`HTTP <span class="hljs-subst">${response.status}</span>: <span class="hljs-subst">${response.statusText}</span>`</span>);
        }
      } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
        retries++;
        <span class="hljs-keyword">if</span> (error.name === <span class="hljs-string">'AbortError'</span>) {
          <span class="hljs-built_in">console</span>.error(
            <span class="hljs-string">`Request timeout (attempt <span class="hljs-subst">${retries}</span>/<span class="hljs-subst">${<span class="hljs-built_in">this</span>.config.maxRetries}</span>)`</span>
          );
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-built_in">console</span>.error(
            <span class="hljs-string">`Failed to send logs (attempt <span class="hljs-subst">${retries}</span>/<span class="hljs-subst">${<span class="hljs-built_in">this</span>.config.maxRetries}</span>):`</span>,
            error
          );
        }

        <span class="hljs-keyword">if</span> (retries &lt; <span class="hljs-built_in">this</span>.config.maxRetries) {
          <span class="hljs-keyword">const</span> delay = <span class="hljs-built_in">Math</span>.pow(<span class="hljs-number">2</span>, retries) * <span class="hljs-number">1000</span>;
          <span class="hljs-keyword">const</span> jitter = <span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">1000</span>;
          <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, delay + jitter));
        }
      }
    }
    <span class="hljs-keyword">if</span> (!success) {
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Failed to send logs after all retries. Logs:'</span>, logsToSend);
    }

    <span class="hljs-built_in">this</span>.isFlushing = <span class="hljs-literal">false</span>;
  }
</code></pre>
<p>Key reliability features:</p>
<ul>
<li><p><strong>Exponential backoff</strong>: Prevents overwhelming the server during failures.</p>
</li>
<li><p><strong>Jitter</strong>: Reduces thundering herd problems.</p>
</li>
<li><p><strong>Request timeouts</strong>: Prevents hanging requests.</p>
</li>
<li><p><strong>Selective retry</strong>: Avoids retrying non-recoverable errors.</p>
</li>
</ul>
<h3 id="heading-page-lifecycle-management"><strong>Page Lifecycle Management</strong></h3>
<p>Critical for single-page applications, the logger handles browser lifecycle events:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">config: LoggerConfig</span>) {
  <span class="hljs-keyword">const</span> defaults = {
    batchSize: <span class="hljs-number">10</span>,
    flushInterval: <span class="hljs-number">5000</span>,
    maxRetries: <span class="hljs-number">3</span>,
  };

  <span class="hljs-built_in">this</span>.config = {
    ...defaults,
    ...config,
  };

  <span class="hljs-built_in">this</span>.handleBeforeUnload = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">this</span>.flushSync();
  };
  <span class="hljs-built_in">this</span>.handleVisibilityChange = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">this</span>.onVisibilityChange();
  };

  <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'beforeunload'</span>, <span class="hljs-built_in">this</span>.handleBeforeUnload);
  <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'visibilitychange'</span>, <span class="hljs-built_in">this</span>.handleVisibilityChange);
}

<span class="hljs-keyword">private</span> onVisibilityChange(): <span class="hljs-built_in">void</span> {
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">document</span>.hidden) {
    <span class="hljs-built_in">this</span>.flush();
  }
}

<span class="hljs-keyword">public</span> flushSync(): <span class="hljs-built_in">void</span> {
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.logQueue.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;

  <span class="hljs-keyword">const</span> logsToSend = [...this.logQueue];
  <span class="hljs-built_in">this</span>.logQueue = [];

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.flushTimer) {
    <span class="hljs-built_in">clearTimeout</span>(<span class="hljs-built_in">this</span>.flushTimer);
    <span class="hljs-built_in">this</span>.flushTimer = <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">new</span> Blob([<span class="hljs-built_in">JSON</span>.stringify(logsToSend)], {
    <span class="hljs-keyword">type</span>: <span class="hljs-string">'application/json'</span>,
  });
  navigator.sendBeacon(<span class="hljs-built_in">this</span>.config.endpoint, blob);
}
</code></pre>
<p>The <code>sendBeacon</code> API ensures log delivery even when users navigate away from the page, providing better log coverage for user journeys.</p>
<h2 id="heading-server-side-implementation"><strong>Server-Side Implementation</strong></h2>
<h3 id="heading-proxy-endpoint"><strong>Proxy Endpoint</strong></h3>
<p>The .NET server provides a minimal proxy with CORS support:</p>
<pre><code class="lang-csharp">app.MapPost(<span class="hljs-string">"/collector"</span>, (LogEntry[] logEntries) =&gt;
{
    <span class="hljs-keyword">if</span> (logEntries != <span class="hljs-literal">null</span> &amp;&amp; logEntries.Length &gt; <span class="hljs-number">0</span>)
    {
        <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> logEntry <span class="hljs-keyword">in</span> logEntries)
        {
            <span class="hljs-keyword">var</span> individualJson = JsonSerializer.Serialize(logEntry, jsonOptions);
            Log.Logger.ForwardToSplunk(individualJson);
        }
    }
    <span class="hljs-keyword">return</span> Results.Ok(<span class="hljs-keyword">new</span> { timestamp = DateTime.UtcNow });
});
</code></pre>
<p>This approach:</p>
<ul>
<li><p><strong>Accepts batched logs</strong>: Reduces HTTP overhead.</p>
</li>
<li><p><strong>Maintains structure</strong>: Preserves client-generated log format.</p>
</li>
</ul>
<h3 id="heading-custom-splunk-formatter"><strong>Custom Splunk Formatter</strong></h3>
<p>The key innovation is the <code>RawJsonFormatter</code> class:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> Serilog.Events;
<span class="hljs-keyword">using</span> Serilog.Formatting;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">RawJsonFormatter</span> : <span class="hljs-title">ITextFormatter</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Format</span>(<span class="hljs-params">Serilog.Events.LogEvent logEvent, TextWriter output</span>)</span>
    {
        <span class="hljs-keyword">if</span> (logEvent.Properties.TryGetValue(<span class="hljs-string">"RawJson"</span>, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> rawJsonProperty) &amp;&amp;
            rawJsonProperty <span class="hljs-keyword">is</span> ScalarValue scalarValue &amp;&amp;
            scalarValue.Value <span class="hljs-keyword">is</span> <span class="hljs-keyword">string</span> rawJson)
        {
            output.Write(rawJson);
        }
        <span class="hljs-keyword">else</span>
        {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotSupportedException(<span class="hljs-string">"RawJsonFormatter only supports log events with RawJson property"</span>);
        }
    }
}
</code></pre>
<p>This formatter bypasses Serilog's standard JSON serialization, allowing client-generated JSON to pass through unchanged to Splunk. This preserves the exact structure and field names required for compatibility.</p>
<h3 id="heading-serilog-extension"><strong>Serilog Extension</strong></h3>
<p>The extension method simplifies the forwarding process:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">SplunkJsonLoggerExtensions</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ForwardToSplunk</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> ILogger logger, <span class="hljs-keyword">string</span> rawJson</span>)</span>
    {
        logger.Information(<span class="hljs-string">"Raw JSON data received from client {@RawJson}"</span>, rawJson);
    }
}
</code></pre>
<p>By using Serilog's structured logging with the <code>@</code> operator, the raw JSON becomes a property that the custom formatter can extract. The <code>@</code> ensures that:</p>
<ul>
<li><p><strong>No double-encoding</strong>: The JSON string is not serialized again.</p>
</li>
<li><p><strong>Preserved structure</strong>: The raw JSON maintains its exact format.</p>
</li>
<li><p><strong>Direct access</strong>: The formatter can extract the string property directly.</p>
</li>
</ul>
<h3 id="heading-configuration-1"><strong>Configuration</strong></h3>
<p>The Serilog configuration demonstrates the complete pipeline:</p>
<pre><code class="lang-csharp">Log.Logger = <span class="hljs-keyword">new</span> LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.EventCollector(
        <span class="hljs-string">"&lt;SPLUNK_HOST&gt;"</span>,<span class="hljs-comment">// The Splunk host that is configured with an Event Collector</span>
        <span class="hljs-string">"&lt;EVENT_COLLECTOR_TOKEN&gt;"</span>, <span class="hljs-comment">//The token provided to authenticate to the Splunk Event Collector</span>
        <span class="hljs-keyword">new</span> RawJsonFormatter(), <span class="hljs-comment">//The text formatter used to render log events into a JSON format</span>
        <span class="hljs-string">"services/collector/event"</span>, <span class="hljs-comment">//Splunk Event Collector uri</span>
        LogEventLevel.Information, <span class="hljs-comment">//The minimum log event level required in order to write an event to the sink.</span>
        <span class="hljs-number">2</span>,      <span class="hljs-comment">// The interval in seconds that the queue should be instpected for batching</span>
        <span class="hljs-number">100</span>,    <span class="hljs-comment">// The size of the batch</span>
        <span class="hljs-number">1000</span>    <span class="hljs-comment">// Maximum number of events in the queue</span>
    )
    .CreateLogger();
</code></pre>
<h2 id="heading-running-locally">Running Locally</h2>
<p>This section provides a complete step-by-step guide to running the logging solution on our local development environment.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Ensure we have the following tools installed:</p>
<ul>
<li><p><strong>Node.js 24+</strong>: Required for the React client application.</p>
</li>
<li><p><strong>.NET 9 SDK or later</strong>: Required for the <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core proxy server.</p>
</li>
<li><p><strong>Docker and Docker Compose</strong>: Required for running Splunk locally.</p>
</li>
<li><p><strong>Git</strong>: For cloning the repository.</p>
</li>
</ul>
<h3 id="heading-clone-the-repository">Clone the Repository</h3>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/raulnq/ui-logger-to-proxy.git
<span class="hljs-built_in">cd</span> ui-logger-to-proxy
</code></pre>
<h3 id="heading-start-splunk-container">Start Splunk Container</h3>
<p>Start the Splunk container using Docker Compose:</p>
<pre><code class="lang-bash">docker-compose up -d splunk
</code></pre>
<p><strong>Wait for Splunk to initialize</strong> (this typically takes 3-5 minutes). We can monitor the startup progress:</p>
<pre><code class="lang-bash">docker-compose logs -f splunk
</code></pre>
<p>Look for the message indicating Splunk has started successfully.</p>
<h3 id="heading-configure-splunk-index">Configure Splunk Index</h3>
<ol>
<li><p><strong>Access Splunk Web UI</strong>: Navigate to <a target="_blank" href="http://localhost:8000">http://localhost:8000</a></p>
</li>
<li><p><strong>Login credentials</strong>:</p>
<ul>
<li><p>Username: <code>admin</code></p>
</li>
<li><p>Password: <code>splunk123456.</code></p>
</li>
</ul>
</li>
<li><p><strong>Create Index</strong>:</p>
<ul>
<li><p>Go to <strong>Settings</strong> &gt; <strong>Indexes</strong></p>
</li>
<li><p>Click <strong>New Index</strong></p>
</li>
<li><p>Index Name: <code>my-index</code></p>
</li>
<li><p>Click <strong>Save</strong></p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-start-the-net-proxy-server">Start the .NET Proxy Server</h3>
<p>Open a new terminal window and start the server:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> server
dotnet restore
dotnet run
</code></pre>
<p>The server will start on <a target="_blank" href="http://localhost:5244"><code>http://localhost:5244</code></a>. We should see output similar to:</p>
<pre><code class="lang-typescript">info: Microsoft.Hosting.Lifetime[<span class="hljs-number">0</span>]
      Now listening on: http:<span class="hljs-comment">//localhost:5244</span>
info: Microsoft.Hosting.Lifetime[<span class="hljs-number">0</span>]
      Application started. Press Ctrl+C to shut down.
</code></pre>
<h3 id="heading-start-the-react-client">Start the React Client</h3>
<p>Open another terminal window and start the client:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> client
npm install
npm run dev
</code></pre>
<p>The client will start on <a target="_blank" href="http://localhost:5173"><code>http://localhost:5173</code></a>. We should see:</p>
<pre><code class="lang-typescript">  VITE v4.x.x  ready <span class="hljs-keyword">in</span> xxx ms

  ➜  Local:   http:<span class="hljs-comment">//localhost:5173/</span>
  ➜  Network: use --host to expose
</code></pre>
<h3 id="heading-test-the-complete-pipeline">Test the Complete Pipeline</h3>
<ol>
<li><p><strong>Open the React app</strong>: Navigate to <a target="_blank" href="http://localhost:5173">http://localhost:5173</a></p>
</li>
<li><p><strong>Generate test logs</strong>:</p>
<ul>
<li><p>Enter a message in the text area</p>
</li>
<li><p>Click "Send Log Message"</p>
</li>
</ul>
</li>
<li><p><strong>Verify server reception</strong>: Check the .NET server console for log entries</p>
</li>
<li><p><strong>Verify Splunk indexing</strong>:</p>
<ul>
<li><p>Go to Splunk Web UI (<a target="_blank" href="http://localhost:8000">http://localhost:8000</a>)</p>
</li>
<li><p>Navigate to <strong>Search &amp; Reporting</strong></p>
</li>
<li><p>Search query: <code>index="my-index"</code></p>
</li>
<li><p>We should see the log entries from the client.</p>
</li>
</ul>
</li>
</ol>
<p>Thanks and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Hono: Setting up the development environment]]></title><description><![CDATA[In the rapidly evolving backend ecosystem, developers are constantly searching for frameworks that balance performance, simplicity, and modern developer experience. While established frameworks like Express and Fastify dominate the Node.js landscape,...]]></description><link>https://blog.raulnq.com/hono-setting-up-the-development-environment</link><guid isPermaLink="true">https://blog.raulnq.com/hono-setting-up-the-development-environment</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[hono]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Sun, 09 Nov 2025 23:10:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762624856707/a599c220-bff8-4d48-ae55-083c53404a49.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the rapidly evolving backend ecosystem, developers are constantly searching for frameworks that balance performance, simplicity, and modern developer experience. While established frameworks like Express and Fastify dominate the Node.js landscape, a new contender, Hono, is quickly gaining attention for its minimalistic design and exceptional performance across multiple runtimes.</p>
<p>This article provides a technical overview of Hono, its architectural principles, and provides a step-by-step guide to get started with it.</p>
<h2 id="heading-what-is-hono">What is Hono?</h2>
<p><a target="_blank" href="https://hono.dev/">Hono</a> is a modern, lightweight web framework designed for building high-performance APIs and web applications across multiple JavaScript runtimes. Created by <a target="_blank" href="https://github.com/yusukebe">Yusuke Wada</a> in 2021, <a target="_blank" href="https://github.com/honojs/hono">Hono</a> (炎 - meaning "flame" in Japanese) has rapidly gained traction in the TypeScript ecosystem due to its exceptional speed, minimal footprint, and runtime-agnostic design.</p>
<p>Unlike traditional Node.js frameworks, <a target="_blank" href="https://hono.dev/docs/">Hono</a> is runtime-agnostic. It runs seamlessly on:</p>
<ul>
<li><p><a target="_blank" href="https://hono.dev/docs/getting-started/cloudflare-workers">Cloudflare Workers</a></p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/getting-started/vercel">Vercel</a></p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/getting-started/deno">Deno</a></p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/getting-started/bun">Bun</a></p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/getting-started/nodejs">Node.js</a></p>
</li>
<li><p><a target="_blank" href="https://hono.dev/docs/getting-started/aws-lambda">AWS Lambda</a></p>
</li>
<li><p>and any other environment supporting the <a target="_blank" href="https://fetch.spec.whatwg.org/">Fetch API</a> standard.</p>
</li>
</ul>
<p>This flexibility makes Hono an excellent choice for edge computing, serverless architectures, and high-throughput APIs where low latency and cold start performance are critical. At its core, Hono emphasizes three main principles:</p>
<ul>
<li><p><strong>Speed</strong>: Built for performance; minimal overhead compared to Express.</p>
</li>
<li><p><strong>Simplicity</strong>: Small, readable, and predictable API inspired by frameworks like Express and Koa.</p>
</li>
<li><p><strong>Type Safety</strong>: Full TypeScript support with static typing and schema validation tools.</p>
</li>
</ul>
<h2 id="heading-why-hono-is-a-good-alternative-today">Why Hono Is a Good Alternative Today?</h2>
<h3 id="heading-edge-native-design">Edge-Native Design</h3>
<p>Hono was built with <strong>edge runtimes</strong> in mind. Unlike Express, which assumes a long-running Node.js process, Hono applications can deploy directly to Cloudflare Workers or Vercel Functions without any adjustments.</p>
<h3 id="heading-web-standards-compliance">Web Standards Compliance</h3>
<p>Hono relies on <a target="_blank" href="https://hono.dev/docs/concepts/web-standard">Web Standards</a> APIs, which means:</p>
<ul>
<li><p>Code written in Hono is more portable.</p>
</li>
<li><p>Developers learn transferable skills rather than framework-specific APIs.</p>
</li>
<li><p>Future runtime migrations require minimal refactoring.</p>
</li>
<li><p>The framework benefits from ongoing standards improvements.</p>
</li>
</ul>
<h3 id="heading-developer-experience">Developer Experience</h3>
<p>Developers coming from Express will find Hono intuitive. Its route handling and middleware system are similar, but optimized for modern runtimes.</p>
<h3 id="heading-built-in-typescript-and-middleware-ecosystem">Built-in TypeScript and Middleware Ecosystem</h3>
<p>Hono provides excellent TypeScript support out of the box, making type-safe development natural. It also offers an expanding ecosystem of official and third-party <a target="_blank" href="https://hono.dev/docs/guides/middleware">middleware</a>.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<ul>
<li><p><a target="_blank" href="https://nodejs.org/es/download"><strong>Node.js</strong></a> (version 24.11.0 or higher) installed.</p>
</li>
<li><p><a target="_blank" href="https://git-scm.com/downloads"><strong>Git</strong></a> installed.</p>
</li>
<li><p><a target="_blank" href="https://code.visualstudio.com/">Visual Studio Code</a>.</p>
</li>
</ul>
<h2 id="heading-project-initialization"><strong>Project Initialization</strong></h2>
<p>For simplicity, we will use Node.js as the runtime and npm as the package manager. Run the following commands:</p>
<pre><code class="lang-powershell">npm create hono@latest node<span class="hljs-literal">-hono</span><span class="hljs-literal">-api</span> -- -<span class="hljs-literal">-template</span> nodejs -<span class="hljs-literal">-install</span> -<span class="hljs-literal">-pm</span> npm
<span class="hljs-built_in">cd</span> node<span class="hljs-literal">-hono</span><span class="hljs-literal">-api</span>
</code></pre>
<h2 id="heading-typescript">TypeScript</h2>
<p>Hono uses TypeScript and includes a default <code>tsconfig.json</code> file, which we will update as follows:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ESNext"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"NodeNext"</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"sourceMap"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"verbatimModuleSyntax"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"types"</span>: [<span class="hljs-string">"node"</span>],
    <span class="hljs-attr">"jsx"</span>: <span class="hljs-string">"react-jsx"</span>,
    <span class="hljs-attr">"jsxImportSource"</span>: <span class="hljs-string">"hono/jsx"</span>,
    <span class="hljs-attr">"forceConsistentCasingInFileNames"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"resolveJsonModule"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noUnusedParameters"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noUnusedLocals"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noFallthroughCasesInSwitch"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"allowUnreachableCode"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"./dist"</span>,
    <span class="hljs-attr">"noErrorTruncation"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noPropertyAccessFromIndexSignature"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"paths"</span>: {
      <span class="hljs-attr">"@/*"</span>: [<span class="hljs-string">"./src/*"</span>]
    }
  },
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"**/node_modules"</span>, <span class="hljs-string">"**/.*/"</span>, <span class="hljs-string">"dist"</span>],
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/**/*"</span>, <span class="hljs-string">"tests/**/*"</span>]
}
</code></pre>
<p>Let's dive into all the options:</p>
<ul>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#rootDir"><code>rootDir</code></a>: Specifies the root directory of our input files. TypeScript uses this to control the output directory structure. When files are compiled, their relative path from <code>rootDir</code> is preserved in the <code>outDir</code>. The default value is the longest common path of all non-declaration input files. Determines <strong>how the output structure is organized.</strong></p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#include"><code>include</code></a>: Specifies which files TypeScript should compile. It's an array of glob patterns that tells the compiler which source files to process. These filenames are resolved relative to the directory containing the <code>tsconfig.json</code> file. Determines <strong>what files</strong> to compile.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#include"><code>exclude</code></a>: Specifies which should be skipped when resolving <code>include</code>. The remaining files are compiled. It's also resolved relative to the directory containing <code>tsconfig.json</code>.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#outDir"><code>outDir</code></a>: Specifies the output directory where TypeScript places all compiled JavaScript files.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#target"><code>target</code></a>: Specifies which version of JavaScript the TypeScript compiler should output. The special <code>ESNext</code> value refers to the highest version of TypeScript that our version supports.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#module"><code>module</code></a>: Specifies which <strong>module system</strong> to use for organizing code (imports/exports). <code>NodeNext</code> tells TypeScript to use <strong>Node.js's native module resolution</strong>,</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#strict"><code>strict</code></a>: Enables all strict type-checking options at once.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax"><code>verbatimModuleSyntax</code></a>: Requires us to use <strong>exact import/export syntax</strong> that matches our module system. Forces us to be explicit about type-only imports.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#skipLibCheck"><code>skipLibCheck</code></a>: Skips type checking of <strong>declaration files</strong> (<code>.d.ts</code> files in <code>node_modules</code>).</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#types"><code>types</code></a>: By default, all visible <code>@types</code> packages are included in our compilation. Specifies which type definition packages to include from <code>@types</code>, preventing unnecessary type definitions from being loaded.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#jsx"><code>jsx</code></a>: Specifies how JSX syntax should be transformed.</p>
<ul>
<li><p><code>preserve</code>: Keep JSX as-is (<code>.jsx</code> output).</p>
</li>
<li><p><code>react</code>: Transform to <code>React.createElement()</code> calls (classic).</p>
</li>
<li><p><code>react-jsx</code>: Modern transform (automatic runtime, no need to import React).</p>
</li>
<li><p><code>react-jsxdev</code>: Development version with debugging info.</p>
</li>
<li><p><code>react-native</code> - Preserves JSX syntax but outputs <code>.js</code> files instead of <code>.jsx</code>.</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#jsxImportSource"><code>jsxImportSource</code></a>: Specifies which package provides the JSX runtime.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#sourceMap"><code>sourceMap</code></a>: Generates source map files (<code>.js.map</code>). These files allow debuggers and other tools to display the original TypeScript source code when actually working with the emitted JavaScript files.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#forceConsistentCasingInFileNames"><code>forceConsistentCasingInFileNames</code></a>: Enforces that file names in imports match the actual casing on disk.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#resolveJsonModule"><code>resolveJsonModule</code></a>: Allows us to import JSON files directly as modules with full type safety.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#esModuleInterop"><code>esModuleInterop</code></a>: Allows default imports from CommonJS modules.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#noUnusedParameters"><code>noUnusedParameters</code></a>: Reports an error when function parameters are declared but never used. Parameters declaration with names starting with an underscore (<code>_</code>) are exempt from the unused parameter checking.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#noUnusedLocals"><code>noUnusedLocals</code></a>: Reports an error when local variables are declared but never used.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#noFallthroughCasesInSwitch"><code>noFallthroughCasesInSwitch</code></a>: Reports an error when a <code>case</code> in a switch statement falls through to the next case without a <code>break</code>, <code>return</code>, or <code>throw</code>.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#allowUnreachableCode"><code>allowUnreachableCode</code></a>: Controls whether TypeScript reports errors for code that can never be executed.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#noErrorTruncation"><code>noErrorTruncation</code></a>: Prevents TypeScript from truncating long error messages.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#noPropertyAccessFromIndexSignature"><code>noPropertyAccessFromIndexSignature</code></a>: Forces you to use bracket notation <code>obj['key']</code> instead of dot notation <code>obj.key</code> when accessing properties defined by index signatures.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#noUncheckedIndexedAccess"><code>noUncheckedIndexedAccess</code></a>: Makes array/index access return potentially <code>undefined</code>, forcing you to handle missing values.</p>
</li>
<li><p><a target="_blank" href="https://www.typescriptlang.org/tsconfig/#paths"><code>paths</code></a>: Defines custom module path mappings for cleaner imports, like creating aliases for directories.</p>
</li>
</ul>
<p>We are not explicitly defining the <code>rootDir</code>. Depending on which files we process, TypeScript will automatically set it up. For example:</p>
<pre><code class="lang-json">Input files from include patterns:
├── src/index.ts
├── src/routes/api.ts
├── src/utils/helper.ts
├── tests/api.test.ts
├── tests/unit/user.test.ts
└── tests/integration/db.test.ts
</code></pre>
<p>The calculated <code>rootDir</code> will be <code>.</code>.</p>
<pre><code class="lang-json">Input files from include pattern:
├── src/index.ts
├── src/routes/api.ts
└── src/utils/helper.ts
</code></pre>
<p>Calculated <code>rootDir</code> will be <code>./src</code>.</p>
<p>TypeScript's compiler (<code>tsc</code>) converts our TypeScript code to JavaScript but doesn't handle path aliases in the output, which can lead to runtime errors. <code>tsc-alias</code> is a tool that resolves TypeScript path aliases in our compiled JavaScript output.</p>
<pre><code class="lang-powershell">npm install -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span> tsc<span class="hljs-literal">-alias</span>
</code></pre>
<p>Modify the default <code>build</code> script like this:</p>
<pre><code class="lang-json">{
  ...
  <span class="hljs-attr">"scripts"</span>: {
    ...
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"tsc &amp;&amp; tsc-alias"</span>,
    ...
  }
  ...
}
</code></pre>
<h2 id="heading-code-formatter">Code Formatter</h2>
<p>We will use <a target="_blank" href="https://prettier.io/">Prettier</a>, a code formatter that keeps our project's style consistent. Run the following command to install it as a development dependency:</p>
<pre><code class="lang-powershell">npm install -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span> prettier
</code></pre>
<p>Create a <code>prettier.config.ts</code> file in the root of the project:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> Config } <span class="hljs-keyword">from</span> <span class="hljs-string">"prettier"</span>;

<span class="hljs-keyword">const</span> config: Config = {
  trailingComma: <span class="hljs-string">"es5"</span>,
  singleQuote: <span class="hljs-literal">true</span>,
  arrowParens: <span class="hljs-string">"avoid"</span>,
  endOfLine: <span class="hljs-string">"crlf"</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> config;
</code></pre>
<ul>
<li><p><a target="_blank" href="https://prettier.io/docs/options#trailing-commas"><code>trailingComma</code></a>: Add trailing commas where valid in ES5.</p>
</li>
<li><p><a target="_blank" href="https://prettier.io/docs/options#quotes"><code>singleQuote</code></a>: Use single quotes (<code>'</code>) instead of double quotes (<code>"</code>) for strings.</p>
</li>
<li><p><a target="_blank" href="https://prettier.io/docs/options#arrow-function-parentheses"><code>arrowParens</code></a>: Omit parentheses when possible for single-parameter arrow functions.</p>
</li>
<li><p><a target="_blank" href="https://prettier.io/docs/options#end-of-line"><code>endOfLine</code></a>: Use <code>CRLF</code> line endings (for Windows users only).</p>
</li>
</ul>
<blockquote>
<p>TypeScript support requires Node.js&gt;=22.6.0, and <code>--experimental-strip-types</code> is required before Node.js v24.3.0.</p>
</blockquote>
<p>Create a <a target="_blank" href="https://prettier.io/docs/ignore"><code>.prettierignore</code></a> file to exclude some files:</p>
<pre><code class="lang-plaintext">dist/
package-lock.json
</code></pre>
<blockquote>
<p>By default, prettier ignores files in version control systems directories (".git", ".jj", ".sl", ".svn", and ".hg") and <code>node_modules</code> (unless the <a target="_blank" href="https://prettier.io/docs/cli#--with-node-modules"><code>--with-node-modules</code> CLI option</a> <a target="_blank" href="https://prettier.io/docs/cli#--with-node-modules">is specified)</a>.</p>
</blockquote>
<p>Add the following scripts to the <code>package.json</code> file:</p>
<pre><code class="lang-json">{
  ...
  <span class="hljs-attr">"scripts"</span>: {
    ...
    <span class="hljs-attr">"format"</span>: <span class="hljs-string">"prettier --write ."</span>,
    <span class="hljs-attr">"format:check"</span>: <span class="hljs-string">"prettier --check ."</span>
    ...
  }
  ...
}
</code></pre>
<p>install the <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode">Prettier - Code formatter</a> extension for a better development experience. Create a <code>.vscode/settings.json</code> file in the project root:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
  <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<p>This configuration sets Prettier as the default formatter and automatically formats the code on save.</p>
<h2 id="heading-code-analyzer">Code Analyzer</h2>
<p><a target="_blank" href="https://eslint.org/"><strong>ESLint</strong></a> is a static code analyzer that helps us identify problems in our code. Run the following command to install it:</p>
<pre><code class="lang-powershell">npm install -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span> jiti
npm install -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span> eslint<span class="hljs-literal">-config</span><span class="hljs-literal">-prettier</span>
npm init @eslint/config@latest
</code></pre>
<blockquote>
<p>For Deno and Bun, TypeScript configuration files are natively supported; for Node.js, we must install the optional dev dependency <a target="_blank" href="https://github.com/unjs/jiti"><code>jiti</code></a>.</p>
</blockquote>
<p>Follow the prompts to set up ESLint according to our preferences. In our case, we choose:</p>
<pre><code class="lang-powershell">√ What <span class="hljs-keyword">do</span> you want to lint? · javascript
√ How would you like to use ESLint? · problems
√ What <span class="hljs-built_in">type</span> of modules does your project use? · esm
√ Which framework does your project use? · none
√ Does your project use TypeScript? · No / Yes
√ <span class="hljs-built_in">Where</span> does your code run? · browser
√ Which language <span class="hljs-keyword">do</span> you want your configuration file be written <span class="hljs-keyword">in</span>? · ts
i The config that you<span class="hljs-string">'ve selected requires the following dependencies:

eslint, @eslint/js, globals, typescript-eslint
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm</span>
</code></pre>
<p>This will create an <code>eslint.config.ts</code> file with a basic configuration that we will replace as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> jseslint <span class="hljs-keyword">from</span> <span class="hljs-string">"@eslint/js"</span>;
<span class="hljs-keyword">import</span> tseslint <span class="hljs-keyword">from</span> <span class="hljs-string">"typescript-eslint"</span>;
<span class="hljs-keyword">import</span> { defineConfig, globalIgnores } <span class="hljs-keyword">from</span> <span class="hljs-string">"eslint/config"</span>;
<span class="hljs-keyword">import</span> prettierConfig <span class="hljs-keyword">from</span> <span class="hljs-string">'eslint-config-prettier'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig(
  globalIgnores([<span class="hljs-string">"dist/**/*"</span>]),
  jseslint.configs.recommended,
  tseslint.configs.recommended,
  prettierConfig
);
</code></pre>
<ul>
<li><p><code>jseslint.configs.recommended</code>: Enables <a target="_blank" href="https://www.npmjs.com/package/@eslint/js">recommended</a> JavaScript linting rules.</p>
</li>
<li><p><code>tseslint.configs.recommended</code>: Enables <a target="_blank" href="https://typescript-eslint.io/users/configs/#recommended">recommended</a> TypeScript linting rules.</p>
</li>
<li><p><a target="_blank" href="https://typescript-eslint.io/users/what-about-formatting/#suggested-usage---prettier"><code>prettierConfig</code></a>: Disables all previous ESLint formatting rules that conflict with Prettier.</p>
</li>
<li><p><a target="_blank" href="https://eslint.org/docs/latest/use/configure/ignore"><code>globalIgnores</code></a>: Ignores all files in the <code>dist</code> directory.</p>
</li>
</ul>
<p>Add the following scripts to the <code>package.json</code> file:</p>
<pre><code class="lang-json">{
  ...
  <span class="hljs-attr">"scripts"</span>: {
    ...
    <span class="hljs-attr">"lint"</span>: <span class="hljs-string">"eslint ."</span>,
    <span class="hljs-attr">"lint:fix"</span>: <span class="hljs-string">"eslint . --fix"</span>
    <span class="hljs-string">"lint:format"</span>: <span class="hljs-string">"npm run lint:fix &amp;&amp; npm run format"</span>
    ...
  }
  ...
}
</code></pre>
<p>Install the <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint"><strong>ESLint</strong></a> extension for a better development experience. Update the <code>.vscode/settings.json</code> file with the following content:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
  <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"editor.codeActionsOnSave"</span>: {
    <span class="hljs-attr">"source.fixAll.eslint"</span>: <span class="hljs-string">"explicit"</span>
  }
}
</code></pre>
<h2 id="heading-environment-variables">Environment Variables</h2>
<p>To work with environment variables, we will use the following packages:</p>
<ul>
<li><p><a target="_blank" href="https://www.dotenv.org/">Dotenv</a>: Reads <code>.env</code> files and loads them into <code>process.env</code>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/motdotla/dotenv-expand">Dotenv-expand</a>: Allows referencing other environment variables within <code>.env</code>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/kentcdodds/cross-env">Cross-env</a>: Sets environment variables that work on all operating systems.</p>
</li>
<li><p><a target="_blank" href="https://zod.dev/">Zod</a>: Generates TypeScript types from schemas and validates data at runtime, not just at compile time.</p>
</li>
</ul>
<pre><code class="lang-powershell">npm install dotenv dotenv<span class="hljs-literal">-expand</span> cross<span class="hljs-literal">-env</span> zod
</code></pre>
<p>Create a <code>.env</code> file in the project root:</p>
<pre><code class="lang-plaintext">PORT=5000
</code></pre>
<p>Create the <code>/src/env.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { config } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
<span class="hljs-keyword">import</span> { expand } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv-expand"</span>;
<span class="hljs-keyword">import</span> { ZodError, z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;

<span class="hljs-keyword">const</span> ENVSchema = z.object({
  NODE_ENV: z
    .enum([<span class="hljs-string">"development"</span>, <span class="hljs-string">"production"</span>, <span class="hljs-string">"test"</span>])
    .default(<span class="hljs-string">"development"</span>),
  PORT: z.coerce.number().default(<span class="hljs-number">3000</span>),
});

expand(config());

<span class="hljs-keyword">try</span> {
  ENVSchema.parse(process.env);
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-keyword">if</span> (error <span class="hljs-keyword">instanceof</span> ZodError) {
    <span class="hljs-keyword">const</span> e = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(
      <span class="hljs-string">`Environment validation failed:\n <span class="hljs-subst">${z.treeifyError(error)}</span>`</span>,
    );
    e.stack = <span class="hljs-string">""</span>;
    <span class="hljs-keyword">throw</span> e;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Unexpected error during environment validation:"</span>, error);
    <span class="hljs-keyword">throw</span> error;
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ENV = ENVSchema.parse(process.env);
</code></pre>
<p>This file validates and loads environment variables using Zod for type safety. What does it do?</p>
<ul>
<li><p><strong>Loads</strong> <code>.env</code> <strong>file:</strong></p>
<ul>
<li><p><code>config()</code> loads variables from the <code>.env</code> file.</p>
</li>
<li><p><code>expand()</code> resolves variable references like <code>${OTHER_VAR}</code>.</p>
</li>
</ul>
</li>
<li><p>Defines expected environment variables:</p>
<ul>
<li><p><code>NODE_ENV</code> must be one of: <code>development</code>, <code>production</code>, or <code>test</code> (defaults to <code>development</code>).</p>
</li>
<li><p><code>PORT</code> must be a number (coerced from string, defaults to <code>3000</code>).</p>
</li>
</ul>
</li>
<li><p>Validates environment variables:</p>
<ul>
<li><p>Checks <code>process.env</code> against the schema.</p>
</li>
<li><p>Throws a readable error if validation fails.</p>
</li>
</ul>
</li>
</ul>
<p>This pattern ensures our app fails fast with clear errors if the environment configuration is wrong. Replace the <code>index.ts</code> file with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hono/node-server"</span>;
<span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">"hono"</span>;
<span class="hljs-keyword">import</span> { ENV } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/env.js"</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono();

app.get(<span class="hljs-string">"/"</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">"Hello Hono!"</span>);
});

serve(
  {
    fetch: app.fetch,
    port: ENV.PORT,
  },
  <span class="hljs-function">(<span class="hljs-params">info</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>);
  },
);
</code></pre>
<p>Modify the following scripts in the <code>package.json</code> file:</p>
<pre><code class="lang-json">{
  ...
  <span class="hljs-attr">"scripts"</span>: {
    ...
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"cross-env NODE_ENV=development tsx watch src/index.ts"</span>,
    ...
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"cross-env NODE_ENV=development node dist/index.js"</span>,
    ...
  }
  ...
}
</code></pre>
<h2 id="heading-debugging">Debugging</h2>
<p>Create a <code>.vscode/launch.json</code> file with the following content:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.2.0"</span>,
  <span class="hljs-attr">"configurations"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"node"</span>,
      <span class="hljs-attr">"request"</span>: <span class="hljs-string">"launch"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Debug TypeScript"</span>,
      <span class="hljs-attr">"program"</span>: <span class="hljs-string">"${workspaceFolder}/dist/index.js"</span>,
      <span class="hljs-attr">"preLaunchTask"</span>: <span class="hljs-string">"npm: build"</span>,
      <span class="hljs-attr">"outFiles"</span>: [<span class="hljs-string">"${workspaceFolder}/dist/**/*.js"</span>],
      <span class="hljs-attr">"sourceMaps"</span>: <span class="hljs-literal">true</span>
    }
  ]
}
</code></pre>
<p>This is a VS Code debugger configuration for debugging our TypeScript Node.js application.</p>
<ul>
<li><p><code>"type": "node"</code>: Specifies this is a Node.js application debugger.</p>
</li>
<li><p><code>"request": "launch"</code>: Launches a new Node.js process.</p>
</li>
<li><p><code>"name": "Debug TypeScript"</code>: The name that appears in VS Code's debug dropdown.</p>
</li>
<li><p><code>"program": "${workspaceFolder}/dist/index.js"</code>: The entry point file to run. Points to compiled JavaScript in <code>dist</code>, not your TypeScript source.</p>
</li>
<li><p><code>"preLaunchTask": "npm: build"</code>: Runs <code>npm run build</code> before debugging starts. Ensures TypeScript is compiled to JavaScript first.</p>
</li>
<li><p><code>"outFiles": ["${workspaceFolder}/dist/**/*.js"]</code>: Tells debugger where compiled JavaScript files are located. Used for mapping breakpoints</p>
</li>
<li><p><code>"sourceMaps": true</code>: Enables source map support. Let's us debug TypeScript code instead of compiled JavaScript. Works because our <code>tsconfig.json</code> has <code>"sourceMap": true</code>.</p>
</li>
</ul>
<p>Press <code>F5</code> or click <code>Debug TypeScript</code> in the Run and Debug panel.</p>
<blockquote>
<p>Ensure the program property matches our app's entry point. With our current TypeScript setup, when a test is implemented, the new value must be changed to <code>${workspaceFolder}/dist/src/index.js</code>.</p>
</blockquote>
<h2 id="heading-git-hooks">Git Hooks</h2>
<p><a target="_blank" href="https://typicode.github.io/husky/">Husky</a> allows you to run scripts before commits and pushes via <a target="_blank" href="https://git-scm.com/book/ms/v2/Customizing-Git-Git-Hooks">git hooks</a>, ensuring code quality standards are maintained automatically. Install it by running the following command:</p>
<pre><code class="lang-powershell">npm install -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span> husky
</code></pre>
<p>Initialize Husky:</p>
<pre><code class="lang-powershell">npx husky init
</code></pre>
<blockquote>
<p>The init command simplifies setting up Husky in a project. It creates a <code>pre-commit</code> script in <code>.husky/</code> and updates the prepare script in <code>package.json</code>.</p>
</blockquote>
<p>Edit the <code>.husky/pre-commit</code> file to run Prettier and ESLint:</p>
<pre><code class="lang-plaintext">npm run lint
npm run format:check
</code></pre>
<p>Husky will modify the package.json file by adding the following script:</p>
<pre><code class="lang-json">{
  ...
  <span class="hljs-attr">"scripts"</span>: {
    ...
    <span class="hljs-attr">"prepare"</span>: <span class="hljs-string">"husky"</span>
    ...
  }
  ...
}
</code></pre>
<h2 id="heading-commit-messages">Commit Messages</h2>
<p><a target="_blank" href="https://commitlint.js.org/"><strong>Commitlint</strong></a> is a tool that validates commit messages to ensure they follow a consistent format and conventional standards. Install it by running the following command:</p>
<pre><code class="lang-powershell">npm install -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span> @commitlint/config<span class="hljs-literal">-conventional</span> @commitlint/<span class="hljs-built_in">cli</span> @commitlint/prompt<span class="hljs-literal">-cli</span>
</code></pre>
<p>Create the <code>commitlint.config.ts</code> file in the project root:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { UserConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'@commitlint/types'</span>;

<span class="hljs-keyword">const</span> Configuration: UserConfig = {
  <span class="hljs-keyword">extends</span>: [<span class="hljs-string">'@commitlint/config-conventional'</span>],
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Configuration;
</code></pre>
<p>This configuration sets up commit message linting for our project. The <code>@commitlint/config-conventional</code> package implements the <a target="_blank" href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits</a> specification. The commit message should be structured as follows:</p>
<pre><code class="lang-plaintext">type(scope?): subject
body?
footer?
</code></pre>
<p>Create the <code>.husky/commit-msg</code> file with the following content:</p>
<pre><code class="lang-plaintext">npx --no-install commitlint --edit $1
</code></pre>
<p>The <code>@commitlint/prompt-cli</code> package helps us create commit messages that follow the commit convention set in the <code>commitlint.config.js</code> file. To make prompt-cli easy to use, add a new script to the <code>package.json</code> file:</p>
<pre><code class="lang-json">{
  ...
  <span class="hljs-attr">"scripts"</span>: {
    ...
    <span class="hljs-attr">"commit"</span>: <span class="hljs-string">"commit"</span>
    ...
  }
  ...
}
</code></pre>
<p><code>"prepare"</code> is a special npm lifecycle script that runs automatically at specific times:</p>
<ul>
<li><p><strong>After</strong> <code>npm install</code> (when someone clones our repo and installs dependencies).</p>
</li>
<li><p><strong>Before</strong> <code>npm publish</code> (when publishing a package).</p>
</li>
</ul>
<p>In our project, we will run the <code>husky</code> command to set up Git hooks in the <code>.husky</code> folder.</p>
<h2 id="heading-visual-studio-code-optimizations">Visual Studio Code Optimizations</h2>
<p>Open the <code>.vscode/settings.json</code> file and update the content as follows:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"editor.defaultFormatter"</span>: <span class="hljs-string">"esbenp.prettier-vscode"</span>,
  <span class="hljs-attr">"editor.formatOnSave"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"editor.codeActionsOnSave"</span>: {
    <span class="hljs-attr">"source.fixAll.eslint"</span>: <span class="hljs-string">"explicit"</span>
  },
  <span class="hljs-attr">"search.exclude"</span>: {
    <span class="hljs-attr">"**/node_modules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"**/dist"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"**/.git"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"files.exclude"</span>: {
    <span class="hljs-attr">"**/node_modules"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"files.watcherExclude"</span>: {
    <span class="hljs-attr">"**/node_modules/**"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"**/dist/**"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"**/.git/**"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"typescript.tsserver.maxTsServerMemory"</span>: <span class="hljs-number">4096</span>,
  <span class="hljs-attr">"typescript.tsserver.enableTracing"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"editor.minimap.enabled"</span>: <span class="hljs-literal">false</span>
}
</code></pre>
<ul>
<li><p><code>search.exclude</code>: Excludes irrelevant directories from search results.</p>
</li>
<li><p><code>files.exclude</code>: Hides <code>node_modules</code> from the sidebar.</p>
</li>
<li><p><code>files.watcherExclude</code>: Stops watching files.</p>
</li>
<li><p><code>typescript.tsserver.maxTsServerMemory</code>: Modifies TS server memory</p>
</li>
<li><p><code>typescript.tsserver.enableTracing</code>: Reduces overhead from TypeScript debugging.</p>
</li>
<li><p><code>editor.minimap.enabled</code>: Removes the code minimap for more space.</p>
</li>
</ul>
<p>Now we are ready to start working on our app. In future articles, we will review many Hono features, so stay tuned. You can find all the code <a target="_blank" href="https://github.com/raulnq/node-hono-api">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Node.js and Express: Testing]]></title><description><![CDATA[Testing is a fundamental practice in modern software development that helps ensure code quality, reliability, and maintainability. In the context of Node.js and Express applications, testing provides several critical benefits:

Early Bug Detection: I...]]></description><link>https://blog.raulnq.com/nodejs-and-express-testing</link><guid isPermaLink="true">https://blog.raulnq.com/nodejs-and-express-testing</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Express]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Sun, 26 Oct 2025 01:49:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761428976232/8f83b359-1152-4a7a-84e3-4af7f0866f28.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Testing is a fundamental practice in modern software development that helps ensure code quality, reliability, and maintainability. In the context of Node.js and Express applications, testing provides several critical benefits:</p>
<ul>
<li><p><strong>Early Bug Detection</strong>: Identify issues before they reach production</p>
</li>
<li><p><strong>Confidence in Refactoring</strong>: Safely modify code knowing tests will catch regressions</p>
</li>
<li><p><strong>Documentation</strong>: Tests serve as living documentation of how our API should behave</p>
</li>
<li><p><strong>Faster Development</strong>: Automated tests are faster than manual testing</p>
</li>
<li><p><strong>Better Design</strong>: Writing testable code often leads to better architecture</p>
</li>
</ul>
<p>This article demonstrates how to implement testing in a Node.js and Express application using the built-in Node.js test runner, Supertest for HTTP assertions, and Faker for generating test data.</p>
<h2 id="heading-testing-stack-overview"><strong>Testing Stack Overview</strong></h2>
<h3 id="heading-nodejs-test-runner">Node.js Test Runner</h3>
<p>Starting with version 18, Node.js includes a built-in <a target="_blank" href="https://nodejs.org/es/learn/test-runner/using-test-runner">test runner</a> that provides:</p>
<ul>
<li><p><strong>Zero Dependencies</strong>: No need for external test frameworks.</p>
</li>
<li><p><strong>Native Integration</strong>: First-class support from the Node.js team.</p>
</li>
<li><p><strong>Modern API</strong>: Supports async/await and promises natively.</p>
</li>
<li><p><strong>Test Organization</strong>: Provides <code>describe</code>, <code>it</code>, and hooks like <code>beforeEach</code> and <code>afterEach</code>.</p>
</li>
<li><p><strong>Built-in Assertions</strong>: Includes the <code>assert</code> module for making assertions.</p>
</li>
<li><p><strong>Fast Execution</strong>: Optimized for performance</p>
</li>
</ul>
<h3 id="heading-supertest">Supertest</h3>
<p><a target="_blank" href="https://www.npmjs.com/package/supertest">Supertest</a> is an HTTP assertion library that makes testing Express applications straightforward:</p>
<ul>
<li><p><strong>Fluent API</strong>: Chainable methods for making requests and asserting responses.</p>
</li>
<li><p><strong>Express Integration</strong>: Works seamlessly with Express applications.</p>
</li>
<li><p><strong>No Server Startup Required</strong>: Can test our Express app without manually starting a server.</p>
</li>
<li><p><strong>Comprehensive Assertions</strong>: Built-in methods for checking status codes, headers, and response bodies.</p>
</li>
</ul>
<h3 id="heading-faker">Faker</h3>
<p><a target="_blank" href="https://www.npmjs.com/package/@faker-js/faker">Faker</a> generates realistic test data:</p>
<ul>
<li><p><strong>Diverse Data Types</strong>: Can generate names, emails, addresses, numbers, and more.</p>
</li>
<li><p><strong>Consistency</strong>: Supports seeding for reproducible test data.</p>
</li>
<li><p><strong>Localization</strong>: Supports multiple locales for region-specific data.</p>
</li>
<li><p><strong>Realistic Data</strong>: Produces data that mimics real-world scenarios better than hardcoded values.</p>
</li>
</ul>
<h2 id="heading-project-structure"><strong>Project Structure</strong></h2>
<p>The testing setup in the <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/openapi">reference repository</a> follows this structure:</p>
<pre><code class="lang-plaintext">tests/
├── setup.js              # Test configuration and teardown
├── todos/
    ├── addTodo.test.js   # Tests for adding todos
    ├── checkTodo.test.js # Tests for checking todos
    ├── findTodo.test.js  # Tests for finding todos
    ├── uncheckTodo.test.js # Tests for unchecking todos
    └── todoDsl.js        # Domain-specific language for tests
</code></pre>
<h2 id="heading-installation"><strong>Installation</strong></h2>
<p>First, install the required testing dependencies:</p>
<pre><code class="lang-powershell">npm install -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span> supertest @faker<span class="hljs-literal">-js</span>/faker
</code></pre>
<h3 id="heading-separating-app-from-server">Separating App from Server</h3>
<p>A crucial aspect of making our Express application testable is separating the app configuration from the server startup. This allows Supertest to create test instances without actually starting the server. The application is split into two files: <code>app.js</code> and <code>server.js</code>. Create the <code>app.js</code> file with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
<span class="hljs-keyword">import</span> todosRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/todos/routes.js'</span>;
<span class="hljs-keyword">import</span> healthRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/health.js'</span>;
<span class="hljs-keyword">import</span> { errorHandler, NotFoundError } <span class="hljs-keyword">from</span> <span class="hljs-string">'./middlewares/errorHandler.js'</span>;
<span class="hljs-keyword">import</span> morgan <span class="hljs-keyword">from</span> <span class="hljs-string">'morgan'</span>;
<span class="hljs-keyword">import</span> expressWinston <span class="hljs-keyword">from</span> <span class="hljs-string">'express-winston'</span>;
<span class="hljs-keyword">import</span> logger <span class="hljs-keyword">from</span> <span class="hljs-string">'./config/logger.js'</span>;
<span class="hljs-keyword">import</span> helmet <span class="hljs-keyword">from</span> <span class="hljs-string">'helmet'</span>;
<span class="hljs-keyword">import</span> cors <span class="hljs-keyword">from</span> <span class="hljs-string">'cors'</span>;
<span class="hljs-keyword">import</span> swaggerRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/swagger.js'</span>;

dotenv.config();
<span class="hljs-keyword">const</span> app = express();
app.use(helmet());
app.use(
  cors({
    <span class="hljs-attr">origin</span>: process.env.ALLOWED_ORIGIN,
  })
);
app.use(express.json());
app.use(morgan(<span class="hljs-string">'dev'</span>));
app.use(
  expressWinston.logger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'HTTP {{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms'</span>,
  })
);
app.use(<span class="hljs-string">'/api-docs'</span>, swaggerRoutes);
app.use(<span class="hljs-string">'/health'</span>, healthRoutes);
app.use(<span class="hljs-string">'/api/todos'</span>, todosRoutes);
app.all(<span class="hljs-string">'/*splat'</span>, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> pathSegments = req.params.splat;
  <span class="hljs-keyword">const</span> fullPath = pathSegments.join(<span class="hljs-string">'/'</span>);
  next(<span class="hljs-keyword">new</span> NotFoundError(<span class="hljs-string">`The requested URL /<span class="hljs-subst">${fullPath}</span> does not exist`</span>));
});
app.use(
  expressWinston.errorLogger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'{{err.message}} {{res.statusCode}} {{req.method}}'</span>,
  })
);
app.use(errorHandler);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> app;
</code></pre>
<ul>
<li><p><strong>Configuration Only</strong>: This file sets up middleware, routes, and error handlers, but doesn't start the server</p>
</li>
<li><p><strong>Export the App</strong>: The Express app is exported as the default export.</p>
</li>
<li><p><strong>No</strong> <code>app.listen()</code>: Crucially, there's no call to start listening on a port.</p>
</li>
<li><p><strong>Environment Variables</strong>: Loaded via dotenv for both development and testing.</p>
</li>
</ul>
<p>Next, update the <code>server.js</code> file as follows:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> app <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.js'</span>;

<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">3000</span>;

process.on(<span class="hljs-string">'uncaughtException'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  process.exit(<span class="hljs-number">1</span>);
});

<span class="hljs-keyword">const</span> server = app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on port <span class="hljs-subst">${PORT}</span>`</span>);
});

process.on(<span class="hljs-string">'unhandledRejection'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  server.close(<span class="hljs-function">() =&gt;</span> {
    process.exit(<span class="hljs-number">1</span>);
  });
});
</code></pre>
<ul>
<li><p><strong>Server Startup Only</strong>: This file is responsible solely for starting the HTTP server.</p>
</li>
<li><p><strong>Imports the App</strong>: Gets the configured Express app from <code>app.js</code>.</p>
</li>
<li><p><strong>Production Entry Point</strong>: This is what runs when we start our application normally.</p>
</li>
<li><p><strong>Not Used in Tests</strong>: Tests import <code>app.js</code> directly, bypassing this file.</p>
</li>
</ul>
<h2 id="heading-handling-authentication-in-tests"><strong>Handling Authentication in Tests</strong></h2>
<p>The <code>auth.js</code> file shows how to bypass authentication during testing:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifyJWT</span>(<span class="hljs-params">options</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (process.env.NODE_ENV === <span class="hljs-string">'test'</span>) {
      req.user = { <span class="hljs-attr">sub</span>: <span class="hljs-string">'test'</span>, <span class="hljs-attr">email</span>: <span class="hljs-string">'test@example.com'</span> };
      <span class="hljs-keyword">return</span> next();
    }
    <span class="hljs-comment">// ... authentication logic</span>
  };
}
</code></pre>
<ul>
<li><p>In the test environment, authentication is bypassed.</p>
</li>
<li><p>This allows testing business logic without dealing with token generation.</p>
</li>
<li><p>The <code>req.user</code> object is populated with test data.</p>
</li>
<li><p>Production code remains unchanged.</p>
</li>
</ul>
<h2 id="heading-database-configuration-for-testing"><strong>Database Configuration for Testing</strong></h2>
<p>When testing applications that interact with databases, it's crucial to have a proper database configuration that works across different environments. The <code>knexfile.js</code> handles this configuration.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
dotenv.config();

<span class="hljs-keyword">const</span> config = {
  <span class="hljs-attr">development</span>: {
    <span class="hljs-attr">client</span>: <span class="hljs-string">'pg'</span>,
    <span class="hljs-attr">connection</span>: process.env.CONNECTION_STRING,
    <span class="hljs-attr">migrations</span>: {
      <span class="hljs-attr">directory</span>: <span class="hljs-string">'./migrations'</span>,
      <span class="hljs-attr">extension</span>: <span class="hljs-string">'js'</span>,
    },
    <span class="hljs-attr">pool</span>: {
      <span class="hljs-attr">min</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">max</span>: <span class="hljs-number">10</span>,
    },
  },
  <span class="hljs-attr">test</span>: {
    <span class="hljs-attr">client</span>: <span class="hljs-string">'pg'</span>,
    <span class="hljs-attr">connection</span>: process.env.CONNECTION_STRING,
    <span class="hljs-attr">migrations</span>: {
      <span class="hljs-attr">directory</span>: <span class="hljs-string">'./migrations'</span>,
      <span class="hljs-attr">extension</span>: <span class="hljs-string">'js'</span>,
    },
    <span class="hljs-attr">pool</span>: {
      <span class="hljs-attr">min</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">max</span>: <span class="hljs-number">10</span>,
    },
  },
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> config;
</code></pre>
<ul>
<li><p><strong>Environment-Specific Configurations</strong>: Separate configurations for development and test environments allow different database settings</p>
</li>
<li><p><strong>Connection String</strong>: Reads from environment variables, allowing different databases for different environments</p>
</li>
</ul>
<h2 id="heading-test-setup-and-teardown"><strong>Test Setup and Teardown</strong></h2>
<p>The <code>setup.js</code> file handles global test configuration:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { after } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> db <span class="hljs-keyword">from</span> <span class="hljs-string">'../src/config/database.js'</span>;

after(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> db.destroy();
});
</code></pre>
<ul>
<li><p>The <code>after</code> hook runs once after all tests complete.</p>
</li>
<li><p>The <code>db.destroy()</code> closes all database connections in the pool. Without this, the Node.js process would wait indefinitely for connections to close.</p>
</li>
</ul>
<h2 id="heading-creating-a-test-dsl-domain-specific-language"><strong>Creating a Test DSL (Domain-Specific Language)</strong></h2>
<p>A test DSL provides reusable functions that abstract common testing operations, allowing for more efficient and consistent testing. The <code>todoDsl.js</code> file demonstrates this pattern:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> request <span class="hljs-keyword">from</span> <span class="hljs-string">'supertest'</span>;
<span class="hljs-keyword">import</span> app <span class="hljs-keyword">from</span> <span class="hljs-string">'../../src/app.js'</span>;
<span class="hljs-keyword">import</span> { faker } <span class="hljs-keyword">from</span> <span class="hljs-string">'@faker-js/faker'</span>;
<span class="hljs-keyword">import</span> assert <span class="hljs-keyword">from</span> <span class="hljs-string">'node:assert'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> randomTodo = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">title</span>: faker.lorem.sentence(<span class="hljs-number">15</span>),
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addTodo = <span class="hljs-keyword">async</span> (todo, errors) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> request(app).post(<span class="hljs-string">'/api/todos'</span>).send(todo);

  <span class="hljs-keyword">if</span> (errors === <span class="hljs-literal">undefined</span>) {
    assert.strictEqual(response.status, <span class="hljs-number">201</span>);
    assert(response.body.id);
  } <span class="hljs-keyword">else</span> {
    assert.strictEqual(response.status, <span class="hljs-number">400</span>);
    assert.deepStrictEqual(response.body.detail, errors);
  }

  <span class="hljs-keyword">return</span> response.body;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> findTodo = <span class="hljs-keyword">async</span> (todoId, error) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> request(app).get(<span class="hljs-string">`/api/todos/<span class="hljs-subst">${todoId}</span>`</span>).send();

  <span class="hljs-keyword">if</span> (error === <span class="hljs-literal">undefined</span>) {
    assert.strictEqual(response.status, <span class="hljs-number">200</span>);
    assert(response.body.id);
  } <span class="hljs-keyword">else</span> {
    assert.strictEqual(response.status, <span class="hljs-number">404</span>);
    assert.strictEqual(response.body.detail, error);
  }

  <span class="hljs-keyword">return</span> response.body;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> checkTodo = <span class="hljs-keyword">async</span> (todoId, error) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> request(app).post(<span class="hljs-string">`/api/todos/<span class="hljs-subst">${todoId}</span>/check`</span>).send();

  <span class="hljs-keyword">if</span> (error === <span class="hljs-literal">undefined</span>) {
    assert.strictEqual(response.status, <span class="hljs-number">200</span>);
    assert(response.body.id);
  } <span class="hljs-keyword">else</span> {
    assert(
      response.status === <span class="hljs-number">404</span> || response.status === <span class="hljs-number">400</span>,
      <span class="hljs-string">`Expected status 404 or 400, but got <span class="hljs-subst">${response.status}</span>`</span>
    );
    assert.strictEqual(response.body.detail, error);
  }

  <span class="hljs-keyword">return</span> response.body;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> uncheckTodo = <span class="hljs-keyword">async</span> (todoId, error) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> request(app)
    .post(<span class="hljs-string">`/api/todos/<span class="hljs-subst">${todoId}</span>/uncheck`</span>)
    .send();

  <span class="hljs-keyword">if</span> (error === <span class="hljs-literal">undefined</span>) {
    assert.strictEqual(response.status, <span class="hljs-number">200</span>);
    assert(response.body.id);
  } <span class="hljs-keyword">else</span> {
    assert(
      response.status === <span class="hljs-number">404</span> || response.status === <span class="hljs-number">400</span>,
      <span class="hljs-string">`Expected status 404 or 400, but got <span class="hljs-subst">${response.status}</span>`</span>
    );
    assert.strictEqual(response.body.detail, error);
  }

  <span class="hljs-keyword">return</span> response.body;
};
</code></pre>
<p>In the <code>randomTodo</code> function, instead of hardcoded values like "Task 1", we use Faker to generate realistic data. This makes tests more robust and catches issues that only appear with varied data.</p>
<h2 id="heading-writing-test-cases">Writing Test Cases</h2>
<p>Time to write the tests using our previously created DSL. The file <code>addTodo.tests.js</code> contains the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> <span class="hljs-string">'../setup.js'</span>;
<span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> assert <span class="hljs-keyword">from</span> <span class="hljs-string">'node:assert'</span>;
<span class="hljs-keyword">import</span> { addTodo, randomTodo } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todoDsl.js'</span>;

describe(<span class="hljs-string">'Add todos'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should create a new todo'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> todo = randomTodo();
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> addTodo(todo);
    assert.strictEqual(result.title, todo.title);
    assert.strictEqual(result.completed, <span class="hljs-literal">false</span>);
    assert(result.created_at);
  });

  test(<span class="hljs-string">'should throw an error for invalid todo'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> todo = {
      <span class="hljs-attr">title</span>: <span class="hljs-string">''</span>,
    };

    <span class="hljs-keyword">await</span> addTodo(todo, [<span class="hljs-string">'title is a required field'</span>]);
  });
});
</code></pre>
<p>The <code>findTodo.test.js</code> file contains just one test:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> <span class="hljs-string">'../setup.js'</span>;
<span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> { addTodo, randomTodo, findTodo } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todoDsl.js'</span>;

describe(<span class="hljs-string">'Find todo'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should return an existing todo'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> todo = randomTodo();
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> addTodo(todo);
    <span class="hljs-keyword">await</span> findTodo(result.id);
  });
});
</code></pre>
<p>This test demonstrates isolation. It first creates a todo, then verifies it can be retrieved. This ensures tests don't depend on pre-existing database state. The <code>checkTodo.test.js</code> and <code>uncheckTodo.test.js</code> validate state transitions:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> <span class="hljs-string">'../setup.js'</span>;
<span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> { addTodo, randomTodo, checkTodo } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todoDsl.js'</span>;

describe(<span class="hljs-string">'Check todo'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should mark todo as completed'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> todo = randomTodo();

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> addTodo(todo);

    <span class="hljs-keyword">await</span> checkTodo(result.id);
  });

  test(<span class="hljs-string">'should throw an error for non-existent todo'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> nonExistentId = <span class="hljs-string">'01982e32-b58f-7051-a9ed-61e1f125b07c'</span>;

    <span class="hljs-keyword">await</span> checkTodo(nonExistentId, <span class="hljs-string">'Todo not found'</span>);
  });

  test(<span class="hljs-string">'should throw an error for invalid todo ID format'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> invalidId = <span class="hljs-string">'invalid-id'</span>;

    <span class="hljs-keyword">await</span> checkTodo(
      invalidId,
      <span class="hljs-string">'The provided todoId does not match the UUIDv7 format'</span>
    );
  });
});
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> <span class="hljs-string">'../setup.js'</span>;
<span class="hljs-keyword">import</span> { test, describe } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:test'</span>;
<span class="hljs-keyword">import</span> { addTodo, randomTodo, checkTodo, uncheckTodo } <span class="hljs-keyword">from</span> <span class="hljs-string">'./todoDsl.js'</span>;

describe(<span class="hljs-string">'Uncheck todo'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should mark todo as not completed'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> todo = randomTodo();

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> addTodo(todo);
    <span class="hljs-keyword">await</span> checkTodo(result.id);
    <span class="hljs-keyword">await</span> uncheckTodo(result.id);
  });

  test(<span class="hljs-string">'should uncheck an already unchecked todo'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> todo = randomTodo();

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> addTodo(todo);
    <span class="hljs-keyword">await</span> uncheckTodo(result.id);
  });

  test(<span class="hljs-string">'should throw an error for non-existent todo'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> nonExistentId = <span class="hljs-string">'01982e32-b58f-7051-a9ed-61e1f125b07c'</span>;

    <span class="hljs-keyword">await</span> uncheckTodo(nonExistentId, <span class="hljs-string">'Todo not found'</span>);
  });

  test(<span class="hljs-string">'should throw an error for invalid todo ID format'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> invalidId = <span class="hljs-string">'invalid-id'</span>;

    <span class="hljs-keyword">await</span> uncheckTodo(
      invalidId,
      <span class="hljs-string">'The provided todoId does not match the UUIDv7 format'</span>
    );
  });
});
</code></pre>
<h2 id="heading-running-tests">Running Tests</h2>
<p>Update the <code>package.json</code> file to include a test script:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"cross-env NODE_ENV=test node --test tests"</span>
  }
}
</code></pre>
<p>Execute tests using npm:</p>
<pre><code class="lang-powershell">npm test
</code></pre>
<p>The Node.js test runner will:</p>
<ul>
<li><p>Discover all test files matching the pattern.</p>
</li>
<li><p>Execute tests in parallel (by default).</p>
</li>
<li><p>Display results with detailed output.</p>
</li>
<li><p>Exit with appropriate code (0 for success, 1 for failure).</p>
</li>
</ul>
<p>You can find all the code <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/tests">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Querying AWS Application Load Balancer Logs]]></title><description><![CDATA[AWS Application Load Balancer (ALB) logs are a critical source of information for troubleshooting, security analysis, and performance optimization. These logs capture detailed information about every request processed by our load balancer, including ...]]></description><link>https://blog.raulnq.com/querying-aws-application-load-balancer-logs</link><guid isPermaLink="true">https://blog.raulnq.com/querying-aws-application-load-balancer-logs</guid><category><![CDATA[AWS]]></category><category><![CDATA[AWS Glue]]></category><category><![CDATA[aws athena]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Thu, 16 Oct 2025 01:24:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760568449086/982fc49f-64d8-4f0a-b7db-31437318d182.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS Application Load Balancer (ALB) logs are a critical source of information for troubleshooting, security analysis, and performance optimization. These logs capture detailed information about every request processed by our load balancer, including client IP addresses, request paths, response codes, latencies, and more.</p>
<p>However, raw ALB logs stored in S3 are difficult to analyze at scale. They are stored as compressed text files, organized by date and time, making manual inspection impractical for high-traffic applications. This is where querying capabilities become essential.</p>
<h2 id="heading-understanding-the-aws-components">Understanding the AWS Components</h2>
<p>Before we start the implementation, let's quickly go over the AWS components we will be discussing:</p>
<h3 id="heading-aws-athena">AWS Athena</h3>
<p><a target="_blank" href="https://docs.aws.amazon.com/athena/latest/ug/what-is.html">AWS Athena</a> is a serverless, interactive query service that allows us to analyze data directly in Amazon S3 using standard SQL. We don't need to set up or manage any infrastructure—we simply define our table schema and start querying.</p>
<p>For AWS ALB logs, Athena excels because:</p>
<ul>
<li><p>It scales automatically with our query complexity</p>
</li>
<li><p>We only pay for the data scanned (typically $5 per TB)</p>
</li>
<li><p>It integrates natively with S3, where AWS ALB logs are stored</p>
</li>
</ul>
<h3 id="heading-aws-glue-data-catalog">AWS Glue Data Catalog</h3>
<p>The <a target="_blank" href="https://docs.aws.amazon.com/prescriptive-guidance/latest/serverless-etl-aws-glue/aws-glue-data-catalog.html">AWS Glue Data Catalog</a> is a centralized metadata repository that stores table definitions, schemas, and partition information. It acts as a persistent metadata store for our data lakes. When we create a table in AWS Athena, we are actually creating an entry in the AWS Glue Data Catalog.</p>
<p>Think of it as a database catalog that tells AWS Athena where our data lives in S3, what format it's in, and how to interpret it—without moving or copying the actual data.</p>
<h3 id="heading-aws-glue-crawler">AWS Glue Crawler</h3>
<p><a target="_blank" href="https://docs.aws.amazon.com/glue/latest/dg/add-crawler.html">AWS Glue Crawler</a> is an automated service that scans our data sources (like S3 buckets), infers schemas, and automatically creates or updates table definitions in the AWS Glue Data Catalog. It's useful for discovering data structures, but has limitations we'll discuss next.</p>
<h3 id="heading-partitions-in-aws-glue-athena">Partitions in AWS Glue / Athena</h3>
<p>In AWS Glue and Athena, which both use the same Data Catalog, a <a target="_blank" href="https://docs.aws.amazon.com/athena/latest/ug/partitions.html">partition</a> is a logical subdivision of a dataset, often based on a key like date. Without partitions, AWS Athena scans every file in the S3 path for each query, leading to higher costs and lower performance.</p>
<h2 id="heading-solution-approaches">Solution Approaches</h2>
<p>There are three main approaches to querying AWS ALB logs with AWS Athena:</p>
<h3 id="heading-aws-glue-crawler-automated-schema-discovery">AWS Glue Crawler (Automated Schema Discovery)</h3>
<p><strong>How it works</strong>: AWS Glue Crawler scans our S3 bucket containing AWS ALB logs, automatically detects the schema, and creates a table in the AWS Glue Data Catalog.</p>
<p><strong>Limitation</strong>: While the AWS Glue Crawler can discover partitions during its run, it automatically infers partitions only if the folder names follow the <code>key=value</code> format, such as:</p>
<pre><code class="lang-bash">s3://&lt;bucket&gt;/.../year=2025/month=10/day=15/
</code></pre>
<p>That is the <a target="_blank" href="https://athena.guide/articles/hive-style-partitioning">Hive-style</a> partition naming convention that AWS Glue understands natively. Unfortunately, AWS ALB log paths look like this:</p>
<pre><code class="lang-bash">s3://&lt;bucket&gt;/&lt;prefix&gt;/AWSLogs/&lt;account-id&gt;/elasticloadbalancing/&lt;region&gt;/2025/10/15/
</code></pre>
<h3 id="heading-aws-lambda-with-s3-event-triggers-automated-partitioning">AWS Lambda with S3 Event Triggers (Automated Partitioning)</h3>
<p><strong>How it works</strong>: This approach combines AWS Glue Crawler with AWS Lambda functions. When new AWS ALB log files arrive in S3, an event trigger invokes a function that creates the corresponding partition in the catalog.</p>
<p><strong>Limitation</strong>: While this solves the partition issue, it introduces unnecessary complexity.</p>
<ul>
<li><p>Executions are triggered for every log file AWS ALB writes (which can be hundreds or thousands per hour in high-traffic scenarios).</p>
</li>
<li><p>Most of these executions are redundant since partitions are typically organized by day or hour.</p>
</li>
</ul>
<h3 id="heading-aws-athena-with-partition-projection-recommended">AWS Athena with Partition Projection (Recommended)</h3>
<p><strong>How it works</strong>: Partition projection is an AWS Athena feature that dynamically generates partition information at query time without storing it in the AWS Glue Data Catalog. We define partition patterns in the table properties, and AWS Athena calculates which partitions to read based on our query filters. So, this is the solution we will explain in detail.</p>
<p>Assuming we already have our AWS ALB writing logs to S3. Create an AWS Athena table that uses partition projection. Execute this SQL in the Athena query editor:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EXTERNAL</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> alb_logs (
    <span class="hljs-keyword">type</span> <span class="hljs-keyword">string</span>,
    <span class="hljs-built_in">time</span> <span class="hljs-keyword">string</span>,
    elb <span class="hljs-keyword">string</span>,
    client_ip <span class="hljs-keyword">string</span>,
    client_port <span class="hljs-built_in">int</span>,
    target_ip <span class="hljs-keyword">string</span>,
    target_port <span class="hljs-built_in">int</span>,
    request_processing_time <span class="hljs-keyword">double</span>,
    target_processing_time <span class="hljs-keyword">double</span>,
    response_processing_time <span class="hljs-keyword">double</span>,
    elb_status_code <span class="hljs-built_in">int</span>,
    target_status_code <span class="hljs-keyword">string</span>,
    received_bytes <span class="hljs-built_in">bigint</span>,
    sent_bytes <span class="hljs-built_in">bigint</span>,
    request_verb <span class="hljs-keyword">string</span>,
    request_url <span class="hljs-keyword">string</span>,
    request_proto <span class="hljs-keyword">string</span>,
    user_agent <span class="hljs-keyword">string</span>,
    ssl_cipher <span class="hljs-keyword">string</span>,
    ssl_protocol <span class="hljs-keyword">string</span>,
    target_group_arn <span class="hljs-keyword">string</span>,
    trace_id <span class="hljs-keyword">string</span>,
    domain_name <span class="hljs-keyword">string</span>,
    chosen_cert_arn <span class="hljs-keyword">string</span>,
    matched_rule_priority <span class="hljs-keyword">string</span>,
    request_creation_time <span class="hljs-keyword">string</span>,
    actions_executed <span class="hljs-keyword">string</span>,
    redirect_url <span class="hljs-keyword">string</span>,
    error_reason <span class="hljs-keyword">string</span>,
    target_port_list <span class="hljs-keyword">string</span>,
    target_status_code_list <span class="hljs-keyword">string</span>,
    classification <span class="hljs-keyword">string</span>,
    classification_reason <span class="hljs-keyword">string</span>
)
PARTITIONED <span class="hljs-keyword">BY</span> (
    <span class="hljs-keyword">year</span> <span class="hljs-keyword">string</span>,
    <span class="hljs-keyword">month</span> <span class="hljs-keyword">string</span>,
    <span class="hljs-keyword">day</span> <span class="hljs-keyword">string</span>
)
<span class="hljs-keyword">ROW</span> <span class="hljs-keyword">FORMAT</span> SERDE <span class="hljs-string">'org.apache.hadoop.hive.serde2.RegexSerDe'</span>
<span class="hljs-keyword">WITH</span> SERDEPROPERTIES (
    <span class="hljs-string">'serialization.format'</span> = <span class="hljs-string">'1'</span>,
    <span class="hljs-string">'input.regex'</span> = <span class="hljs-string">'([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\s]+?)\" \"([^\s]+)\" \"([^ ]*)\" \"([^ ]*)\"'</span>
)
LOCATION <span class="hljs-string">'s3://&lt;bucket&gt;/&lt;prefix&gt;/AWSLogs/&lt;account-id&gt;/elasticloadbalancing/&lt;region&gt;/'</span>
TBLPROPERTIES (
    <span class="hljs-string">'projection.enabled'</span> = <span class="hljs-string">'true'</span>,
    <span class="hljs-string">'projection.year.type'</span> = <span class="hljs-string">'integer'</span>,
    <span class="hljs-string">'projection.year.range'</span> = <span class="hljs-string">'2025,2099'</span>,
    <span class="hljs-string">'projection.year.digits'</span> = <span class="hljs-string">'4'</span>,
    <span class="hljs-string">'projection.month.type'</span> = <span class="hljs-string">'integer'</span>,
    <span class="hljs-string">'projection.month.range'</span> = <span class="hljs-string">'01,12'</span>,
    <span class="hljs-string">'projection.month.digits'</span> = <span class="hljs-string">'2'</span>,
    <span class="hljs-string">'projection.day.type'</span> = <span class="hljs-string">'integer'</span>,
    <span class="hljs-string">'projection.day.range'</span> = <span class="hljs-string">'01,31'</span>,
    <span class="hljs-string">'projection.day.digits'</span> = <span class="hljs-string">'2'</span>,
    <span class="hljs-string">'storage.location.template'</span> = <span class="hljs-string">'s3://&lt;bucket&gt;/&lt;prefix&gt;/AWSLogs/&lt;account-id&gt;/elasticloadbalancing/&lt;region&gt;/${year}/${month}/${day}'</span>
);
</code></pre>
<p><strong>Schema Definition</strong>: The column definitions match the AWS ALB log format. Each field corresponds to a specific piece of information in the log entry (client IP, response codes, timing, etc.). AWS ALB logs follow a specific format documented by AWS.</p>
<p><strong>PARTITIONED BY</strong>: We define three partition columns: <code>year</code>, <code>month</code>, and <code>day</code>. These align with how AWS ALB organizes logs in S3. Partitioning is crucial because it allows Athena to skip scanning irrelevant data, dramatically reducing query costs and improving performance.</p>
<p><strong>ROW FORMAT SERDE</strong>: We use <code>RegexSerDe</code> to parse the log entries. AWS ALB logs are space-delimited with quoted strings, and this regex pattern extracts each field correctly. The regex handles edge cases like missing values (represented by <code>-</code>) and quoted fields that may contain spaces.</p>
<p><strong>LOCATION</strong>: This points to the base path where AWS ALB writes logs. Note that it doesn't include the year/month/day folders—those are handled by partition projection.</p>
<p><strong>TBLPROPERTIES</strong>: Partition projection configuration.</p>
<ul>
<li><p><code>projection.enabled = 'true'</code>: Activates partition projection for this table.</p>
</li>
<li><p><code>projection.year.type = 'integer'</code>: Defines year as an integer partition. The range <code>2025,2099</code> covers the expected lifespan of your logs. Adjust these values based on your needs.</p>
</li>
<li><p><code>projection.month.type = 'integer'</code>: Defines month as an integer partition. The range <code>1,12</code> covers all months. The <code>digits = '2'</code> ensures single-digit months are zero-padded (01, 02, etc.) to match S3 paths.</p>
</li>
<li><p><code>projection.day.type = 'integer'</code>: Similar to month, covers days 1-31 with zero-padding.</p>
</li>
<li><p><code>storage.location.template</code>: This is critical; it tells AWS Athena how to construct S3 paths for each partition combination. The variables <code>${year}/${month}/${day}</code> are replaced with actual values at query time.</p>
</li>
</ul>
<p>Now you can query our logs using standard SQL. Here are practical examples:</p>
<p><strong>Find all 500 errors in the last 24 hours.</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> 
    <span class="hljs-built_in">time</span>,
    client_ip,
    request_verb,
    request_url,
    elb_status_code,
    target_status_code,
    target_ip
<span class="hljs-keyword">FROM</span> alb_logs
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">year</span> = <span class="hljs-string">'2025'</span>
  <span class="hljs-keyword">AND</span> <span class="hljs-keyword">month</span> = <span class="hljs-string">'10'</span>
  <span class="hljs-keyword">AND</span> <span class="hljs-keyword">day</span> = <span class="hljs-string">'15'</span>
  <span class="hljs-keyword">AND</span> elb_status_code = <span class="hljs-number">500</span>
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> <span class="hljs-built_in">time</span> <span class="hljs-keyword">DESC</span>;
</code></pre>
<p><strong>Explanation</strong>: This query filters by partition columns first (<code>year</code>, <code>month</code>, <code>day</code>), which is essential for performance. Athena will only scan files from October 15, 2025, ignoring all other days. Always filter by partition columns when possible to minimize data scanned.</p>
<p><strong>Analyze request latency by endpoint.</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> 
    request_url,
    <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">as</span> request_count,
    <span class="hljs-keyword">ROUND</span>(<span class="hljs-keyword">AVG</span>(target_processing_time), <span class="hljs-number">3</span>) <span class="hljs-keyword">as</span> avg_target_time,
    <span class="hljs-keyword">ROUND</span>(<span class="hljs-keyword">AVG</span>(request_processing_time), <span class="hljs-number">3</span>) <span class="hljs-keyword">as</span> avg_request_time,
    <span class="hljs-keyword">ROUND</span>(<span class="hljs-keyword">AVG</span>(response_processing_time), <span class="hljs-number">3</span>) <span class="hljs-keyword">as</span> avg_response_time,
    <span class="hljs-keyword">ROUND</span>(<span class="hljs-keyword">MAX</span>(target_processing_time), <span class="hljs-number">3</span>) <span class="hljs-keyword">as</span> max_target_time
<span class="hljs-keyword">FROM</span> alb_logs
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">year</span> = <span class="hljs-string">'2025'</span>
  <span class="hljs-keyword">AND</span> <span class="hljs-keyword">month</span> = <span class="hljs-string">'10'</span>
  <span class="hljs-keyword">AND</span> <span class="hljs-keyword">day</span> &gt;= <span class="hljs-string">'14'</span>
  <span class="hljs-keyword">AND</span> <span class="hljs-keyword">day</span> &lt;= <span class="hljs-string">'15'</span>
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> request_url
<span class="hljs-keyword">HAVING</span> <span class="hljs-keyword">COUNT</span>(*) &gt; <span class="hljs-number">100</span>
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> avg_target_time <span class="hljs-keyword">DESC</span>;
</code></pre>
<p><strong>Explanation</strong>: This query analyzes performance across multiple days (14th and 15th). It calculates average and max latencies for each endpoint. The <code>HAVING COUNT(*) &gt; 100</code> ensures we only look at endpoints with significant traffic, avoiding skewed statistics from rarely-hit URLs.</p>
<p><strong>Identify top client IPs by request volume</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> 
    client_ip,
    <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">as</span> request_count,
    <span class="hljs-keyword">COUNT</span>(<span class="hljs-keyword">DISTINCT</span> request_url) <span class="hljs-keyword">as</span> unique_urls,
    <span class="hljs-keyword">SUM</span>(received_bytes) <span class="hljs-keyword">as</span> total_received_bytes,
    <span class="hljs-keyword">SUM</span>(sent_bytes) <span class="hljs-keyword">as</span> total_sent_bytes
<span class="hljs-keyword">FROM</span> alb_logs
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">year</span> = <span class="hljs-string">'2025'</span>
  <span class="hljs-keyword">AND</span> <span class="hljs-keyword">month</span> = <span class="hljs-string">'10'</span>
  <span class="hljs-keyword">AND</span> <span class="hljs-keyword">day</span> = <span class="hljs-string">'15'</span>
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> client_ip
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> request_count <span class="hljs-keyword">DESC</span>;
</code></pre>
<p><strong>Explanation</strong>: Useful for identifying potential DDoS attacks or heavy users. This query groups by client IP and aggregates request counts and bandwidth usage. The <code>unique_urls</code> metric can help distinguish between legitimate users and suspicious scanning behavior (many requests to diverse URLs often indicate reconnaissance).</p>
<p>Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Node.js and Express: OpenAPI]]></title><description><![CDATA[API documentation is often treated as an afterthought in software development, yet it plays a critical role in the success of any API. Poor or missing documentation leads to integration delays, increased support requests, and frustrated developers. O...]]></description><link>https://blog.raulnq.com/nodejs-and-express-openapi</link><guid isPermaLink="true">https://blog.raulnq.com/nodejs-and-express-openapi</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Express]]></category><category><![CDATA[OpenApi]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Sat, 04 Oct 2025 01:47:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759500717182/87702b8b-915b-477f-857a-39af8a0d797b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>API documentation is often treated as an afterthought in software development, yet it plays a critical role in the success of any API. Poor or missing documentation leads to integration delays, increased support requests, and frustrated developers. <a target="_blank" href="https://swagger.io/docs/specification/v3_0/about/">OpenAPI</a> addresses this challenge by providing a standardized format for describing REST APIs.</p>
<p>In this article, we'll explore how to integrate OpenAPI documentation into a Node.js and Express application using <a target="_blank" href="https://www.npmjs.com/package/swagger-autogen"><code>swagger-autogen</code></a> and <a target="_blank" href="https://www.npmjs.com/package/swagger-ui-express"><code>swagger-ui-express</code></a>. We will build upon an existing Express application, adding comprehensive API documentation that serves as both a reference for consumers and a living contract for our API.</p>
<h2 id="heading-setup">Setup</h2>
<p>We will use the application from <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/azure-entraid">nodejs-express</a> as our starting point. This is a basic Express application with features for managing To-Do tasks. Once downloaded, install the required dependencies:</p>
<pre><code class="lang-powershell">npm install swagger<span class="hljs-literal">-autogen</span> swagger<span class="hljs-literal">-ui</span><span class="hljs-literal">-express</span>
</code></pre>
<ul>
<li><p><a target="_blank" href="https://www.npmjs.com/package/swagger-autogen"><strong>swagger-autogen</strong></a>: Automatically generates <a target="_blank" href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#openapi-specification">OpenAPI</a> specifications by analyzing our Express routes and comments. This approach keeps documentation close to code, reducing maintenance overhead.</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/swagger-ui-express"><strong>swagger-ui-express</strong></a>: Serves an interactive documentation UI that allows developers to explore and test our API directly from the browser.</p>
</li>
</ul>
<h2 id="heading-generating-openapi-specification">Generating OpenAPI Specification</h2>
<p>Create a new file <code>swagger.js</code> in the project root. This script will generate the OpenAPI specification:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> swaggerAutogen <span class="hljs-keyword">from</span> <span class="hljs-string">'swagger-autogen'</span>;
<span class="hljs-keyword">import</span> { todoSchemas } <span class="hljs-keyword">from</span> <span class="hljs-string">'./src/features/todos/schemas.js'</span>;
<span class="hljs-keyword">import</span> { errorSchemas } <span class="hljs-keyword">from</span> <span class="hljs-string">'./src/middlewares/schemas.js'</span>;
<span class="hljs-keyword">const</span> HOST = process.env.HOST || <span class="hljs-string">'localhost:5000'</span>;
<span class="hljs-keyword">const</span> SCHEMA = process.env.SCHEMA || <span class="hljs-string">'http'</span>;
<span class="hljs-keyword">const</span> doc = {
  <span class="hljs-attr">info</span>: {
    <span class="hljs-attr">title</span>: <span class="hljs-string">'My API'</span>,
    <span class="hljs-attr">description</span>: <span class="hljs-string">'API Documentation'</span>,
    <span class="hljs-attr">version</span>: <span class="hljs-string">'1.0.0'</span>,
  },
  <span class="hljs-attr">servers</span>: [{ <span class="hljs-attr">url</span>: <span class="hljs-string">`<span class="hljs-subst">${SCHEMA}</span>://<span class="hljs-subst">${HOST}</span>`</span> }],
  <span class="hljs-attr">components</span>: {
    <span class="hljs-attr">securitySchemes</span>: {
      <span class="hljs-attr">bearerAuth</span>: {
        <span class="hljs-attr">type</span>: <span class="hljs-string">'http'</span>,
        <span class="hljs-attr">scheme</span>: <span class="hljs-string">'bearer'</span>,
        <span class="hljs-attr">bearerFormat</span>: <span class="hljs-string">'JWT'</span>,
        <span class="hljs-attr">description</span>: <span class="hljs-string">'JWT Bearer token for user authentication'</span>,
      },
    },
    <span class="hljs-attr">schemas</span>: {
      ...todoSchemas,
      ...errorSchemas,
    },
    <span class="hljs-attr">parameters</span>: {
      <span class="hljs-attr">pageNumber</span>: {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'pageNumber'</span>,
        <span class="hljs-attr">in</span>: <span class="hljs-string">'query'</span>,
        <span class="hljs-attr">description</span>: <span class="hljs-string">'Page number for pagination'</span>,
        <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">default</span>: <span class="hljs-number">1</span>,
        <span class="hljs-attr">schema</span>: {
          <span class="hljs-attr">type</span>: <span class="hljs-string">'integer'</span>,
        },
      },
      <span class="hljs-attr">pageSize</span>: {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'pageSize'</span>,
        <span class="hljs-attr">in</span>: <span class="hljs-string">'query'</span>,
        <span class="hljs-attr">description</span>: <span class="hljs-string">'Page size for pagination'</span>,
        <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">default</span>: <span class="hljs-number">10</span>,
        <span class="hljs-attr">schema</span>: {
          <span class="hljs-attr">type</span>: <span class="hljs-string">'integer'</span>,
        },
      },
    },
    <span class="hljs-attr">responses</span>: {
      <span class="hljs-attr">unauthorizedError</span>: {
        <span class="hljs-attr">description</span>: <span class="hljs-string">'Unauthorized'</span>,
        <span class="hljs-attr">content</span>: {
          <span class="hljs-string">'application/json'</span>: {
            <span class="hljs-attr">schema</span>: { <span class="hljs-attr">$ref</span>: <span class="hljs-string">'#/components/schemas/unauthorizedError'</span> },
          },
        },
      },
      <span class="hljs-attr">validationError</span>: {
        <span class="hljs-attr">description</span>: <span class="hljs-string">'Validation Error'</span>,
        <span class="hljs-attr">content</span>: {
          <span class="hljs-string">'application/json'</span>: {
            <span class="hljs-attr">schema</span>: { <span class="hljs-attr">$ref</span>: <span class="hljs-string">'#/components/schemas/validationError'</span> },
          },
        },
      },
      <span class="hljs-attr">notFoundError</span>: {
        <span class="hljs-attr">description</span>: <span class="hljs-string">'Not Found'</span>,
        <span class="hljs-attr">content</span>: {
          <span class="hljs-string">'application/json'</span>: {
            <span class="hljs-attr">schema</span>: { <span class="hljs-attr">$ref</span>: <span class="hljs-string">'#/components/schemas/notFoundError'</span> },
          },
        },
      },
    },
  },
};

<span class="hljs-keyword">const</span> outputFile = <span class="hljs-string">'./swagger-output.json'</span>;
<span class="hljs-keyword">const</span> endpointsFiles = [<span class="hljs-string">'./src/server.js'</span>];

swaggerAutogen({ <span class="hljs-attr">openapi</span>: <span class="hljs-string">'3.0.0'</span>, <span class="hljs-attr">autoQuery</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">autoHeaders</span>: <span class="hljs-literal">false</span> })(
  outputFile,
  endpointsFiles,
  doc
);
</code></pre>
<p>The <code>doc</code> variable contains the basic information for our API documentation:</p>
<ul>
<li><p><code>info</code>: General information about the API.</p>
</li>
<li><p><code>servers</code>: List of server objects where the API is hosted.</p>
</li>
<li><p><code>components</code>: The object contains reusable parts of the API specification:</p>
<ul>
<li><p><code>securitySchemes</code>: Defines authentication methods.</p>
</li>
<li><p><code>schemas</code>: Defines input and/or output data types.</p>
</li>
<li><p><code>parameters</code>: Defines reusable query/path/header parameters.</p>
</li>
<li><p><code>responses</code>: Define reusable responses.</p>
</li>
</ul>
</li>
</ul>
<p>Then the <a target="_blank" href="https://swagger-autogen.github.io/docs/getting-started/advanced-usage"><code>swaggerAutogen</code></a> method scans our route files (<code>./src/server.js</code>) and produces the final <code>swagger-output.json</code>. To enable OpenAPI, we set the <code>3.0.0</code> version as an <a target="_blank" href="https://swagger-autogen.github.io/docs/options">option</a> in the <code>swaggerAutogen</code> method. One last thing to note is that we are disabling the automatic headers and query recognition. Add a script to the <code>package.json</code> file to run this generator:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"swagger"</span>: <span class="hljs-string">"node swagger.js"</span>
  }
}
</code></pre>
<h2 id="heading-schemas">Schemas</h2>
<p>One of the best features of swagger-autogen is its ability to take example objects and automatically infer <a target="_blank" href="https://swagger-autogen.github.io/docs/openapi-3/schemas-and-components">schemas</a> for them. Create the <code>/src/features/todos/schemas.js</code> file with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> todoSchemas = {
  <span class="hljs-attr">todo</span>: {
    <span class="hljs-attr">id</span>: <span class="hljs-string">'01994462-a4d6-73bc-98fc-b861a38b1c0a'</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">'title'</span>,
    <span class="hljs-attr">completed</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">created_at</span>: <span class="hljs-string">'2023-10-10T12:00:00Z'</span>,
  },
  <span class="hljs-attr">todoList</span>: {
    <span class="hljs-attr">items</span>: [{ <span class="hljs-attr">$ref</span>: <span class="hljs-string">'#/components/schemas/todo'</span> }],
    <span class="hljs-attr">pageNumber</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">pageSize</span>: <span class="hljs-number">10</span>,
    <span class="hljs-attr">totalPages</span>: <span class="hljs-number">5</span>,
    <span class="hljs-attr">totalItems</span>: <span class="hljs-number">50</span>,
  },
  <span class="hljs-attr">addTodo</span>: {
    <span class="hljs-attr">$title</span>: <span class="hljs-string">'title'</span>,
  },
};
</code></pre>
<p>Create the <code>/src/middlewares/schemas.js</code> file with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> errorSchemas = {
  <span class="hljs-attr">validationError</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-string">'/problems/validation-error'</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">'ValidationError'</span>,
    <span class="hljs-attr">detail</span>: [<span class="hljs-string">'Error description'</span>],
    <span class="hljs-attr">instance</span>: <span class="hljs-string">'resource path'</span>,
    <span class="hljs-attr">status</span>: <span class="hljs-number">400</span>,
  },
  <span class="hljs-attr">unauthorizedError</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-string">'/problems/unauthorized'</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">'Unauthorized'</span>,
    <span class="hljs-attr">detail</span>: <span class="hljs-string">'Error description'</span>,
    <span class="hljs-attr">instance</span>: <span class="hljs-string">'resource path'</span>,
    <span class="hljs-attr">status</span>: <span class="hljs-number">401</span>,
  },
  <span class="hljs-attr">notFoundError</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-string">'/problems/resource-not-found'</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">'NotFoundError'</span>,
    <span class="hljs-attr">detail</span>: <span class="hljs-string">'Error description'</span>,
    <span class="hljs-attr">instance</span>: <span class="hljs-string">'resource path'</span>,
    <span class="hljs-attr">status</span>: <span class="hljs-number">404</span>,
  },
};
</code></pre>
<p>The resulting OpenAPI specification is something like:</p>
<pre><code class="lang-json"><span class="hljs-string">"schemas"</span>: {
  <span class="hljs-attr">"todo"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
      <span class="hljs-attr">"id"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"01994462-a4d6-73bc-98fc-b861a38b1c0a"</span>
      },
      <span class="hljs-attr">"title"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"title"</span>
      },
      <span class="hljs-attr">"completed"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"boolean"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-literal">false</span>
      },
      <span class="hljs-attr">"created_at"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"2023-10-10T12:00:00Z"</span>
      }
    }
  },
  <span class="hljs-attr">"todoList"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
      <span class="hljs-attr">"items"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"array"</span>,
        <span class="hljs-attr">"items"</span>: {
          <span class="hljs-attr">"$ref"</span>: <span class="hljs-string">"#/components/schemas/todo"</span>
        }
      },
      <span class="hljs-attr">"pageNumber"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-number">1</span>
      },
      <span class="hljs-attr">"pageSize"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-number">10</span>
      },
      <span class="hljs-attr">"totalPages"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-number">5</span>
      },
      <span class="hljs-attr">"totalItems"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-number">50</span>
      }
    }
  },
  <span class="hljs-attr">"addTodo"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
      <span class="hljs-attr">"title"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"title"</span>
      }
    },
    <span class="hljs-attr">"required"</span>: [
      <span class="hljs-string">"title"</span>
    ]
  },
  <span class="hljs-attr">"validationError"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
      <span class="hljs-attr">"type"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"/problems/validation-error"</span>
      },
      <span class="hljs-attr">"title"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"ValidationError"</span>
      },
      <span class="hljs-attr">"detail"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"array"</span>,
        <span class="hljs-attr">"example"</span>: [
          <span class="hljs-string">"Error description"</span>
        ],
        <span class="hljs-attr">"items"</span>: {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>
        }
      },
      <span class="hljs-attr">"instance"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"resource path"</span>
      },
      <span class="hljs-attr">"status"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-number">400</span>
      }
    }
  },
  <span class="hljs-attr">"unauthorizedError"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
      <span class="hljs-attr">"type"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"/problems/unauthorized"</span>
      },
      <span class="hljs-attr">"title"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"Unauthorized"</span>
      },
      <span class="hljs-attr">"detail"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"Error description"</span>
      },
      <span class="hljs-attr">"instance"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"resource path"</span>
      },
      <span class="hljs-attr">"status"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-number">401</span>
      }
    }
  },
  <span class="hljs-attr">"notFoundError"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
      <span class="hljs-attr">"type"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"/problems/resource-not-found"</span>
      },
      <span class="hljs-attr">"title"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"NotFoundError"</span>
      },
      <span class="hljs-attr">"detail"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"Error description"</span>
      },
      <span class="hljs-attr">"instance"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-string">"resource path"</span>
      },
      <span class="hljs-attr">"status"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"example"</span>: <span class="hljs-number">404</span>
      }
    }
  }
}
</code></pre>
<p>As we can see, this feature is particularly convenient, especially when dealing with structures that are easy to infer. Besides that, placing the object examples near the feature where they are used helps us keep the documentation up to date.</p>
<h2 id="heading-endpoint-documentation"><strong>Endpoint Documentation</strong></h2>
<p>Swagger-autogen reads the comments in the endpoints to enhance the documentation. Let's enhance the route in the <code>src/features/todos/listTodos.js</code> file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> listTodos = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-comment">/*
  #swagger.tags = ['Todos']
  #swagger.summary = 'Get all todos'
  #swagger.description = 'Retrieve a paginated list of todos with optional filtering by title and completion status'
  #swagger.parameters['pageNumber'] = {$ref: '#/components/parameters/pageNumber'}
  #swagger.parameters['pageSize'] = {$ref: '#/components/parameters/pageSize'}
  #swagger.parameters['title'] = {
        in: 'query',
        description: 'Title of the todo item',
        required: false,
        type: 'string'
      }
  #swagger.parameters['completed'] = {
        in: 'query',
        description: 'Completion status of the todo item',
        required: false,
        type: 'boolean'
      }
  #swagger.responses[200] = {
    description: 'Successfully retrieved todos',
    content: {
      'application/json': {
        schema: {
          $ref: '#/components/schemas/todoList'
        }
      }
    }
  },
  #swagger.responses[400] = {$ref: '#/components/responses/validationError'}
  */</span>
  <span class="hljs-comment">// ...existing code...</span>
};
</code></pre>
<p>This comment block contains swagger-autogen directives within our Express handler. Swagger-autogen reads these <code>#swagger.*</code> lines during generation and uses them to create the OpenAPI specification. They do not impact the function's runtime behavior; they are only for generating documentation.</p>
<ul>
<li><p><code>#swagger.tags = ['Todos']</code>: Groups this operation under the Todos <a target="_blank" href="https://swagger-autogen.github.io/docs/endpoints/tags/">tag</a> in the UI.</p>
</li>
<li><p><code>#swagger.summary</code>: Short one-line <a target="_blank" href="https://swagger-autogen.github.io/docs/endpoints/summary/">text</a> shown in the list of operations.</p>
</li>
<li><p><code>#swagger.description</code>: Longer human <a target="_blank" href="https://swagger-autogen.github.io/docs/endpoints/description/">description</a> shown on the operation detail panel.</p>
</li>
<li><p><code>#swagger.parameters['pageNumber']</code> and <code>#swagger.parameters['pageSize']</code>: Reuses a predefined <a target="_blank" href="https://swagger-autogen.github.io/docs/openapi-3/parameters">parameter</a> defined under <code>components.parameters</code>.</p>
</li>
<li><p><code>#swagger.parameters['title']</code> and <code>#swagger.parameters['completed']</code>: Declares a <a target="_blank" href="https://swagger-autogen.github.io/docs/openapi-3/parameters">parameter</a>. OpenAPI parameters should include a <code>schema</code> object (<code>schema: { type: 'string' }</code>). Swagger-autogen accepts the shorthand <code>type: 'string'</code> in comments and will convert it into the proper OpenAPI shape when generating the final specification.</p>
</li>
<li><p><code>#swagger.responses[200]</code>: Declares the 200 OK success <a target="_blank" href="https://swagger-autogen.github.io/docs/openapi-3/responses">response</a>.</p>
</li>
<li><p><code>#swagger.responses[400]</code>: Reuses a predefined <a target="_blank" href="https://swagger-autogen.github.io/docs/openapi-3/responses">response</a> under <code>components.responses</code>.</p>
</li>
</ul>
<p>Let's apply the same approach in the route located in the <code>src/features/todos/listTodos.js</code> file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addTodo = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-comment">/*
  #swagger.tags = ['Todos']
  #swagger.summary = 'Create a new todo'
  #swagger.description = 'Create a new todo item with a title and default completion status'
  #swagger.requestBody = {
    required: true,
    content: {
      'application/json': {
        schema: {
          $ref: '#/components/schemas/addTodo'
        }
      }
    }
  }
  #swagger.responses[201] = {
    description: 'Todo created successfully',
    content: {
      'application/json': {
        schema: {
          $ref: '#/components/schemas/todo'
        }
      }
    }
  }
  #swagger.responses[400] = {$ref: '#/components/responses/validationError'}
  #swagger.responses[401] = {$ref: '#/components/responses/unauthorizedError'}
  #swagger.security = [{
    bearerAuth: []
  }]
  */</span>
  <span class="hljs-comment">// ...existing code...</span>
};
</code></pre>
<ul>
<li><p><code>#swagger.requestBody</code>: Declares the expected <a target="_blank" href="https://swagger-autogen.github.io/docs/openapi-3/request-body">request body</a>.</p>
</li>
<li><p><code>#swagger.security</code>: Adds an operation-level <a target="_blank" href="https://swagger-autogen.github.io/docs/openapi-3/authentication/">security</a> requirement. <code>bearerAuth</code> must be declared under <code>components.securitySchemes</code></p>
</li>
</ul>
<p>The <code>src/features/todos/checkTodo.js</code>, <code>src/features/todos/uncheckTodo.js</code>, and <code>src/features/todos/findTodo.js</code> files are almost identical. Let's make the changes in the last one:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> findTodo = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-comment">/*
  #swagger.tags = ['Todos']
  #swagger.summary = 'Get a specific todo by ID'
  #swagger.description = 'Retrieve a single todo item by its unique identifier'
  #swagger.parameters['todoId'] = {
    in: 'path',
    description: 'Todo item unique identifier',
    required: true,
    type: 'string'
  }
  #swagger.responses[200] = {
    description: 'Todo found successfully',
    content: {
      'application/json': {
        schema: {
          $ref: '#/components/schemas/todo'
        }
      }
    }
  }
  #swagger.responses[400] = {$ref: '#/components/responses/validationError'}
  #swagger.responses[404] = {$ref: '#/components/responses/notFoundError'}
   */</span>
  <span class="hljs-comment">// ...existing code...</span>
};
</code></pre>
<p>By using the swagger-autogen directives, we keep the documentation close to the code. This proximity makes it less likely for the documentation to become outdated.</p>
<h2 id="heading-swagger-ui">Swagger UI</h2>
<p>Once we have generated the documentation, it's time to serve it using swagger-ui-express. Create a <code>src/routes/swagger.js</code> file with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { readFileSync } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span>;
<span class="hljs-keyword">import</span> swaggerUi <span class="hljs-keyword">from</span> <span class="hljs-string">'swagger-ui-express'</span>;
<span class="hljs-keyword">const</span> router = express.Router();

<span class="hljs-keyword">const</span> swaggerFile = <span class="hljs-built_in">JSON</span>.parse(readFileSync(<span class="hljs-string">'./swagger-output.json'</span>, <span class="hljs-string">'utf-8'</span>));

router.use(<span class="hljs-string">'/'</span>, swaggerUi.serve, swaggerUi.setup(swaggerFile));

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>First, we load the generated <code>swagger-output.json</code> file and then mount the Swagger UI in the Express router. Finally, in the <code>src/server.js</code> file, we add that router to our Express app under the <code>/api-docs</code> path:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// ...existing code...</span>
<span class="hljs-keyword">import</span> swaggerRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/swagger.js'</span>;

process.on(<span class="hljs-string">'uncaughtException'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  process.exit(<span class="hljs-number">1</span>);
});
dotenv.config();
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">3000</span>;
<span class="hljs-keyword">const</span> app = express();
app.use(helmet());
app.use(
  cors({
    <span class="hljs-attr">origin</span>: process.env.ALLOWED_ORIGIN,
  })
);
app.use(express.json());
app.use(morgan(<span class="hljs-string">'dev'</span>));
app.use(
  expressWinston.logger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'HTTP {{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms'</span>,
  })
);
app.use(<span class="hljs-string">'/api-docs'</span>, swaggerRoutes);
<span class="hljs-comment">// ...existing code...</span>
</code></pre>
<p>Run the application and visit <a target="_blank" href="http://localhost:5000/api-docs"><code>http://localhost:5000/api-docs</code></a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759539933308/403e719a-f111-48ad-bc42-e833ccb3e886.png" alt class="image--center mx-auto" /></p>
<p>You can find all the code <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/openapi">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Building a YouTube Video Transcription API with Node.js]]></title><description><![CDATA[YouTube video transcripts are valuable for content analysis, accessibility, and creating searchable content archives. While npm libraries like youtube-transcript once provided easy access to this data, many have become unreliable due to YouTube's fre...]]></description><link>https://blog.raulnq.com/building-a-youtube-video-transcription-api-with-nodejs</link><guid isPermaLink="true">https://blog.raulnq.com/building-a-youtube-video-transcription-api-with-nodejs</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Express]]></category><category><![CDATA[playwright]]></category><category><![CDATA[webscraping ]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Fri, 26 Sep 2025 18:28:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758905569363/14e2c19f-ed13-42ec-8e2a-0dd46086e730.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>YouTube video transcripts are valuable for content analysis, accessibility, and creating searchable content archives. While npm libraries like <a target="_blank" href="https://github.com/Kakulukian/youtube-transcript">youtube-transcript</a> once provided easy access to this data, many have become unreliable due to YouTube's frequent internal interface changes. In this article, we'll build a YouTube transcription API using Node.js, Express, and Playwright as an alternative method.</p>
<h2 id="heading-project-overview"><strong>Project Overview</strong></h2>
<p>Our API will consist of several key components:</p>
<ul>
<li><p><strong>Express.js server</strong> (<code>server.js</code>) - Main application entry point</p>
</li>
<li><p><strong>Transcript extractor</strong> (<code>transcript.js</code>) - Playwright-based scraping logic</p>
</li>
<li><p><strong>Error handling</strong> (<code>errorHandler.js</code>) - RFC 7807 compliant error responses</p>
</li>
<li><p><strong>Security middleware</strong> (<code>securityHandler.js</code>) - API key authentication</p>
</li>
<li><p><strong>Docker containerization</strong> for easy deployment</p>
</li>
</ul>
<h2 id="heading-setting-up-the-development-environment"><strong>Setting Up the Development Environment</strong></h2>
<p>As mentioned in the <a target="_blank" href="https://blog.raulnq.com/nodejs-and-express-setting-up-the-development-environment">Node.js and Express development environment setup article</a>, we'll use a modern Node.js development setup with ESLint, Prettier, and Husky for code quality. Once the environment is ready, run the following command:</p>
<pre><code class="lang-powershell">npm i express playwright express<span class="hljs-literal">-healthcheck</span> http<span class="hljs-literal">-problem</span><span class="hljs-literal">-details</span> morgan
</code></pre>
<p>Key dependencies explained:</p>
<ul>
<li><p><strong>Express 5.x:</strong> Latest Express.js for the REST API.</p>
</li>
<li><p><strong>Playwright</strong>: Reliable browser automation for scraping.</p>
</li>
<li><p><strong>http-problem-details</strong>: RFC 7807 compliant error responses.</p>
</li>
<li><p><strong>morgan</strong>: HTTP request logging.</p>
</li>
<li><p><strong>express-healthcheck</strong>: Built-in health monitoring.</p>
</li>
</ul>
<h2 id="heading-implementing-error-handling"><strong>Implementing Error Handling</strong></h2>
<p>Before building the main functionality, let's establish robust error handling using the RFC 7807 Problem Details standard in the <code>errorHandler.js</code> file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { ProblemDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">'http-problem-details'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Error</span> </span>{
  <span class="hljs-keyword">constructor</span>(error, type, status, data = null) {
    <span class="hljs-built_in">super</span>(error);
    <span class="hljs-built_in">this</span>.type = type;
    <span class="hljs-built_in">this</span>.status = status;
    <span class="hljs-built_in">this</span>.detail = error;
    <span class="hljs-built_in">this</span>.data = data;
  }
}

<span class="hljs-comment">// eslint-disable-next-line no-unused-vars</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> errorHandler = <span class="hljs-function">(<span class="hljs-params">err, req, res, next</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Error <span class="hljs-subst">${err.status || <span class="hljs-number">500</span>}</span>: <span class="hljs-subst">${err.message}</span>`</span>, {
    <span class="hljs-attr">url</span>: req.originalUrl,
    <span class="hljs-attr">method</span>: req.method,
    <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString(),
  });

  <span class="hljs-keyword">if</span> (err <span class="hljs-keyword">instanceof</span> AppError) {
    <span class="hljs-keyword">const</span> problem = <span class="hljs-keyword">new</span> ProblemDocument({
      <span class="hljs-attr">type</span>: <span class="hljs-string">'/problems/'</span> + err.type,
      <span class="hljs-attr">title</span>: err.type,
      <span class="hljs-attr">status</span>: err.status,
      <span class="hljs-attr">detail</span>: err.detail,
      <span class="hljs-attr">instance</span>: req.originalUrl,
    });

    <span class="hljs-keyword">if</span> (err.data) {
      <span class="hljs-built_in">Object</span>.assign(problem, err.data);
    }

    res.status(err.status).json(problem);
  } <span class="hljs-keyword">else</span> {
    res.status(<span class="hljs-number">500</span>).json(
      <span class="hljs-keyword">new</span> ProblemDocument({
        <span class="hljs-attr">type</span>: <span class="hljs-string">'/problems/internal-server-error'</span>,
        <span class="hljs-attr">title</span>: <span class="hljs-string">'InternalServerError'</span>,
        <span class="hljs-attr">status</span>: <span class="hljs-number">500</span>,
        <span class="hljs-attr">instance</span>: req.originalUrl,
      })
    );
  }
};
</code></pre>
<p>This error handler provides:</p>
<ul>
<li><p><strong>Structured error responses</strong> following the RFC 7807 standard.</p>
</li>
<li><p><strong>Detailed logging</strong> with request context.</p>
</li>
<li><p><strong>Consistent error format</strong> across all endpoints.</p>
</li>
<li><p><strong>Optional debug data</strong> (like screenshots for debugging).</p>
</li>
</ul>
<h2 id="heading-building-the-transcript-extractor"><strong>Building the Transcript Extractor</strong></h2>
<p>The core functionality lies in the <code>transcript.js</code> file, which uses Playwright to extract transcripts from YouTube:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { chromium } <span class="hljs-keyword">from</span> <span class="hljs-string">'playwright'</span>;
<span class="hljs-keyword">import</span> { AppError } <span class="hljs-keyword">from</span> <span class="hljs-string">'./errorHandler.js'</span>;

<span class="hljs-keyword">const</span> USER_AGENT =
  process.env.USER_AGENT ||
  <span class="hljs-string">'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'</span>;

<span class="hljs-keyword">const</span> selectors = {
  <span class="hljs-attr">expand</span>: process.env.EXPAND_SELECTOR || <span class="hljs-string">'tp-yt-paper-button#expand'</span>,
  <span class="hljs-attr">notFound</span>:
    process.env.NOT_FOUND_SELECTOR ||
    <span class="hljs-string">'div.promo-title:has-text("This video isn\'t available anymore"), div.promo-title:has-text("Este video ya no está disponible")'</span>,
  <span class="hljs-attr">showTranscript</span>:
    process.env.SHOW_TRANSCRIPT_SELECTOR ||
    <span class="hljs-string">'button[aria-label="Show transcript"], button[aria-label="Mostrar transcripción"]'</span>,
  <span class="hljs-attr">viewCount</span>: process.env.VIEW_COUNT_SELECTOR || <span class="hljs-string">'yt-formatted-string#info span'</span>,
  <span class="hljs-attr">transcriptSegment</span>:
    process.env.TRANSCRIPT_SEGMENT_SELECTOR ||
    <span class="hljs-string">'ytd-transcript-segment-renderer'</span>,
  <span class="hljs-attr">transcript</span>: process.env.TRANSCRIPT_SELECTOR || <span class="hljs-string">'ytd-transcript-renderer'</span>,
  <span class="hljs-attr">text</span>: process.env.TRANSCRIPT_TEXT_SELECTOR || <span class="hljs-string">'.segment-text'</span>,
};
</code></pre>
<p>The selector configuration approach provides several advantages:</p>
<ul>
<li><p><strong>Environment-based customization</strong> for different YouTube layouts.</p>
</li>
<li><p><strong>Easy maintenance</strong> when YouTube changes its interface.</p>
</li>
<li><p><strong>Multi-language support</strong> through configurable selectors.</p>
</li>
<li><p><strong>Fallback defaults</strong> for common interface elements.</p>
</li>
</ul>
<p>Here is the main extraction logic:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscript</span>(<span class="hljs-params">videoId</span>) </span>{
  <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> chromium.launch({
    <span class="hljs-attr">headless</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">args</span>: [<span class="hljs-string">'--no-sandbox'</span>, <span class="hljs-string">'--disable-setuid-sandbox'</span>],
  });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> context = <span class="hljs-keyword">await</span> browser.newContext({
      <span class="hljs-attr">userAgent</span>: USER_AGENT,
    });

    <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> context.newPage();

    <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">`https://www.youtube.com/watch?v=<span class="hljs-subst">${videoId}</span>`</span>, {
      <span class="hljs-attr">waitUntil</span>: <span class="hljs-string">'networkidle'</span>,
      <span class="hljs-attr">timeout</span>: <span class="hljs-number">30000</span>,
    });

    <span class="hljs-keyword">const</span> errorElement = <span class="hljs-keyword">await</span> page.$(selectors.notFound);
    <span class="hljs-keyword">if</span> (errorElement) {
      <span class="hljs-keyword">const</span> screenshot = <span class="hljs-keyword">await</span> page.screenshot({
        <span class="hljs-attr">fullPage</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'png'</span>,
      });
      <span class="hljs-keyword">const</span> base64Screenshot = screenshot.toString(<span class="hljs-string">'base64'</span>);
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">'Video not found or unavailable'</span>, <span class="hljs-string">'not_found'</span>, <span class="hljs-number">404</span>, {
        <span class="hljs-attr">screenshot</span>: <span class="hljs-string">`data:image/png;base64,<span class="hljs-subst">${base64Screenshot}</span>`</span>,
      });
    }

    <span class="hljs-keyword">const</span> expandButton = <span class="hljs-keyword">await</span> page.$(selectors.expand);
    <span class="hljs-keyword">if</span> (!expandButton) {
      <span class="hljs-keyword">const</span> screenshot = <span class="hljs-keyword">await</span> page.screenshot({
        <span class="hljs-attr">fullPage</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'png'</span>,
      });
      <span class="hljs-keyword">const</span> base64Screenshot = screenshot.toString(<span class="hljs-string">'base64'</span>);
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">'Expand button not found'</span>, <span class="hljs-string">'validation'</span>, <span class="hljs-number">400</span>, {
        <span class="hljs-attr">screenshot</span>: <span class="hljs-string">`data:image/png;base64,<span class="hljs-subst">${base64Screenshot}</span>`</span>,
      });
    }

    <span class="hljs-keyword">await</span> expandButton.click({ <span class="hljs-attr">timeout</span>: <span class="hljs-number">5000</span> });

    <span class="hljs-keyword">const</span> showTranscriptButton = <span class="hljs-keyword">await</span> page.$(selectors.showTranscript);
    <span class="hljs-keyword">if</span> (!showTranscriptButton) {
      <span class="hljs-keyword">const</span> screenshot = <span class="hljs-keyword">await</span> page.screenshot({
        <span class="hljs-attr">fullPage</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'png'</span>,
      });
      <span class="hljs-keyword">const</span> base64Screenshot = screenshot.toString(<span class="hljs-string">'base64'</span>);
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(
        <span class="hljs-string">'Show transcript button not found'</span>,
        <span class="hljs-string">'validation'</span>,
        <span class="hljs-number">400</span>,
        {
          <span class="hljs-attr">screenshot</span>: <span class="hljs-string">`data:image/png;base64,<span class="hljs-subst">${base64Screenshot}</span>`</span>,
        }
      );
    }

    <span class="hljs-keyword">await</span> showTranscriptButton.click({ <span class="hljs-attr">timeout</span>: <span class="hljs-number">5000</span> });

    <span class="hljs-keyword">await</span> page.waitForSelector(selectors.transcript, { <span class="hljs-attr">timeout</span>: <span class="hljs-number">10000</span> });

    <span class="hljs-keyword">const</span> transcript = <span class="hljs-keyword">await</span> page.$$eval(
      selectors.transcriptSegment,
      <span class="hljs-function">(<span class="hljs-params">nodes, textSelector</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> nodes.map(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> n.querySelector(textSelector)?.innerText.trim());
      },
      selectors.text
    );

    <span class="hljs-keyword">const</span> [viewsText] = <span class="hljs-keyword">await</span> page.$$eval(selectors.viewCount, <span class="hljs-function"><span class="hljs-params">nodes</span> =&gt;</span>
      nodes.map(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> n.innerText.trim())
    );

    <span class="hljs-keyword">const</span> views = <span class="hljs-built_in">parseInt</span>(viewsText.replace(<span class="hljs-regexp">/[^0-9]/g</span>, <span class="hljs-string">''</span>), <span class="hljs-number">10</span>) || <span class="hljs-number">0</span>;

    <span class="hljs-keyword">return</span> { <span class="hljs-attr">transcript</span>: transcript.join(<span class="hljs-string">' '</span>), views };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">if</span> (error <span class="hljs-keyword">instanceof</span> AppError) {
      <span class="hljs-keyword">throw</span> error;
    }
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(
      <span class="hljs-string">`Failed to fetch transcript: <span class="hljs-subst">${error.message}</span>`</span>,
      <span class="hljs-string">'error'</span>,
      <span class="hljs-number">500</span>
    );
  } <span class="hljs-keyword">finally</span> {
    <span class="hljs-keyword">await</span> browser.close();
  }
}
</code></pre>
<p>Key implementation details:</p>
<ul>
<li><p><strong>Browser Configuration</strong>: Headless Chromium with security flags for containerized environments.</p>
</li>
<li><p><strong>Robust Navigation</strong>: Network idle waiting ensures full page load.</p>
</li>
<li><p><strong>Error Detection</strong>: Proactive checking for video availability.</p>
</li>
<li><p><strong>Screenshot Debugging</strong>: Captures page state for troubleshooting.</p>
</li>
<li><p><strong>Resource Cleanup</strong>: Always closes the browser to prevent memory leaks.</p>
</li>
</ul>
<h2 id="heading-adding-security-with-api-key-authentication"><strong>Adding Security with API Key Authentication</strong></h2>
<p>The <code>securityHandler.js</code> file implements optional API key authentication:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { AppError } <span class="hljs-keyword">from</span> <span class="hljs-string">'./errorHandler.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> validateApiKey = <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> apiKey = req.headers[<span class="hljs-string">'x-api-key'</span>];
  <span class="hljs-keyword">const</span> expectedApiKey = process.env.API_KEY;

  <span class="hljs-keyword">if</span> (!expectedApiKey) {
    <span class="hljs-keyword">return</span> next();
  }

  <span class="hljs-keyword">if</span> (!apiKey) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">'API key is required'</span>, <span class="hljs-string">'authentication'</span>, <span class="hljs-number">401</span>);
  }

  <span class="hljs-keyword">if</span> (apiKey !== expectedApiKey) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">'Invalid API key'</span>, <span class="hljs-string">'authentication'</span>, <span class="hljs-number">401</span>);
  }

  next();
};
</code></pre>
<p>This middleware design allows for:</p>
<ul>
<li><p><strong>Optional authentication</strong> works without an API key if not configured.</p>
</li>
<li><p><strong>Header-based authentication</strong> using the <code>X-API-Key</code> header.</p>
</li>
<li><p><strong>Consistent error responses</strong> through our error handling system.</p>
</li>
</ul>
<h2 id="heading-building-the-express-server"><strong>Building the Express Server</strong></h2>
<p>The <code>server.js</code> file ties everything together:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> getTranscript <span class="hljs-keyword">from</span> <span class="hljs-string">'./transcript.js'</span>;
<span class="hljs-keyword">import</span> morgan <span class="hljs-keyword">from</span> <span class="hljs-string">'morgan'</span>;
<span class="hljs-keyword">import</span> { errorHandler, AppError } <span class="hljs-keyword">from</span> <span class="hljs-string">'./errorHandler.js'</span>;
<span class="hljs-keyword">import</span> { validateApiKey } <span class="hljs-keyword">from</span> <span class="hljs-string">'./securityHandler.js'</span>;
<span class="hljs-keyword">import</span> healthcheck <span class="hljs-keyword">from</span> <span class="hljs-string">'express-healthcheck'</span>;

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">5000</span>;

<span class="hljs-keyword">const</span> videoIdRegex = <span class="hljs-regexp">/^[a-zA-Z0-9_-]{11}$/</span>;

app.use(morgan(<span class="hljs-string">'dev'</span>));
app.use(
  <span class="hljs-string">'/live'</span>,
  healthcheck({
    <span class="hljs-attr">healthy</span>: <span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">status</span>: <span class="hljs-string">'healthy'</span>,
      <span class="hljs-attr">uptime</span>: process.uptime(),
      <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
    }),
  })
);
app.get(<span class="hljs-string">'/transcript/:videoId'</span>, validateApiKey, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> { videoId } = req.params;

  <span class="hljs-keyword">if</span> (!videoId) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">'Video ID is required'</span>, <span class="hljs-string">'validation'</span>, <span class="hljs-number">400</span>);
  }

  <span class="hljs-keyword">if</span> (!videoIdRegex.test(videoId)) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AppError(<span class="hljs-string">'Invalid video ID format'</span>, <span class="hljs-string">'validation'</span>, <span class="hljs-number">400</span>);
  }

  <span class="hljs-keyword">const</span> { transcript, views } = <span class="hljs-keyword">await</span> getTranscript(videoId);

  res.status(<span class="hljs-number">200</span>).json({
    transcript,
    views,
  });
});

app.use(errorHandler);

app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on port <span class="hljs-subst">${PORT}</span>`</span>);
});
</code></pre>
<p>The server implementation includes:</p>
<ul>
<li><p><strong>Input Validation</strong>: YouTube video ID format validation using regex.</p>
</li>
<li><p><strong>Health Monitoring</strong>: <code>/live</code> endpoint for deployment health checks.</p>
</li>
<li><p><strong>Request Logging</strong>: Morgan middleware for HTTP request logging.</p>
</li>
<li><p><strong>Error Handling</strong>: Global error middleware catches all exceptions.</p>
</li>
</ul>
<h2 id="heading-environment-configuration"><strong>Environment Configuration</strong></h2>
<p>The <code>.env.example</code> file shows all configurable options:</p>
<pre><code class="lang-javascript">PORT=<span class="hljs-number">5000</span>
API_KEY=your-secret-api-key-here
USER_AGENT=Mozilla/<span class="hljs-number">5.0</span> (Windows NT <span class="hljs-number">10.0</span>; Win64; x64) AppleWebKit/<span class="hljs-number">537.36</span> (KHTML, like Gecko) Chrome/<span class="hljs-number">91.0</span><span class="hljs-number">.4472</span><span class="hljs-number">.124</span> Safari/<span class="hljs-number">537.36</span>
EXPAND_SELECTOR=tp-yt-paper-button#expand
NOT_FOUND_SELECTOR=div.promo-title:has-text(<span class="hljs-string">"This video isn't available anymore"</span>), div.promo-title:has-text(<span class="hljs-string">"Este video ya no está disponible"</span>)
SHOW_TRANSCRIPT_SELECTOR=button[aria-label=<span class="hljs-string">"Show transcript"</span>], button[aria-label=<span class="hljs-string">"Mostrar transcripción"</span>]
VIEW_COUNT_SELECTOR=yt-formatted-string#info span
TRANSCRIPT_SEGMENT_SELECTOR=ytd-transcript-segment-renderer
TRANSCRIPT_SELECTOR=ytd-transcript-renderer
TRANSCRIPT_TEXT_SELECTOR=.segment-text
NODE_ENV=production
</code></pre>
<p>This configuration approach enables:</p>
<ul>
<li><p><strong>Deployment flexibility</strong> across different environments.</p>
</li>
<li><p><strong>Quick adaptation</strong> to YouTube HTML changes.</p>
</li>
<li><p><strong>Multi-language support</strong> through localized selectors.</p>
</li>
<li><p><strong>Security configuration</strong> through environment variables.</p>
</li>
</ul>
<h2 id="heading-containerization-with-docker"><strong>Containerization with Docker</strong></h2>
<p>The <code>Dockerfile</code> creates a production-ready container:</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Use Node.js LTS version with Debian slim for better Playwright compatibility</span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20</span>-slim AS base
<span class="hljs-comment"># Install system dependencies required for Playwright</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; apt-get install -y \
    ca-certificates \
    fonts-liberation \
    libasound2 \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libc6 \
    libcairo2 \
    libcups2 \
    libdbus-1-3 \
    libexpat1 \
    libfontconfig1 \
    libgbm1 \
    libgcc1 \
    libglib2.0-0 \
    libgtk-3-0 \
    libnspr4 \
    libnss3 \
    libpango-1.0-0 \
    libpangocairo-1.0-0 \
    libstdc++6 \
    libx11-6 \
    libx11-xcb1 \
    libxcb1 \
    libxcomposite1 \
    libxcursor1 \
    libxdamage1 \
    libxext6 \
    libxfixes3 \
    libxi6 \
    libxrandr2 \
    libxrender1 \
    libxss1 \
    libxtst6 \
    lsb-release \
    wget \
    xdg-utils \
    &amp;&amp; rm -rf /var/lib/apt/lists/*</span>
<span class="hljs-comment"># Set working directory</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-comment"># Copy package files</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package*.json ./</span>
<span class="hljs-comment"># Install dependencies</span>
<span class="hljs-keyword">FROM</span> base AS dependencies
<span class="hljs-keyword">RUN</span><span class="bash"> npm ci --omit=dev --ignore-scripts &amp;&amp; npm cache clean --force</span>
<span class="hljs-comment"># Install only Playwright browsers (without system deps)</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npx playwright install chromium</span>
<span class="hljs-comment"># Production stage</span>
<span class="hljs-keyword">FROM</span> base AS production
<span class="hljs-comment"># Create non-root user for security</span>
<span class="hljs-keyword">RUN</span><span class="bash"> groupadd -r nodejs &amp;&amp; useradd -r -g nodejs nodejs</span>
<span class="hljs-comment"># Copy node_modules from dependencies stage</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=dependencies --chown=nodejs:nodejs /app/node_modules ./node_modules</span>
<span class="hljs-comment"># Copy Playwright browsers from dependencies stage</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=dependencies --chown=nodejs:nodejs /root/.cache/ms-playwright /home/nodejs/.cache/ms-playwright</span>
<span class="hljs-comment"># Copy application files</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --chown=nodejs:nodejs . .</span>
<span class="hljs-comment"># Remove development files if they exist</span>
<span class="hljs-keyword">RUN</span><span class="bash"> rm -f .env.example .gitignore README.md</span>
<span class="hljs-comment"># Switch to non-root user</span>
<span class="hljs-keyword">USER</span> nodejs
<span class="hljs-comment"># Expose port</span>
<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">5000</span>
<span class="hljs-comment"># Start the application</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"node"</span>, <span class="hljs-string">"server.js"</span>]</span>
</code></pre>
<p>The Docker setup provides:</p>
<ul>
<li><p><strong>Multi-stage builds</strong> for smaller final images.</p>
</li>
<li><p><strong>Security hardening</strong> with a non-root user.</p>
</li>
<li><p><strong>Playwright optimization</strong> with pre-installed browsers.</p>
</li>
<li><p><strong>Production readiness</strong> with minimal attack surface.</p>
</li>
</ul>
<p>The <code>docker-compose.yml</code> configuration is optimized for deployment with <a target="_blank" href="https://coolify.io/">Coolify</a>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3.8'</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">youtube-transcript-api:</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">.</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'5000:5000'</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">NODE_ENV=production</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">PORT=5000</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span>
        [
          <span class="hljs-string">'CMD'</span>,
          <span class="hljs-string">'wget'</span>,
          <span class="hljs-string">'--no-verbose'</span>,
          <span class="hljs-string">'--tries=1'</span>,
          <span class="hljs-string">'--spider'</span>,
          <span class="hljs-string">'http://localhost:5000/live'</span>,
        ]
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">3</span>
      <span class="hljs-attr">start_period:</span> <span class="hljs-string">10s</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
</code></pre>
<h2 id="heading-next-steps"><strong>Next Steps</strong></h2>
<p>To enhance this API further, consider implementing:</p>
<ul>
<li><p><strong>Browser Reuse</strong>: Consider implementing browser instance pooling for high-traffic scenarios.</p>
</li>
<li><p><strong>Caching</strong>: Add caching for frequently requested transcripts and/or transcript persistence.</p>
</li>
</ul>
<p>You can find all the code <a target="_blank" href="https://github.com/raulnq/youtube-transcript-api">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Node.js and Express: API Security]]></title><description><![CDATA[API security is a critical concern in modern application development. With the increasing number of data breaches and sophisticated attacks, implementing robust security measures for APIs is no longer optional. A single vulnerability can expose sensi...]]></description><link>https://blog.raulnq.com/nodejs-and-express-api-security</link><guid isPermaLink="true">https://blog.raulnq.com/nodejs-and-express-api-security</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Express]]></category><category><![CDATA[CORS]]></category><category><![CDATA[JWT]]></category><category><![CDATA[helmet]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Sun, 14 Sep 2025 02:49:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757768022681/d68a8d1a-26a6-4c26-93a6-2ddec03ec673.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>API security is a critical concern in modern application development. With the increasing number of data breaches and sophisticated attacks, implementing robust security measures for APIs is no longer optional. A single vulnerability can expose sensitive data, compromise user accounts, and damage our organization’s reputation.</p>
<p>This article is part of our comprehensive <a target="_blank" href="https://blog.raulnq.com/series/nodejs">Node.js and Express</a> series, focusing specifically on implementing essential API security practices. We will cover three fundamental security pillars every API should adopt: JWT verification, security headers, and CORS configuration.</p>
<p>By the end of this article, we will have practical, production-ready implementations of these security measures, along with a deep understanding of why each is crucial and how they work together to create a robust security posture.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before diving into implementation details, ensure we have the following in place:</p>
<ul>
<li><p>A registered application in <a target="_blank" href="https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app">Microsoft Entra ID</a> for the API:</p>
<ul>
<li>Exposing a scope named <code>invoke</code>.</li>
</ul>
</li>
<li><p>Postman installed for API testing.</p>
</li>
<li><p>A registered application in <a target="_blank" href="https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app">Microsoft Entra ID</a> for Postman:</p>
<ul>
<li><p>Redirect URI: <a target="_blank" href="https://oauth.pstmn.io/v1/callback"><code>https://oauth.pstmn.io/v1/callback</code></a>.</p>
</li>
<li><p>API permission for the <code>invoke</code> scope</p>
</li>
<li><p>A valid client secret.</p>
</li>
</ul>
</li>
<li><p>Base Project<strong>:</strong> We will build upon the <a target="_blank" href="https://github.com/raulnq/nodejs-express">nodejs-express repository</a>.</p>
</li>
</ul>
<h2 id="heading-jwt-verification">JWT Verification</h2>
<p>JSON Web Tokens (JWTs) have become the de facto standard for API authentication in modern applications. Their stateless nature — where all required information is contained within the token — makes APIs more scalable.</p>
<p>While the general recommendation is to use official libraries, the <a target="_blank" href="https://github.com/AzureAD/passport-azure-ad"><code>passport-azure-ad</code></a> package (previously popular for Azure Entra ID integration) is now deprecated. Instead, we will use <a target="_blank" href="https://www.npmjs.com/package/jsonwebtoken"><code>jsonwebtoken</code></a> to verify and decode tokens, and <a target="_blank" href="https://www.npmjs.com/package/jwks-rsa"><code>jwks-rsa</code></a> to obtain signing keys from the JWKS (JSON Web Key Set) endpoint.</p>
<pre><code class="lang-powershell">npm i jwks<span class="hljs-literal">-rsa</span> jsonwebtoken
</code></pre>
<p>Add the following to the <code>.env</code> file:</p>
<pre><code class="lang-plaintext">TENANT_ID=&lt;MY_API_APP_REGISTRATION_TENANT_ID&gt;
CLIENT_ID=&lt;MY_API_APP_REGISTRATION_CLIENT_ID&gt;
</code></pre>
<p>In <code>src/middlewares/errorHandler.js</code>, add:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UnauthorizedError</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppError</span> </span>{
  <span class="hljs-keyword">constructor</span>(error) {
    <span class="hljs-built_in">super</span>(error, <span class="hljs-string">'unauthorized'</span>, <span class="hljs-number">401</span>, <span class="hljs-string">'Unauthorized'</span>);
  }
}
</code></pre>
<p>Then create <code>src/middlewares/auth.js</code> with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> jwt <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonwebtoken'</span>;
<span class="hljs-keyword">import</span> jwksClient <span class="hljs-keyword">from</span> <span class="hljs-string">'jwks-rsa'</span>;
<span class="hljs-keyword">import</span> { UnauthorizedError } <span class="hljs-keyword">from</span> <span class="hljs-string">'./errorHandler.js'</span>;

<span class="hljs-keyword">const</span> client = jwksClient({
  <span class="hljs-attr">jwksUri</span>: <span class="hljs-string">`https://login.microsoftonline.com/<span class="hljs-subst">${process.env.TENANT_ID}</span>/discovery/v2.0/keys`</span>,
  <span class="hljs-attr">cache</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">cacheMaxAge</span>: <span class="hljs-number">600000</span>,
});

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKey</span>(<span class="hljs-params">header, callback</span>) </span>{
  client.getSigningKey(header.kid, <span class="hljs-function">(<span class="hljs-params">err, key</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (err) {
      <span class="hljs-keyword">return</span> callback(err);
    }
    <span class="hljs-keyword">const</span> signingKey = key.getPublicKey();
    callback(<span class="hljs-literal">null</span>, signingKey);
  });
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifyJWT</span>(<span class="hljs-params">options</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> authHeader = req.headers[<span class="hljs-string">'authorization'</span>];
    <span class="hljs-keyword">if</span> (!authHeader) {
      <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> UnauthorizedError(<span class="hljs-string">'Missing authorization header'</span>));
    }

    <span class="hljs-keyword">const</span> parts = authHeader.split(<span class="hljs-string">' '</span>);
    <span class="hljs-keyword">if</span> (parts.length !== <span class="hljs-number">2</span> || parts[<span class="hljs-number">0</span>] !== <span class="hljs-string">'Bearer'</span> || !parts[<span class="hljs-number">1</span>]) {
      <span class="hljs-keyword">return</span> next(
        <span class="hljs-keyword">new</span> UnauthorizedError(
          <span class="hljs-string">'Invalid authorization header format. Expected "Bearer &lt;token&gt;"'</span>
        )
      );
    }
    <span class="hljs-keyword">const</span> token = parts[<span class="hljs-number">1</span>];
    jwt.verify(
      token,
      getKey,
      {
        <span class="hljs-attr">algorithms</span>: [<span class="hljs-string">'RS256'</span>],
        <span class="hljs-attr">audience</span>: <span class="hljs-string">`api://<span class="hljs-subst">${process.env.CLIENT_ID}</span>`</span>,
        <span class="hljs-attr">issuer</span>: <span class="hljs-string">`https://sts.windows.net/<span class="hljs-subst">${process.env.TENANT_ID}</span>/`</span>,
      },
      <span class="hljs-function">(<span class="hljs-params">err, decoded</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (err) {
          <span class="hljs-keyword">return</span> next(<span class="hljs-keyword">new</span> UnauthorizedError(err.message));
        }
        <span class="hljs-keyword">if</span> (options.scopes &amp;&amp; options.scopes.length &gt; <span class="hljs-number">0</span>) {
          <span class="hljs-keyword">const</span> tokenScopes = decoded.scp ? decoded.scp.split(<span class="hljs-string">' '</span>) : [];
          <span class="hljs-keyword">const</span> hasScopes = options.scopes.every(<span class="hljs-function"><span class="hljs-params">scope</span> =&gt;</span>
            tokenScopes.includes(scope)
          );

          <span class="hljs-keyword">if</span> (!hasScopes) {
            <span class="hljs-keyword">return</span> next(
              <span class="hljs-keyword">new</span> UnauthorizedError(
                <span class="hljs-string">`Missing scopes: <span class="hljs-subst">${options.scopes.join(<span class="hljs-string">', '</span>)}</span>`</span>
              )
            );
          }
        }
        req.user = decoded;
        next();
      }
    );
  };
}
</code></pre>
<p>When Azure Entra ID issues a JWT, it signs it with a private key. To verify the token, we need the corresponding public key, which we obtain from the JWKS endpoint. Each JWT contains a <code>kid</code> field in its header, which identifies the signing key. The <code>getKey</code> function handles extracting the <code>kid</code> and finding the matching public key using the <code>jwksClient</code>.</p>
<p>Once retrieved, <code>jwt.verify</code> uses the public key to validate the signature. We also check that the token was issued by our tenant (via the <code>iss</code> field), intended for our audience (via the <code>aud</code> claim), and signed with the expected algorithm. Optionally, the middleware verifies that required scopes are present. A successfully validated token is then attached to <code>req.user</code> for downstream use.</p>
<p>Update <code>src/features/todos/routes.js</code> with:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { addTodo, addTodoSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./addTodo.js'</span>;
<span class="hljs-keyword">import</span> { findTodo, ensureTodoFound } <span class="hljs-keyword">from</span> <span class="hljs-string">'./findTodo.js'</span>;
<span class="hljs-keyword">import</span> { checkTodo } <span class="hljs-keyword">from</span> <span class="hljs-string">'./checkTodo.js'</span>;
<span class="hljs-keyword">import</span> { uncheckTodo } <span class="hljs-keyword">from</span> <span class="hljs-string">'./uncheckTodo.js'</span>;
<span class="hljs-keyword">import</span> { listTodos, listTodosSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./listTodos.js'</span>;
<span class="hljs-keyword">import</span> { paginationParam } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../middlewares/paginationParam.js'</span>;
<span class="hljs-keyword">import</span> { schemaValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../middlewares/schemaValidator.js'</span>;
<span class="hljs-keyword">import</span> { verifyJWT } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../middlewares/auth.js'</span>;

<span class="hljs-keyword">const</span> router = express.Router();

router.param(<span class="hljs-string">'todoId'</span>, ensureTodoFound);

router
  .post(
    <span class="hljs-string">'/'</span>,
    verifyJWT({ <span class="hljs-attr">scopes</span>: [<span class="hljs-string">'invoke'</span>] }),
    schemaValidator({ <span class="hljs-attr">body</span>: addTodoSchema }),
    addTodo
  )
  .get(<span class="hljs-string">'/:todoId'</span>, findTodo)
  .post(<span class="hljs-string">'/:todoId/check'</span>, checkTodo)
  .post(<span class="hljs-string">'/:todoId/uncheck'</span>, uncheckTodo)
  .get(
    <span class="hljs-string">'/'</span>,
    schemaValidator({ <span class="hljs-attr">query</span>: listTodosSchema }),
    paginationParam,
    listTodos
  );

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>In the code above, we use our newly created middleware. To test the feature, we are using Postman to generate the token and include it in our request to the endpoint:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757788972249/84b4e9e3-f2c2-406b-ac87-09654d2d4f2d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-securing-headers-with-helmet">Securing Headers with Helmet</h2>
<p>HTTP security headers are the first line of defense against many common web vulnerabilities. <a target="_blank" href="https://github.com/helmetjs/helmet">Helmet</a> secures Express applications by setting various default HTTP headers.</p>
<ul>
<li><p><code>Content-Security-Policy</code>: Allow-list of permitted resources (scripts, styles, images, frames). Helps prevent XSS, data injection, and malicious resource loading.</p>
</li>
<li><p><code>Cross-Origin-Opener-Policy</code>: Isolates browsing context from other origins. Helps enable cross-origin isolation and reduce side-channel attacks.</p>
</li>
<li><p><code>Cross-Origin-Resource-Policy</code>: Controls whether other sites can load our resources (scripts, images). Prevents data leaks.</p>
</li>
<li><p><code>Origin-Agent-Cluster</code>: Isolates JavaScript memory per origin, reducing cross-subdomain attacks.</p>
</li>
<li><p><code>Referrer-Policy</code>: Controls what is sent in the <code>Referer</code> header. Prevents leaking sensitive query/path info.</p>
</li>
<li><p><code>Strict-Transport-Security</code>: Forces browsers to use HTTPS, preventing downgrade attacks.</p>
</li>
<li><p><code>X-Content-Type-Options</code>: Prevents MIME-sniffing. Mitigates injection attacks.</p>
</li>
<li><p><code>X-DNS-Prefetch-Control</code>: Controls DNS prefetching (performance/privacy tuning).</p>
</li>
<li><p><code>X-Download-Options</code>: Forces "Save As" on downloads (IE only).</p>
</li>
<li><p><code>X-Frame-Options</code>: Prevents clickjacking by blocking <code>&lt;iframe&gt;</code> embedding.</p>
</li>
<li><p><code>X-Permitted-Cross-Domain-Policies</code>: Restricts Adobe Flash/Acrobat cross-domain requests.</p>
</li>
<li><p><code>X-Powered-By</code>: Removed by Helmet to avoid exposing server details.</p>
</li>
<li><p><code>X-XSS-Protection</code>: Disabled by Helmet due to legacy browser issues.</p>
</li>
</ul>
<p>Install Helmet:</p>
<pre><code class="lang-powershell">npm i helmet
</code></pre>
<p>Enable in the <code>src/server.js</code> file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> helmet <span class="hljs-keyword">from</span> <span class="hljs-string">'helmet'</span>;
<span class="hljs-keyword">const</span> app = express();
app.use(helmet());
</code></pre>
<h2 id="heading-cors-configuration">CORS Configuration</h2>
<p>Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts requests from different domains, protocols, or ports. Proper configuration is crucial:</p>
<ul>
<li><p><strong>Prevents unauthorized access:</strong> Stops malicious websites from making requests to our API on behalf of users.</p>
</li>
<li><p><strong>Protects sensitive data:</strong> Prevents data from being shared with unauthorized origins.</p>
</li>
<li><p><strong>Mitigates CSRF attacks:</strong> Adds an additional layer of defense.</p>
</li>
</ul>
<p>Run the following command to install the <a target="_blank" href="https://github.com/expressjs/cors">CORS</a> Express middleware:</p>
<pre><code class="lang-powershell">npm install cors
</code></pre>
<p>Add to the <code>.env</code> file:</p>
<pre><code class="lang-plaintext">ALLOWED_ORIGIN=http://localhost:5000
</code></pre>
<p>The final <code>src/server.js</code> file will look like this:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
<span class="hljs-keyword">import</span> todosRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/todos/routes.js'</span>;
<span class="hljs-keyword">import</span> healthRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/health.js'</span>;
<span class="hljs-keyword">import</span> { errorHandler, NotFoundError } <span class="hljs-keyword">from</span> <span class="hljs-string">'./middlewares/errorHandler.js'</span>;
<span class="hljs-keyword">import</span> morgan <span class="hljs-keyword">from</span> <span class="hljs-string">'morgan'</span>;
<span class="hljs-keyword">import</span> expressWinston <span class="hljs-keyword">from</span> <span class="hljs-string">'express-winston'</span>;
<span class="hljs-keyword">import</span> logger <span class="hljs-keyword">from</span> <span class="hljs-string">'./config/logger.js'</span>;
<span class="hljs-keyword">import</span> helmet <span class="hljs-keyword">from</span> <span class="hljs-string">'helmet'</span>;
<span class="hljs-keyword">import</span> cors <span class="hljs-keyword">from</span> <span class="hljs-string">'cors'</span>;

process.on(<span class="hljs-string">'uncaughtException'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  process.exit(<span class="hljs-number">1</span>);
});
dotenv.config();
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">3000</span>;
<span class="hljs-keyword">const</span> app = express();
app.use(helmet());
app.use(
  cors({
    <span class="hljs-attr">origin</span>: process.env.ALLOWED_ORIGIN,
  })
);
app.use(express.json());
app.use(morgan(<span class="hljs-string">'dev'</span>));
app.use(
  expressWinston.logger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'HTTP {{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms'</span>,
  })
);
app.use(<span class="hljs-string">'/health'</span>, healthRoutes);
app.use(<span class="hljs-string">'/api/todos'</span>, todosRoutes);
app.all(<span class="hljs-string">'/*splat'</span>, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> pathSegments = req.params.splat;
  <span class="hljs-keyword">const</span> fullPath = pathSegments.join(<span class="hljs-string">'/'</span>);
  next(<span class="hljs-keyword">new</span> NotFoundError(<span class="hljs-string">`The requested URL /<span class="hljs-subst">${fullPath}</span> does not exist`</span>));
});
app.use(
  expressWinston.errorLogger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'{{err.message}} {{res.statusCode}} {{req.method}}'</span>,
  })
);
app.use(errorHandler);
<span class="hljs-keyword">const</span> server = app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on port <span class="hljs-subst">${PORT}</span>`</span>);
});

process.on(<span class="hljs-string">'unhandledRejection'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  server.close(<span class="hljs-function">() =&gt;</span> {
    process.exit(<span class="hljs-number">1</span>);
  });
});
</code></pre>
<p>You can find the code <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/azure-entraid">here</a>. Thank you, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Node.js and Express: Health Checks]]></title><description><![CDATA[Health checks are essential in modern APIs, allowing monitoring systems, load balancers, and container orchestrators to determine if an application instance is functioning correctly. They serve as the foundation for automated failure detection, scali...]]></description><link>https://blog.raulnq.com/nodejs-and-express-health-checks</link><guid isPermaLink="true">https://blog.raulnq.com/nodejs-and-express-health-checks</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Express]]></category><category><![CDATA[health-checks]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Mon, 08 Sep 2025 18:31:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757350519614/79004d27-1f48-4af7-9008-bfd2c065ddea.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Health checks are essential in modern APIs, allowing monitoring systems, load balancers, and container orchestrators to determine if an application instance is functioning correctly. They serve as the foundation for automated failure detection, scaling decisions, and deployment strategies in production environments.</p>
<p>Two distinct types of health checks are crucial:</p>
<ul>
<li><p><strong>Liveness check</strong> verifies that the application process is running and responsive, ensuring the application instance remains alive.</p>
</li>
<li><p><strong>Readiness check</strong> determines if the application instance is ready to serve traffic by validating that all required dependencies are available and functional.</p>
</li>
</ul>
<p>This distinction is critical: a failing liveness check typically results in the application instance being restarted, while a failing readiness check removes the instance from the load balancer without shutting it down.</p>
<p>This post shows how to implement health checks using Express, the <a target="_blank" href="https://github.com/lennym/express-healthcheck">express-healthcheck</a> library for streamlined endpoint creation, <a target="_blank" href="https://github.com/sindresorhus/p-timeout">p-timeout</a> for preventing hanging requests, and parallel execution patterns for optimal performance. We will build health endpoints that monitor PostgreSQL database and Seq logging service.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>This implementation builds upon the base project available <a target="_blank" href="https://github.com/raulnq/nodejs-express">here</a>. Install the required dependencies:</p>
<pre><code class="lang-powershell">npm install express<span class="hljs-literal">-healthcheck</span> p<span class="hljs-literal">-timeout</span>
</code></pre>
<p>The <code>express-healthcheck</code> library provides a clean abstraction for health check endpoint creation, while <code>p-timeout</code> ensures our dependency checks do not hang indefinitely.</p>
<h2 id="heading-understanding-health-checks">Understanding Health Checks</h2>
<p>Health checks should be lightweight, fast, and provide meaningful status information. The <code>express-healthcheck</code> library simplifies this by providing middleware that handles common health check patterns. Here is the fundamental structure of a health check endpoint:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> healthCheck <span class="hljs-keyword">from</span> <span class="hljs-string">'express-healthcheck'</span>

app.use(<span class="hljs-string">'/health/live'</span>, healthCheck({
  <span class="hljs-attr">healthy</span>: <span class="hljs-function">() =&gt;</span> ({ <span class="hljs-attr">status</span>: <span class="hljs-string">'healthy'</span> })
}));
</code></pre>
<p>However, production-level health checks require additional considerations:</p>
<ul>
<li><p><strong>Timeouts</strong>: Dependency checks must have strict time limits to prevent cascade failures.</p>
</li>
<li><p><strong>Parallel execution</strong>: Multiple dependency checks should run concurrently for faster response times.</p>
</li>
<li><p><strong>Detailed error reporting</strong>: Failed checks should provide specific error information for debugging.</p>
</li>
</ul>
<h2 id="heading-implementing-liveness-check">Implementing Liveness Check</h2>
<p>The liveness check focuses solely on verifying that the application instance is responsive and not deadlocked. It should not depend on external services, as dependency failures should not trigger restarts. Create <code>src/routes/health.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> healthcheck <span class="hljs-keyword">from</span> <span class="hljs-string">'express-healthcheck'</span>;
<span class="hljs-keyword">const</span> router = express.Router();

router.use(
  <span class="hljs-string">'/live'</span>,
  healthcheck({
    <span class="hljs-attr">healthy</span>: <span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">status</span>: <span class="hljs-string">'healthy'</span>,
      <span class="hljs-attr">uptime</span>: process.uptime(),
      <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
    }),
  })
);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>This liveness implementation returns a response that includes process uptime and timestamp for operational visibility, but avoids any I/O operations that could fail due to external factors.</p>
<h2 id="heading-implementing-readiness-check">Implementing Readiness Check</h2>
<p>The readiness check validates all critical dependencies required for serving requests. This implementation checks both PostgreSQL database connectivity and SEQ logging service availability using parallel execution with timeouts. Modify the <code>src/routes/health.js</code> with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> healthcheck <span class="hljs-keyword">from</span> <span class="hljs-string">'express-healthcheck'</span>;
<span class="hljs-keyword">import</span> pTimeout <span class="hljs-keyword">from</span> <span class="hljs-string">'p-timeout'</span>;
<span class="hljs-keyword">const</span> router = express.Router();
<span class="hljs-keyword">import</span> db <span class="hljs-keyword">from</span> <span class="hljs-string">'../config/database.js'</span>;

router.use(
  <span class="hljs-string">'/live'</span>,
  healthcheck({
    <span class="hljs-attr">healthy</span>: <span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">status</span>: <span class="hljs-string">'healthy'</span>,
      <span class="hljs-attr">uptime</span>: process.uptime(),
      <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
    }),
  })
);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkSeq</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> pTimeout(fetch(<span class="hljs-string">`<span class="hljs-subst">${process.env.SEQ_UI_URL}</span>/health`</span>), {
      <span class="hljs-attr">milliseconds</span>: process.env.HEALTH_CHECK_TIMEOUT,
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Seq check timeout'</span>,
    });
    <span class="hljs-keyword">if</span> (res.ok) <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'up'</span> };
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>, <span class="hljs-attr">error</span>: res.statusText };
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">if</span> (err <span class="hljs-keyword">instanceof</span> AggregateError) {
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>,
        <span class="hljs-attr">error</span>: err.errors.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.message).join(<span class="hljs-string">', '</span>),
      };
    }
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>, <span class="hljs-attr">error</span>: err.message };
  }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkPostgres</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> pTimeout(db.raw(<span class="hljs-string">'SELECT 1'</span>), {
      <span class="hljs-attr">milliseconds</span>: process.env.HEALTH_CHECK_TIMEOUT,
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Postgres check timeout'</span>,
    });
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'up'</span> };
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">if</span> (err <span class="hljs-keyword">instanceof</span> AggregateError) {
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>,
        <span class="hljs-attr">error</span>: err.errors.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.message).join(<span class="hljs-string">', '</span>),
      };
    }
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>, <span class="hljs-attr">error</span>: err.message };
  }
}

router.use(
  <span class="hljs-string">'/ready'</span>,
  healthcheck({
    <span class="hljs-attr">healthy</span>: <span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">status</span>: <span class="hljs-string">'healthy'</span>,
      <span class="hljs-attr">uptime</span>: process.uptime(),
      <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
    }),
    <span class="hljs-attr">test</span>: <span class="hljs-keyword">async</span> callback =&gt; {
      <span class="hljs-keyword">const</span> [postgresResult, seqResult] = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
        checkPostgres(),
        checkSeq(),
      ]);
      <span class="hljs-keyword">const</span> allUp = [postgresResult, seqResult].every(
        <span class="hljs-function"><span class="hljs-params">result</span> =&gt;</span> result.status === <span class="hljs-string">'up'</span>
      );

      <span class="hljs-keyword">if</span> (!allUp) {
        callback({
          <span class="hljs-attr">status</span>: <span class="hljs-string">'unhealthy'</span>,
          <span class="hljs-attr">dependencies</span>: {
            <span class="hljs-attr">postgres</span>: { ...postgresResult },
            <span class="hljs-attr">seq</span>: { ...seqResult },
          },
          <span class="hljs-attr">uptime</span>: process.uptime(),
          <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
        });
      }
      callback();
    },
  })
);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>This implementation demonstrates several key patterns:</p>
<ul>
<li><p><strong>Parallel execution</strong>: <code>Promise.all()</code> runs both database and SEQ checks concurrently, reducing total response time.</p>
</li>
<li><p><strong>Timeout enforcement</strong>: Each dependency check is wrapped with <code>pTimeout</code> to prevent hanging requests.</p>
</li>
<li><p><strong>Error handling</strong>: Handling the <code>AggregateError</code> type, which can occur with network operations.</p>
</li>
<li><p><strong>Test function</strong>: The <code>test</code> function in the <code>express-healthcheck</code> middleware serves as a custom validation hook that determines whether the endpoint should return a healthy or unhealthy response. The <code>test</code> function uses a callback pattern to communicate results:</p>
<ul>
<li><p><code>callback()</code>:</p>
<ul>
<li><p>Returns HTTP 200 status.</p>
</li>
<li><p>Uses the response from the <code>healthy</code> function.</p>
</li>
</ul>
</li>
<li><p><code>callback(errorData)</code>:</p>
<ul>
<li><p>Returns HTTP 500 (Internal Server Error) status.</p>
</li>
<li><p>Uses the provided <code>errorData</code> as the response body.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Dependency validation</strong>: Checks if all dependencies are healthy using <code>every()</code>.</p>
</li>
<li><p><strong>Selective error reporting</strong>: Detailed error information is included only for failed dependencies.</p>
</li>
<li><p><strong>Graceful degradation</strong>: Individual dependency failures don't crash the entire health check.</p>
</li>
</ul>
<h2 id="heading-bringing-it-all-together">Bringing It All Together</h2>
<p>Update the <code>src/server.js</code> file with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> healthcheck <span class="hljs-keyword">from</span> <span class="hljs-string">'express-healthcheck'</span>;
<span class="hljs-keyword">import</span> pTimeout <span class="hljs-keyword">from</span> <span class="hljs-string">'p-timeout'</span>;
<span class="hljs-keyword">const</span> router = express.Router();
<span class="hljs-keyword">import</span> db <span class="hljs-keyword">from</span> <span class="hljs-string">'../config/database.js'</span>;

router.use(
  <span class="hljs-string">'/live'</span>,
  healthcheck({
    <span class="hljs-attr">healthy</span>: <span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">status</span>: <span class="hljs-string">'healthy'</span>,
      <span class="hljs-attr">uptime</span>: process.uptime(),
      <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
    }),
  })
);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkSeq</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> pTimeout(fetch(<span class="hljs-string">`<span class="hljs-subst">${process.env.SEQ_UI_URL}</span>/health`</span>), {
      <span class="hljs-attr">milliseconds</span>: <span class="hljs-built_in">parseInt</span>(process.env.HEALTH_CHECK_TIMEOUT) || <span class="hljs-number">1000</span>,
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Seq check timeout'</span>,
    });
    <span class="hljs-keyword">if</span> (res.ok) <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'up'</span> };
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>, <span class="hljs-attr">error</span>: res.statusText };
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">if</span> (err <span class="hljs-keyword">instanceof</span> AggregateError) {
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>,
        <span class="hljs-attr">error</span>: err.errors.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.message).join(<span class="hljs-string">', '</span>),
      };
    }
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>, <span class="hljs-attr">error</span>: err.message };
  }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkPostgres</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> pTimeout(db.raw(<span class="hljs-string">'SELECT 1'</span>), {
      <span class="hljs-attr">milliseconds</span>: <span class="hljs-built_in">parseInt</span>(process.env.HEALTH_CHECK_TIMEOUT) || <span class="hljs-number">1000</span>,
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Postgres check timeout'</span>,
    });
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'up'</span> };
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">if</span> (err <span class="hljs-keyword">instanceof</span> AggregateError) {
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>,
        <span class="hljs-attr">error</span>: err.errors.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.message).join(<span class="hljs-string">', '</span>),
      };
    }
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">'down'</span>, <span class="hljs-attr">error</span>: err.message };
  }
}

router.use(
  <span class="hljs-string">'/ready'</span>,
  healthcheck({
    <span class="hljs-attr">healthy</span>: <span class="hljs-function">() =&gt;</span> ({
      <span class="hljs-attr">status</span>: <span class="hljs-string">'healthy'</span>,
      <span class="hljs-attr">uptime</span>: process.uptime(),
      <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
    }),
    <span class="hljs-attr">test</span>: <span class="hljs-keyword">async</span> callback =&gt; {
      <span class="hljs-keyword">const</span> [postgresResult, seqResult] = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
        checkPostgres(),
        checkSeq(),
      ]);
      <span class="hljs-keyword">const</span> allUp = [postgresResult, seqResult].every(
        <span class="hljs-function"><span class="hljs-params">result</span> =&gt;</span> result.status === <span class="hljs-string">'up'</span>
      );

      <span class="hljs-keyword">if</span> (!allUp) {
        callback({
          <span class="hljs-attr">status</span>: <span class="hljs-string">'unhealthy'</span>,
          <span class="hljs-attr">dependencies</span>: {
            <span class="hljs-attr">postgres</span>: { ...postgresResult },
            <span class="hljs-attr">seq</span>: { ...seqResult },
          },
          <span class="hljs-attr">uptime</span>: process.uptime(),
          <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
        });
      }
      callback();
    },
  })
);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>Expected response when dependencies fail (readiness):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"status"</span>: <span class="hljs-string">"unhealthy"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"postgres"</span>: {
      <span class="hljs-attr">"status"</span>: <span class="hljs-string">"down"</span>,
      <span class="hljs-attr">"error"</span>: <span class="hljs-string">"connect ECONNREFUSED ::1:5432, connect ECONNREFUSED 127.0.0.1:5432"</span>
    },
    <span class="hljs-attr">"seq"</span>: {
      <span class="hljs-attr">"status"</span>: <span class="hljs-string">"down"</span>,
      <span class="hljs-attr">"error"</span>: <span class="hljs-string">"fetch failed"</span>
    }
  },
  <span class="hljs-attr">"uptime"</span>: <span class="hljs-number">4.2737912</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-number">1757354296619</span>
}
</code></pre>
<p>Expected response when all dependencies are healthy (readiness):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"status"</span>: <span class="hljs-string">"healthy"</span>,
  <span class="hljs-attr">"uptime"</span>: <span class="hljs-number">4.4563746</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-number">1757354383440</span>
}
</code></pre>
<p>You can find all the code <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/health-checks">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item><item><title><![CDATA[Node.js and Express: Structured Logging with SEQ]]></title><description><![CDATA[Logging is a critical aspect of modern applications that directly impacts debugging capabilities, monitoring effectiveness, and operational visibility. While many developers start with simple console log statements during development, production appl...]]></description><link>https://blog.raulnq.com/nodejs-and-express-structured-logging-with-seq</link><guid isPermaLink="true">https://blog.raulnq.com/nodejs-and-express-structured-logging-with-seq</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Express]]></category><category><![CDATA[seq]]></category><category><![CDATA[Structured logging]]></category><category><![CDATA[morgan]]></category><dc:creator><![CDATA[Raul Naupari]]></dc:creator><pubDate>Tue, 02 Sep 2025 04:43:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1756651447299/dc5ec205-8d62-4f5b-96d1-47abf3290cf3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Logging is a critical aspect of modern applications that directly impacts debugging capabilities, monitoring effectiveness, and operational visibility. While many developers start with simple console log statements during development, production applications require a more sophisticated approach to capture, structure, and analyze log data.</p>
<p>Traditional unstructured logging produces human-readable messages that are difficult for machines to parse and analyze. Consider this typical log entry:</p>
<pre><code class="lang-plaintext">[29/Aug/2025:22:50:02 +0000] "POST /api/todos HTTP/1.1" 201 155
</code></pre>
<p>While readable, this format makes it challenging to:</p>
<ul>
<li><p>Query logs programmatically.</p>
</li>
<li><p>Create dashboards and alerts.</p>
</li>
<li><p>Perform statistical analysis.</p>
</li>
<li><p>Correlate events across distributed systems.</p>
</li>
</ul>
<p>Structured logging addresses these limitations by organizing log data into key-value pairs or JSON objects, enabling powerful querying and analysis capabilities. This article demonstrates how to implement structured logging in Node.js applications using Winston and Seq. The starting code is available <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/validations-and-exception-handling">here</a>.</p>
<h2 id="heading-seq">Seq</h2>
<p><a target="_blank" href="https://datalust.co/docs/an-overview-of-seq">Seq</a> is a centralized logging server built specifically for structured log data. It provides powerful search capabilities, real-time monitoring, and intuitive visualization of log events. Update the <code>docker-compose.yml</code> file as follows to run Seq locally:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">postgres:</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">postgres-server</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'5432:5432'</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">mydb</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">myuser</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">mypassword</span>
  <span class="hljs-attr">seq:</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">seq-server</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">datalust/seq:latest</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'5341:5341'</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'8080:80'</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">ACCEPT_EULA:</span> <span class="hljs-string">Y</span>
</code></pre>
<p>This configuration exposes Seq on two ports:</p>
<ul>
<li><p><strong>Port 8080</strong>: Web interface for viewing and searching logs</p>
</li>
<li><p><strong>Port 5341</strong>: HTTP ingestion endpoint for receiving log events</p>
</li>
</ul>
<p>Run the following command to start the Docker Compose file:</p>
<pre><code class="lang-powershell">npm run docker:up
</code></pre>
<p>Navigate to <a target="_blank" href="http://localhost"><code>http://localhost</code></a><code>:8080</code> in our browser. The Seq interface provides:</p>
<ul>
<li><p><strong>Real-time log streaming</strong>: View logs as they arrive.</p>
</li>
<li><p><strong>Advanced search capabilities</strong>: Query logs using Seq's powerful expression language.</p>
</li>
<li><p><strong>Dashboard creation</strong>: Build custom views for monitoring specific metrics.</p>
</li>
<li><p><strong>Alert configuration</strong>: Set up notifications for critical events.</p>
</li>
</ul>
<h2 id="heading-structured-logging-with-winston">Structured Logging with Winston</h2>
<p><a target="_blank" href="https://github.com/winstonjs/winston">Winston</a> is the most popular logging library for Node.js, providing flexible configuration options and multiple transport mechanisms. We will configure Winston to send structured logs to Seq. First, install the required dependencies:</p>
<pre><code class="lang-powershell">npm install winston @datalust/winston<span class="hljs-literal">-seq</span>
</code></pre>
<p>Create the <code>src/config/logger.js</code> file with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> winston <span class="hljs-keyword">from</span> <span class="hljs-string">'winston'</span>;
<span class="hljs-keyword">import</span> { SeqTransport } <span class="hljs-keyword">from</span> <span class="hljs-string">'@datalust/winston-seq'</span>;

<span class="hljs-keyword">const</span> logger = winston.createLogger({
  <span class="hljs-attr">level</span>: <span class="hljs-string">'info'</span>,
  <span class="hljs-attr">format</span>: winston.format.combine(
    winston.format.timestamp({
      <span class="hljs-attr">format</span>: <span class="hljs-string">'YYYY-MM-DD HH:mm:ss'</span>,
    }),
    winston.format.errors({ <span class="hljs-attr">stack</span>: <span class="hljs-literal">true</span> }),
    winston.format.json()
  ),
  <span class="hljs-attr">defaultMeta</span>: {
    <span class="hljs-attr">application</span>: <span class="hljs-string">'todo-api'</span>,
  },
  <span class="hljs-attr">transports</span>: [
    <span class="hljs-keyword">new</span> SeqTransport({
      <span class="hljs-attr">serverUrl</span>: process.env.SEQ_URL,
      <span class="hljs-attr">apiKey</span>: process.env.SEQ_API_KEY,
      <span class="hljs-attr">handleExceptions</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">handleRejections</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">onError</span>: <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Seq failed to send log:'</span>, e.message);
      },
    }),
  ],
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> logger;
</code></pre>
<p>Let's walk through the Winston logger configuration step by step:</p>
<ul>
<li><p><code>level</code>: Sets the minimum log level to info. This means <code>error</code>, <code>warn</code>, and <code>info</code> logs will be processed.</p>
</li>
<li><p><code>format</code>: This is used to transform, structure, or style our log messages before they are sent to a transport. Formats are defined using <code>winston.format</code> and can be chained using <code>winston.format.combine</code>.</p>
<ul>
<li><p><code>format.timestamp</code>: Adds a timestamp field.</p>
</li>
<li><p><code>format.errors({ stack: true })</code>: Includes stack traces from errors.</p>
</li>
<li><p><code>format.json</code>: Outputs logs as JSON objects.</p>
</li>
</ul>
</li>
<li><p><code>defaultMeta</code>: Adds default fields to every log entry automatically.</p>
</li>
<li><p><code>transports</code>: A transport is essentially a storage/output mechanism for our logs.<br />  It defines where the logs go once Winston has formatted them.</p>
<ul>
<li><p><strong>SeqTransport:</strong> The SeqTransport is a custom Winston transport used to send logs directly to Seq.</p>
<ul>
<li><p><code>serverUrl</code>: Seq ingestion endpoint.</p>
</li>
<li><p><code>apiKey</code>: Optional authentication key.</p>
</li>
<li><p><code>handleExceptions</code>: Logs uncaught exceptions.</p>
</li>
<li><p><code>handleRejections</code>: Logs unhandled promise rejections.</p>
</li>
<li><p><code>onError</code>: Graceful fallback when Seq is unavailable.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="heading-custom-log-messages">Custom Log Messages</h3>
<p>To write custom log messages, import the logger and start using it. Update the <code>src/features/todos/addTodo.js</code> file with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> db <span class="hljs-keyword">from</span> <span class="hljs-string">'../../config/database.js'</span>;
<span class="hljs-keyword">import</span> { v7 <span class="hljs-keyword">as</span> uuidv7 } <span class="hljs-keyword">from</span> <span class="hljs-string">'uuid'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> yup <span class="hljs-keyword">from</span> <span class="hljs-string">'yup'</span>;
<span class="hljs-keyword">import</span> logger <span class="hljs-keyword">from</span> <span class="hljs-string">'../../config/logger.js'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addTodoSchema = yup.object({
  <span class="hljs-attr">title</span>: yup.string().required(),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> addTodo = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> todo = {
    <span class="hljs-attr">id</span>: uuidv7(),
    <span class="hljs-attr">title</span>: req.body.title,
    <span class="hljs-attr">completed</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">created_at</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
  };
  logger.info(<span class="hljs-string">'Adding a new todo {id}'</span>, { <span class="hljs-attr">id</span>: todo.id, <span class="hljs-attr">title</span>: todo.title });
  <span class="hljs-keyword">await</span> db(<span class="hljs-string">'todos'</span>).insert(todo);
  res.status(<span class="hljs-number">201</span>).json(todo);
};
</code></pre>
<h3 id="heading-http-request-logging">HTTP Request Logging</h3>
<p>Implementing request logging can be a good reason to write middleware in Express. Fortunately, someone else has already done this:</p>
<pre><code class="lang-powershell">npm install express<span class="hljs-literal">-winston</span>
</code></pre>
<p>The <a target="_blank" href="https://github.com/bithavoc/express-winston?tab=readme-ov-file">express-winston</a> package provides the <code>expressWinston.logger(options)</code> function to create a middleware to log our HTTP requests. This middleware should be placed before any routes.</p>
<pre><code class="lang-javascript">app.use(
  expressWinston.logger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'HTTP {{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms'</span>,
  })
);
</code></pre>
<p>The <code>winstonInstance</code> is set to reuse the instance we already exported in <code>src/config/logger.js</code>. Our logs will show up as structured JSON objects in Seq:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@t"</span>: <span class="hljs-string">"2025-09-02T02:37:58.4610000Z"</span>,
  <span class="hljs-attr">"@mt"</span>: <span class="hljs-string">"HTTP POST /api/todos 201 67ms"</span>,
  <span class="hljs-attr">"@m"</span>: <span class="hljs-string">"HTTP POST /api/todos 201 67ms"</span>,
  <span class="hljs-attr">"@i"</span>: <span class="hljs-string">"9e6cf66c"</span>,
  <span class="hljs-attr">"meta"</span>: {
    <span class="hljs-attr">"req"</span>: {
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"/api/todos"</span>,
      <span class="hljs-attr">"headers"</span>: {
        <span class="hljs-attr">"user-agent"</span>: <span class="hljs-string">"vscode-restclient"</span>,
        <span class="hljs-attr">"content-type"</span>: <span class="hljs-string">"application/json"</span>,
        <span class="hljs-attr">"accept-encoding"</span>: <span class="hljs-string">"gzip, deflate"</span>,
        <span class="hljs-attr">"content-length"</span>: <span class="hljs-string">"60"</span>,
        <span class="hljs-attr">"host"</span>: <span class="hljs-string">"localhost:5000"</span>,
        <span class="hljs-attr">"connection"</span>: <span class="hljs-string">"close"</span>
      },
      <span class="hljs-attr">"method"</span>: <span class="hljs-string">"POST"</span>,
      <span class="hljs-attr">"httpVersion"</span>: <span class="hljs-string">"1.1"</span>,
      <span class="hljs-attr">"originalUrl"</span>: <span class="hljs-string">"/api/todos"</span>,
      <span class="hljs-attr">"query"</span>: {

      }
    },
    <span class="hljs-attr">"res"</span>: {
      <span class="hljs-attr">"statusCode"</span>: <span class="hljs-number">201</span>
    },
    <span class="hljs-attr">"responseTime"</span>: <span class="hljs-number">67</span>
  },
  <span class="hljs-attr">"application"</span>: <span class="hljs-string">"todo-api"</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-09-01 21:37:58"</span>
}
</code></pre>
<h3 id="heading-error-logging">Error Logging</h3>
<p>Just like HTTP request logging, <a target="_blank" href="https://github.com/bithavoc/express-winston?tab=readme-ov-file">express-winston</a> provides the <code>expressWinston.errorLogger(options)</code> function to create a middleware for logging errors. This middleware must be placed after all routes and before any custom error handlers.</p>
<pre><code class="lang-javascript">app.use(
  expressWinston.errorLogger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'{{err.message}} {{res.statusCode}} {{req.method}}'</span>,
  })
);
</code></pre>
<h2 id="heading-extra-morgan">Extra: Morgan</h2>
<p><a target="_blank" href="https://github.com/expressjs/morgan">Morgan</a> is a console HTTP request logger middleware primarily used in development environments.</p>
<ul>
<li><p>Logs details about each incoming HTTP request (method, URL, status code, response time, etc.).</p>
</li>
<li><p>Provides predefined logging formats (like <code>dev</code>, <code>tiny</code>, <code>combined</code>, etc.).</p>
</li>
<li><p>Can be customized to log only what we need.</p>
</li>
<li><p>The logs can be written to a stream, using <code>process.stdout</code> by default.</p>
</li>
</ul>
<blockquote>
<p>Use Morgan for quick setup and standardized HTTP logging</p>
</blockquote>
<p>Run the following command to install Morgan:</p>
<pre><code class="lang-powershell">npm install morgan
</code></pre>
<p>The complete <code>src/server.js</code> file, including Winston and Morgan, will be:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
<span class="hljs-keyword">import</span> todosRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./features/todos/routes.js'</span>;
<span class="hljs-keyword">import</span> { errorHandler, NotFoundError } <span class="hljs-keyword">from</span> <span class="hljs-string">'./middlewares/errorHandler.js'</span>;
<span class="hljs-keyword">import</span> morgan <span class="hljs-keyword">from</span> <span class="hljs-string">'morgan'</span>;
<span class="hljs-keyword">import</span> expressWinston <span class="hljs-keyword">from</span> <span class="hljs-string">'express-winston'</span>;
<span class="hljs-keyword">import</span> logger <span class="hljs-keyword">from</span> <span class="hljs-string">'./config/logger.js'</span>;

process.on(<span class="hljs-string">'uncaughtException'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  process.exit(<span class="hljs-number">1</span>);
});
dotenv.config();
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">3000</span>;
<span class="hljs-keyword">const</span> app = express();
app.use(express.json());
app.use(morgan(<span class="hljs-string">'dev'</span>));

app.use(
  expressWinston.logger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'HTTP {{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms'</span>,
  })
);
app.use(<span class="hljs-string">'/api/todos'</span>, todosRoutes);
app.all(<span class="hljs-string">'/*splat'</span>, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> pathSegments = req.params.splat;
  <span class="hljs-keyword">const</span> fullPath = pathSegments.join(<span class="hljs-string">'/'</span>);
  next(<span class="hljs-keyword">new</span> NotFoundError(<span class="hljs-string">`The requested URL /<span class="hljs-subst">${fullPath}</span> does not exist`</span>));
});
app.use(
  expressWinston.errorLogger({
    <span class="hljs-attr">winstonInstance</span>: logger,
    <span class="hljs-attr">msg</span>: <span class="hljs-string">'{{err.message}} {{res.statusCode}} {{req.method}}'</span>,
  })
);
app.use(errorHandler);
<span class="hljs-keyword">const</span> server = app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on port <span class="hljs-subst">${PORT}</span>`</span>);
});

process.on(<span class="hljs-string">'unhandledRejection'</span>, <span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(err.name, err.message);
  server.close(<span class="hljs-function">() =&gt;</span> {
    process.exit(<span class="hljs-number">1</span>);
  });
});
</code></pre>
<p>You can find all the code <a target="_blank" href="https://github.com/raulnq/nodejs-express/tree/logging">here</a>. Thanks, and happy coding.</p>
]]></content:encoded></item></channel></rss>