<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alvin Lee</title>
    <description>The latest articles on Forem by Alvin Lee (@alvinslee).</description>
    <link>https://forem.com/alvinslee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F579432%2F7fa8b4ec-dd50-4096-9013-b11933e1cd94.jpeg</url>
      <title>Forem: Alvin Lee</title>
      <link>https://forem.com/alvinslee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alvinslee"/>
    <language>en</language>
    <item>
      <title>The Messaging Challenges No One Talks About in Regulated, Air-Gapped, and Hybrid Environments</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Thu, 08 Jan 2026 01:42:47 +0000</pubDate>
      <link>https://forem.com/alvinslee/the-messaging-challenges-no-one-talks-about-in-regulated-air-gapped-and-hybrid-environments-kfm</link>
      <guid>https://forem.com/alvinslee/the-messaging-challenges-no-one-talks-about-in-regulated-air-gapped-and-hybrid-environments-kfm</guid>
      <description>&lt;p&gt;The modern platform engineering mandate is clear: adopt Kubernetes, embrace microservices, and accelerate velocity.&lt;/p&gt;

&lt;p&gt;In theory, this leads to efficiency; in practice, if you operate within highly regulated sectors — &lt;strong&gt;Finance, Utilities, Defense, Healthcare, etc.&lt;/strong&gt; — the journey often slows down due to significant networking and compliance requirements.&lt;/p&gt;

&lt;p&gt;While the wider developer community utilizes fully managed queues and streaming services (like AWS SQS or Confluent Cloud), enterprise architects in regulated spaces are confronted with a &lt;strong&gt;fundamental modernization challenge&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do you leverage the agility of cloud-native architecture when your security policy strictly forbids external data egress, necessitates air-gapped deployments, and mandates immutable audit trails for every transaction?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The standard answers — legacy middleware and vanilla open-source solutions — often fall short, creating a gap between operational security requirements and modernization goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Modernization Dilemma
&lt;/h2&gt;

&lt;p&gt;For regulated enterprises, the attempt to modernize messaging infrastructure typically forces architects to navigate two difficult options. Both introduce complexity and can delay migration projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Constraints of Legacy Middleware
&lt;/h3&gt;

&lt;p&gt;Platforms like &lt;a href="https://www.ibm.com/products/mq" rel="noopener noreferrer"&gt;IBM MQ&lt;/a&gt; or &lt;a href="https://www.tibco.com/" rel="noopener noreferrer"&gt;TIBCO&lt;/a&gt; have served the enterprise well for decades. They are trusted and proven. However, their architecture is often at odds with the dynamic, ephemeral nature of Kubernetes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Architectural Differences:&lt;/strong&gt; Legacy middleware was designed for static environments where IP addresses rarely change and servers run for years. Kubernetes is dynamic; pods are created and destroyed in seconds. Using a static, heavy-weight message broker to track thousands of ephemeral microservices creates an architecture that requires significant manual configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The “Integration Overhead”:&lt;/strong&gt; Modernizing with legacy tools often shifts engineering effort from &lt;em&gt;innovation&lt;/em&gt; to &lt;em&gt;integration&lt;/em&gt;. Developers forced to use older protocols or heavy client libraries in modern languages (like Go, Rust, or Python) spend considerable time writing custom wrappers just to maintain basic connectivity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scaling Costs:&lt;/strong&gt; In a containerized world, the goal is to scale horizontally — adding lightweight instances as load increases. Legacy licensing models, often based on CPU cores or host counts, can make this scaling strategy cost-prohibitive.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The Complexity of Self-Managed Open Source
&lt;/h3&gt;

&lt;p&gt;The alternative is often vanilla open-source solutions like &lt;a href="https://kafka.apache.org/" rel="noopener noreferrer"&gt;Kafka&lt;/a&gt; or &lt;a href="https://www.rabbitmq.com/" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;. While technically capable, these tools assume an operational environment that is often unavailable inside a secure perimeter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;“Day 2” Operational Complexity:&lt;/strong&gt; Cloud providers simplify these systems with managed control planes. When you deploy them on-premise without that automation, you inherit the full operational responsibility. Managing dependencies, rebalancing partitions, handling upgrades, and recovering from node failures in an air-gapped environment — where you cannot simply pull the latest Helm chart — requires a dedicated team.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security Configuration:&lt;/strong&gt; Most open-source projects prioritize features over enterprise governance. To make them compliant, teams must manually configure security mechanisms — setting up authentication, authorization, and audit logging. This often results in a complex platform that is difficult to upgrade and maintain over time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The “No Egress” Constraint:&lt;/strong&gt; Many “Cloud-Native” tools inadvertently rely on external connectivity — whether for pulling dependencies or sending telemetry. In a strictly air-gapped network with “No Egress” policies, these tools may require complex workarounds (like proxy tunnels) to function correctly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; Architects face a difficult trade-off. Staying on legacy systems limits velocity, but moving to standard open-source tools increases operational overhead and compliance complexity. A purpose-built solution is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes-Native Messaging for Trust and Control
&lt;/h2&gt;

&lt;p&gt;A third option is to use a &lt;em&gt;Kubernetes-native&lt;/em&gt; message broker. This type of message broker is engineered specifically to resolve this trade-off by delivering a Kubernetes-native messaging backbone that is &lt;strong&gt;security-first&lt;/strong&gt; and &lt;strong&gt;operationally self-sufficient.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s look at the advantages of a Kubernetes-native messaging platform using as an example a product I’ve been using lately, &lt;a href="https://kubemq.io/" rel="noopener noreferrer"&gt;KubeMQ&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. One Platform, All Messaging Patterns
&lt;/h3&gt;

&lt;p&gt;Eliminate the complexity of maintaining multiple message brokers for different needs. A Kubernetes-native message broker like KubeMQ unifies all major messaging patterns into a single cluster.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consolidated Infrastructure:&lt;/strong&gt; Instead of running Kafka for streaming, RabbitMQ for queuing, and gRPC for request/reply, you run one broker that handles &lt;strong&gt;Pub/Sub, Queues, Streams, and RPC&lt;/strong&gt; in one lightweight platform. This reduces the infrastructure footprint and simplifies the architecture for your development teams.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Operational Simplicity (Easy to Use and Manage)
&lt;/h3&gt;

&lt;p&gt;Designed for low operational overhead.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Dedicated “Messaging Team” Required:&lt;/strong&gt; Unlike complex open-source products that might require a dedicated team of engineers to keep running, KubeMQ is designed to be easily deployed and managed by a single DevOps engineer or developer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. True Air-Gapped Capability and Zero Egress
&lt;/h3&gt;

&lt;p&gt;KubeMQ is designed to run disconnected. There is no requirement for external connectivity for licensing, metrics, or management. You can deploy the container in a high-security data center, and it functions independently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero External Dependencies:&lt;/strong&gt; You do not need to open firewall ports for a vendor’s control plane. All management and monitoring tools are included and run &lt;em&gt;inside&lt;/em&gt; your perimeter, ensuring total data sovereignty.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Security &amp;amp; Audit: Deep Policy Enforcement
&lt;/h3&gt;

&lt;p&gt;Compliance requires not just encryption, but verifiable control over access and activity.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Integrated RBAC and SSO:&lt;/strong&gt; KubeMQ enforces Role-Based Access Control that integrates with your enterprise SSO/LDAP services. This ensures that only authenticated microservices with specific cluster roles can access designated channels or topics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Immutable Audit and Retention:&lt;/strong&gt; The platform provides built-in mechanisms for retaining message history and action logs. This gives auditors a clear trail of every action taken within the message bus — a requirement for regulated compliance frameworks like &lt;strong&gt;PCI-DSS or HIPAA&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Architecting for Hybrid and Edge Resilience
&lt;/h3&gt;

&lt;p&gt;Modern infrastructure is rarely consolidated. It is distributed across headquarters, remote data centers, and field edge devices.&lt;/p&gt;

&lt;p&gt;KubeMQ’s &lt;strong&gt;Bridges and Connectors&lt;/strong&gt; allow for secure message replication across segregated environments. This allows you to synchronize data between On-Prem and Cloud without exposing the core network, and manage Day 2 operations declaratively via GitOps, reducing operational risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-Life Use Case: Unifying Critical Electricity Infrastructure
&lt;/h2&gt;

&lt;p&gt;Let’s look at a real-world example: a &lt;strong&gt;major electricity transmission system operator in Europe&lt;/strong&gt;. This operator manages critical national infrastructure, meaning their systems must be 100% reliable, secure, and operate strictly within a private, air-gapped environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Challenge: Bridging Legacy and Innovation&lt;/strong&gt; The organization operated a diverse messaging environment, with critical data flowing through legacy systems based on &lt;strong&gt;RabbitMQ&lt;/strong&gt; and &lt;strong&gt;ActiveMQ&lt;/strong&gt;. While robust, these systems were difficult to integrate with their new initiative: building modern, Kubernetes-based microservices to improve grid efficiency. They needed a way to allow new applications to consume data from legacy mainframes without engaging in a high-risk project to rewrite the core legacy code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: A Kubernetes-native messaging solution as a Non-Intrusive Bridge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than replacing their legacy systems immediately, they used their new messaging solution to wrap and extend them. Using unconnected &lt;strong&gt;Sources&lt;/strong&gt; and &lt;strong&gt;Targets&lt;/strong&gt;, they built a bi-directional integration layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inbound:&lt;/strong&gt; Sources connect to the legacy RabbitMQ queues, consuming AMQP messages and converting them into KubeMQ events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Outbound:&lt;/strong&gt; The modern microservices process this data and publish results. Targets then translate these results back into AMQP and push them to the legacy queues.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Value Delivered:&lt;/strong&gt; This integration provided three distinct strategic advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Risk-Free Modernization:&lt;/strong&gt; They achieved a modernization of their architecture without changing any code in their mission-critical legacy systems. The old systems operate exactly as before, ensuring stability for the national grid.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accelerated Development:&lt;/strong&gt; The digital team was able to start building advanced microservices immediately. By consuming normalized data from the message broker, they were decoupled from the complexities of the legacy environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Future-Proof Foundation:&lt;/strong&gt; They have effectively abstracted the underlying protocol. This gives the organization the flexibility to decommission the old brokers at their own pace, moving fully to a modern infrastructure without disrupting business logic.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Modernize Without Compromise
&lt;/h2&gt;

&lt;p&gt;In the regulated sector, control is synonymous with security. Relying on external services or adapting incompatible tools is not always a sustainable strategy.&lt;/p&gt;

&lt;p&gt;A Kubernetes-native messaging platform provides your platform engineering team with the agility they need, while providing the security and compliance team with the control and visibility they require.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>eventdriven</category>
      <category>architecture</category>
      <category>pubsub</category>
    </item>
    <item>
      <title>Why Your UEBA Isn’t Working (and how to fix it)</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Sat, 13 Dec 2025 00:51:23 +0000</pubDate>
      <link>https://forem.com/alvinslee/why-your-ueba-isnt-working-and-how-to-fix-it-5b59</link>
      <guid>https://forem.com/alvinslee/why-your-ueba-isnt-working-and-how-to-fix-it-5b59</guid>
      <description>&lt;p&gt;&lt;strong&gt;UEBA (User Entity Behavior Analysis)&lt;/strong&gt; is a security layer that uses machine learning and analytics to detect threats by analyzing patterns in user and entity behavior.&lt;/p&gt;

&lt;p&gt;Here’s an oversimplified example of UEBA: suppose you live in Chicago. You’ve lived there for several years and rarely travel. But suddenly there’s a charge to your credit card from a restaurant in Italy. Someone is using your card to pay for their lasagna! Luckily, your credit card company recognizes the behavior as suspicious, flags the transaction, and stops it from settling. This is easy for your credit card company to flag: they have plenty of historical information on your habits and have created a set of logical rules and analytics for when to flag your transactions.&lt;/p&gt;

&lt;p&gt;But most threats are not this easy to detect. Attackers are continuously becoming more sophisticated and learning to work around established rules.&lt;/p&gt;

&lt;p&gt;As a result, &lt;strong&gt;traditional UEBA that&lt;/strong&gt; relies primarily on static, rigid rules &lt;strong&gt;is no longer enough to protect your systems.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The End of Traditional UEBA–or, Why Your UEBA No Longer Works
&lt;/h2&gt;

&lt;p&gt;Many UEBA tools were built around &lt;strong&gt;static rules and predefined behavioral thresholds&lt;/strong&gt;. Those approaches were useful for catching predictable, well-understood behavior patterns, but are not great in modern environments where user activity, applications, and attacker behavior change constantly. Soon, AI agents will act on behalf of users and introduce even more diversity.&lt;/p&gt;

&lt;p&gt;Here are the main limitations of traditional, static-rule-driven UEBA:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Static behavioral thresholds don’t adapt to real user behavior over time.&lt;/strong&gt; They rely on fixed assumptions (e.g., “alert if X happens more than Y times”), which quickly become outdated as user behavior and environments evolve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rules require continuous manual tuning.&lt;/strong&gt; Security teams spend time chasing false positives or rewriting rules as user behavior changes, which slows response and increases operational overhead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Isolated detection logic lacks context.&lt;/strong&gt; Legacy UEBA often analyzes events in silos, instead of correlating activity across identity, endpoint, network, and application layers. This limits the ability to detect subtle behavioral anomalies.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a result, certain types of threats that blend into normal activity can go unnoticed despite the presence of rules.&lt;/p&gt;

&lt;p&gt;So if legacy UEBA is not effective …what’s the solution?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Modern Enterprises Actually Need from UEBA
&lt;/h2&gt;

&lt;p&gt;Modern enterprises need UEBA systems that can do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Immediately detect attacks&lt;/strong&gt;. When attackers can morph in an instant, you need a security layer that moves just as fast.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recognize attacks that are highly sophisticated and complex&lt;/strong&gt;. Attacks are no longer simple enough to be caught by a set of rules — even advanced ones backed with behavioral analytics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Integrate seamlessly with existing security operations&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s look at each in more detail and how it can be achieved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Immediately detect attacks (without a long training period)
&lt;/h3&gt;

&lt;p&gt;Traditional UEBA training periods leave organizations vulnerable for months and chronically behind on detecting the latest threats. A typical three to six month learning period creates a huge security gap.&lt;/p&gt;

&lt;p&gt;Day-one detection capabilities for behavioral threats and compromised accounts require first-seen and outlier rules that can spot anomalous behavior immediately without waiting for machine learning models to mature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need near-instant behavioral baselines&lt;/strong&gt;. How?&lt;/p&gt;

&lt;p&gt;Luckily, most organizations already have the data they need: years of historical log information sitting in their Security Information and Event Management (SIEM) systems. Modern UEBA systems use this information to create behavioral baselines instantly.&lt;/p&gt;

&lt;p&gt;For example, companies like &lt;a href="https://www.sumologic.com/" rel="noopener noreferrer"&gt;Sumo Logic&lt;/a&gt; — who advocate for the “&lt;em&gt;log everything&lt;/em&gt;” approach, have tools that use the data you already have to create powerful baselines in just minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detect highly sophisticated attacks
&lt;/h3&gt;

&lt;p&gt;Today’s attacks blend in with normal business operations. Correlation rules miss behavioral threats that show only subtle anomalies; they can’t identify compromised accounts or insider threats that are performing normal-looking activities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modern UEBA solutions must be able to detect first-seen activities&lt;/strong&gt;, such as unusual file sharing via OneDrive. They need to catch access to new proxy categories and suspicious cloud service usage that don’t match historical user behavior.&lt;/p&gt;

&lt;p&gt;This comes down to using the right tools. For example, &lt;a href="https://www.microsoft.com/en-us/security/business/siem-and-xdr/microsoft-sentinel" rel="noopener noreferrer"&gt;Microsoft Sentinel&lt;/a&gt; can identify unusual Azure identity behaviors such as abnormal cloud service access patterns that could indicate account compromise or data exfiltration. And Sumo Logic has &lt;a href="https://help.sumologic.com/docs/cse/rules/write-first-seen-rule/" rel="noopener noreferrer"&gt;first-seen&lt;/a&gt; and &lt;a href="https://help.sumologic.com/docs/cse/rules/write-outlier-rule/" rel="noopener noreferrer"&gt;outlier&lt;/a&gt; rules that can detect when an attacker is trying to use a network sniffing tool. They catch endpoint enumeration and suspicious email forwarding rules that deviate from established patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integration with existing security operations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;UEBA delivers meaningful value&lt;/strong&gt; when it fits naturally into existing security workflows. Security teams rely on SIEM, SOAR, identity systems, and endpoint tools to build a complete picture of activity across their environment. UEBA works best when its behavioral insights are delivered in the same place analysts already investigate and respond to alerts.&lt;/p&gt;

&lt;p&gt;Effective UEBA solutions therefore integrate directly with the broader security platform, allowing behavioral anomalies to be correlated with logs, identity events, and threat intelligence. This unified context helps analysts make faster, more accurate decisions without switching tools or managing separate consoles.&lt;/p&gt;

&lt;p&gt;Flexibility is also important. Organizations must be able to adjust detection logic and behavioral thresholds to match their environment, risk tolerance, and operational needs. When UEBA is tightly integrated and adaptable, it becomes an extension of the security workflow rather than an additional system to maintain*&lt;em&gt;.&lt;/em&gt;*&lt;/p&gt;

&lt;h2&gt;
  
  
  UEBA as the Foundation for AI Security Agents
&lt;/h2&gt;

&lt;p&gt;UEBA hasn’t been replaced by AI. Instead, UEBA has become the way to train AI. AI-powered detection and response solutions perform best when they ingest &lt;strong&gt;clean, comprehensive behavior baselines&lt;/strong&gt;, and that’s exactly what mature UEBA can provide.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI agents need quality behavioral baselines
&lt;/h3&gt;

&lt;p&gt;AI security agents aren’t magic. They follow the GIGO (garbage in, garbage out) principle just like any other data-intensive system. Feed an AI agent high-quality behavioral data, and it will thrive. But if you feed it insufficient or poor-quality data, then you’ll become part of the &lt;a href="https://fortune.com/2025/08/18/mit-report-95-percent-generative-ai-pilots-at-companies-failing-cfo/" rel="noopener noreferrer"&gt;95% statistic of AI pilots that fail to deliver real business value&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Structured UEBA rules also give the agents specialist knowledge, such as who should log in where, how often a service account connects to S3, and typical overnight file volumes. AI agents can learn (and extend) these rulesets.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI detects, then AI responds
&lt;/h3&gt;

&lt;p&gt;Security teams often drown in alerts. Teams can’t keep up. But when UEBA feeds AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First-seen rules become automatic triggers. Instead of waiting for an analyst, an agent can begin gathering data and context within seconds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI can rank threats, helping to make sure human attention is given to the events with the biggest deviation or highest blast radius.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Entity relationship maps derived from UEBA help agents model lateral-movement risk and choose containment tactics (for example: quarantine the host, revoke credentials, etc.).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the system can reliably detect threats, you can take it to the next level and allow your AI agents to take action, too.&lt;/p&gt;

&lt;h3&gt;
  
  
  From UEBA rules to autonomous security operations
&lt;/h3&gt;

&lt;p&gt;Manual security operations have a scaling problem. They can’t keep pace with modern threat volumes and complexity. As a result, organizations miss threats or burn out security analysts with overwhelming alert fatigue.&lt;/p&gt;

&lt;p&gt;But with UEBA first-seen rules, AI agents can immediately begin collecting evidence and correlating events when anomalous behavior is detected. Outlier detection can feed AI-driven risk scoring and prioritization algorithms. And behavioral baselines can ensure that automated responses are based on a solid understanding of what constitutes legitimate versus suspicious activity within the specific organizational context.&lt;/p&gt;

&lt;p&gt;You can still have a human in the loop, authorizing each action recommended by the AI system. Eventually, you may delegate action to the AI system as well, with humans merely being notified after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building AI-Ready Behavioral Foundations Now
&lt;/h2&gt;

&lt;p&gt;Modern UEBA platforms are already generating AI-compatible behavioral data. These platforms structure their outputs in ways that AI agents can easily consume and act upon. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.anthropic.com/engineering/writing-tools-for-agents" rel="noopener noreferrer"&gt;Ongoing discovery&lt;/a&gt; of the best ways to format and organize data to fit optimally into the context of LLMs and how to provide them with effective tools&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signal-clustering algorithms to reduce the noise that might confuse an AI agent’s decision-making. This ensures that only meaningful behavioral anomalies reach automated systems for action.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rule customization and match lists provide structured data that AI agents can interpret and act upon. This creates clear decision trees for autonomous responses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Historical baseline capabilities create rich training datasets without waiting months for AI deployment. Organizations can leverage years of existing log data. AI agents can begin operating with sophisticated behavioral understanding from day one rather than starting with blank behavioral models.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these capabilities already in place, organizations can transition seamlessly from manual to automated security operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;When implementing UEBA, focus on true principles and actionable strategies:&lt;/p&gt;

&lt;p&gt;1. &lt;strong&gt;Ensure comprehensive, high‑quality data integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use all relevant data sources: existing logs, new telemetry, identity systems, endpoints, and cloud apps to build complete behavioral profiles. If critical data is missing you should collect it and add it to the UEBA’s scope. For example, a user’s calendar showing a business trip to Tokyo is very relevant when the system detects login attempts from Japan.&lt;/p&gt;

&lt;p&gt;2. &lt;strong&gt;Accelerate meaningful baselines using historical data and rapid observation periods&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Leverage historical log data to establish baselines quickly, but expect this to take a couple days to a few weeks. Supplement with fresh data as needed to ensure the baseline reflects current behaviors. For example, if an employee moves to a different team the system should expect a change in behavior.&lt;/p&gt;

&lt;p&gt;3. &lt;strong&gt;Integrate UEBA insights with your current security workflows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;UEBA should capitalize on SIEM and other security tools to deliver high-impact alerts and operational value. Avoid requiring extensive new infrastructure unless necessary, and always align the solution to your organization’s needs.&lt;/p&gt;

&lt;p&gt;These approaches deliver immediate value and adapt to changes to maximize the coverage and accuracy of behavioral analytics.&lt;/p&gt;

&lt;p&gt;Your success metrics matter just as much as your implementation. Track the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;How many sophisticated threats UEBA catches that your traditional systems miss&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The reduction in dwell time for compromised accounts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Coverage improvements for lateral movement and unknown attack patterns&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Analyst efficiency gains from richer contextual alerts.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These metrics prove value to stakeholders and help you continuously refine your approach.&lt;/p&gt;

&lt;p&gt;While classic rule-based UEBA relied on manual configuration, today’s UEBA platforms enhance these foundations with autonomous analytics using statistical models, adaptive baselines, and AI-driven outlier detection.&lt;/p&gt;

&lt;p&gt;Functions like &lt;strong&gt;first-seen&lt;/strong&gt; and &lt;strong&gt;outlier detection&lt;/strong&gt; do leverage rules, but they operate as part of a dynamic, context-aware system rather than a set of static match expressions. AI agents continuously monitor and analyze vast streams of behavioral data, learning what constitutes normal activity and identifying subtle anomalies that may indicate emerging threats. By correlating signals across users, devices, and time, these agents enable real-time, adaptive detection and response. This elevates security operations from manually maintained static rule matching to intelligent and proactive protection.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>devsecops</category>
      <category>analytics</category>
      <category>security</category>
    </item>
    <item>
      <title>Log Spikes? No Sweat: How Top DevOps Teams Tame Bursty Workloads</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Fri, 11 Jul 2025 16:05:53 +0000</pubDate>
      <link>https://forem.com/alvinslee/log-spikes-no-sweat-how-top-devops-teams-tame-bursty-workloads-2el0</link>
      <guid>https://forem.com/alvinslee/log-spikes-no-sweat-how-top-devops-teams-tame-bursty-workloads-2el0</guid>
      <description>&lt;p&gt;Taylor Swift ticket sales brought the entire platform to its knees… a crypto exchange saw 10x its regular traffic during a price swing… holiday deals dropped at midnight, and retail sites scrambled to keep up. These weren’t just high-traffic moments. They were log storms.&lt;/p&gt;

&lt;p&gt;For DevOps teams in bursty verticals like media, fintech, gaming, and retail, moments like these are make-or-break. Whether the spike is planned (like a product drop) or unpredictable (like an influencer mention), what happens behind the scenes is intense: logging pipelines can flood, ingestion costs can surge, and dashboards often freeze when you need them the most.&lt;/p&gt;

&lt;p&gt;We’re going to look at how top-performing teams prepare for these log surges before they hit. You’ll learn how modern observability platforms offer scalable architectures, dynamic ingestion, and pricing models that flex with demand, not against it. We’ll also unpack real-world scenarios where things didn’t go to plan, and show how a different approach could have mitigated all the firefighting in the dark.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes a “Bursty” Vertical So Challenging
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Troubles with scaling: Ticketmaster’s meltdown
&lt;/h3&gt;

&lt;p&gt;First, let’s look at the challenge of scaling. The hardest part about operating in a bursty vertical isn’t just scaling, it’s doing it fast, without losing visibility or blowing up your budget.&lt;/p&gt;

&lt;p&gt;Ticketmaster’s meltdown during the Taylor Swift Eras Tour pre-sale became a masterclass in what can go wrong when systems are unprepared for simultaneous, global demand spikes. The platform saw over &lt;a href="https://business.ticketmaster.com/press-release/taylor-swift-the-eras-tour-onsale-explained" rel="noopener noreferrer"&gt;3.5 billion system requests&lt;/a&gt; in a single day, four times the previous peak. And it wasn’t just the front-end that struggled. Back-end observability pipelines were reportedly overwhelmed, which slowed root cause analysis and delayed recovery efforts. This was a case where Ticketmaster knew there was going to be significant demand, but they just didn’t expect it to be this large.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbutenyyey1jlz6dc2uk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbutenyyey1jlz6dc2uk.png" alt=" " width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For teams in e-commerce, media, fintech, and gaming, these bursts can arrive with little to no warning. Flash sales, viral moments, breaking news, or crypto market moves create sudden demand that can outpace even the best predictive scaling models. Even well-orchestrated campaigns like a product drop or a limited-edition NFT mint can spark volumes that dwarf normal baselines.&lt;/p&gt;

&lt;p&gt;Again, the challenge here isn’t only application scale, it’s also &lt;strong&gt;observability scale&lt;/strong&gt;. Log volumes don’t just grow linearly with traffic; they often spike exponentially. API calls increase. Errors multiply. Security events balloon. Suddenly, what was a manageable logging setup turns into a firehose of data, one that traditional logging tools aren’t built to handle.&lt;/p&gt;

&lt;p&gt;Why? Most legacy log management systems depend on rigid ingestion pipelines and fixed retention pricing. When volume surges, either logs are dropped, throttled, or stored at unsustainable costs. &lt;strong&gt;Worse, engineering teams often choose to ingest only specific logs, filtering out data that might later prove critical for debugging or forensics.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That decision, under pressure, can backfire.&lt;/p&gt;

&lt;h3&gt;
  
  
  Losing trust: Robinhood goes dark
&lt;/h3&gt;

&lt;p&gt;Our second challenge is reliable triage and finding a root cause.&lt;/p&gt;

&lt;p&gt;Consider the &lt;a href="https://finance.yahoo.com/news/robinhood-restores-crypto-trading-dogecoin-061300060.html" rel="noopener noreferrer"&gt;Robinhood outages during a surge in Dogecoin trading&lt;/a&gt;. While the company cited “unprecedented volumes,” users and analysts alike noted that the company essentially went dark while they scrambled to figure out what was happening. Transparency was limited for &lt;em&gt;hours&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Without reliable observability during peak stress, diagnosing failures became guesswork, and trust took a hit.&lt;/p&gt;

&lt;p&gt;In bursty environments, DevOps teams need tools that scale as fast as their demand does — and just as importantly, pricing models that won’t punish them for succeeding. That’s where next-gen observability platforms come in, offering elastic ingestion and smart tiering that keep logs flowing, insights visible, and costs predictable — even when everything else is spiking.&lt;/p&gt;

&lt;p&gt;Let’s now look at some solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modern Observability Platforms That Are “Built for Bursty”
&lt;/h2&gt;

&lt;p&gt;When traffic surges hit, your users expect the same seamless experience they experienced before the surge. And for that, your DevOps team needs observability tools that don’t melt down under pressure.&lt;/p&gt;

&lt;p&gt;That’s why modern observability platforms have embraced architectures &lt;em&gt;built for bursty&lt;/em&gt;. They are designed from the ground up to scale dynamically, preserve full-fidelity logs, and surface insights even when volumes spike unpredictably.&lt;/p&gt;

&lt;p&gt;They use schema-less ingestion, ingest everything models, and AI for triage. Let’s look at each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema-less ingestion
&lt;/h3&gt;

&lt;p&gt;Unlike legacy systems that rely on manual log filtering or hard-coded schemas, today’s observability leaders support &lt;strong&gt;schema-less ingestion&lt;/strong&gt;, meaning you can pump in both structured and unstructured data. This means everything from JSON logs to raw error messages and Slack alerts with no need to reconfigure pipelines.&lt;/p&gt;

&lt;p&gt;Modern platforms such as &lt;a href="https://www.sumologic.com/" rel="noopener noreferrer"&gt;Sumo Logic&lt;/a&gt; are built to handle sudden data surges without missing a beat. Its architecture automatically scales ingestion pipelines and performs real-time indexing to keep dashboards responsive and queries fast, even under duress. According to &lt;a href="https://www.sumologic.com/case-studies/infor" rel="noopener noreferrer"&gt;engineers&lt;/a&gt;, during a major production event where log ingestion volumes more than doubled, the platform maintained performance while increasing cost by only 10% thanks to its elastic scaling design. This kind of efficiency is critical during high-pressure events, like election nights or viral product launches, where teams can’t afford blind spots in their observability stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ingest everything
&lt;/h3&gt;

&lt;p&gt;Traditional platforms force teams to overprovision for peak load. However, modern platforms offer pay-as-you-go pricing and innovative models, such as flex credits, which enable teams to temporarily “burst” without incurring higher ongoing costs. The best platforms also offer “ingest everything” plans where you can log everything, but only pay for what you actually use.&lt;/p&gt;

&lt;p&gt;More modern pricing plans, like the ones above, keep budgets in check and eliminate the guesswork from capacity planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI for triage
&lt;/h3&gt;

&lt;p&gt;The real game-changer? Built-in machine learning. When log volume doubles or triples, humans can’t sift through everything.&lt;/p&gt;

&lt;p&gt;Tools like Sumo Logic’s anomaly detection and LogReduce automatically cluster repetitive log lines, highlight deviations from baseline patterns, and tee up root causes before customers notice. That’s how teams keep downtime short and postmortems informative. If your observability platform can’t scale, index, and surface insights in real time during your most critical hours, it’s not ready for bursty workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Top Teams Stay Calm During the Storm
&lt;/h2&gt;

&lt;p&gt;Not only do you need the right tools to handle bursts, you also need the right mindset and training. When the logs start flying and dashboards light up like a Christmas tree, panic is easy — &lt;strong&gt;but the best teams stay calm because they are ready for chaos&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stress-test your log pipelines
&lt;/h3&gt;

&lt;p&gt;Leading DevOps teams practice what’s essentially chaos engineering for observability: they don’t just test app resilience under load — they deliberately stress-test their log pipelines.&lt;/p&gt;

&lt;p&gt;At Netflix, for example, engineers regularly simulate outages and surges as part of their “&lt;a href="https://netflixtechblog.com/the-netflix-simian-army-16e57fbab116" rel="noopener noreferrer"&gt;Failure Injection Testing&lt;/a&gt;” framework, which includes observability components to ensure monitoring tools perform under pressure.&lt;/p&gt;

&lt;p&gt;But you don’t have to operate at Netflix’s scale to benefit from the same mindset. Effective teams simulate log floods during load tests, which push traffic through staging environments while tracking how ingestion, indexing, and alerting respond to the increased load. Tools like Grafana’s &lt;a href="https://k6.io/" rel="noopener noreferrer"&gt;k6&lt;/a&gt; and &lt;a href="https://locust.io/" rel="noopener noreferrer"&gt;Locust&lt;/a&gt; can simulate thousands of requests per second, while synthetic log generators mimic bursty error scenarios.&lt;/p&gt;

&lt;p&gt;Key metrics to watch during these tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ingestion throughput: Are logs being dropped, delayed, or backed up?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alert latency: Are critical alerts still firing on time?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Storage tier transitions: Are logs routed to cold or cheaper storage as designed?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use verbose logs
&lt;/h3&gt;

&lt;p&gt;Also, teams can apply &lt;strong&gt;intelligent partitioning&lt;/strong&gt;, i.e., routing verbose debug logs to lower-cost tiers while maintaining high-value security or performance logs in hot storage. Dynamic sampling and routing rules ensure you’re not overwhelmed, and more importantly, that you don’t lose signal during the noise.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Non-verbose logs (high value):&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Keep these in hot storage; they contain immediately useful information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-06-17T13:02:11Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failed login attempt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"923188"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid password"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;Verbose logs (low value):&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;These logs may happen millions of times a day, while they aren’t often useful for daily metrics, during bursty periods, they could be a leading indicator of a problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-06-17T13:02:12Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEBUG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Parsed user agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.42"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Take advantage of runbooks
&lt;/h2&gt;

&lt;p&gt;Operational readiness also means people, not just tools. Top teams develop &lt;strong&gt;runbooks&lt;/strong&gt;, specifically tailored documentation for burst scenarios, including volume-based alerting that adapts thresholds based on time windows or historical norms. Clear escalation paths and role assignments reduce confusion when seconds count.&lt;/p&gt;

&lt;p&gt;The difference between chaos and control? Preparation. The best observability platforms support that prep — and the best teams treat bursty events as drills they’ve already rehearsed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Bursty workloads are no longer rare exceptions — they’re the new normal in high-velocity industries like e-commerce, media, fintech, and gaming. From viral product drops to trading frenzies, these moments create not only traffic spikes but also observability crises.&lt;/p&gt;

&lt;p&gt;Traditional log management tools often fail under pressure, either throttling data or overwhelming teams with noise. That’s why top DevOps teams rely on observability platforms purpose-built for scale, speed, and flexibility. With schema-less ingestion, elastic scalability, and usage-based pricing models like flex credits, these platforms don’t just keep logs flowing; they keep insight accessible when it matters most. The best teams don’t wait for a spike to test their resilience: they rehearse chaos, simulate bursts, and fine-tune alerting strategies so they can act with confidence, not confusion. Because in a world where digital performance is tied directly to business success, the ability to weather log storms isn’t a luxury — it’s a competitive edge.&lt;/p&gt;

</description>
      <category>observability</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Human Side of Logs: What Unstructured Data Is Trying to Tell You</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Mon, 12 May 2025 16:39:17 +0000</pubDate>
      <link>https://forem.com/alvinslee/the-human-side-of-logs-what-unstructured-data-is-trying-to-tell-you-3hi1</link>
      <guid>https://forem.com/alvinslee/the-human-side-of-logs-what-unstructured-data-is-trying-to-tell-you-3hi1</guid>
      <description>&lt;p&gt;It’s Friday afternoon, and your dashboards look great. Charts are green. CPU usage is stable. Database query times are within your SLA. You’re feeling great and ready for the weekend.&lt;/p&gt;

&lt;p&gt;But little do you know, there’s a significant issue being overlooked by all your metrics—and it’s about to ruin your weekend. &lt;/p&gt;

&lt;p&gt;Unfortunately, you don’t know about the problem yet. That’s because there’s a disparity between your metrics and the actual user experience. Your dashboards might &lt;em&gt;look&lt;/em&gt; great, but your users are telling a different story.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It happens to even the best of us. On April 17, 2025, for example, &lt;a href="https://www.tomsguide.com/news/live/walmart-outage-apr-17-2025" rel="noopener noreferrer"&gt;Walmart's website experienced a significant outage. Users were unable to add items to their carts or&lt;/a&gt; access certain pages. Revenue was lost, user complaints surged on X and &lt;a href="https://downdetector.com/" rel="noopener noreferrer"&gt;Downdetector, and undoub&lt;/a&gt;tedly someone was woken up from a deep sleep to log on and help get it fixed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Critical signals don’t always show up in CPU graphs or 5xx error charts. They surface in chat threads, Downdetector complaints, and even failed login attempt logs. The first signals often come from people, not probes.&lt;/p&gt;

&lt;p&gt;Why? Because traditional monitoring tools focus on &lt;em&gt;structured&lt;/em&gt; data, such as CPU usage, memory consumption, database usage, and network throughput. And while these metrics are essential, they can miss the nuances of user behavior and experience. &lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;unstructured data&lt;/strong&gt;—such as error messages, user feedback, and logs—can tell a more thorough story. It can provide critical insights into system issues that structured data can overlook and that you otherwise wouldn’t know about—until it’s too late.​ &lt;/p&gt;

&lt;p&gt;The signs are there—if you're listening.&lt;/p&gt;

&lt;p&gt;In this article, I’ll explore why unstructured data matters, where you should look for unstructured data, what signals to watch for, and how observability platforms can help you tap into unstructured data without drowning you in noise. &lt;/p&gt;

&lt;h2&gt;
  
  
  Why Unstructured Data Matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Structured data&lt;/strong&gt; is the data that comes in the formats you expect—rows, columns, numbers, stats—and tells you all about what logically happened. It’s the duration of an API call, the response status code, or the CPU load on a node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unstructured data&lt;/strong&gt;, on the other hand, is the messy data. And it’s everywhere. It can be found in support tickets, bug reports, chat threads, error messages, and offhand complaints. It’s the data that often arrives in natural language, not numeric values. It’s messy, not always clear in meaning, and as the name suggests, it’s unstructured. &lt;/p&gt;

&lt;p&gt;But unstructured data is critical. It tells you where confusion lives, where intent breaks down, and where the user’s mental model clashes with what the software does. It captures the emotions, intents, and frustrations that users feel when systems misbehave. &lt;/p&gt;

&lt;p&gt;And when ingested, interpreted, and aggregated, important patterns can emerge. For example, unstructured data can start to paint a picture if your app is seeing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A surge in password reset attempts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rage clicks after a UI release&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A sudden drop in engagement&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Support tickets clustered around a broken journey&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes your best clues aren’t in the metrics—they’re in this unstructured data. Structured observability gives you the dashboard. Unstructured data gives you the story. And if you're not reading both, you're missing half the plot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Signals Should You Watch For?
&lt;/h2&gt;

&lt;p&gt;So where should you look for unstructured data—and what should you watch for? There are many sources. Here are a few to start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pay attention to &lt;strong&gt;session logs&lt;/strong&gt; that show users repeatedly attempting the same action. That’s not just a retry—it’s friction. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Watch for &lt;strong&gt;freeform error messages&lt;/strong&gt; that never get piped into your dashboard. That’s often where the real context behind a failure lives. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don’t ignore the &lt;strong&gt;chatter on Slack, in Jira, or even on social media&lt;/strong&gt;. When three engineers complain about the same “sluggish page,” chances are there’s a performance regression in your latency graph smoothed over.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Even &lt;strong&gt;vague user feedback&lt;/strong&gt; can be invaluable. A spike in “can’t log in” support tickets may be attributed to session expiry handling, rather than infrastructure failure. You’ll only catch it if you’re collecting and analyzing the whole narrative—system logs, yes, but also what people say and do when something doesn’t work the way they expect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Watch for &lt;strong&gt;security anomalies&lt;/strong&gt;. Failed login attempts, credential stuffing, token mismatches—these may not trigger alarms if thresholds aren’t breached, but patterns buried in raw logs can signal a threat weeks before your SIEM lights up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Observability Platforms Can Wrangle Unstructured Data
&lt;/h2&gt;

&lt;p&gt;One of the biggest misconceptions about unstructured data is that it has to be cleaned, labeled, and modeled before it’s useful.&lt;/p&gt;

&lt;p&gt;Yes, that was true in the past. Teams often spent hours writing regular expressions or building brittle parsers just to extract a few fields from a messy log line.&lt;/p&gt;

&lt;p&gt;But that’s no longer the case.&lt;/p&gt;

&lt;p&gt;Modern observability platforms are designed to ingest unstructured data at scale without requiring perfect formatting or predefined schemas. You can pump in raw error messages, user reviews, support tickets, and Slack threads—and the platform handles the rest. Machine learning, natural language processing, and pattern recognition do the bulk of the work.&lt;/p&gt;

&lt;p&gt;That means you don’t need a data wrangler to find value. A modern observability platform can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automatically surface spikes in login failures by IP address block or geographic location.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cluster similar feedback into themes using &lt;a href="https://en.wikipedia.org/wiki/Sentiment_analysis" rel="noopener noreferrer"&gt;sentiment analysis&lt;/a&gt;, even if the wording varies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Correlate failed transactions to specific deployments, even when the logs don’t follow strict naming conventions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be effective, you need an observability platform that can ingest &lt;em&gt;both&lt;/em&gt; structured and unstructured data. One that provides a comprehensive view of system health. One that, by analyzing unstructured data, helps you to identify and address issues proactively—before your weekend is ruined.​&lt;/p&gt;

&lt;p&gt;For example, here’s a screenshot from &lt;a href="https://www.sumologic.com/" rel="noopener noreferrer"&gt;Sumo Logic&lt;/a&gt; showing a parse statement from a section of unstructured log data, and how it helps you make sense of the data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmpte3wlnwjfnmzwxwkr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmpte3wlnwjfnmzwxwkr.png" alt="Image description" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a modern platform like this, you can use a “schema-on-read” approach. You just store the data as is, then analyze it when needed. And if you can get &lt;a href="https://www.sumologic.com/pricing/" rel="noopener noreferrer"&gt;friendly pricing&lt;/a&gt;, you won’t have to worry about the amount of ingest; you can ingest everything—system logs, application traces, and behavioral data—and explore it later without gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Use Cases
&lt;/h2&gt;

&lt;p&gt;For example, let’s say you work at an e-commerce company that experiences a sudden surge in negative comments on social media and customer reviews, all of which mention difficulties with the checkout process. Traditional monitoring tools, focused on structured data like transaction success rates, show no anomalies. &lt;/p&gt;

&lt;p&gt;But by employing &lt;a href="https://brand24.com/blog/product-sentiment-analysis/" rel="noopener noreferrer"&gt;sentiment analysis&lt;/a&gt; on unstructured data sources, you identify a pattern: customers are frustrated with a recent update to the checkout interface. This insight means you can promptly address the issue, improve &lt;a href="https://contentsquare.com/guides/sentiment-analysis/examples/" rel="noopener noreferrer"&gt;customer satisfaction&lt;/a&gt;, and stop any potential revenue loss.&lt;/p&gt;

&lt;p&gt;When observability platforms process behavioral signals at scale, the value isn’t just technical—it’s operational and financial. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;E-commerce teams can identify and resolve friction in the checkout flow before it tanks conversion rates. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SaaS platforms can correlate a rise in support volume with a regression in a recent release and take action before churn increases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SRE and platform teams can detect misconfigurations or silent failures earlier, which means reduced incident duration and lower downtime costs. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of pattern recognition turns what used to be support overhead into strategic insight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You’re already tracking the numbers—latency, error rates, CPU usage. But that’s only half the story. The other half lives in the messy commotion: Slack rants, support tickets, log messages, and user reviews that don’t fit into a tidy schema. &lt;/p&gt;

&lt;p&gt;Unstructured data is where behavioral signals live. It can show you &lt;em&gt;why&lt;/em&gt; things are breaking, not just &lt;em&gt;what&lt;/em&gt; is broken. It captures confusion, intent, and frustration long before structured telemetry raises a red flag. &lt;/p&gt;

&lt;p&gt;If you're responsible for user experience, reliability, or security, you can’t afford to ignore what people are saying—or how they're interacting—with your product. The tools exist to make unstructured data useful. Now it’s a matter of putting them to work. The human side of logs is talking. Start listening.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>datascience</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Concept to Cloud: Building with Cursor and the Heroku MCP Server</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Tue, 22 Apr 2025 21:33:55 +0000</pubDate>
      <link>https://forem.com/alvinslee/from-concept-to-cloud-building-with-cursor-and-the-heroku-mcp-server-2oii</link>
      <guid>https://forem.com/alvinslee/from-concept-to-cloud-building-with-cursor-and-the-heroku-mcp-server-2oii</guid>
      <description>&lt;p&gt;I’ve been experimenting with &lt;a href="https://www.cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; as a development tool, and it’s been surprisingly helpful in my day-to-day workflow. It’s not just that it writes code — it understands context, offers suggestions in the right moments, and even anticipates what I’m about to do next.&lt;/p&gt;

&lt;p&gt;When I saw the &lt;a href="https://blog.heroku.com/introducing-official-heroku-mcp-server" rel="noopener noreferrer"&gt;announcement&lt;/a&gt; about the Heroku MCP Server, I got curious. Could I use Cursor to go beyond just writing code — and actually build and deploy an app to Heroku — primarily via chat prompts and responses? I decided to try it out.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through how I used Cursor to build a simple SvelteKit app and deploy it to Heroku, powered by the new MCP integration. Would it work? Was it smooth? I thought I would test drive it to see. Come along for the ride.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the Model Context Protocol (MCP)?
&lt;/h2&gt;

&lt;p&gt;MCP is an open standard that lets LLMs interact with external tools in a structured, programmatic way. Instead of just generating code or text based on context, MCP support lets your AI system take real actions — like make API calls or execute commands — based on what you ask.&lt;/p&gt;

&lt;p&gt;In practice, this turns Cursor into more than just an AI-enhanced code editor. With MCP support, Cursor becomes a command center. I could potentially spin up cloud infrastructure, query databases, or scaffold new projects — all through prompts in the chat.&lt;/p&gt;

&lt;p&gt;The Heroku MCP Server introduces this capability for the Heroku platform. That means I can ask Cursor to do things like create a Heroku app, scale dynos, or attach add-ons — without ever leaving my editor or opening a terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Cursor to build my SvelteKit app
&lt;/h2&gt;

&lt;p&gt;I wanted Cursor to take a shot at building a to-do list as a single-page application using SvelteKit, backed by PostgreSQL. This should be simple enough for Cursor, and it would save me time, since I have little experience with Svelte. These are the steps I took.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open Cursor to a new project folder
&lt;/h3&gt;

&lt;p&gt;I started with an empty folder in &lt;code&gt;~/project&lt;/code&gt;. I had a blank canvas and was ready (for Cursor) to get to work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fpnuur2djhmfz4v5iub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fpnuur2djhmfz4v5iub.png" alt="Cursor - blank canvas" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Describe the task
&lt;/h3&gt;

&lt;p&gt;I explained to Cursor what I wanted to do. To give it a little help with Svelte, I pointed it to &lt;a href="https://svelte.dev/docs/llms" rel="noopener noreferrer"&gt;Svelte’s LLM-friendly documentation&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I would like to build a “Todo List” single page application using Svelte/SvelteKit. The documentation for Svelte can be found here: &lt;a href="https://svelte.dev/llms-full.txt" rel="noopener noreferrer"&gt;https://svelte.dev/llms-full.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My application needs to handle listing to-dos, adding to-dos, marking to-dos as complete/incomplete, and deleting to-dos.&lt;/p&gt;

&lt;p&gt;It should be backed by a Postgres database.&lt;/p&gt;

&lt;p&gt;Eventually, I will deploy this application to Heroku with a Postgres add-on. But I also want to test it locally. I have a local instance of Postgres running. So, I can either give you local db credentials, or you can use the connection string from Heroku once the add-on is running.&lt;/p&gt;

&lt;p&gt;Please create my application in the current folder.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was the response, so I gave Cursor a little guidance as to my preferences.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faftkuoxmevwgbfv5yjgk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faftkuoxmevwgbfv5yjgk.png" alt="Cursor initialize project" width="603" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cursor worked through creating the app and adding the necessary dependencies. When it hit speed bumps (like command line options that were outdated), it worked through them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhgj6b0de0iye161uu66b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhgj6b0de0iye161uu66b.png" alt="Cursor builds Svelte project" width="591" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Let it code!
&lt;/h3&gt;

&lt;p&gt;It was time for Cursor to begin implementing the application. At this point, it had only been about two minutes since I opened Cursor to get started.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg70gw3vg04xo2m0czsv5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg70gw3vg04xo2m0czsv5.png" alt="Cursor writes code" width="593" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a matter of seconds, Cursor generated the code and saved the files necessary for my application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up local database
&lt;/h3&gt;

&lt;p&gt;Next, Cursor gave me next steps related to setting up my database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxedtv5gec84ky26ed3o2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxedtv5gec84ky26ed3o2.png" alt="Cursor guides psql setup" width="582" height="101"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I told Cursor that I would want some help with this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please give me the commands to create that database and run the schema. And also, create the .env file for me. My local postgres database runs on localhost, port 5432. The username is postgres and the password is postgres.&lt;/p&gt;

&lt;p&gt;I probably need source control too. Please initialize a git repo for my project too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cursor set up my &lt;code&gt;.env&lt;/code&gt; file and gave me the &lt;code&gt;psql&lt;/code&gt; commands I needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59ew49wdf1m51c21x0nc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59ew49wdf1m51c21x0nc.png" alt="Cursor create psql commands" width="604" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialize a git repository
&lt;/h3&gt;

&lt;p&gt;Cursor continued on, setting up source control for my project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ttbc1f9xeqlcu3f7cna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ttbc1f9xeqlcu3f7cna.png" alt="Cursor initializes git repo" width="596" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Test application locally
&lt;/h3&gt;

&lt;p&gt;Next, Cursor told me it was ready for me to test on a local development server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbq7ns1924b4d9la4tpp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbq7ns1924b4d9la4tpp.png" alt="Cursor says it is time to test" width="593" height="113"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Really? All the coding was done and I was ready to test? I clicked &lt;strong&gt;Run command&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgcbpkjnxk1qnxwjd9te.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgcbpkjnxk1qnxwjd9te.png" alt="Cursor spins up dev server" width="587" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, I opened my browser to &lt;a href="http://localhost:5173/" rel="noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt; to test out the app. It worked. Listing, adding, marking as complete, and deleting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0pw17saal0os560cda2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0pw17saal0os560cda2.png" alt="Local testing successful" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, Cursor had finished building my app. It had been about five minutes since I started. We’re ready to deploy to Heroku. And now, we get to use Cursor with Heroku’s MCP Server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to Heroku with help from Cursor
&lt;/h2&gt;

&lt;p&gt;Getting up and running with Heroku is already pretty easy. I was ready to see how Heroku task automation with Cursor could enhance my productivity even more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up Cursor to use Heroku’s MCP Server
&lt;/h3&gt;

&lt;p&gt;To connect Cursor with Heroku’s MCP Server, I needed to go through a few simple steps…&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Obtain a Heroku authorization token
&lt;/h4&gt;

&lt;p&gt;From a terminal, I made sure I was authenticated with the Heroku CLI. Then I ran the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;heroku authorizations:create

Creating OAuth Authorization... &lt;span class="k"&gt;done
&lt;/span&gt;Client:      &amp;lt;none&amp;gt;
ID:          03aff7da-87a9-4f9b-9400-5387164390e9
Description: Long-lived user authorization
Scope:       global
Token:       HRKU-1a2b3c4d-5e6f-7890-abcd-abc123def456
Updated at:  Thu Apr 17 2025 12:04:14 GMT-0700 &lt;span class="o"&gt;(&lt;/span&gt;Mountain Standard Time&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;less than a minute ago&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would be the only time that I needed to do this in a terminal. I could use this authorization token for all future Cursor projects.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Create a &lt;code&gt;mcp.json&lt;/code&gt; file
&lt;/h4&gt;

&lt;p&gt;In my project folder, I created a subfolder called &lt;code&gt;.cursor&lt;/code&gt;. In that folder, I created a file called &lt;code&gt;mcp.json&lt;/code&gt; with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"heroku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx -y @heroku/mcp-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"HEROKU_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HRKU-1a2b3c4d-5e6f-7890-abcd-abc123def456"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Enable the new MCP server
&lt;/h4&gt;

&lt;p&gt;As soon as I saved &lt;code&gt;.cursor/mcp.json&lt;/code&gt;, Cursor detected it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjhoy659xkorsg7x0nnql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjhoy659xkorsg7x0nnql.png" alt="Cursor detects MCP tool" width="480" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked &lt;strong&gt;Enable&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Cursor to build and deploy app
&lt;/h3&gt;

&lt;p&gt;With Cursor configured, I just needed to tell it to get to work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please deploy my app the Heroku. You can call it &lt;code&gt;my-svelte-todo-list\&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk3cgw5hbu8ocjto9w1cj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk3cgw5hbu8ocjto9w1cj.png" alt="Cursor creates Heroku app" width="585" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s a promising start. I clicked &lt;strong&gt;Run tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Next, Cursor would add the Postgres add-on. There was a bit of a hiccup here. Cursor didn’t have the right name for the lowest-tier add-on plan, but it quickly solved its problem and continued moving forward.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fky7ph9gaonqmf2fcb7bi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fky7ph9gaonqmf2fcb7bi.png" alt="Cursor adds Heroku postgres add-on" width="577" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the Postgres add-on up and running, I nudged Cursor to move ahead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You’ve created the app and the Postgres database. Help me initialize the database and then deploy the code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cursor used the MCP tool to connect to my Postgres add-on — in the Heroku CLI, that would have been &lt;code&gt;heroku pg:psql&lt;/code&gt;—and to create a table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2745fvjmk6606i5j4rtb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2745fvjmk6606i5j4rtb.png" alt="Cursor initializes database schema on Heroku" width="584" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, Cursor checked that everything was set up to deploy my app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdf6bv7l9rfpvftxv78yk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdf6bv7l9rfpvftxv78yk.png" alt="Cursor preps for deployment" width="582" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, Cursor noticed that there was no Heroku remote set up for my git repository. So, it helped with that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7yx1qgsrkegxxo9a4vl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7yx1qgsrkegxxo9a4vl.png" alt="Cursor creates git remote for Heroku" width="591" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that Cursor needed to run a bash command to set up the remote. It looks like &lt;code&gt;heroku git:remote&lt;/code&gt; isn’t one of the &lt;a href="https://github.com/heroku/heroku-mcp-server?tab=readme-ov-file#available-tools" rel="noopener noreferrer"&gt;available tools in the Heroku MCP Server&lt;/a&gt; (yet).&lt;/p&gt;

&lt;p&gt;I was almost ready for Cursor to deploy. But if Cursor was going to use an MCP tool (&lt;a href="https://github.com/heroku/heroku-mcp-server?tab=readme-ov-file#application-management" rel="noopener noreferrer"&gt;deploy_to_heroku&lt;/a&gt;) instead of have me issue a &lt;code&gt;git push heroku main&lt;/code&gt; command, then my project would need an &lt;code&gt;app.json&lt;/code&gt; file. I reminded Cursor of this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I think I also need an app.json file so that I can deploy to Heroku (with the Heroku MCP tool).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cursor created the file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsxvarjn92q9a8hg6as0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsxvarjn92q9a8hg6as0.png" alt="Cursor creates app.json" width="583" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, Cursor was ready to deploy to Heroku with the MCP tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdokd1la8rsz5qgxqtd0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdokd1la8rsz5qgxqtd0w.png" alt="Cursor ready to deply" width="581" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a few seconds, this is what Cursor told me:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00w4o68nj6hcbhypgtd1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00w4o68nj6hcbhypgtd1.png" alt="Cursor deploys to Heroku" width="581" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Successful deployment? Just like that? I went to my Heroku app URL to be sure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1bkc9h0jem9q02q9uz4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1bkc9h0jem9q02q9uz4v.png" alt="Cloud deployment successful" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes! Successful deployment!&lt;/p&gt;

&lt;h2&gt;
  
  
  Other things that Cursor can do with the Heroku MCP Server
&lt;/h2&gt;

&lt;p&gt;Cursor said the deployment was successful, but asked if there were other things I wanted it to do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdma2h1jj16njcdlkp0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdma2h1jj16njcdlkp0f.png" alt="Cursor can do other things through Heroku MCP" width="582" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hmm, check application status or view the logs? Ok, sure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feuz3s8pu5o0a3esjtfgi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feuz3s8pu5o0a3esjtfgi.png" alt="Cursor summarizes application status" width="588" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cursor used the MCP tools to check on my dyno and summarize recent logs. Nice!&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts: A new way to build
&lt;/h2&gt;

&lt;p&gt;Cursor didn’t just help me scaffold a project or write a frontend for a framework I have little experience with. It carried my entire project from concept to cloud. That was pretty sweet. Heroku already makes deployment straightforward, but using Cursor and the MCP Server made the whole experience feel nearly automated.&lt;/p&gt;

&lt;p&gt;This wasn’t just about saving time. It was about shifting how I interact with tools I already know and trust. The idea that I can go from “I want to build…” to “It’s live!” without switching contexts is exciting — and something worth exploring further on more ambitious projects.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>heroku</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Deploying a Scala Play Application to Heroku</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Tue, 08 Apr 2025 15:39:00 +0000</pubDate>
      <link>https://forem.com/alvinslee/deploying-a-scala-play-application-to-heroku-2ie6</link>
      <guid>https://forem.com/alvinslee/deploying-a-scala-play-application-to-heroku-2ie6</guid>
      <description>&lt;p&gt;I’ve been a web developer for years, but I haven’t touched Java in a long time — like, late-90s long. Back then, Java development felt cumbersome: lots of boilerplate and complex configurations. It was not exactly a pleasant experience for building simple web apps. So when I recently started exploring &lt;a href="https://www.scala-lang.org/" rel="noopener noreferrer"&gt;Scala&lt;/a&gt; and the &lt;a href="https://www.playframework.com/" rel="noopener noreferrer"&gt;Play Framework&lt;/a&gt;, I was curious more than anything. Has the Java developer experience gotten better? Is it actually something I’d want to use today?&lt;/p&gt;

&lt;p&gt;Scala runs on the Java Virtual Machine, but it brings a more expressive, modern syntax to the table. It’s often used in backend development, especially when performance and scalability matter. Play is a web framework built for Scala, designed to be fast, reactive, and developer-friendly. And you use &lt;a href="https://www.scala-sbt.org/" rel="noopener noreferrer"&gt;sbt&lt;/a&gt;, which is Scala’s build tool — roughly comparable to Maven or Gradle in the Java world.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through setting up a basic Scala Play app, running it locally, and then deploying it to Heroku. My hope is to show you how to get your app running smoothly — without needing to know much about the JVM or how Play works under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing the Example App
&lt;/h3&gt;

&lt;p&gt;To keep things simple, I’m starting with an existing sample project: the &lt;a href="https://github.com/playframework/play-samples/tree/3.0.x/play-scala-rest-api-example" rel="noopener noreferrer"&gt;Play Scala REST API&lt;/a&gt; example. It’s a small application that exposes a few endpoints for creating and retrieving blog posts. All the data is stored in memory, so there’s no database to configure — perfect for testing and deployment experiments.&lt;/p&gt;

&lt;p&gt;To make things easier for this walkthrough, I’ve cloned that sample repo and made a few tweaks to prep it for Heroku. You can follow along using my &lt;a href="https://github.com/alvinslee/scala-api-heroku" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; This isn’t a production-ready app, and that’s the point. It’s just robust enough to explore how Play works and see what running and deploying a Scala app actually feels like.&lt;/p&gt;

&lt;h4&gt;
  
  
  Taking a quick look at the code
&lt;/h4&gt;

&lt;p&gt;Before we deploy anything, let’s take a quick tour of the app itself. It’s a small codebase, but there are a few things worth pointing out — especially if you’re new to Scala or curious about how it compares to more traditional Java. I learned a lot about the ins and outs of the Play Framework for building an API by reading this &lt;a href="https://github.com/playframework/play-samples/blob/3.0.x/play-scala-rest-api-example/docs/src/main/paradox/part-1/index.md" rel="noopener noreferrer"&gt;basic explanation page&lt;/a&gt;. Here are some key points:&lt;/p&gt;

&lt;h4&gt;
  
  
  Case class to model resources
&lt;/h4&gt;

&lt;p&gt;For this REST API, the basic resource is the blog post, which has an &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;link&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, and &lt;code&gt;body&lt;/code&gt;. The simplest way to model this is with a Scala &lt;a href="https://docs.scala-lang.org/tour/case-classes.html" rel="noopener noreferrer"&gt;case class&lt;/a&gt; that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happens at the top of &lt;a href="https://github.com/alvinslee/scala-api-heroku/blob/main/app/v1/post/PostResourceHandler.scala" rel="noopener noreferrer"&gt;&lt;code&gt;app/v1/post/PostResourceHandler.scala&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Routing is clean and centralized
&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://github.com/alvinslee/scala-api-heroku/blob/main/conf/routes" rel="noopener noreferrer"&gt;&lt;code&gt;conf/routes&lt;/code&gt;&lt;/a&gt; file maps HTTP requests to a router in a format that’s easy to read and change. It feels closer to something like Express or Flask than an XML-based Java config. In our example, the file is just one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;         &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;               &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;PostRouter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the router at &lt;a href="https://github.com/alvinslee/scala-api-heroku/blob/main/app/v1/post/PostRouter.scala" rel="noopener noreferrer"&gt;&lt;code&gt;app/v1/post/PostRouter.scala&lt;/code&gt;&lt;/a&gt; defines how different requests within this path map to controller methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nv"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;index&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nv"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;process&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="s"&gt;"/$id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nv"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;show&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is pretty clear. A &lt;code&gt;GET&lt;/code&gt; request to the path root takes us to the controller’s &lt;code&gt;index&lt;/code&gt; method, while a &lt;code&gt;POST&lt;/code&gt; request takes us to its &lt;code&gt;process&lt;/code&gt; method. Meanwhile, a &lt;code&gt;GET&lt;/code&gt; request with an included blog post &lt;code&gt;id&lt;/code&gt; will take us to the &lt;code&gt;show&lt;/code&gt; method and pass along the given &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Controllers are concise
&lt;/h4&gt;

&lt;p&gt;That brings us to our controller, at &lt;a href="https://github.com/alvinslee/scala-api-heroku/blob/main/app/v1/post/PostController.scala" rel="noopener noreferrer"&gt;&lt;code&gt;app/v1/post/PostController.scala&lt;/code&gt;&lt;/a&gt;. Each endpoint is a method that returns an &lt;code&gt;Action&lt;/code&gt;, which works with the JSON result using Play’s built-in helpers. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;AnyContent&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;PostAction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;async&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;implicit&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nv"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"show: id = $id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;postResourceHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;lookup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;map&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nc"&gt;Ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;toJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s very little boilerplate — no need for separate interface declarations or verbose annotations.&lt;/p&gt;

&lt;h4&gt;
  
  
  JSON is handled with implicit values
&lt;/h4&gt;

&lt;p&gt;Play uses implicit values to handle JSON serialization and deserialization. You define an implicit &lt;code&gt;Format&lt;/code&gt;[&lt;code&gt;PostResource&lt;/code&gt;] using Play JSON’s macros, and then Play just knows how to turn your objects into JSON and back again. No manual parsing or verbose configuration needed. We see this in &lt;a href="https://github.com/alvinslee/scala-api-heroku/blob/5ef4986647d7635c73b87856c55a4ecc6e172554/app/v1/post/PostResourceHandler.scala#L19" rel="noopener noreferrer"&gt;&lt;code&gt;app/v1/post/PostResourceHandler.scala&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**
    * Mapping to read/write a PostResource out as a JSON value.
    */&lt;/span&gt;
    &lt;span class="k"&gt;implicit&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;PostResource&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;format&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Modern, expressive syntax
&lt;/h4&gt;

&lt;p&gt;As I dig around through the code, I see some of Scala’s more expressive features in action — things like &lt;a href="https://docs.scala-lang.org/overviews/collections/maps.html" rel="noopener noreferrer"&gt;&lt;code&gt;map&lt;/code&gt;&lt;/a&gt; operations and pattern matching with &lt;a href="https://docs.scala-lang.org/tour/pattern-matching.html" rel="noopener noreferrer"&gt;&lt;code&gt;match&lt;/code&gt;&lt;/a&gt;. The syntax looks new at first, but it quickly feels like a streamlined blend of Java and JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the App Locally
&lt;/h3&gt;

&lt;p&gt;Before deploying to the cloud, it’s always a good idea to make sure the app runs locally. This helps us catch any obvious issues and lets us poke around the API.&lt;/p&gt;

&lt;p&gt;On my machine, I make sure to have Java and &lt;code&gt;sbt&lt;/code&gt; installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;java &lt;span class="nt"&gt;--version&lt;/span&gt;
openjdk 17.0.14 2025-01-21
OpenJDK Runtime Environment &lt;span class="o"&gt;(&lt;/span&gt;build 17.0.14+7-Ubuntu-120.04&lt;span class="o"&gt;)&lt;/span&gt;
OpenJDK 64-Bit Server VM &lt;span class="o"&gt;(&lt;/span&gt;build 17.0.14+7-Ubuntu-120.04, mixed mode, sharing&lt;span class="o"&gt;)&lt;/span&gt;

~/project&lt;span class="nv"&gt;$ &lt;/span&gt;sbt &lt;span class="nt"&gt;--version&lt;/span&gt;
sbt version &lt;span class="k"&gt;in &lt;/span&gt;this project: 1.10.6
sbt script version: 1.10.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I run the following from my project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;sbt run

&lt;span class="o"&gt;[&lt;/span&gt;info] welcome to sbt 1.10.6 &lt;span class="o"&gt;(&lt;/span&gt;Ubuntu Java 17.0.14&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;info] loading settings &lt;span class="k"&gt;for &lt;/span&gt;project scala-api-heroku-build from plugins.sbt...
&lt;span class="o"&gt;[&lt;/span&gt;info] loading project definition from /home/alvin/repositories/devspotlight/heroku/scala/scala-api-heroku/project
&lt;span class="o"&gt;[&lt;/span&gt;info] loading settings &lt;span class="k"&gt;for &lt;/span&gt;project root from build.sbt...
&lt;span class="o"&gt;[&lt;/span&gt;info] loading settings &lt;span class="k"&gt;for &lt;/span&gt;project docs from build.sbt...
&lt;span class="o"&gt;[&lt;/span&gt;info]   __              __
&lt;span class="o"&gt;[&lt;/span&gt;info]   &lt;span class="se"&gt;\ \ &lt;/span&gt;    ____   / /____ _ __  __
&lt;span class="o"&gt;[&lt;/span&gt;info]    &lt;span class="se"&gt;\ \ &lt;/span&gt;  / __ &lt;span class="se"&gt;\ &lt;/span&gt;/ // __ &lt;span class="sb"&gt;`&lt;/span&gt;// / / /
&lt;span class="o"&gt;[&lt;/span&gt;info]    / /  / /_/ // // /_/ // /_/ /
&lt;span class="o"&gt;[&lt;/span&gt;info]   /_/  / .___//_/ &lt;span class="se"&gt;\_&lt;/span&gt;_,_/ &lt;span class="se"&gt;\_&lt;/span&gt;_, /
&lt;span class="o"&gt;[&lt;/span&gt;info]       /_/               /____/
&lt;span class="o"&gt;[&lt;/span&gt;info] 
&lt;span class="o"&gt;[&lt;/span&gt;info] Version 3.0.6 running Java 17.0.14
&lt;span class="o"&gt;[&lt;/span&gt;info] 
&lt;span class="o"&gt;[&lt;/span&gt;info] Play is run entirely by the community. Please consider contributing and/or donating:
&lt;span class="o"&gt;[&lt;/span&gt;info] https://www.playframework.com/sponsors
&lt;span class="o"&gt;[&lt;/span&gt;info] 

&lt;span class="nt"&gt;---&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Running the application, auto-reloading is enabled&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;---&lt;/span&gt;

INFO  p.c.s.PekkoHttpServer - Listening &lt;span class="k"&gt;for &lt;/span&gt;HTTP on /[0:0:0:0:0:0:0:0]:9000

&lt;span class="o"&gt;(&lt;/span&gt;Server started, use Enter to stop and go back to the console...&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With my local server up and listening on port &lt;code&gt;9000&lt;/code&gt;, I open a new terminal and test the API by sending a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:9000/v1/posts | jq

&lt;span class="o"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"1"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/1"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 1"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 1"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"2"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/2"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 2"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 2"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"3"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/3"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 3"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 3"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"4"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/4"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 4"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 4"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"5"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/5"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 5"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 5"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice. That was fast. Next, I want to play around with a few requests — retrieving and creating a blog post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:9000/v1/posts/3 | jq

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"3"&lt;/span&gt;,
  &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/3"&lt;/span&gt;,
  &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 3"&lt;/span&gt;,
  &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 3"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-type:application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{ "title": "Just Another Blog Post", "body": "This is my blog post body." }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    http://localhost:9000/v1/posts | jq


&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"999"&lt;/span&gt;,
  &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/999"&lt;/span&gt;,
  &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"Just Another Blog Post"&lt;/span&gt;,
  &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"This is my blog post body."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Preparing for Deployment to Heroku
&lt;/h3&gt;

&lt;p&gt;Ok, we’re ready to deploy our Scala Play app to Heroku. However, being new to Scala and Play, I’m predisposed to hitting a few speed bumps. I want to cover those so that you can steer clear of them in your development.&lt;/p&gt;

&lt;h4&gt;
  
  
  Understanding the &lt;code&gt;AllowedHostsFilter&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;When I run my app locally with &lt;code&gt;sbt run&lt;/code&gt;, I have no problems sending curl requests and receiving responses. But as soon as I’m in the cloud, I’m using a Heroku app URL, not &lt;code&gt;localhost&lt;/code&gt;. For security, Play has an &lt;a href="https://www.playframework.com/documentation/3.0.x/AllowedHostsFilter" rel="noopener noreferrer"&gt;&lt;code&gt;AllowedHostsFilter&lt;/code&gt;&lt;/a&gt; enabled, which means you need to specify explicitly which hosts can access the app.&lt;/p&gt;

&lt;p&gt;I modify &lt;code&gt;conf/application.conf&lt;/code&gt; to include the following block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="nv"&gt;play&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;hosts&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="kt"&gt;localhost:&lt;/span&gt;&lt;span class="err"&gt;9000"&lt;/span&gt;,
    &lt;span class="kt"&gt;$&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;?PLAY_ALLOWED_HOSTS&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, I can set the &lt;code&gt;PLAY_ALLOWED_HOSTS&lt;/code&gt; config variable for my Heroku app, adding my Heroku app URL to the list of allowed hosts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting a Play secret
&lt;/h4&gt;

&lt;p&gt;Play requires an &lt;a href="https://www.playframework.com/documentation/3.0.x/ApplicationSecret" rel="noopener noreferrer"&gt;application secret&lt;/a&gt; for security. It’s used for signing and encryption. You &lt;em&gt;could&lt;/em&gt; set the application secret in &lt;code&gt;conf/application.conf&lt;/code&gt;, but that hardcodes it into your repo, which isn’t a good practice. Instead, let’s set it at runtime based on an environment config variable. We add the following lines to &lt;code&gt;conf/application.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;play.http.secret.key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"changeme"&lt;/span&gt;
&lt;span class="py"&gt;play.http.secret.key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${?PLAY_SECRET}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line sets a default secret key, while the second line sets the secret to come from our config variable, &lt;code&gt;PLAY_SECRET&lt;/code&gt;, if it is set.&lt;/p&gt;

&lt;h4&gt;
  
  
  sbt run versus running the compiled binary
&lt;/h4&gt;

&lt;p&gt;Lastly, I want to talk a little bit about &lt;code&gt;sbt run&lt;/code&gt;. But first, let’s rewind: on my first attempt at deploying to Heroku, I thought I would just spin up my server by having Heroku execute &lt;code&gt;sbt run&lt;/code&gt;. When I did that, my dyno kept crashing. I didn’t realize how memory-intensive this mode was. It’s meant for development, not production.&lt;/p&gt;

&lt;p&gt;During that first attempt, I turned on &lt;a href="https://devcenter.heroku.com/articles/log-runtime-metrics" rel="noopener noreferrer"&gt;&lt;code&gt;log-runtime-metrics&lt;/code&gt;&lt;/a&gt; for my Heroku app. My app was using over 800M in memory — way too much for my little Eco Dyno which was only 512M. If I wanted to use &lt;code&gt;sbt run&lt;/code&gt;, I would have needed a Performance-M dyno. That didn’t seem right.&lt;/p&gt;

&lt;p&gt;As I read &lt;a href="https://www.playframework.com/documentation/3.0.x/Deploying" rel="noopener noreferrer"&gt;Play’s documentation on deploying an application&lt;/a&gt;, I realized that I should run my app through the compiled binary that Play creates via another command, &lt;code&gt;sbt stage&lt;/code&gt;. This version of my app is precompiled and much more memory efficient — down to around 180MB. Assuming &lt;code&gt;sbt stage&lt;/code&gt; would run first, I just needed to modify my startup command to run the binary.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why I went with Heroku
&lt;/h4&gt;

&lt;p&gt;I chose Heroku for running my Scala app because it removes a lot of the setup friction. I don’t need to manually configure a server or install dependencies. Heroku knows that this is a Scala app (by detecting the presence of &lt;a href="https://github.com/alvinslee/scala-api-heroku/blob/main/project/build.properties" rel="noopener noreferrer"&gt;&lt;code&gt;project/build.properties&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/alvinslee/scala-api-heroku/blob/main/build.sbt" rel="noopener noreferrer"&gt;&lt;code&gt;build.sbt&lt;/code&gt;&lt;/a&gt; files) and applies its &lt;a href="https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-scala" rel="noopener noreferrer"&gt;buildpack for Scala&lt;/a&gt;. This means automatic handling of things like dependency resolution and compilation by running &lt;code&gt;sbt stage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For someone just experimenting with Scala and Play, this kind of zero-config deployment is ideal. I can focus on understanding the framework and the codebase without getting sidetracked by infrastructure. Once I sorted out a few gotchas from early on — like the &lt;code&gt;AllowedHostsFilter&lt;/code&gt;, Play secret, and startup command — deployment was quick and repeatable.&lt;/p&gt;

&lt;p&gt;Alright, let’s go!&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying the app to Heroku
&lt;/h3&gt;

&lt;p&gt;For deployment, I have the &lt;a href="https://devcenter.heroku.com/articles/heroku-cli" rel="noopener noreferrer"&gt;Heroku CLI&lt;/a&gt; installed, and I authenticate with &lt;code&gt;heroku login&lt;/code&gt;. My first step is to create a new Heroku app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;heroku apps:create scala-blog-rest-api


Creating ⬢ scala-blog-rest-api... &lt;span class="k"&gt;done
&lt;/span&gt;https://scala-blog-rest-api-5d26d52bd1e4.herokuapp.com/ | https://git.heroku.com/scala-blog-rest-api.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Create the Procfile
&lt;/h4&gt;

&lt;p&gt;Next, I need to create the &lt;code&gt;Procfile&lt;/code&gt; that tells Heroku how to spin up my app. In my project root, I create the file, which contains this one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;web:&lt;/span&gt; &lt;span class="err"&gt;target/universal/stage/bin/play-scala-rest-api-example&lt;/span&gt; &lt;span class="py"&gt;-Dhttp.port&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${PORT}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the command for starting up this Heroku web process is &lt;em&gt;not&lt;/em&gt; &lt;code&gt;sbt ru&lt;/code&gt;n. Instead, we execute the binary found in &lt;code&gt;target/universal/stage/bin/play-scala-rest-api-example&lt;/code&gt;. That’s the binary compiled after running &lt;code&gt;sbt stage&lt;/code&gt;. Where does the &lt;code&gt;play-scala-rest-api-example&lt;/code&gt; file name come from? This is set in &lt;code&gt;build.sbt&lt;/code&gt;. Make sure that name is consistent between &lt;code&gt;build.sbt&lt;/code&gt; and your &lt;code&gt;Procfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, we set the port dynamically at runtime to the value of the environment variable &lt;code&gt;PORT&lt;/code&gt;. Heroku sets the &lt;code&gt;PORT&lt;/code&gt; environment variable when it starts up our app, so we just need to let our app know what that port is.&lt;/p&gt;

&lt;h4&gt;
  
  
  Specify the JDK version
&lt;/h4&gt;

&lt;p&gt;Next, I create a one-line file called &lt;code&gt;system.properties&lt;/code&gt;, &lt;a href="https://devcenter.heroku.com/articles/customizing-the-jdk#specify-a-jdk-version" rel="noopener noreferrer"&gt;specifying the JDK version&lt;/a&gt; I want to use for my app. Since my local machine uses openjdk 17, my &lt;code&gt;system.properties&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;java.runtime.version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;17&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Set Heroku app config variables
&lt;/h4&gt;

&lt;p&gt;To make sure everything is in order, we need to set a few config variables for our app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PLAY_SECRET&lt;/code&gt;: A string of our choosing, &lt;a href="https://www.playframework.com/documentation/3.0.x/ApplicationSecret#Requirements-for-an-application-secret" rel="noopener noreferrer"&gt;required to be at least 256 bits&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PLAY_ALLOWED_HOSTS&lt;/code&gt;: Our Heroku app URL, which is then included in the &lt;code&gt;AllowedHostsFilter&lt;/code&gt; used in &lt;code&gt;conf/application.conf&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We set our config variables like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;heroku config:set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;PLAY_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ga87Dd*A7$^SFsrpywMWiyyskeEb9&amp;amp;D$hG!ctWxrp^47HCYI'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;PLAY_ALLOWED_HOSTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'scala-blog-rest-api-5d26d52bd1e4.herokuapp.com'&lt;/span&gt;

Setting PLAY_SECRET, PLAY_ALLOWED_HOSTS and restarting ⬢ scala-blog-rest-api... &lt;span class="k"&gt;done&lt;/span&gt;, v2
PLAY_ALLOWED_HOSTS: scala-blog-rest-api-5d26d52bd1e4.herokuapp.com
PLAY_SECRET:        ga87Dd&lt;span class="k"&gt;*&lt;/span&gt;A7&lt;span class="nv"&gt;$^&lt;/span&gt;SFsrpywMWiyyskeEb9&amp;amp;D&lt;span class="nv"&gt;$hG&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;ctWxrp^47HCYI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Push code to Heroku
&lt;/h4&gt;

&lt;p&gt;With our &lt;code&gt;Procfile&lt;/code&gt; created and our config variables set, we’re ready to push our code to Heroku.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;git push heroku main


remote: Resolving deltas: 100% &lt;span class="o"&gt;(&lt;/span&gt;14/14&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Updated 95 paths from 4dc4853
remote: Compressing &lt;span class="nb"&gt;source &lt;/span&gt;files... &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Building &lt;span class="nb"&gt;source&lt;/span&gt;:
remote: 
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Building on the Heroku-24 stack
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Determining which buildpack to use &lt;span class="k"&gt;for &lt;/span&gt;this app
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Play 2.x - Scala app detected
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Installing Azul Zulu OpenJDK 17.0.14
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Priming Ivy cache... &lt;span class="k"&gt;done
&lt;/span&gt;remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Running: sbt compile stage
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Collecting dependency information
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dropping ivy cache from the slug
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dropping sbt boot &lt;span class="nb"&gt;dir &lt;/span&gt;from the slug
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dropping sbt cache &lt;span class="nb"&gt;dir &lt;/span&gt;from the slug
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dropping compilation artifacts from the slug
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Discovering process types
remote:        Procfile declares types -&amp;gt; web
remote: 
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Compressing...
remote:        Done: 128.4M
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Launching...
remote:        Released v5
remote:        https://scala-blog-rest-api-5d26d52bd1e4.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
To https://git.heroku.com/scala-blog-rest-api.git
 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;new branch]      main -&amp;gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I love how I only need to create a &lt;code&gt;Procfile&lt;/code&gt; and set some config variables, and then Heroku takes care of the rest of it for me.&lt;/p&gt;

&lt;h4&gt;
  
  
  Time to test
&lt;/h4&gt;

&lt;p&gt;All that’s left to do is test my Scala Play app with a few curl requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://scala-blog-rest-api-5d26d52bd1e4.herokuapp.com/v1/posts &lt;span class="se"&gt;\&lt;/span&gt;
       | jq

&lt;span class="o"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"1"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/1"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 1"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 1"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"2"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/2"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 2"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 2"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"3"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/3"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 3"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 3"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"4"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/4"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 4"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 4"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"5"&lt;/span&gt;,
    &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/5"&lt;/span&gt;,
    &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 5"&lt;/span&gt;,
    &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 5"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://scala-blog-rest-api-5d26d52bd1e4.herokuapp.com/v1/posts/4 &lt;span class="se"&gt;\&lt;/span&gt;
       | jq

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"4"&lt;/span&gt;,
  &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/4"&lt;/span&gt;,
  &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"title 4"&lt;/span&gt;,
  &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"blog post 4"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-type:application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"My blog title","body":"this is my blog post"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    https://scala-blog-rest-api-5d26d52bd1e4.herokuapp.com/v1/posts &lt;span class="se"&gt;\&lt;/span&gt;
    | jq

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"999"&lt;/span&gt;,
  &lt;span class="s2"&gt;"link"&lt;/span&gt;: &lt;span class="s2"&gt;"/v1/posts/999"&lt;/span&gt;,
  &lt;span class="s2"&gt;"title"&lt;/span&gt;: &lt;span class="s2"&gt;"My blog title"&lt;/span&gt;,
  &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"this is my blog post"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API server works. Deployment was smooth and simple. And I learned a lot about Scala and Play along the way. All in all … it’s been a good day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;Jumping into Scala after decades away from Java — and being more used to building web apps with JavaScript — wasn’t as jarring as I expected. Scala’s syntax felt concise and modern, and working with case classes and async controllers reminded me a bit of patterns I already use in Node.js. There’s still a learning curve with the tooling and how things are structured in Play — but overall, it felt approachable, not overwhelming.&lt;/p&gt;

&lt;p&gt;Deployment to the cloud had a few gotchas, especially around how Play handles allowed hosts, secrets, and memory usage in production. But once I understood how those pieces worked, getting the app live on Heroku was straightforward. With just a few config changes and a proper startup command, the process was clean and repeatable.&lt;/p&gt;

&lt;p&gt;For a first-time Scala build and deployment, I couldn’t have asked for a smoother experience. Are you ready to give it a try?&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>java</category>
      <category>scala</category>
      <category>heroku</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Have You Heard About Cloud Native Buildpacks?</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Tue, 11 Mar 2025 15:11:02 +0000</pubDate>
      <link>https://forem.com/alvinslee/have-you-heard-about-cloud-native-buildpacks-1io6</link>
      <guid>https://forem.com/alvinslee/have-you-heard-about-cloud-native-buildpacks-1io6</guid>
      <description>&lt;p&gt;Do you ever get tired of fiddling with a Dockerfile? Dockerfiles and Docker images are a great way to package your app for reusable, containerized deployments. However, writing and maintaining a Dockerfile is not always intuitive, and it takes up time that could otherwise be used for adding features to your app. Enter Cloud Native Buildpacks. Buildpacks exist to pull together everything your app needs to run and put it into an Open Container Initiative (OCI) image — no Dockerfile required.&lt;/p&gt;

&lt;p&gt;For all the developers out there who need a container build process that’s easy to use and will save them time and headaches, Cloud Native Buildpacks might be the solution they’re looking for. Interested? I’ll tell you more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Cloud Native Buildpacks?
&lt;/h2&gt;

&lt;p&gt;Broadly speaking, a &lt;strong&gt;buildpack&lt;/strong&gt; takes application code and makes it runnable through a build process. So then, &lt;a href="https://buildpacks.io/" rel="noopener noreferrer"&gt;Cloud Native Buildpacks&lt;/a&gt; take your application source code and turn it into runnable, reproducible OCI images, implementing your requirements for image security, performance optimization, and container build order. It’s like having the exact Dockerfile you need — only you don’t need to write one.&lt;/p&gt;

&lt;p&gt;While most developers can write a Dockerfile, few are experts in either Docker or infrastructure. Too many apps have Dockerfiles that are cobbled together from code snippets found across the web — often a mash-up of Copilot, Stack Overflow, and ChatGPT. Dockerfile errors can lead to insecure and poorly performing applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Native Buildpacks&lt;/strong&gt; take on this burden, automatically applying best practices for each language or framework. A &lt;strong&gt;builder&lt;/strong&gt; can then utilize any number of &lt;strong&gt;buildpacks&lt;/strong&gt;, automatically detecting which buildpacks are needed and applying them to build an application. Here are the buildpacks that Heroku’s builder currently supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pack builder inspect heroku/builder:24
Inspecting builder: heroku/builder:24

REMOTE:

Description: Ubuntu 24.04 AMD64+ARM64 base image with buildpacks &lt;span class="k"&gt;for&lt;/span&gt; .NET, Go, Java, Node.js, PHP, Python, Ruby &amp;amp; Scala.

...

Buildpacks:
  ID                           NAME                         VERSION
  heroku/deb-packages          Heroku .deb Packages         0.0.3  
  heroku/dotnet                Heroku .NET                  0.1.10 
  heroku/go                    Heroku Go                    0.5.2  
  heroku/gradle                Heroku Gradle                6.0.4  
  heroku/java                  Heroku Java                  6.0.4  
  heroku/jvm                   Heroku OpenJDK               6.0.4  
  heroku/maven                 Heroku Maven                 6.0.4  
  heroku/nodejs                Heroku Node.js               3.4.5  
  heroku/nodejs-corepack       Heroku Node.js Corepack      3.4.5  
  heroku/nodejs-engine         Heroku Node.js Engine        3.4.5  
  heroku/nodejs-npm-engine     Heroku Node.js npm Engine    3.4.5  
  heroku/nodejs-npm-install    Heroku Node.js npm Install   3.4.5  
  heroku/nodejs-pnpm-engine    Heroku Node.js pnpm Engine   3.4.5  
  heroku/nodejs-pnpm-install   Heroku Node.js pnpm &lt;span class="nb"&gt;install  &lt;/span&gt;3.4.5  
  heroku/nodejs-yarn           Heroku Node.js Yarn          3.4.5  
  heroku/php                   Heroku PHP                   0.2.0  
  heroku/procfile              Heroku Procfile              4.0.0  
  heroku/python                Heroku Python                0.23.0 
  heroku/ruby                  Heroku Ruby                  5.0.1  
  heroku/sbt                   Heroku sbt                   6.0.4  
  heroku/scala                 Heroku Scala                 6.0.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other builders, like the ones from &lt;a href="https://paketo.io/docs/howto/builders/" rel="noopener noreferrer"&gt;Paketo&lt;/a&gt; or &lt;a href="https://cloud.google.com/docs/buildpacks/builders" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt;, also bring an array of buildpacks. All in all, the Cloud Native Buildpacks ecosystem is growing and maturing, which is exciting for developers!&lt;/p&gt;

&lt;p&gt;For those of you familiar with Heroku, you’ve already been enjoying the &lt;strong&gt;buildpack&lt;/strong&gt; experience. With &lt;code&gt;git push heroku main&lt;/code&gt;, you’ve been able to deploy directly to Heroku, with no Dockerfile required. Cloud Native Buildpacks build on the Heroku buildpack experience, taking what was once a vendor-specific implementation and turning it into a CNCF standard that’s usable on any cloud platform.&lt;/p&gt;

&lt;p&gt;In short, &lt;strong&gt;Cloud Native Buildpacks&lt;/strong&gt; allow developers to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Deploy applications more easily than ever&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;… in a standard-based fashion without lock-in&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;… all while applying container best practices&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;… and without making developers tinker with Dockerfiles.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;p&gt;Sounds great, right? With all these benefits, let’s look at some specific cases where you could benefit from using Cloud Native Buildpacks.&lt;/p&gt;

&lt;p&gt;Any place where you would ordinarily need a Dockerfile is an opportunity to use a buildpack. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Node.js web application&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Python microservice&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A heterogeneous application that uses multiple languages or frameworks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Building applications for deployment on cloud platforms such as AWS, Azure, and Heroku&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to note is this: &lt;strong&gt;While buildpacks are &lt;em&gt;declarative&lt;/em&gt;, Dockerfiles are &lt;em&gt;procedural&lt;/em&gt;.&lt;/strong&gt; With a buildpack, you simply declare that you want a given application built with a given builder or buildpack. In contrast, a Dockerfile requires you to define the commands and the order in which those commands are run to build your application. As such, buildpacks don’t currently offer the level of configurability that’s available within a Dockerfile, so it might not meet the needs of some more advanced use cases.&lt;/p&gt;

&lt;p&gt;That said, there is no vendor lock-in with Cloud Native Buildpacks. They simply build an OCI image. Need more customization and options than are available in the buildpack? Simply replace the builder in your build pipeline with your Dockerfile and a standard OCI image build, and you are good to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Walkthrough
&lt;/h2&gt;

&lt;p&gt;Let’s do a quick walkthrough of how to use Cloud Native Buildpacks.&lt;/p&gt;

&lt;p&gt;To get started with buildpacks as an app developer, your first step should be to install the &lt;a href="https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/" rel="noopener noreferrer"&gt;Pack CLI tool&lt;/a&gt;. This tool allows you to build an application with buildpacks. Follow the installation instructions for your operating system.&lt;/p&gt;

&lt;p&gt;Additionally, if you don’t have it already, you’ll need a &lt;a href="https://hub.docker.com/search?type=edition&amp;amp;offering=community" rel="noopener noreferrer"&gt;Docker daemon&lt;/a&gt; for the builder to build your app, and for you to run your image. With these two tools installed, you’re ready to begin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build a sample app
&lt;/h3&gt;

&lt;p&gt;With access to the &lt;code&gt;pack&lt;/code&gt; tool, you’re ready to try it out by building a sample application. I’ll be running this inside a Next.js application. Need a sample application to test out the buildpack on? &lt;a href="https://github.com/vercel/next.js/tree/canary/examples" rel="noopener noreferrer"&gt;Here is a full directory of Next.js sample applications&lt;/a&gt;. You can also try out any application you have on hand.&lt;/p&gt;

&lt;p&gt;Once you have your application ready, start by seeing what builder the pack tool suggests. In your shell, navigate to your app directory and run this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pack builder suggest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my Ubuntu installation, for my Next.js application, the &lt;code&gt;pack&lt;/code&gt; tool suggests the following builders:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvcyc5eg0flivc201omc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvcyc5eg0flivc201omc.png" alt="Image description" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s try the suggested Heroku buildpack (&lt;code&gt;heroku/builder:24&lt;/code&gt;). To use this one, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pack build my-app &lt;span class="nt"&gt;--builder&lt;/span&gt; heroku/builder:24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build time will vary depending on the size of your application; for me, building the app took 30 seconds. With that, my image was ready to go. We can run the image with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4j9hc1glfa47geqlqdz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4j9hc1glfa47geqlqdz.png" alt="Image description" width="795" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that’s it! We’ve successfully built an OCI image of our Next.js application — &lt;em&gt;without&lt;/em&gt; using a Dockerfile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional configurations
&lt;/h3&gt;

&lt;p&gt;What if you need to configure something inside the buildpack? For this, you would reference the buildpack(s) that were selected by your builder. For example, for my Next.js app, I can see in the logs that the builder selected two buildpacks: &lt;a href="https://github.com/heroku/buildpacks-nodejs/tree/main/buildpacks/nodejs-engine" rel="noopener noreferrer"&gt;nodejs-engine&lt;/a&gt; and &lt;a href="https://github.com/heroku/buildpacks-nodejs/blob/main/buildpacks/nodejs-yarn/README.md" rel="noopener noreferrer"&gt;nodejs-yarn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp4aro5uoy4qck7gj7e35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp4aro5uoy4qck7gj7e35.png" alt="Image description" width="326" height="76"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s say that I want to specify the yarn version used by the buildpack. First, I would go to the &lt;a href="https://github.com/heroku/buildpacks-nodejs/blob/main/buildpacks/nodejs-yarn/README.md#packagemanager" rel="noopener noreferrer"&gt;nodejs-yarn buildpack Readme&lt;/a&gt;, where I see that I can specify the yarn version in my &lt;code&gt;package.json&lt;/code&gt; file with a &lt;code&gt;packageManager&lt;/code&gt; key. I would modify my file to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"packageManager"&lt;/span&gt;: &lt;span class="s2"&gt;"yarn@1.22.22"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, all I would need to do is run &lt;code&gt;pack build my-app --builder heroku/builder:24&lt;/code&gt; again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Cloud Native Buildpacks are an exciting new way to build container images for our applications. By removing the need for a Dockerfile, they make it faster than ever to get our application packaged and deployed. Plus, as they build standard container images, there is no vendor lock-in.&lt;/p&gt;

&lt;p&gt;Cloud Native Buildpacks are in preview on many platforms, which means that the feature set is light but fast growing. Heroku, which has &lt;a href="https://blog.heroku.com/heroku-cloud-native-buildpacks" rel="noopener noreferrer"&gt;open sourced their Cloud Native Buildpacks&lt;/a&gt;, is bringing them to their &lt;a href="https://blog.heroku.com/next-generation-heroku-platform" rel="noopener noreferrer"&gt;next generation platform&lt;/a&gt;, too. I’m looking forward to seeing how Cloud Native Buildpacks enable secure, speedy application deployment across the cloud platform community.&lt;/p&gt;

</description>
      <category>containers</category>
      <category>cloudnative</category>
      <category>docker</category>
      <category>heroku</category>
    </item>
    <item>
      <title>FastHTML and Heroku</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Mon, 13 Jan 2025 18:38:28 +0000</pubDate>
      <link>https://forem.com/alvinslee/fasthtml-and-heroku-139e</link>
      <guid>https://forem.com/alvinslee/fasthtml-and-heroku-139e</guid>
      <description>&lt;p&gt;When creating a new app or service, what begins as learning just one new tool can quickly turn into needing a whole set of tools and frameworks. For Python devs, jumping into HTML, CSS, and JavaScript to build a usable app can be daunting. For web devs, many Python-first backend tools work in JavaScript but are often outdated. You’re left with a choice: Stick with JavaScript or switch to Python for access to the latest features.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fastht.ml/" rel="noopener noreferrer"&gt;FastHTML&lt;/a&gt; bridges the gap between these two groups. For Python devs, it makes creating a web app straightforward—no JavaScript required! For web devs, it makes creating a Python app quick and easy, with the option to extend using JavaScript—you’re not locked in.&lt;/p&gt;

&lt;p&gt;As a web developer, I’m always looking for ways to make Python dev more accessible. So, let’s see how quickly we can build and deploy a FastHTML app. I’ll follow the &lt;a href="https://docs.fastht.ml/tutorials/by_example.html#full-example-2---image-generation-app" rel="noopener noreferrer"&gt;image generation tutorial&lt;/a&gt; and then deploy it to &lt;a href="https://www.heroku.com/" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt;. Let’s go!&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Intro to FastHTML&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Never heard of FastHTML before? Here’s &lt;a href="https://github.com/AnswerDotAI/fasthtml" rel="noopener noreferrer"&gt;how FastHTML describes itself&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;FastHTML is a new next-generation web framework for fast, scalable web applications with minimal, compact code. It’s designed to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Powerful and expressive enough to build the most advanced, interactive web apps you can imagine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fast and lightweight, so you can write less code and get more done.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy to learn and use, with a simple, intuitive syntax that makes it easy to build complex apps quickly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FastHTML promises to enable you to generate usable, lightweight apps quickly. Too many web apps are bloated and heavy, requiring a lot of processing and bandwidth for simple tasks. Most web apps just need something simple, beautiful, and easy to use. FastHTML aims to make that task easy.&lt;/p&gt;

&lt;p&gt;You may have heard of &lt;a href="https://github.com/fastapi" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt;, designed to make creating APIs with Python a breeze. FastHTML is inspired by FastAPI’s philosophy, seeking to do the same for frontend applications. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Opinionated about simplicity and ease of use&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Part of the &lt;a href="https://about.fastht.ml/vision" rel="noopener noreferrer"&gt;FastHTML vision&lt;/a&gt; is to “make it the easiest way to create quick prototypes, and also the easiest way to create scalable, powerful, rich applications.” As a developer tool, FastHTML seems to be opinionated about the right things—simplicity and ease of use without limiting you in the future.&lt;/p&gt;

&lt;p&gt;FastHTML gets you up and running quickly while also making it easy for your users. It does this by selecting key core technologies such as &lt;a href="https://asgi.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;ASGI&lt;/a&gt; and &lt;a href="https://htmx.org/" rel="noopener noreferrer"&gt;HTMX&lt;/a&gt;. The &lt;a href="https://about.fastht.ml/foundation" rel="noopener noreferrer"&gt;foundations&lt;/a&gt; page from FastHTML introduces these technologies and gives the basics (though you don’t need to know about these to get started).&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Get Up and Running Quickly&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The tutorials from FastHTML offer several examples of different apps, each with its own use case. I was curious about the &lt;a href="https://docs.fastht.ml/tutorials/by_example.html#full-example-2---image-generation-app" rel="noopener noreferrer"&gt;Image Generation App tutorial&lt;/a&gt; and wanted to see how quickly I could get a text-to-image model into a real, working app. The verdict? It was fast. Really fast.&lt;/p&gt;

&lt;p&gt;In less than 60 lines of code, I created a fully functioning web app where a user can type in a prompt and receive an image from the free &lt;a href="https://pollinations.ai/" rel="noopener noreferrer"&gt;Pollinations&lt;/a&gt; text-to-image model.&lt;/p&gt;

&lt;p&gt;Here’s a short demo of the tutorial app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feorcrfjvkd08if2pyhzh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feorcrfjvkd08if2pyhzh.gif" alt="image generation app demo" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this tutorial app, I got a brief glimpse of the power of FastHTML. I learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Submit data through a form&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Interact with external APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Display some loading text while waiting&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What’s impressive is that it only took one tiny Python file to complete this, and the final app is lightweight and looks good. Here’s the file I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastcore.parallel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threaded&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fasthtml.common&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replicate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;picolink&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;

&lt;span class="c1"&gt;# Store our generations
&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;folder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gens/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Main page
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;inp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new-prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Enter a prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;hx_post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gen-list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hx_swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;afterbegin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gen_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gen-list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Image Generation Demo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;H1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Magic Image Generation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gen_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;container&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# A pending preview keeps polling this route until we return the image preview
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generation_preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gens/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Img&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/gens/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gen-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generating...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gen-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                   &lt;span class="n"&gt;hx_post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/generations/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;hx_trigger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;every 1s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hx_swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;outerHTML&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/generations/{id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generation_preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# For images, CSS, etc.
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{fname:path}.{ext:static}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;FileResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Generation route
&lt;/span&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;generate_and_save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;clear_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new-prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Enter a prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hx_swap_oob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generation_preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;clear_input&lt;/span&gt;

&lt;span class="c1"&gt;# URL (for image generation)  
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; 
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://image.pollinations.ai/prompt/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%20&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?model=flux&amp;amp;width=1024&amp;amp;height=1024&amp;amp;seed=42&amp;amp;nologo=true&amp;amp;enhance=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@threaded&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_and_save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;full_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app:app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking for more functionality? The tutorial continues, adding some CSS styling, user sessions, and even payment tracking with Stripe. While I didn’t go through it all the way, the potential is clear: lots of functionality and usability without a lot of boilerplate or using both Python and JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Deploy Quickly to Heroku&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Okay, so now that I have a pure Python app running locally, what do I need to do to deploy it? Heroku makes this easy. I added a single file called &lt;code&gt;Procfile&lt;/code&gt; with just one line in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;web: python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple text file tells Heroku how to run the app. With the Procfile in place, I can use the &lt;a href="https://devcenter.heroku.com/articles/heroku-cli" rel="noopener noreferrer"&gt;Heroku CLI&lt;/a&gt; to create and deploy my app. And it’s fast…from zero to done in less than 45 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8nldejxwe0wi13u3umc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8nldejxwe0wi13u3umc.gif" alt="deploy to Heroku" width="500" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With two commands, I created my project, built it, and deployed it to Heroku. And let’s just do a quick check. Did it actually work?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fst3yfncynx0mugbodi4i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fst3yfncynx0mugbodi4i.png" alt="final result" width="800" height="879"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And it’s up for the world to see!&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When I find a new tool that makes it easier and quicker to build an app, my mind starts spinning with the possibilities. If it’s that easy, then maybe next time I need to spin up something, I can do it this way and integrate it with this tool, and that other thing… So much of programming is assembling the right tools for the job.&lt;/p&gt;

&lt;p&gt;FastHTML has opened the door to a whole set of Python-based applications for me, and Heroku makes it easy to get those apps off my local machine and into the world. That said, several of the foundations of FastHTML are new to me, and I look forward to understanding them more deeply as I use it more.&lt;/p&gt;

&lt;p&gt;I hope you have fun with FastHTML and Heroku! Happy coding!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>developer</category>
      <category>web</category>
    </item>
    <item>
      <title>Enhancing GenAI Applications With KubeMQ: Efficiently Scaling Retrieval-Augmented Generation (RAG)</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Thu, 12 Dec 2024 15:41:03 +0000</pubDate>
      <link>https://forem.com/alvinslee/enhancing-genai-applications-with-kubemq-efficiently-scaling-retrieval-augmented-generation-rag-51lp</link>
      <guid>https://forem.com/alvinslee/enhancing-genai-applications-with-kubemq-efficiently-scaling-retrieval-augmented-generation-rag-51lp</guid>
      <description>&lt;p&gt;As the adoption of &lt;a href="https://dzone.com/articles/introduction-generative-ai-empowering-enterprises" rel="noopener noreferrer"&gt;Generative AI (GenAI)&lt;/a&gt; surges across industries, organizations are increasingly leveraging &lt;a href="https://dzone.com/articles/optimizing-generative-ai-with-retrieval-augmented" rel="noopener noreferrer"&gt;Retrieval-Augmented Generation (RAG)&lt;/a&gt; techniques to bolster their AI models with real-time, context-rich data. Managing the complex flow of information in such applications poses significant challenges, particularly when dealing with continuously generated data at scale. &lt;a href="https://kubemq.io/" rel="noopener noreferrer"&gt;KubeMQ&lt;/a&gt;, a robust message broker, emerges as a solution to streamline the routing of multiple RAG processes, ensuring efficient data handling in GenAI applications.  &lt;/p&gt;

&lt;p&gt;To further enhance the efficiency and scalability of RAG workflows, integrating a high-performance database like &lt;a href="https://www.falkordb.com/" rel="noopener noreferrer"&gt;FalkorDB&lt;/a&gt; is essential. FalkorDB provides a reliable and scalable storage solution for the dynamic knowledge bases that RAG systems depend on, ensuring rapid data retrieval and seamless integration with messaging systems like KubeMQ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding RAG in GenAI Workflows
&lt;/h2&gt;

&lt;p&gt;RAG is a paradigm that enhances generative AI models by integrating a retrieval mechanism, allowing models to access external knowledge bases during inference. This approach significantly improves the accuracy, relevance, and timeliness of generated responses by grounding them in the most recent and pertinent information available.&lt;/p&gt;

&lt;p&gt;In typical GenAI workflows employing RAG, the process involves multiple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query processing&lt;/strong&gt;: Interpreting the user's input to understand intent and context&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieval&lt;/strong&gt;: Fetching relevant documents or data from a dynamic knowledge base, such as FalkorDB, which ensures quick and efficient access to the most recent and pertinent information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generation&lt;/strong&gt;: Producing a response using both the input and the retrieved data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Response delivery&lt;/strong&gt;: Providing the final, enriched output back to the user&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Scaling these steps, especially in environments where data is continuously generated and updated, necessitates an efficient and reliable mechanism for data flow between the various components of the RAG pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Critical Role of KubeMQ in RAG Processing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Handling Continuous Data Streams at Scale
&lt;/h3&gt;

&lt;p&gt;In scenarios such as IoT networks, social media platforms, or real-time analytics systems, new data is incessantly produced, and AI models must adapt swiftly to incorporate this information. Traditional request-response architectures can become bottlenecks under high-throughput conditions, leading to latency issues and degraded performance.&lt;/p&gt;

&lt;p&gt;KubeMQ manages high-throughput messaging scenarios by providing a scalable and robust infrastructure for efficient data routing between services. By integrating KubeMQ into the RAG pipeline, each new data point is published to a message queue or stream, ensuring that retrieval components have immediate access to the latest information without overwhelming the system. This &lt;a href="https://dzone.com/refcardz/getting-started-with-real-time-analytics" rel="noopener noreferrer"&gt;real-time data&lt;/a&gt; handling capability is crucial for maintaining the relevance and accuracy of GenAI outputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serving as the Optimal Router
&lt;/h3&gt;

&lt;p&gt;KubeMQ offers a variety of messaging patterns — including queues, streams, publish-subscribe (pub/sub), and Remote Procedure Calls (RPC) — making it a versatile and powerful router within a RAG pipeline. Its low latency and high-performance characteristics ensure prompt message delivery, which is essential for real-time GenAI applications where delays can significantly impact user experience and system efficacy.&lt;/p&gt;

&lt;p&gt;Moreover, KubeMQ's ability to handle complex routing logic allows for sophisticated data distribution strategies. This ensures that different components of the AI system receive precisely the data they need, when they need it, without unnecessary duplication or delays.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating FalkorDB for Enhanced Data Management
&lt;/h3&gt;

&lt;p&gt;While KubeMQ efficiently routes messages between services, &lt;strong&gt;FalkorDB&lt;/strong&gt; complements this by providing a scalable and high-performance graph database solution for storing and retrieving the vast amounts of data required by RAG processes. This integration ensures that as new data flows through KubeMQ, it is seamlessly stored in FalkorDB, making it readily available for retrieval operations without introducing latency or bottlenecks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enhancing Scalability and Reliability
&lt;/h3&gt;

&lt;p&gt;As GenAI applications grow in both user base and data volume, scalability becomes a paramount concern. KubeMQ is scalable, supporting horizontal scaling to accommodate increased load seamlessly. It ensures that as the number of RAG processes increases or as data generation accelerates, the messaging infrastructure remains robust and responsive.&lt;/p&gt;

&lt;p&gt;Additionally, KubeMQ provides message persistence and fault tolerance. In the event of system failures or network disruptions, KubeMQ ensures that messages are not lost and that the system can recover gracefully. This reliability is critical in maintaining the integrity of AI applications that users depend on for timely and accurate information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eliminating the Need for Dedicated Routing Services
&lt;/h3&gt;

&lt;p&gt;Implementing custom routing services for data handling in RAG pipelines can be resource-intensive and complex. It often requires significant development effort to build, maintain, and scale these services, diverting focus from core AI application development.&lt;/p&gt;

&lt;p&gt;By adopting KubeMQ, organizations eliminate the need to create bespoke routing solutions. KubeMQ provides out-of-the-box functionality that addresses the routing needs of RAG processes, including complex routing patterns, message filtering, and priority handling. This not only reduces development and maintenance overhead but also accelerates time-to-market for GenAI solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unified Access via REST and SDK
&lt;/h3&gt;

&lt;p&gt;KubeMQ offers multiple interfaces for interacting with its message broker capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;REST API&lt;/strong&gt;: Enables language-agnostic integration, allowing services written in any programming language to send and receive messages over &lt;a href="https://dzone.com/refcardz/http-hypertext-transfer-0" rel="noopener noreferrer"&gt;HTTP&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SDKs&lt;/strong&gt;: Provides client libraries for various programming languages (such as &lt;a href="https://dzone.com/refcardz/core-python" rel="noopener noreferrer"&gt;Python&lt;/a&gt;, &lt;a href="https://dzone.com/refcardz/core-java" rel="noopener noreferrer"&gt;Java&lt;/a&gt;, &lt;a href="https://dzone.com/articles/tools-to-improve-your-golang-code-quality" rel="noopener noreferrer"&gt;Go&lt;/a&gt;, and &lt;a href="https://dzone.com/refcardz/coredotnet" rel="noopener noreferrer"&gt;.NET&lt;/a&gt;), facilitating more efficient communication patterns and better performance through native integrations&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This flexibility allows developers to choose the most appropriate method for their specific use case, simplifying the architecture and accelerating development cycles. A single touchpoint for data routing streamlines communication between different components of the RAG pipeline, enhancing overall system coherence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing KubeMQ in a RAG Pipeline: A Detailed Example
&lt;/h2&gt;

&lt;p&gt;The code example showcases how to build a movie information retrieval system by integrating KubeMQ into a RAG pipeline. It sets up a server that ingests movie URLs from Rotten Tomatoes to build a knowledge graph using GPT-4. Users can interact with this system through a chat client, sending movie-related queries and receiving AI-generated responses. This use case demonstrates how to handle continuous data ingestion and real-time query processing in a practical application, utilizing KubeMQ for efficient message handling and inter-service communication within the context of movies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data ingestion service&lt;/strong&gt;: Captures and publishes new data to KubeMQ streams as it becomes available&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieval service&lt;/strong&gt;: Subscribe to the KubeMQ stream to receive updates and refresh the knowledge base&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generation service&lt;/strong&gt;: Listens for query requests, interacts with the AI model, and generates responses&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Response service&lt;/strong&gt;: Sends the generated responses back to users through appropriate channels&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Setting Up KubeMQ
&lt;/h3&gt;

&lt;p&gt;Ensure that KubeMQ is operational, which can be achieved by deploying it using &lt;a href="https://dzone.com/refcardz/getting-started-with-docker-1" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 50000:50000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 9090:9090 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;KUBEMQ_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command starts KubeMQ with the necessary ports exposed for REST and gRPC communications.&lt;/p&gt;

&lt;h3&gt;
  
  
  RAG Server Side
&lt;/h3&gt;

&lt;p&gt;This code (&lt;a href="https://github.com/kubemq-io/kubemq-graph-rag" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;) implements a RAG server that processes chat queries and manages knowledge sources using KubeMQ for message handling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# server.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kubemq.common&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kubemq.cq&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;CQClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QueryMessageReceived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QueryResponseMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QueriesSubscription&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kubemq.queues&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;QueuesClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphrag_sdk.models.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAiGenerativeModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphrag_sdk.model_config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KnowledgeGraphModelConfig&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphrag_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KnowledgeGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ontology&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphrag_sdk.source&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RAGServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cq_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CQClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:50000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queues_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueuesClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:50000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiGenerativeModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ontology.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;ontology&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;ontology&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Ontology&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ontology&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KnowledgeGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;movies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;model_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KnowledgeGraphModelConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="n"&gt;ontology&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ontology&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;QueryMessageReceived&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received chat message: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chat response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueryResponseMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;query_received&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;is_executed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cq_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error processing chat message: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cq_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QueryResponseMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;query_received&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;is_executed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;))&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pull_from_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_set&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
           &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queues_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rag-sources-queue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error pulling message from queue: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="k"&gt;continue&lt;/span&gt;
               &lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
               &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                   &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received source: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, adding to knowledge graph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
               &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_sources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_set&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;  &lt;span class="c1"&gt;# Only log if not shutting down
&lt;/span&gt;                   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error processing sources: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subscribe_to_chat_queries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_set&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;  &lt;span class="c1"&gt;# Only log if not shutting down
&lt;/span&gt;               &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

       &lt;span class="n"&gt;cancellation_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

       &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cq_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe_to_queries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueriesSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                   &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rag-chat-query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;on_receive_query_callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;on_error_callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="p"&gt;),&lt;/span&gt;
               &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cancellation_token&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;

           &lt;span class="c1"&gt;# Wait for shutdown signal
&lt;/span&gt;           &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_set&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
               &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


           &lt;span class="c1"&gt;# Cancel subscription when shutdown is requested
&lt;/span&gt;           &lt;span class="n"&gt;cancellation_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

       &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_set&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
               &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error in subscription thread: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

       &lt;span class="n"&gt;chat_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe_to_chat_queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;queue_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pull_from_queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;chat_thread&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue_thread&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

       &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daemon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# Make threads daemon so they exit when main thread exits
&lt;/span&gt;           &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

       &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAG server started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyboardInterrupt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Shutting down gracefully...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cq_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queues_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

       &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Initiating shutdown sequence...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Signal all threads to stop
&lt;/span&gt;
       &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Wait up to 5 seconds for each thread
&lt;/span&gt;           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_alive&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
               &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Warning: Thread &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; did not shutdown cleanly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

       &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shutdown complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;rag_server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RAGServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="n"&gt;rag_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server runs two main threads: one that subscribes to chat queries through a channel called "rag-chat-query" and processes them using a knowledge graph with GPT-4, and another that continuously pulls from a queue called "rag-sources-queue" to add new sources to the knowledge graph. The knowledge graph is initialized with a custom ontology loaded from a JSON file and uses OpenAI's GPT-4 model for processing. The server implements graceful shutdown handling and error management, ensuring that all threads are properly terminated when the server is stopped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending Source Data to Ingest Into RAG Knowledge Graph
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sources_client.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kubemq.queues&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SourceClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:50000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;send_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_queues_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="nc"&gt;QueueMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rag-sources-queue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;send_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
           &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message send error, error:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;send_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SourceClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.rottentomatoes.com/m/side_by_side_2012&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.rottentomatoes.com/m/matrix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.rottentomatoes.com/m/matrix_revolutions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.rottentomatoes.com/m/matrix_reloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.rottentomatoes.com/m/speed_1994&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.rottentomatoes.com/m/john_wick_chapter_4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code implements a simple client that sends movie URLs to the RAG server through KubeMQ's queue system. Specifically, it creates a &lt;code&gt;SourceClient&lt;/code&gt; class that connects to KubeMQ and sends messages to the "rag-sources-queue" channel, which is the same queue that the RAG server monitors. When run as a main program, it sends a list of Rotten Tomatoes movie URLs (including Matrix movies, John Wick, and Speed) to be processed and added to the knowledge graph by the RAG server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send and Receive Questions and Answers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#chat_client.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kubemq.cq&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:50000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_query_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QueryMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rag-chat-query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="n"&gt;timeout_in_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
       &lt;span class="p"&gt;))&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sending first question:  Who is the director of the movie The Matrix?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Who is the director of the movie The Matrix?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sending second question:  How this director connected to Keanu Reeves?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How this director connected to Keanu Reeves?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code implements a chat client that communicates with the RAG server through KubeMQ's query system. The &lt;code&gt;ChatClient&lt;/code&gt; class sends messages to the "rag-chat-query" channel and waits for responses, with a 30-second timeout for each query. When run as a main program, it demonstrates the client's functionality by sending two related questions about The Matrix's director and their connection to Keanu Reeves, printing each response as it receives them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Repository
&lt;/h3&gt;

&lt;p&gt;All code examples can be found in my &lt;a href="https://github.com/alvinslee/kubemq-graph-rag" rel="noopener noreferrer"&gt;fork of the original GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Integrating KubeMQ into RAG pipelines for GenAI applications provides a scalable, reliable, and efficient mechanism for handling continuous data streams and complex inter-process communications. By serving as a unified router with versatile messaging patterns, KubeMQ simplifies the overall architecture, reduces the need for custom routing solutions, and accelerates development cycles.&lt;/p&gt;

&lt;p&gt;Furthermore, incorporating FalkorDB enhances data management by offering a high-performance knowledge base that seamlessly integrates with KubeMQ. This combination ensures optimized data retrieval and storage, supporting the dynamic requirements of RAG processes.&lt;/p&gt;

&lt;p&gt;The ability to handle high-throughput scenarios, combined with features like persistence and fault tolerance, ensures that GenAI applications remain responsive and reliable, even under heavy loads or in the face of system disruptions.&lt;/p&gt;

&lt;p&gt;By leveraging KubeMQ and FalkorDB, organizations can focus on enhancing their AI models and delivering valuable insights and services, confident that their data routing infrastructure is robust and capable of meeting the demands of modern AI workflows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Playwright and Chrome Browser Testing in Heroku</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Tue, 03 Dec 2024 15:59:09 +0000</pubDate>
      <link>https://forem.com/alvinslee/playwright-and-chrome-browser-testing-in-heroku-37m3</link>
      <guid>https://forem.com/alvinslee/playwright-and-chrome-browser-testing-in-heroku-37m3</guid>
      <description>&lt;p&gt;I’ve always loved watching my unit tests run (and pass). They’re fast, and passing tests give me the assurance that my individual pieces behave like they’re supposed to. Conversely, I often struggled to prioritize end-to-end tests for the browser because writing and running them was gruelingly slow.&lt;/p&gt;

&lt;p&gt;Fortunately, the tools for end-to-end in-browser testing have gotten much better and faster over the years. And with a headless browser setup, I can run my browser tests as part of my CI.&lt;/p&gt;

&lt;p&gt;Recently, I came across this &lt;a href="https://blog.heroku.com/testing-react-app-chrome-heroku-ci" rel="noopener noreferrer"&gt;Heroku blog post&lt;/a&gt; talking about automating in-browser testing with headless Chrome within Heroku CI. Heroku has a buildpack that installs headless Chrome, which you can invoke for your tests in the CI pipeline.&lt;/p&gt;

&lt;p&gt;The example setup from the blog post was a React app tested with &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; and &lt;a href="https://jestjs.io/" rel="noopener noreferrer"&gt;Jest&lt;/a&gt;. That’s a great start … but what if I use &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; instead of Puppeteer? Is it possible?&lt;/p&gt;

&lt;p&gt;I decided to investigate. As it turns out — yes, you can do this with Playwright too! So, I captured the steps you would need to get Playwright tests running on the headless Chrome browser used in Heroku CI. In this post, I’ll walk you through the steps to get set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Word on Browser Automation for End-to-End Testing
&lt;/h2&gt;

&lt;p&gt;End-to-end testing captures how users actually interact with your app in a browser, validating complete workflows. Playwright makes this process pretty seamless with testing in Chrome, Firefox, and Safari. Of course, running a full slate of browser tests in CI is pretty heavy, which is why headless mode helps.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-chrome-for-testing" rel="noopener noreferrer"&gt;Chrome for Testing buildpack&lt;/a&gt; from Heroku installs Chrome on a Heroku app, so you can run your Playwright tests in Heroku CI with a really lightweight setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to the Application for Testing
&lt;/h2&gt;

&lt;p&gt;Since I was just trying this out, I forked the &lt;a href="https://github.com/heroku-examples/chrome-for-testing-example" rel="noopener noreferrer"&gt;GitHub repo that was originally referenced&lt;/a&gt; in the Heroku blog post. The application was a simple React app with a link, a text input, and a submit button. There were three tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Verify that the link works and redirects to the right location.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verify that the text input properly displays the user input.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verify that submitting the form updates the text displayed on the page.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pretty simple. Now, I just needed to change the code to use Playwright instead of Puppeteer and Jest. Oh, and I also wanted to use &lt;a href="https://pnpm.io/" rel="noopener noreferrer"&gt;pnpm&lt;/a&gt; instead of npm. Here’s a link to &lt;a href="https://github.com/alvinslee/chrome-for-testing-example" rel="noopener noreferrer"&gt;my forked GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modify the Code to Use Playwright
&lt;/h2&gt;

&lt;p&gt;Let’s walk through the steps I took to modify the code. I started with my forked repo, identical to the &lt;code&gt;heroku-examples&lt;/code&gt; repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use pnpm
&lt;/h3&gt;

&lt;p&gt;I wanted to use pnpm instead of npm. (Personal preference.) So, here’s what I did first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;corepack &lt;span class="nb"&gt;enable &lt;/span&gt;pnpm


~/project&lt;span class="nv"&gt;$ &lt;/span&gt;corepack use pnpm@latest

Installing pnpm@9.12.3 &lt;span class="k"&gt;in &lt;/span&gt;the project…
…
Progress: resolved 1444, reused 1441, downloaded 2, added 1444, &lt;span class="k"&gt;done&lt;/span&gt;
…
Done &lt;span class="k"&gt;in &lt;/span&gt;14.4s

~/project&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;rm &lt;/span&gt;package-lock.json


~/project&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="c"&gt;# just to show everything's good&lt;/span&gt;


Lockfile is up to &lt;span class="nb"&gt;date&lt;/span&gt;, resolution step is skipped
Already up to &lt;span class="nb"&gt;date
&lt;/span&gt;Done &lt;span class="k"&gt;in &lt;/span&gt;1.3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add Playwright to the project
&lt;/h3&gt;

&lt;p&gt;Next, I removed Puppeteer and Jest, and I added Playwright.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm remove &lt;span class="se"&gt;\&lt;/span&gt;
           babel-jest jest jest-puppeteer @testing-library/jest-dom

~/project&lt;span class="nv"&gt;$ $ &lt;/span&gt;pnpm create playwright
Getting started with writing end-to-end tests with Playwright:
Initializing project &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;
✔ Do you want to use TypeScript or JavaScript? · JavaScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? &lt;span class="o"&gt;(&lt;/span&gt;y/N&lt;span class="o"&gt;)&lt;/span&gt; · &lt;span class="nb"&gt;false&lt;/span&gt;
✔ Install Playwright browsers &lt;span class="o"&gt;(&lt;/span&gt;can be &lt;span class="k"&gt;done &lt;/span&gt;manually via &lt;span class="s1"&gt;'pnpm exec playwright install'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;? &lt;span class="o"&gt;(&lt;/span&gt;Y/n&lt;span class="o"&gt;)&lt;/span&gt; · &lt;span class="nb"&gt;false&lt;/span&gt;
✔ Install Playwright operating system dependencies &lt;span class="o"&gt;(&lt;/span&gt;requires &lt;span class="nb"&gt;sudo&lt;/span&gt; / root - can be &lt;span class="k"&gt;done &lt;/span&gt;manually via &lt;span class="s1"&gt;'sudo pnpm exec playwright install-deps'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;? &lt;span class="o"&gt;(&lt;/span&gt;y/N&lt;span class="o"&gt;)&lt;/span&gt; · &lt;span class="nb"&gt;false

&lt;/span&gt;Installing Playwright Test &lt;span class="o"&gt;(&lt;/span&gt;pnpm add &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @playwright/test&lt;span class="o"&gt;)&lt;/span&gt;…
…
Installing Types &lt;span class="o"&gt;(&lt;/span&gt;pnpm add &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @types/node&lt;span class="o"&gt;)&lt;/span&gt;…
…
Done &lt;span class="k"&gt;in &lt;/span&gt;2.7s
Writing playwright.config.js.
Writing tests/example.spec.js.
Writing tests-examples/demo-todo-app.spec.js.
Writing package.json.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also removed the Jest configuration section from &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Playwright to use Chromium only
&lt;/h3&gt;

&lt;p&gt;You can run your Playwright tests in Chrome, Firefox, and Safari. Since I was focused on Chrome, I removed the other browsers from the &lt;code&gt;projects&lt;/code&gt; section of the generated &lt;code&gt;playwright.config.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/* Configure projects for major browsers */&lt;/span&gt;
  &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="c1"&gt;//    {&lt;/span&gt;
&lt;span class="c1"&gt;//      name: 'firefox',&lt;/span&gt;
&lt;span class="c1"&gt;//      use: { ...devices['Desktop Firefox'] },&lt;/span&gt;
&lt;span class="c1"&gt;//    },&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//    {&lt;/span&gt;
&lt;span class="c1"&gt;//      name: 'webkit',&lt;/span&gt;
&lt;span class="c1"&gt;//      use: { ...devices['Desktop Safari'] },&lt;/span&gt;
&lt;span class="c1"&gt;//    },&lt;/span&gt;


  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Exchange the Puppeteer test code for Playwright test code
&lt;/h3&gt;

&lt;p&gt;The original code had a Puppeteer test file at &lt;code&gt;src/tests/puppeteer.test.js&lt;/code&gt;. I moved that file to &lt;code&gt;tests/playwright.spec.js&lt;/code&gt;. Then, I updated the test to use Playwright’s conventions, which mapped over quite cleanly. The new test file looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROOT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="name"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitButtonSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button[type="submit"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;greetingSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h5#greeting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John Doe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Playwright link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should navigate to Playwright documentation page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href="https://playwright.dev/"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;resolves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;| Playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Text input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should display the entered text in the text input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputSelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify the input value&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inputValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputSelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputValue&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Form submission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should display the "Hello, X" message after form submission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedGreeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputSelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submitButtonSelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greetingSelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;greetingText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greetingSelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greetingText&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedGreeting&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Remove &lt;code&gt;start-server-and-test&lt;/code&gt;, using Playwright’s webServer instead
&lt;/h3&gt;

&lt;p&gt;To test my React app, I needed to spin it up (at &lt;code&gt;http://localhost:8080&lt;/code&gt;) in a separate process first, and then I could run my tests. This would be the case whether I used Puppeteer or Playwright. With Puppeteer, the Heroku example used the &lt;code&gt;start-server-and-test&lt;/code&gt; package. However, you can configure Playwright to spin up the app before running tests. This is pretty convenient!&lt;/p&gt;

&lt;p&gt;I removed &lt;code&gt;start-server-and-test&lt;/code&gt; from my project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm remove start-server-and-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;playwright.config.js&lt;/code&gt;, I uncommented the webServer section at the bottom, modifying it to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/* Run your local dev server before starting the tests */&lt;/span&gt;
  &lt;span class="nx"&gt;webServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nl"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pnpm start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://127.0.0.1:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;reuseExistingServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I removed the &lt;code&gt;test:ci&lt;/code&gt; script from the original &lt;code&gt;package.json&lt;/code&gt; file. Instead, my test script looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playwright test --project=chromium --reporter list"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install Playwright browser on my local machine
&lt;/h3&gt;

&lt;p&gt;Playwright installs the latest browser binaries to use for its tests. So, on my local machine, I needed Playwright to install its version of Chromium.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium


Downloading Chromium 130.0.6723.31 &lt;span class="o"&gt;(&lt;/span&gt;playwright build v1140&lt;span class="o"&gt;)&lt;/span&gt;
from https://playwright.azureedge.net/builds/chromium/1140/chromium-linux.zip
164.5 MiB &lt;span class="o"&gt;[====================]&lt;/span&gt; 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Chrome for Testing buildpack on Heroku installs the browser we’ll use for testing. We’ll set up our CI so that Playwright uses that browser instead of spending the time and resources installing its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run tests locally
&lt;/h3&gt;

&lt;p&gt;With that, I was all set. It was time to try out my tests locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;chromium &lt;span class="nt"&gt;--reporter&lt;/span&gt; list

Running 3 tests using 3 workers

  ✓  1 &lt;span class="o"&gt;[&lt;/span&gt;chromium] &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright.spec.js:21:3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Text input &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; should display the entered text &lt;span class="k"&gt;in &lt;/span&gt;the text input &lt;span class="o"&gt;(&lt;/span&gt;911ms&lt;span class="o"&gt;)&lt;/span&gt;
  ✘  2 &lt;span class="o"&gt;[&lt;/span&gt;chromium] &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright.spec.js:14:3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Playwright &lt;span class="nb"&gt;link&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; should navigate to Playwright documentation page &lt;span class="o"&gt;(&lt;/span&gt;5.2s&lt;span class="o"&gt;)&lt;/span&gt;
  ✓  3 &lt;span class="o"&gt;[&lt;/span&gt;chromium] &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright.spec.js:31:3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Form submission &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; should display the &lt;span class="s2"&gt;"Hello, X"&lt;/span&gt; message after form submission &lt;span class="o"&gt;(&lt;/span&gt;959ms&lt;span class="o"&gt;)&lt;/span&gt;

...
      - waiting &lt;span class="k"&gt;for &lt;/span&gt;locator&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a[href="https://playwright.dev/"]'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;


      13 | test.describe&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Playwright link'&lt;/span&gt;, &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      14 |   &lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'should navigate to Playwright documentation page'&lt;/span&gt;, async &lt;span class="o"&gt;({&lt;/span&gt; page &lt;span class="o"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 15 |     await page.click&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a[href="https://playwright.dev/"]'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
         |                ^
      16 |     await expect&lt;span class="o"&gt;(&lt;/span&gt;page.title&lt;span class="o"&gt;())&lt;/span&gt;.resolves.toMatch&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'| Playwright'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      17 |   &lt;span class="o"&gt;})&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      18 | &lt;span class="o"&gt;})&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh! That’s right. I modified my test to expect the link in the app to take me to Playwright’s documentation instead of Puppeteer’s. I needed to update &lt;code&gt;src/App.js&lt;/code&gt; at line 19:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://playwright.dev/"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"noopener"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Playwright Documentation
&lt;span class="nt"&gt;&amp;lt;/Link&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, it was time to run the tests again…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;chromium &lt;span class="nt"&gt;--reporter&lt;/span&gt; list

Running 3 tests using 3 workers

  ✓  1 &lt;span class="o"&gt;[&lt;/span&gt;chromium] &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright.spec.js:21:3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Text input &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; should display the entered text &lt;span class="k"&gt;in &lt;/span&gt;the text input &lt;span class="o"&gt;(&lt;/span&gt;1.1s&lt;span class="o"&gt;)&lt;/span&gt;
  ✓  2 &lt;span class="o"&gt;[&lt;/span&gt;chromium] &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright.spec.js:14:3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Playwright &lt;span class="nb"&gt;link&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; should navigate to Playwright documentation page &lt;span class="o"&gt;(&lt;/span&gt;1.1s&lt;span class="o"&gt;)&lt;/span&gt;
  ✓  3 &lt;span class="o"&gt;[&lt;/span&gt;chromium] &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; playwright.spec.js:31:3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Form submission &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; should display the &lt;span class="s2"&gt;"Hello, X"&lt;/span&gt; message after form submission &lt;span class="o"&gt;(&lt;/span&gt;1.1s&lt;span class="o"&gt;)&lt;/span&gt;

  3 passed &lt;span class="o"&gt;(&lt;/span&gt;5.7s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests passed! Next, it was time to get us onto Heroku CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy to Heroku to Use CI Pipeline
&lt;/h2&gt;

&lt;p&gt;I followed the instructions in the Heroku blog post to get my app set up in a Heroku CI pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Heroku pipeline
&lt;/h3&gt;

&lt;p&gt;In Heroku, I created a new pipeline and connected it to my forked GitHub repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhyaiqyj6ui6zq4n5q8k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhyaiqyj6ui6zq4n5q8k.png" alt="Create a Heroku Pipeline" width="649" height="649"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, I added my app to staging.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5x7uuwzn9npn1pdexij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5x7uuwzn9npn1pdexij.png" alt="Add app to staging" width="697" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, I went to the &lt;strong&gt;Tests&lt;/strong&gt; tab and clicked &lt;strong&gt;Enable Heroku CI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftplnk9jaimwb86y5o6hv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftplnk9jaimwb86y5o6hv.png" alt="Enable Heroku CI" width="660" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, I modified the &lt;code&gt;app.json&lt;/code&gt; file to remove the test script which was set to call &lt;code&gt;npm test:ci&lt;/code&gt;. I had already removed the &lt;code&gt;test:ci&lt;/code&gt; script from my &lt;code&gt;package.json&lt;/code&gt; file. The &lt;code&gt;test&lt;/code&gt; script in &lt;code&gt;package.json&lt;/code&gt; was now the one to use, and Heroku CI would look for that one by default.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;app.json&lt;/code&gt; file, which made sure to use the Chrome for Testing buildpack, looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"buildpacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heroku-community/chrome-for-testing"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heroku/nodejs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initial test run
&lt;/h3&gt;

&lt;p&gt;I pushed my code to GitHub, and this triggered a test run in Heroku CI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F47j0p6xkrvi3ig0jb8nk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F47j0p6xkrvi3ig0jb8nk.png" alt="Failing Test" width="407" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The test run failed, but I wasn’t worried. I knew there would be some Playwright configuration to do.&lt;/p&gt;

&lt;p&gt;Digging around in the test log, I found this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error: browserType.launch: Executable doesn&lt;span class="s1"&gt;'t exist at 
/app/.cache/ms-playwright/chromium-1140/chrome-linux/chrome
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright was looking for the Chrome browser instance. I &lt;em&gt;could&lt;/em&gt; install it with the &lt;code&gt;playwright install chromium&lt;/code&gt; command as part of my CI test setup. But that would defeat the whole purpose of having the Chrome for Testing buildpack. Chrome was already installed; I just needed to point to it properly.&lt;/p&gt;

&lt;p&gt;Looking back in my test setup log for Heroku, I found these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Installed Chrome dependencies &lt;span class="k"&gt;for &lt;/span&gt;heroku-24
Adding executables to PATH
/app/.chrome-for-testing/chrome-linux64/chrome
/app/.chrome-for-testing/chromedriver-linux64/chromedriver
Installed Chrome &lt;span class="k"&gt;for &lt;/span&gt;Testing STABLE version 130.0.6723.91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, the browser I wanted to use was at &lt;code&gt;/app/.chrome-for-testing/chrome-linux64/chrome&lt;/code&gt;. I would just need Playwright to look there for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Helping Playwright find the installed Chrome browser
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you’re not interested in the nitty-gritty details here, you can skip this section and simply copy the full &lt;code&gt;app.json&lt;/code&gt; lower down. This should give you what you need to get up and running with Playwright on Heroku CI.&lt;/p&gt;

&lt;p&gt;In Playwright’s documentation, I found that you can set an environment variable that tells Playwright if you used a &lt;a href="https://playwright.dev/docs/browsers#managing-browser-binaries" rel="noopener noreferrer"&gt;custom location for all of its browser installs&lt;/a&gt;. That env variable is &lt;code&gt;PLAYWRIGHT_BROWSERS_PATH&lt;/code&gt;. I decided to start there.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app.json&lt;/code&gt;, I set an &lt;code&gt;env&lt;/code&gt; variable like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
        &lt;/span&gt;&lt;span class="nl"&gt;"PLAYWRIGHT_BROWSERS_PATH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/app/.chrome-for-testing"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I pushed my code to GitHub to see what would happen with my tests in CI.&lt;/p&gt;

&lt;p&gt;As expected, it failed again. However, the log error showed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error: browserType.launch: Executable doesn&lt;span class="s1"&gt;'t exist at
/app/.chrome-for-testing/chromium-1140/chrome-linux/chrome
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That got me pretty close. I decided that I would do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create the folders needed for where Playwright expects the Chrome browser to be. That would be a command like:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PLAYWRIGHT_BROWSERS_PATH&lt;/span&gt;&lt;span class="s2"&gt;/chromium-1140/chrome-linux"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a symlink in this folder to point to the Chrome binary installed by the Heroku buildpack. That would look something like this:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;$PLAYWRIGHT_BROWSERS_PATH&lt;/span&gt;/chrome-linux64/chrome &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;$PLAYWRIGHT_BROWSERS_PATH&lt;/span&gt;/chromium-1140/chrome-linux/chrome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, I was concerned about whether this would be future-proof. Eventually, Playwright would use a new version of Chromium, and it won’t look in a &lt;code&gt;chromium-1140&lt;/code&gt; folder anymore. How could I figure out where Playwright would look?&lt;/p&gt;

&lt;p&gt;That’s when I discovered you can do a browser installation dry run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium &lt;span class="nt"&gt;--dry-run&lt;/span&gt;


browser: chromium version 130.0.6723.31
  Install location:    /home/alvin/.cache/ms-playwright/chromium-1140
  Download url:        https://playwright.azureedge.net/builds/chromium/1140/chromium-linux.zip
  Download fallback 1: https://playwright-akamai.azureedge.net/builds/chromium/1140/chromium-linux.zip
  Download fallback 2: https://playwright-verizon.azureedge.net/builds/chromium/1140/chromium-linux.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That “Install location” line was crucial. And, if we set &lt;code&gt;PLAYWRIGHT_BROWSERS_PATH&lt;/code&gt;, here is what we would see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ PLAYWRIGHT_BROWSERS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/app/.chrome-for-testing &lt;span class="se"&gt;\&lt;/span&gt;
           pnpm playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium &lt;span class="nt"&gt;--dry-run&lt;/span&gt;


browser: chromium version 130.0.6723.31
  Install location:    /app/.chrome-for-testing/chromium-1140
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s what I want. With a little &lt;code&gt;awk&lt;/code&gt; magic, I did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project&lt;span class="nv"&gt;$ CHROMIUM_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
             &lt;span class="nv"&gt;PLAYWRIGHT_BROWSERS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/app/.chrome-for-testing &lt;span class="se"&gt;\&lt;/span&gt;
               pnpm playwright &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt; chromium &lt;span class="se"&gt;\&lt;/span&gt;
               | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/Install location/ {print $3}'&lt;/span&gt;
           &lt;span class="si"&gt;)&lt;/span&gt;

~/project&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$CHROMIUM_PATH&lt;/span&gt;


/app/.chrome-for-testing/chromium-1140
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all that figured out, I simply needed to add a &lt;code&gt;test-setup&lt;/code&gt; script to &lt;code&gt;app.json&lt;/code&gt;. Because &lt;code&gt;PLAYWRIGHT_BROWSERS_PATH&lt;/code&gt; is already set in &lt;code&gt;env&lt;/code&gt;, my script would be a little simpler. This was my final &lt;code&gt;app.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
        &lt;/span&gt;&lt;span class="nl"&gt;"PLAYWRIGHT_BROWSERS_PATH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/app/.chrome-for-testing"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"buildpacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heroku-community/chrome-for-testing"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heroku/nodejs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"test-setup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHROMIUM_PATH=$(pnpm playwright install --dry-run chromium | awk '/Install location/ {print $3}'); mkdir -p &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CHROMIUM_PATH/chrome-linux&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; ln -s $PLAYWRIGHT_BROWSERS_PATH/chrome-lin
ux64/chrome $CHROMIUM_PATH/chrome-linux/chrome"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ll briefly walk through what &lt;code&gt;test-setup&lt;/code&gt; does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Accounting for &lt;code&gt;PLAYWRIGHT_BROWSERS_PATH&lt;/code&gt;, uses &lt;code&gt;playwright install -- dry-run&lt;/code&gt; with &lt;code&gt;awk&lt;/code&gt; to determine the root folder where Playwright will look for the Chrome browser. Sets this as the value for the &lt;code&gt;CHROMIUM_PATH&lt;/code&gt; variable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creates a new folder (and any necessary parent folders) to &lt;code&gt;CHROMIUM_PATH/chrome-linux&lt;/code&gt;, which is the actual folder where Playwright will look for the &lt;code&gt;chrome&lt;/code&gt; binary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creates a symlink in that folder, for chrome to point to the Heroku buildpack installation of Chrome (&lt;code&gt;/app/.chrome-for-testing/chrome-linux64/chrome&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Run tests again
&lt;/h2&gt;

&lt;p&gt;With my updated &lt;code&gt;app.json&lt;/code&gt; file, Playwright should be able to use the Chrome installation from the buildpack. It was time to run the tests once again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcui2bk18dm5wy2yaf523.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcui2bk18dm5wy2yaf523.png" alt="Passing test" width="475" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Success!&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;test-setup&lt;/code&gt; script ran as expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqpslofkg6ird1dkhcuv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsqpslofkg6ird1dkhcuv.png" alt="Test setup succeeded" width="670" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Playwright was able to access the &lt;code&gt;chrome&lt;/code&gt; binary and run the tests, which passed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5oavipwcqsvtvx6fzup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5oavipwcqsvtvx6fzup.png" alt="Tests succeeded" width="718" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;End-to-end testing for my web applications is becoming less cumbersome, so I’m prioritizing it more and more. In recent days, that has meant using Playwright more too. It’s flexible and fast. And now that I’ve done the work (for me &lt;em&gt;and for you&lt;/em&gt;!) to get it up and running with the Chrome for Testing buildpack in Heroku CI, I can start building up my browser automation test suites once again.&lt;/p&gt;

&lt;p&gt;The code for this walkthrough is available in my &lt;a href="https://github.com/alvinslee/chrome-for-testing-example" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>testing</category>
    </item>
    <item>
      <title>Wow, pnpm, You’re Really Fast</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Wed, 06 Nov 2024 16:25:10 +0000</pubDate>
      <link>https://forem.com/alvinslee/wow-pnpm-youre-really-fast-41o2</link>
      <guid>https://forem.com/alvinslee/wow-pnpm-youre-really-fast-41o2</guid>
      <description>&lt;p&gt;If you’re a &lt;a href="https://dzone.com/refcardz/nodejs" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; developer, then you’re familiar with &lt;a href="https://www.npmjs.com/" rel="noopener noreferrer"&gt;npm&lt;/a&gt; and &lt;a href="https://yarnpkg.com/" rel="noopener noreferrer"&gt;Yarn&lt;/a&gt;. You might even have a strong opinion about using one over the other. For years, developers have been struggling with the bloat — in disk storage and build time — when working with Node.js package managers, &lt;a href="https://dzone.com/articles/a-beginners-guide-to-npm-the-node-package-manager" rel="noopener noreferrer"&gt;especially npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ksgja22p53f6im9qe2h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ksgja22p53f6im9qe2h.gif" alt="NPM Install" width="114" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, along came &lt;a href="https://pnpm.io/" rel="noopener noreferrer"&gt;pnpm&lt;/a&gt;, a package manager that handles package storage differently, saving users space and reducing build time. Here’s &lt;a href="https://medium.com/pnpm/why-should-we-use-pnpm-75ca4bfe7d93" rel="noopener noreferrer"&gt;how pnpm describes the difference&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“When you install a package, we keep it in a global store on your machine, then we create a hard link from it instead of copying. For each version of a module, there is only ever one copy kept on disk. When using npm or yarn for example, if you have 100 packages using lodash, you will have 100 copies of lodash on disk. pnpm allows you to save gigabytes of disk space!”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s no surprise that pnpm is gaining traction, with more and more developers making it their package manager of choice. Along with that growing adoption rate, many developers who run their apps on Heroku (like I do) wanted to see pnpm supported.&lt;/p&gt;

&lt;p&gt;Fortunately, pnpm is available via &lt;a href="https://nodejs.org/api/corepack.html" rel="noopener noreferrer"&gt;Corepack&lt;/a&gt;, which is distributed with Node.js. So, as of May 2024, &lt;a href="https://devcenter.heroku.com/changelog-items/2879" rel="noopener noreferrer"&gt;pnpm is now available in Heroku&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;In this post, we’ll cover what it takes to get started with pnpm on Heroku. And, we’ll also show off some of the storage and build-time benefits you get from using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Primer on pnpm
&lt;/h2&gt;

&lt;p&gt;pnpm was created to solve the longstanding Node.js package manager issue of redundant storage and inefficiencies in &lt;a href="https://dzone.com/articles/nodejs-dependency-management" rel="noopener noreferrer"&gt;dependency handling&lt;/a&gt;. npm and Yarn copy dependencies into each project’s &lt;code&gt;node_modules&lt;/code&gt;. In contrast, pnpm keeps all the packages for all projects in a single global store, then creates hard links to these packages rather than copying them. What does this mean?&lt;/p&gt;

&lt;p&gt;Let’s assume we have a Node.js project that uses &lt;code&gt;lodash&lt;/code&gt;. Naturally, the project will have a &lt;code&gt;node_modules&lt;/code&gt; folder, along with a subfolder called &lt;code&gt;lodash&lt;/code&gt;, filled with files. To be exact, &lt;code&gt;lodash@4.17.21&lt;/code&gt; has 639 files and another subfolder called &lt;code&gt;fp&lt;/code&gt;, with another 415 files.&lt;/p&gt;

&lt;p&gt;That’s over a thousand files for &lt;code&gt;lodash&lt;/code&gt; alone!&lt;/p&gt;

&lt;p&gt;I created six Node.js projects: two with pnpm, two with npm, and two with Yarn. Each of them uses &lt;code&gt;lodash&lt;/code&gt;. Let’s take a look at information for just one of the files in the &lt;code&gt;lodash&lt;/code&gt; dependency folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/six-projects&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; npm-foo/node_modules/lodash/lodash.js
14754214 &lt;span class="nt"&gt;-rw-rw-r--&lt;/span&gt; 544098 npm-foo/node_modules/lodash/lodash.js

~/six-projects&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; npm-bar/node_modules/lodash/lodash.js
14757384 &lt;span class="nt"&gt;-rw-rw-r--&lt;/span&gt; 544098 npm-bar/node_modules/lodash/lodash.js

~/six-projects&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; yarn-foo/node_modules/lodash/lodash.js
14760047 &lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 544098 yarn-foo/node_modules/lodash/lodash.js

~/six-projects&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; yarn-bar/node_modules/lodash/lodash.js
14762739 &lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 544098 yarn-bar/node_modules/lodash/lodash.js

~/six-projects&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pnpm-foo/node_modules/lodash/lodash.js
15922696 &lt;span class="nt"&gt;-rw-rw-r--&lt;/span&gt; 544098 pnpm-foo/node_modules/lodash/lodash.js

~/six-projects&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pnpm-bar/node_modules/lodash/lodash.js
15922696 &lt;span class="nt"&gt;-rw-rw-r--&lt;/span&gt; 544098 pnpm-bar/node_modules/lodash/lodash.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;lodash.js&lt;/code&gt; file is a little over half a megabyte in size. We’re not seeing soft links, so at first glance, it really looks like each project has its own copy of this file. However, that’s not actually the case.&lt;/p&gt;

&lt;p&gt;I used &lt;code&gt;ls&lt;/code&gt; with the &lt;code&gt;-i&lt;/code&gt; flag to display the &lt;a href="https://unix.stackexchange.com/questions/88423/why-do-hard-links-seem-to-take-the-same-space-as-the-originals" rel="noopener noreferrer"&gt;inode&lt;/a&gt; of &lt;code&gt;lodash.js&lt;/code&gt; file. You can see in the &lt;code&gt;pnpm-foo&lt;/code&gt; and &lt;code&gt;pnpm-bar&lt;/code&gt; projects, both files have the same inode (&lt;code&gt;15922696&lt;/code&gt;). They’re pointing to the same file! That’s not the case for npm or Yarn.&lt;/p&gt;

&lt;p&gt;So, if you have a dozen projects that use npm or Yarn, and those projects use &lt;code&gt;lodash&lt;/code&gt;, then you’ll have a dozen different copies of &lt;code&gt;lodash&lt;/code&gt;, along with copies from other dependencies in those projects which themselves use &lt;code&gt;lodash&lt;/code&gt;. In pnpm, every project and dependency that requires this specific version of &lt;code&gt;lodash&lt;/code&gt; points to the same, single, global copy.&lt;/p&gt;

&lt;p&gt;The code for &lt;code&gt;lodash@4.17.21&lt;/code&gt; is just under 5 MB in size. Would you rather have 100 redundant copies of it on your machine, or just one global copy?&lt;/p&gt;

&lt;p&gt;At the end of the day, dependency installation with pnpm is significantly faster, requiring less disk space and fewer resources. For developers working across multiple projects or managing dependencies on cloud platforms, pnpm offers a leaner, faster way to manage packages. This makes pnpm ideal for a streamlined deployment environment like Heroku.&lt;/p&gt;

&lt;p&gt;Are you ready to start using it? Let’s walk through how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started With pnpm
&lt;/h2&gt;

&lt;p&gt;Here’s the version of Node.js we’re working with on our machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;node &lt;span class="nt"&gt;--version&lt;/span&gt;
v20.18.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable and Use pnpm
&lt;/h3&gt;

&lt;p&gt;As we mentioned above, Corepack comes with Node.js, so we simply need to use &lt;code&gt;corepack&lt;/code&gt; to enable and use pnpm. We create a folder for our project. Then, we run these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project-pnpm&lt;span class="nv"&gt;$ &lt;/span&gt;corepack &lt;span class="nb"&gt;enable &lt;/span&gt;pnpm

~/project-pnpm&lt;span class="nv"&gt;$ &lt;/span&gt;corepack use pnpm@latest
Installing pnpm@9.12.2 &lt;span class="k"&gt;in &lt;/span&gt;the project...
Already up to &lt;span class="nb"&gt;date
&lt;/span&gt;Done &lt;span class="k"&gt;in &lt;/span&gt;494ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a &lt;code&gt;package.json&lt;/code&gt; file that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"packageManager"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also generates a &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Next, we add dependencies to our project. For demonstration purposes, we’re copying the list of &lt;code&gt;dependencies&lt;/code&gt; and &lt;code&gt;devDependencies&lt;/code&gt; found in this &lt;a href="https://github.com/pnpm/pnpm.io/blob/main/benchmarks/fixtures/alotta-files/package.json" rel="noopener noreferrer"&gt;benchmarking&lt;/a&gt; &lt;code&gt;package.json&lt;/code&gt; &lt;a href="https://github.com/pnpm/pnpm.io/blob/main/benchmarks/fixtures/alotta-files/package.json" rel="noopener noreferrer"&gt;file on GitHub&lt;/a&gt;. Now, our &lt;code&gt;package.json&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"animate.less"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"autoprefixer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^10.4.17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"babel-core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^6.26.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"babel-eslint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^10.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"webpack-split-by-path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"whatwg-fetch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.6.20"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nan-as"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.6.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageManager"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we install the packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project-pnpm&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Comparing Common Commands
&lt;/h2&gt;

&lt;p&gt;The usage for pnpm is fairly similar to npm or yarn, and so it should be intuitive. Below is a table that compares the different usages for common commands (taken from &lt;a href="https://nodesource.com/blog/nodejs-package-manager-comparative-guide-2024#:~:text=Migrating%20from%20npm" rel="noopener noreferrer"&gt;this post&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furfitf4h7d0jca9fparf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furfitf4h7d0jca9fparf.png" alt="Commands Comparison" width="696" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Heroku Build Speed Comparison
&lt;/h2&gt;

&lt;p&gt;Now that we’ve shown how to get a project up and running with pnpm (it’s pretty simple, right?), we wanted to compare the build times for different package managers when running on Heroku. We set up three projects with identical dependencies — using npm, Yarn, and pnpm.&lt;/p&gt;

&lt;p&gt;First, we log in to the Heroku CLI (&lt;code&gt;heroku login&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Then, we create an app for a project. We’ll show the steps for the npm project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project-npm&lt;span class="nv"&gt;$ &lt;/span&gt;heroku apps:create &lt;span class="nt"&gt;--stack&lt;/span&gt; heroku-24 npm-timing

Creating ⬢ npm-timing... &lt;span class="k"&gt;done&lt;/span&gt;, stack is heroku-24
https://npm-timing-5d4e30a1c656.herokuapp.com/ | https://git.heroku.com/npm-timing.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We found a &lt;a href="https://elements.heroku.com/buildpacks/edmorley/heroku-buildpack-timestamps" rel="noopener noreferrer"&gt;buildpack that adds timestamps&lt;/a&gt; to the build steps in the Heroku log, so that we can calculate the actual build times for our projects. We want to add that buildpack to our project, and have it run before the &lt;a href="https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-nodejs" rel="noopener noreferrer"&gt;standard buildpack for Node.js&lt;/a&gt;. We do that with the following two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project-npm&lt;span class="nv"&gt;$ &lt;/span&gt;heroku buildpacks:add &lt;span class="se"&gt;\&lt;/span&gt;
                 &lt;span class="nt"&gt;--index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
                 https://github.com/edmorley/heroku-buildpack-timestamps.git &lt;span class="se"&gt;\&lt;/span&gt;
                 &lt;span class="nt"&gt;--app&lt;/span&gt; pnpm-timing

~/project-npm&lt;span class="nv"&gt;$ &lt;/span&gt;heroku buildpacks:add &lt;span class="se"&gt;\&lt;/span&gt;
                 &lt;span class="nt"&gt;--index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 heroku/nodejs &lt;span class="se"&gt;\&lt;/span&gt;
                 &lt;span class="nt"&gt;--app&lt;/span&gt; npm-timing

Buildpack added. Next release on npm-timing will use:
1. https://github.com/edmorley/heroku-buildpack-timestamps.git
2. heroku/nodejs
Run git push heroku main to create a new release using these buildpacks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Then, we push up the code for our npm-managed project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project-npm&lt;span class="nv"&gt;$ &lt;/span&gt;git push heroku main

...
remote: Updated 4 paths from 5af8e67
remote: Compressing &lt;span class="nb"&gt;source &lt;/span&gt;files... &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Building &lt;span class="nb"&gt;source&lt;/span&gt;:
remote:
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Building on the Heroku-24 stack
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Using buildpacks:
remote: 1. https://github.com/edmorley/heroku-buildpack-timestamps.git
remote: 2. heroku/nodejs
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Timestamp app detected
remote: &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Node.js app detected
...
remote: 2024-10-22 22:31:29 &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Installing dependencies
remote: 2024-10-22 22:31:29 Installing node modules
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 added 1435 packages, and audited 1436 packages &lt;span class="k"&gt;in &lt;/span&gt;11s
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 184 packages are looking &lt;span class="k"&gt;for &lt;/span&gt;funding
remote: 2024-10-22 22:31:41 run &lt;span class="sb"&gt;`&lt;/span&gt;npm fund&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;details
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 96 vulnerabilities &lt;span class="o"&gt;(&lt;/span&gt;1 low, 38 moderate, 21 high, 36 critical&lt;span class="o"&gt;)&lt;/span&gt;
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 To address issues that &lt;span class="k"&gt;do &lt;/span&gt;not require attention, run:
remote: 2024-10-22 22:31:41 npm audit fix
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 To address all issues possible &lt;span class="o"&gt;(&lt;/span&gt;including breaking changes&lt;span class="o"&gt;)&lt;/span&gt;, run:
remote: 2024-10-22 22:31:41 npm audit fix &lt;span class="nt"&gt;--force&lt;/span&gt;
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 Some issues need review, and may require choosing
remote: 2024-10-22 22:31:41 a different dependency.
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 Run &lt;span class="sb"&gt;`&lt;/span&gt;npm audit&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;details.
remote: 2024-10-22 22:31:41 npm notice
remote: 2024-10-22 22:31:41 npm notice New minor version of npm available! 10.8.2 -&amp;gt; 10.9.0
remote: 2024-10-22 22:31:41 npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.9.0
remote: 2024-10-22 22:31:41 npm notice To update run: npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; npm@10.9.0
remote: 2024-10-22 22:31:41 npm notice
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Build
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Caching build
remote: 2024-10-22 22:31:41 - npm cache
remote: 2024-10-22 22:31:41
remote: 2024-10-22 22:31:41 &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Pruning devDependencies
remote: 2024-10-22 22:31:44
remote: 2024-10-22 22:31:44 up to &lt;span class="nb"&gt;date&lt;/span&gt;, audited 1435 packages &lt;span class="k"&gt;in &lt;/span&gt;4s
remote: 2024-10-22 22:31:44
remote: 2024-10-22 22:31:44 184 packages are looking &lt;span class="k"&gt;for &lt;/span&gt;funding
remote: 2024-10-22 22:31:44 run &lt;span class="sb"&gt;`&lt;/span&gt;npm fund&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;details
remote: 2024-10-22 22:31:45
remote: 2024-10-22 22:31:45 96 vulnerabilities &lt;span class="o"&gt;(&lt;/span&gt;1 low, 38 moderate, 21 high, 36 critical&lt;span class="o"&gt;)&lt;/span&gt;
remote: 2024-10-22 22:31:45
remote: 2024-10-22 22:31:45 To address issues that &lt;span class="k"&gt;do &lt;/span&gt;not require attention, run:
remote: 2024-10-22 22:31:45 npm audit fix
remote: 2024-10-22 22:31:45
remote: 2024-10-22 22:31:45 To address all issues possible &lt;span class="o"&gt;(&lt;/span&gt;including breaking changes&lt;span class="o"&gt;)&lt;/span&gt;, run:
remote: 2024-10-22 22:31:45 npm audit fix &lt;span class="nt"&gt;--force&lt;/span&gt;
remote: 2024-10-22 22:31:45
remote: 2024-10-22 22:31:45 Some issues need review, and may require choosing
remote: 2024-10-22 22:31:45 a different dependency.
remote: 2024-10-22 22:31:45
remote: 2024-10-22 22:31:45 Run &lt;span class="sb"&gt;`&lt;/span&gt;npm audit&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;details.
remote: 2024-10-22 22:31:45 npm notice
remote: 2024-10-22 22:31:45 npm notice New minor version of npm available! 10.8.2 -&amp;gt; 10.9.0
remote: 2024-10-22 22:31:45 npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.9.0
remote: 2024-10-22 22:31:45 npm notice To update run: npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; npm@10.9.0
remote: 2024-10-22 22:31:45 npm notice
remote: 2024-10-22 22:31:45
remote: 2024-10-22 22:31:45 &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Build succeeded!
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We looked at the timing for the following steps, up until the &lt;code&gt;Build succeeded&lt;/code&gt; message near the end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Installing dependencies&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Build&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Pruning devDependencies&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Caching build&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In total, with npm, this build took 16 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We ran the same setup for the pnpm-managed project, also using the timings buildpack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/project-pnpm&lt;span class="nv"&gt;$ &lt;/span&gt;heroku apps:create &lt;span class="nt"&gt;--stack&lt;/span&gt; heroku-24 pnpm-timing

~/project-pnpm&lt;span class="nv"&gt;$ &lt;/span&gt;heroku buildpacks:add &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="nt"&gt;--index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
                  https://github.com/edmorley/heroku-buildpack-timestamps.git &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="nt"&gt;--app&lt;/span&gt; pnpm-timing

~/project-pnpm&lt;span class="nv"&gt;$ &lt;/span&gt;heroku buildpacks:add &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="nt"&gt;--index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 heroku/nodejs &lt;span class="se"&gt;\&lt;/span&gt;
                  &lt;span class="nt"&gt;--app&lt;/span&gt; pnpm-timing

~/project-pnpm&lt;span class="nv"&gt;$ &lt;/span&gt;git push heroku main

…
remote: 2024-10-22 22:38:34 &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Installing dependencies
remote: 2024-10-22 22:38:34        Running &lt;span class="s1"&gt;'pnpm install'&lt;/span&gt; with pnpm-lock.yaml
…
remote: 2024-10-22 22:38:49        
remote: 2024-10-22 22:38:49        dependencies:
remote: 2024-10-22 22:38:49        + animate.less 2.2.0
remote: 2024-10-22 22:38:49        + autoprefixer 10.4.20
remote: 2024-10-22 22:38:49        + babel-core 6.26.3
…
remote: 2024-10-22 22:38:51 &lt;span class="nt"&gt;-----&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Build succeeded!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For the same build with pnpm, it took only 7 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The time savings, we found, isn’t just for that initial installation. Subsequent builds, which use the dependency cache, are also faster with pnpm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2urxi10jsv79wdgd2an.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2urxi10jsv79wdgd2an.png" alt="Package Timings" width="551" height="116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I first started &lt;a href="https://dzone.com/articles/a-comprehensive-exploration-of-nodejs-a-practical" rel="noopener noreferrer"&gt;Node.js development&lt;/a&gt;, I used npm. Several years ago, I switched to Yarn, and that’s what I had been using. . . until recently. Now, I’ve made the switch to pnpm. On my local machine, I’m able to free up substantial disk space. Builds are faster too. And now, with Heroku support for pnpm, this closes the loop so that I can use it exclusively from local development all the way to deployment in the cloud.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>architecture</category>
      <category>cloud</category>
      <category>developer</category>
    </item>
    <item>
      <title>Ending Microservices Chaos: How Architecture Governance Keeps Your Microservices on Track</title>
      <dc:creator>Alvin Lee</dc:creator>
      <pubDate>Tue, 22 Oct 2024 16:06:13 +0000</pubDate>
      <link>https://forem.com/alvinslee/ending-microservices-chaos-how-architecture-governance-keeps-your-microservices-on-track-d1l</link>
      <guid>https://forem.com/alvinslee/ending-microservices-chaos-how-architecture-governance-keeps-your-microservices-on-track-d1l</guid>
      <description>&lt;p&gt;A microservices architecture is the gold standard for building scalable web applications. Gartner estimates that &lt;a href="https://www.gartner.com/peer-community/oneminuteinsights/microservices-architecture-have-engineering-organizations-found-success-u6b" rel="noopener noreferrer"&gt;74% of organizations use microservices for their web applications&lt;/a&gt;, with another 23% planning to use them soon.&lt;/p&gt;

&lt;p&gt;If you’re an IT leader, architect, or developer, you might have experienced the faster deployments, better fault isolation, and easier scaling that come with microservices.&lt;/p&gt;

&lt;p&gt;However, the old maxim, “&lt;em&gt;there’s no such thing as a free lunch&lt;/em&gt;,” rings true here. Because microservices can also lead to significant operational overhead and complexity. Microservices sprawl, unclear component dependencies, service duplication, and anti-patterns like circular dependencies are all part of the chaos that comes hand-in-hand with microservices.&lt;/p&gt;

&lt;p&gt;Perhaps you’ve seen some of the common symptoms of this microservices chaos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Engineering velocity drastically slows down&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Onboarding new developers grows more difficult&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mean time to recovery (MTTR) for outages or performance issues increases&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overall system resiliency diminishes, putting business outcomes at risk&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While it’s still best practice to modernize monolithic applications by breaking them into microservices, &lt;strong&gt;it’s also best practice to proactively guard against the issues that microservices introduce&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mtp4xw9hskrgvm99p5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mtp4xw9hskrgvm99p5g.png" alt="Dependency graphs of legacy monoliths and microservices" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Governance to Control the Chaos
&lt;/h2&gt;

&lt;p&gt;The best way to proactively guard against these problems is by using &lt;strong&gt;architecture governance&lt;/strong&gt;—a collection of rules, processes, and practices that manage and control your software architecture.&lt;/p&gt;

&lt;p&gt;With proper software architecture governance, you can reduce microservices complexity, ramp up developers faster, reduce MTTR, and improve the resiliency of your system, all while building a culture of intentionality.&lt;/p&gt;

&lt;p&gt;(Note that we’re talking about &lt;em&gt;software architecture&lt;/em&gt; governance in this article, not &lt;em&gt;enterprise architecture&lt;/em&gt; governance which has been around for some time and is concerned with the policies, processes, and procedures of enterprise architecture strategies.)&lt;/p&gt;

&lt;p&gt;But exactly how do you build architecture governance?&lt;/p&gt;

&lt;p&gt;Governance, whether we’re talking about access, data, or architecture, is often just establishing a set of rules. But for architecture governance, you need more than just rules. You must start with architectural observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural Observability: The Foundation of Good Governance
&lt;/h2&gt;

&lt;p&gt;To implement effective governance, you first need &lt;strong&gt;comprehensive visibility into your software architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architectural observability&lt;/strong&gt; gives you a continuous, real-time view of your architecture. It provides your team with the necessary insights to manage the architecture and interconnectedness of distributed applications.&lt;/p&gt;

&lt;p&gt;Architectural observability provides documentation from a working system, it shows you systems flows, dependencies, what’s changing, and how your systems interact. It gives your team a thorough understanding of how the architecture works, how it drifts from release to release, and how its changes impact dependent services and resources.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Without&lt;/em&gt; this visibility, you’re in trouble. If you’ve built a microservices project, you’ve probably encountered the results of a lack of visibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;microservices that are difficult to manage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;unclear component dependencies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;service duplication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;anti-patterns such as circular dependencies that increase the risk of outages&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;performance issues.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your overall application resiliency, scalability, and engineering velocity are at high risk. And it only gets worse—the longer you let this lack of visibility continue, the more complex and unmanageable your architecture becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good Observability Equals Good Governance
&lt;/h2&gt;

&lt;p&gt;But &lt;em&gt;with&lt;/em&gt; good architectural observability, you can visualize your architecture layer in real time, gaining an understanding of how your system works and monitoring changes as your architecture evolves.&lt;/p&gt;

&lt;p&gt;With observability, your team can &lt;em&gt;proactively&lt;/em&gt; address issues, such as service sprawl and dependency mismanagement—before they lead to technical debt or system failures.&lt;/p&gt;

&lt;p&gt;And once you have that observability in place—once you know what’s happening and what’s changing—you can now create and enforce your rules of governance.&lt;/p&gt;

&lt;p&gt;You can implement a governance program that sits over your architecture, monitors and controls changes, and allows you to build and manage microservices effectively by proactively applying standards and rules to your architecture and codebase as it evolves.&lt;/p&gt;

&lt;p&gt;Sounds great! But how do you actually build this? Let’s look at an example of how to build this in the real world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Architectural Observability and Governance
&lt;/h2&gt;

&lt;p&gt;First, you need to implement your architectural observability.&lt;/p&gt;

&lt;p&gt;The domain of architectural observability is new and rapidly evolving. For our example, we’ll use one of the platforms at the forefront—&lt;a href="https://vfunction.com/platform/" rel="noopener noreferrer"&gt;vFunction&lt;/a&gt;. vFunction is an AI-driven platform that provides visualizations of software architecture.&lt;/p&gt;

&lt;p&gt;vFunction leverages AI and &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; tracing to enable teams to track every aspect of their architecture and monitor changes between releases. It can quickly identify complex flows, architectural technical debt, and architectural drift.&lt;/p&gt;

&lt;p&gt;For example, vFunction allows you to better understand your distributed applications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Visualize the architecture and automatically create exportable sequence diagrams of all system and data flows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identify drift, such as new services or dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identify resource exclusivity changes (such as multiple new services accessing the same database table) versus previous releases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identify circular dependencies between services that could impact resiliency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identify duplicate functionality/services that may be merged to remove complexity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintain an up-to-date, real-time view of the global microservices ecosystem.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;vFunction uses the same data that APM tools use (so it’s straightforward to install) but uses a different layer of intelligence to address the architectural root cause of issues. This is especially important for managing the sprawl and complexity that result from the rapid and frequent deployment of microservices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rusr8s3oh37ll0hv1vl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rusr8s3oh37ll0hv1vl.png" alt="Example of exportable sequence flow diagram in vFunction" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Example of exportable sequence flow diagram in vFunction&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Rules to Maintain Standards
&lt;/h2&gt;

&lt;p&gt;After implementing your observability, you’re ready to create and enforce your rules of governance. vFunction allows you to create architecture rules and tags and then monitor your architecture in real time against those rules, enforcing your critical governance policies. Any time a deployment or merge request breaks these rules, you can block that action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@kirill.velikanov/what-a-software-architect-should-know-about-the-architecture-governance-37f3a26f9de1#:~:text=Architecture%20Governance,-I%20find%20it&amp;amp;text=And%20IT%20governance%20is%20more,on%20different%20domains%20and%20projects" rel="noopener noreferrer"&gt;What type of rules might your governance enforce&lt;/a&gt;? You could enforce rules such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Dependency requirements. For example, certain services should or should not communicate with other services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restricted resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple services should not depend on the same database table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Changes should not increase service interdependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restrictions on new multi-hop flows that could impact performance and resiliency.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://vfunction.com/solutions/microservices/" rel="noopener noreferrer"&gt;vFunction can watch and send alerts&lt;/a&gt; for all of the above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13ogtp9tgyc6vukavzdd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13ogtp9tgyc6vukavzdd.png" alt="vFunction can watch and send alerts" width="800" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By implementing a focused set of rules and tags, you can create adaptive governance that guides developers in maintaining an optimal, clean, and efficient architecture. You can build architecture governance into your code and your culture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkecdwj4pgwby426cxnj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkecdwj4pgwby426cxnj.png" alt="Rules and tags for adaptive governance" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Microservices chaos can threaten to derail development velocity, system resiliency, and operational efficiency. Architecture governance offers a way out of this chaos—a way to regain control over your architecture and steer it toward greater scalability and stability.&lt;/p&gt;

&lt;p&gt;Whether your organization is just starting to scale with microservices or is already in the depths of microservices sprawl, it’s the right time to introduce architecture governance to ensure that your architecture is efficient and resilient—and ready to support your next phase of growth.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>architecture</category>
      <category>cloud</category>
      <category>developer</category>
    </item>
  </channel>
</rss>
