<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Toni Väisänen: Consultant | Fullstack Developer | ML Engineer]]></title><description><![CDATA[Clojure, data science, and ML. 

Consultant and a software engineer at Metosin.

Used to be a musician and an audio engineer. Got to engineering via DSP. CSE gr]]></description><link>https://tonitalksdev.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 12:43:21 GMT</lastBuildDate><atom:link href="https://tonitalksdev.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How Writing a Game Boy Emulator Changed His Life!]]></title><description><![CDATA[In this engaging remote episode, we are joined by Vincent Cantin, a seasoned developer with a fascinating journey from game development to web development and Clojure. Vincent's career began over 20 years ago when his Game Boy Advance emulator projec...]]></description><link>https://tonitalksdev.com/how-writing-a-game-boy-emulator-changed-his-life</link><guid isPermaLink="true">https://tonitalksdev.com/how-writing-a-game-boy-emulator-changed-his-life</guid><category><![CDATA[mcp]]></category><category><![CDATA[mcp server]]></category><category><![CDATA[podcast]]></category><category><![CDATA[Clojure]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Fri, 08 Aug 2025 16:22:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754669745465/191afc09-3389-462c-8674-6e98d3b45b14.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this engaging remote episode, we are joined by Vincent Cantin, a seasoned developer with a fascinating journey from game development to web development and Clojure. Vincent's career began over 20 years ago when his Game Boy Advance emulator project led him to a game development role in Taiwan. After spending 15 years in the gaming industry, Vincent transitioned to web development, ultimately focusing on Clojure.</p>
<p>In this episode, Vincent shares insights into his latest open-source venture, the MCP Toolkit, which aims to provide tools for writing with Model Context Protocol (MCP) applications using Clojure. Alongside Vincent's personal narrative, we delve into the technical aspects of his projects and discuss the challenges and opportunities in the open-source world—especially the importance of community collaboration and user contributions. Vincent also explores the intersection of AI and programming, discussing his experiments and learnings about AI-driven development.</p>
<p>His insights into using AI for code generation and tooling provide a fresh perspective on modern development practices. Throughout the discussion, Vincent provides a candid look at his professional journey, including his decision to move from Taiwan to Finland and the lifestyle and cultural differences encountered along the way. His story is one of perseverance and adaptability, offering valuable lessons for developers navigating career transitions and exploring the vast potential of open-source collaboration.</p>
<p>Whether you're interested in Clojure, open-source development, or the practical applications of AI in programming, this episode provides a wealth of knowledge and inspiration. Don't forget to subscribe for more insightful conversations.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/9m2DG0_RDro?si=D4WJlF-m2ZNC2096">https://youtu.be/9m2DG0_RDro?si=D4WJlF-m2ZNC2096</a></div>
<p> </p>
<p>Vincent:</p>
<ul>
<li><p>Github: <a target="_blank" href="https://github.com/green-coder">https://github.com/green-coder</a></p>
</li>
<li><p>Blog: <a target="_blank" href="https://blog.404.taipei/">https://blog.404.taipei/</a></p>
</li>
</ul>
<p>Open Source Projets:</p>
<ul>
<li><p>MCP-toolkit: <a target="_blank" href="https://github.com/metosin/mcp-toolkit">https://github.com/metosin/mcp-toolkit</a></p>
</li>
<li><p>Vrac: <a target="_blank" href="https://github.com/metosin/vrac">https://github.com/metosin/vrac</a></p>
</li>
<li><p>Signaali: <a target="_blank" href="https://github.com/metosin/signaali">https://github.com/metosin/signaali</a></p>
</li>
<li><p>Siagent: <a target="_blank" href="https://github.com/metosin/siagent">https://github.com/metosin/siagent</a></p>
</li>
<li><p>Si-Frame: <a target="_blank" href="https://github.com/metosin/si-frame">https://github.com/metosin/si-frame</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[From Code Reviews to Teamwork: Deep Dives]]></title><description><![CDATA[Welcome back to the ToniTalksDev Podcast! In this episode, host Toni reconnects with experienced software engineer Miikka Koskinen for a wide-ranging discussion at the intersection of code, collaboration, and career development.
About Our Guest:
Miik...]]></description><link>https://tonitalksdev.com/from-code-reviews-to-teamwork-deep-dives</link><guid isPermaLink="true">https://tonitalksdev.com/from-code-reviews-to-teamwork-deep-dives</guid><category><![CDATA[software development]]></category><category><![CDATA[code review]]></category><category><![CDATA[podcast]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Tue, 06 May 2025 00:04:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746489784186/f962f1ea-f6a8-4978-8906-4fb3ada85c42.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome back to the ToniTalksDev Podcast! In this episode, host Toni reconnects with experienced software engineer Miikka Koskinen for a wide-ranging discussion at the intersection of code, collaboration, and career development.</p>
<p>About Our Guest:</p>
<p>Miikka Koskinen, based in Helsinki, is a seasoned software engineer with over 15 years of experience spanning mathematics, technology, and open source development. Miikka's journey began with QBasic at age 10 and has evolved through a variety of languages and domains—from embedded systems to large-scale consulting—and contributions to the Clojure ecosystem at Metosin.</p>
<p>Episode Highlights:</p>
<ul>
<li><p>Code Review Culture: Learn how Miikka approaches code review as a process of collaboration and mutual learning, not just gatekeeping. We discuss the value of early feedback, building shared understanding, and fostering psychological safety within engineering teams.</p>
</li>
<li><p>Teamwork &amp; Trust: Miikka shares insights on establishing trust in remote and distributed teams, the dangers of defaulting to blame, and how shared ownership of code leads to healthier team dynamics.</p>
</li>
<li><p>Career Path: Generalist vs. Specialist: Reflect with us on the pros and cons of being a generalist versus a specialist in the software industry. Miikka offers candid thoughts about career development, lifelong learning, and finding your niche (or not!).</p>
</li>
<li><p>Technical Writing &amp; Thought Leadership: Discover the benefits of maintaining a technical blog and how publishing your thoughts—whether on engineering practices or database migrations—can enhance your professional reputation and open new career opportunities.</p>
</li>
<li><p>Open Source &amp; Community: We discuss Miikka’s journey with open source, contributing and maintaining libraries, and the dynamics of knowledge sharing in public and private codebases.</p>
</li>
<li><p>Emerging Tech: Get Miikka's take on S3 object storage’s new capabilities and potential impacts on distributed system design and next-generation database architectures.</p>
</li>
</ul>
<p>Why Watch?</p>
<p>Whether you're interested in evolving your team's code review practices, looking to strengthen collaboration and trust, or evaluating your own growth as a software professional, this episode offers practical strategies and honest reflections.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=TKCK7SyZLmQ">https://www.youtube.com/watch?v=TKCK7SyZLmQ</a></div>
]]></content:encoded></item><item><title><![CDATA[Design Systems, Heavy Metal, and Horror Novels]]></title><description><![CDATA[In this episode, we sit down with Daniel Yuschick, a design systems expert who traded Pennsylvania's heavy metal scene for Helsinki's tech industry. From touring with melodic death metal bands and having his own signature drumsticks to building digit...]]></description><link>https://tonitalksdev.com/design-systems-heavy-metal-and-horror-novels</link><guid isPermaLink="true">https://tonitalksdev.com/design-systems-heavy-metal-and-horror-novels</guid><category><![CDATA[Design Systems]]></category><category><![CDATA[podcast]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Mon, 24 Feb 2025 11:49:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740397718479/655587a7-ef0f-4bde-bcdd-2c6475280655.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this episode, we sit down with Daniel Yuschick, a design systems expert who traded Pennsylvania's heavy metal scene for Helsinki's tech industry. From touring with melodic death metal bands and having his own signature drumsticks to building digital experiences at companies like <a target="_blank" href="https://www.linkedin.com/company/futurice/">Futurice</a><a target="_blank" href="https://www.linkedin.com/company/futurice/">, Daniel</a> shares his unique journey of following his passion for Finnish culture and making Finland his true home.  </p>
<p>The conversation weaves through his experience moving to Finland in 2017, finding love on Tinder, and building a new life in Helsinki. Daniel offers valuable insights into design systems, explaining how they serve as the language of digital products and why timing is crucial when implementing them. He emphasizes the importance of human relationships and trust in successful design system adoption.  </p>
<p>Beyond tech, Daniel opens up about his transition from competitive basketball to more "proper 38-year-old sports" like squash, his enduring love for metal music, and how Finland's legendary Tuska festival coinciding with his birthday weekend feels like destiny. His story is a compelling blend of professional expertise, personal growth, and finding belonging in a new culture.  </p>
<p>This episode offers an authentic look at expatriate life in Finland, the evolution of creative passions, and the intersection of technology and human connection in modern digital product development.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/RslxDgFy3gQ?si=L3skjvBFj25zo3U5">https://youtu.be/RslxDgFy3gQ?si=L3skjvBFj25zo3U5</a></div>
]]></content:encoded></item><item><title><![CDATA[From a Developer to The CEO of Metosin]]></title><description><![CDATA[In this episode, we sit down with Valtteri Harmainen, the CEO of Metosin, to explore his unconventional journey into the world of technology and leadership. You might remember Valtteri from his talk at ClojuTRE, where he shared how he "created his ow...]]></description><link>https://tonitalksdev.com/from-a-developer-to-the-ceo-of-metosin</link><guid isPermaLink="true">https://tonitalksdev.com/from-a-developer-to-the-ceo-of-metosin</guid><category><![CDATA[Clojure]]></category><category><![CDATA[ClojureScript]]></category><category><![CDATA[DeveloperStories]]></category><category><![CDATA[ceo]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Tue, 21 Jan 2025 16:20:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737476166055/118227ae-a391-4b5b-8195-001ce886ee54.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<iframe width="560" height="315" src="https://www.youtube.com/embed/IVQk8ElfdCY?si=LFIm9aXHwdijdpKO"></iframe>

<p>In this episode, we sit down with Valtteri Harmainen, the CEO of Metosin, to explore his unconventional journey into the world of technology and leadership. You might remember Valtteri from his talk at ClojuTRE, where he shared how he "created his own Clojure job". We delve into his early life in Finland, his accidental entry into programming, and how he ultimately landed in the CEO role at Metosin</p>
<p>This isn't just a tech story; it's a personal one. Valtteri recounts how a functional programming course that never happened sparked his interest in Clojure, and how his initial programming project involved fixing a university system that "doesn't work". His path wasn't straightforward, going from philosophy and cognitive science to usability and eventually to programming by necessity. This led to a career that saw him move from developer roles to leadership positions, a pattern he says he kept experiencing: "I ended up in some kind of leadership position"</p>
<p>We discuss his experiences at various companies, including his time at a startup that grew rapidly during the pandemic. Valtteri also shares his transition to Metosin, where he pioneered the account manager role and then stepped into the CEO position. His view on leadership has changed, realizing that "people are not as easy as writing code". He values honesty and transparency in leadership acknowledging that "everything is a compromise" when making decisions</p>
<p>Looking ahead, Valtteri shares Metosin's vision for the future, emphasizing a holistic approach that includes not just technical skills, but also business acumen and people skills, which are described as a "triangle". He explains that while they are strong in Clojure, the real problems companies face are more often related to people, processes, and organization rather than technology. This approach enables Metosin to work with clients on all levels. Valtteri also shares a few laughs about the unexpected challenges in communication, mentioning how "people are the weirdest part" of the entire process.</p>
<p>This episode is a deep dive into the career path of an unconventional leader in tech and the evolution of a software company.</p>
<p>Key Topics:</p>
<ul>
<li><p>Valtteri's journey from philosophy to tech</p>
</li>
<li><p>The accidental start of a programming career</p>
</li>
<li><p>Learning functional programming and Clojure</p>
</li>
<li><p>Transition from programmer to leader</p>
</li>
<li><p>The importance of people skills in business and tech</p>
</li>
<li><p>Metosin's future and its focus on a holistic approach</p>
</li>
<li><p>The challenges of leadership and the value of transparency</p>
</li>
</ul>
<p>This episode provides a comprehensive overview of Valtteri's career and Metosin's approach to software and consulting, making it a must-listen for anyone interested in technology, leadership, or the human side of business.</p>
]]></content:encoded></item><item><title><![CDATA[How Could Clojure Web Development Suck Less]]></title><description><![CDATA[In this episode, we dive into the intricacies of web development with Clojure, exploring how it can be improved and made less cumbersome. We also touch on Rama, as Ben brings more expertise in that area. Additionally, we explore Ben career journey, f...]]></description><link>https://tonitalksdev.com/how-could-clojure-web-development-suck-less</link><guid isPermaLink="true">https://tonitalksdev.com/how-could-clojure-web-development-suck-less</guid><category><![CDATA[rama]]></category><category><![CDATA[Clojure]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Tue, 15 Oct 2024 06:34:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728973409038/65ff1e99-46f7-43a4-8922-7f6f37f89a06.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this episode, we dive into the intricacies of web development with Clojure, exploring how it can be improved and made less cumbersome. We also touch on Rama, as Ben brings more expertise in that area. Additionally, we explore Ben career journey, from working at tech giants like Intel and Apple to embracing Clojure.</p>
<p>Please bear with us. The episode was recorded in a bustling food court, so there might be some background noise. However, the conversation is packed with insights and ideas that are sure to interest you if you're into web development or Clojure.</p>
<p>Topics Covered:</p>
<ul>
<li><p>Enhancing the Clojure web stack</p>
</li>
<li><p>Middleware ordering challenges and solutions</p>
</li>
<li><p>Ben's career journey from Intel and Apple to Clojure</p>
</li>
<li><p>Discussion on Rama and its applications</p>
</li>
<li><p>Performance optimizations in Clojure</p>
</li>
<li><p>The future of Clojure web development</p>
</li>
</ul>
<p>Show Highlights:</p>
<ul>
<li><p>Innovative ideas for middleware management</p>
</li>
<li><p>Insights into Ben's professional background</p>
</li>
<li><p>Practical tips on improving web development workflows</p>
</li>
<li><p>Exploration of performance optimization in Clojure</p>
</li>
</ul>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=Jdj5FGpRnyo">https://www.youtube.com/watch?v=Jdj5FGpRnyo</a></div>
<p> </p>
<p>Stay tuned for our next episode featuring the CEO of Metosin, Valtteri Harmainen. Don't forget to subscribe and hit the notification bell to stay updated with our latest content.</p>
<p>If you enjoyed this episode, please like, share, and subscribe. Your support helps us bring more insightful conversations to the community.</p>
]]></content:encoded></item><item><title><![CDATA[Building an International Software Company]]></title><description><![CDATA[Join us in this insightful video podcast as we sit down with Sandun, the co-founder and CEO of Quality For Us (Q4US). Discover his journey from Sri Lanka to Finland, his extensive work history, and the founding of Q4US. We delve into marketing, sales...]]></description><link>https://tonitalksdev.com/building-an-international-software-company</link><guid isPermaLink="true">https://tonitalksdev.com/building-an-international-software-company</guid><category><![CDATA[software development]]></category><category><![CDATA[Entrepreneurship]]></category><category><![CDATA[podcast]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Mon, 09 Sep 2024 15:17:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725894944803/93753d9a-9e66-45b3-b723-a5a3e322819d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Join us in this insightful video podcast as we sit down with Sandun, the co-founder and CEO of Quality For Us (Q4US). Discover his journey from Sri Lanka to Finland, his extensive work history, and the founding of Q4US. We delve into marketing, sales, and entrepreneurship in the software field. Learn about the challenges and triumphs of running a business, and get inspired by Sandun’s experiences and insights. Don't miss this engaging conversation!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=UVAqLMtzdCE">https://www.youtube.com/watch?v=UVAqLMtzdCE</a></div>
<p> </p>
<hr />
<p>Find Q4US online: <a target="_blank" href="https://q4us.dev">https://q4us.dev</a></p>
<p>Connect with Sandun on LinkedIn <a target="_blank" href="https://www.linkedin.com/in/sandundasanayake/">https://www.linkedin.com/in/sandundasanayake/</a></p>
]]></content:encoded></item><item><title><![CDATA[How do you fix broken links after changing your domain?]]></title><description><![CDATA[A while back, I changed the domain of this blog to rebrand. This broke all of the backlinks I had accumulated. I had been marketing my work online, and I didn't want that work to go to waste.
At that point, I was using a smaller hosting service to ma...]]></description><link>https://tonitalksdev.com/how-do-you-fix-broken-links-after-changing-your-domain</link><guid isPermaLink="true">https://tonitalksdev.com/how-do-you-fix-broken-links-after-changing-your-domain</guid><category><![CDATA[SEO]]></category><category><![CDATA[backlinks]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[dns]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Wed, 04 Sep 2024 07:22:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725434408077/eb4f8ce2-4224-465f-bdf6-0fcf5cb8e610.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A while back, I changed the domain of this blog to rebrand. This broke all of the backlinks I had accumulated. I had been marketing my work online, and I didn't want that work to go to waste.</p>
<p>At that point, I was using a smaller hosting service to manage my DNS, but they didn't have the option to redirect the subdomain to a different domain.</p>
<p>I ended up switching to Cloudflare and using their "rules" to route from the old domain to the new one. From <code>blog.tvaisanen.com</code> to <code>tonitalksdev.com</code>.</p>
<p>The process was pain-free, and now Cloudflare has become my go-to domain registrar and DNS management tool. Let me show you how simple my configuration ended up being.</p>
<h2 id="heading-redirect-rule">Redirect Rule</h2>
<p>All it took was to add one redirect rule for the subdomain.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721124726506/9034c608-1b3b-41aa-827f-ebc17d3d1acb.png" alt class="image--center mx-auto" /></p>
<p>To reroute all the traffic to the new domain.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721124729162/b1887255-288a-49b9-baf7-0e68c86b4483.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Cloudflare's free plan includes tools for custom redirect rules and domain DNS management. You can use your current domain registrar and move the DNS management to Cloudflare, but they also provide domain registration services at a cost. I'm not sponsored by them; I just like the service, and I've transferred all my domains to their platform.</p>
<p>Learn how to add your domain to Cloudflare from their <a target="_blank" href="https://developers.cloudflare.com/learning-paths/get-started/add-domain-to-cf/">documentation</a>.</p>
<p>Once again, thanks for reading.</p>
<p>Feel free to reach out and let me know what you think—social links in the menu.</p>
]]></content:encoded></item><item><title><![CDATA[From 0 to Clojure, Career Change]]></title><description><![CDATA[https://youtu.be/hxmAe7-z60Q?si=c2TELYNP-ETFqP9O
 
Cat Rivers shares her journey from tattooing to software development and back, discussing burnout, coding experiences, and the challenges of career changes. Highlights

🎨 Cat Rivers is a tattoo arti...]]></description><link>https://tonitalksdev.com/podcast-career-change-from-tattooing-to-software-and-back-with-cat-rivers</link><guid isPermaLink="true">https://tonitalksdev.com/podcast-career-change-from-tattooing-to-software-and-back-with-cat-rivers</guid><category><![CDATA[careerchange]]></category><category><![CDATA[Clojure]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[software development]]></category><category><![CDATA[internships]]></category><category><![CDATA[Svelte]]></category><category><![CDATA[burnout]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Sat, 17 Aug 2024 15:22:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726321965237/625c9ebe-edb7-42c6-bf56-1b8c6f17649c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/hxmAe7-z60Q?si=c2TELYNP-ETFqP9O">https://youtu.be/hxmAe7-z60Q?si=c2TELYNP-ETFqP9O</a></div>
<p> </p>
<p>Cat Rivers shares her journey from tattooing to software development and back, discussing burnout, coding experiences, and the challenges of career changes. Highlights</p>
<ul>
<li><p>🎨 Cat Rivers is a tattoo artist and psychologist who briefly ventured into coding.</p>
</li>
<li><p>💻 She experienced burnout while juggling entrepreneurship and sought stability in tech.</p>
</li>
<li><p>🔄 Coding was seen as a potential career change but led to emotional challenges.</p>
</li>
<li><p>📚 She learned JavaScript, TypeScript, and Clojure, noting their complexities.</p>
</li>
<li><p>🤝 Networking helped her land an internship and eventually a job in software development.</p>
</li>
<li><p>🧠 Imposter syndrome and communication barriers are common in tech environments.</p>
</li>
<li><p>🦆 Cat returned to tattooing, emphasizing the need for creative fulfillment.</p>
</li>
</ul>
<blockquote>
<p>Spotify version will be published on the 22. of August.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Typed Configuration Files with Malli & Aero]]></title><description><![CDATA[Most projects start by defining the application configuration file and schemas or code to validate it. The configuration can be as simple as a database connection string. Sometimes, this is read directly from an env variable in the code. In Clojure, ...]]></description><link>https://tonitalksdev.com/typed-configuration-files-with-malli-aero</link><guid isPermaLink="true">https://tonitalksdev.com/typed-configuration-files-with-malli-aero</guid><category><![CDATA[Clojure]]></category><category><![CDATA[software development]]></category><category><![CDATA[configuration]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Wed, 31 Jul 2024 06:43:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722406174770/aa8760d9-81d2-4cd8-98fa-e236fc56f8f4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most projects start by defining the application configuration file and schemas or code to validate it. The configuration can be as simple as a database connection string. Sometimes, this is read directly from an env variable in the code. In Clojure, an EDN file usually describes the required configuration explicitly. This is then loaded into the application memory from a file and, depending on the requirements, further transformed into the right format in different ways. A port number needs to be an integer instead of a string, and so on. But there's an alternative way to do this. Write your configuration files as Malli schemas and validate and transform when reading.</p>
<blockquote>
<p><strong>If you are familiar with Aero and Malli</strong> jump till the end to see how to write <a target="_blank" href="https://tonitalksdev.com/preview/663e4af512dc36e7d39b1a6c#heading-configuration-file-as-malli-schema">configurations as Malli schemas</a>.</p>
</blockquote>
<p>Let's go through the basics of Aero and Malli first before putting them together.</p>
<h2 id="heading-aero">Aero</h2>
<p><a target="_blank" href="https://github.com/juxt/aero">Aero</a> is a popular Clojure library from JUXT that enables us to write the environment variables into the EDN configuration file with <code>#ENV</code> tag literals. It has other features, but for our purposes here, this is enough. Let's see what this means by creating a configuration file for an application that needs to know the user and the shell of the system the application runs on.</p>
<p>Without Aero, we might write something like</p>
<pre><code class="lang-clojure">(<span class="hljs-name">System/getenv</span> <span class="hljs-string">"USER"</span>) <span class="hljs-comment">;; =&gt; "tvaisanen"</span>
(<span class="hljs-name">System/getenv</span> <span class="hljs-string">"SHELL"</span>)<span class="hljs-comment">;; =&gt; "/bin/zsh"</span>
</code></pre>
<p>But we prefer the declarative way</p>
<pre><code class="lang-clojure"><span class="hljs-comment">;; sample-01.edn</span>
{<span class="hljs-symbol">:user</span> #env USER
 <span class="hljs-symbol">:shell</span> #env SHELL}
</code></pre>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> core
  (<span class="hljs-symbol">:require</span> [aero.core <span class="hljs-symbol">:as</span> aero]
            [clojure.java.io <span class="hljs-symbol">:as</span> io]))

(<span class="hljs-name">aero/read-config</span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-01.edn"</span>))
<span class="hljs-comment">;; =&gt; </span>
{<span class="hljs-symbol">:user</span> <span class="hljs-string">"tvaisanen"</span> <span class="hljs-symbol">:shell</span> <span class="hljs-string">"/bin/zsh"</span>}
</code></pre>
<p>Now that the application can access the configuration data, we'd usually do the validation step.</p>
<p>Let's say that we expect the username to be at least five characters and the shell to be <code>bash</code> instead of <code>zsh</code> and we want to throw an error when the requirements are not met.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">validate-config</span> [config]
  (<span class="hljs-name"><span class="hljs-builtin-name">when-not</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">and</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">count</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> config <span class="hljs-symbol">:user</span>))
                 (<span class="hljs-name">clojure.string/ends-with?</span> (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> config <span class="hljs-symbol">:shell</span>)
                                            <span class="hljs-string">"bash"</span>))
    (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"invalid config"</span> config))))

(<span class="hljs-name"><span class="hljs-builtin-name">-&gt;</span></span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-01.edn"</span>)
    (<span class="hljs-name">aero/read-config</span>)
    (<span class="hljs-name">validate-config</span>))
</code></pre>
<pre><code class="lang-clojure">
  Show: Project-Only All 
  Hide: Clojure Java REPL Tooling Duplicates  (<span class="hljs-number">26</span> frames hidden)

<span class="hljs-number">1</span>. Unhandled clojure.lang.ExceptionInfo
   invalid config
   {<span class="hljs-symbol">:user</span> <span class="hljs-string">"tvaisanen"</span>, <span class="hljs-symbol">:shell</span> <span class="hljs-string">"/bin/zsh"</span>}
                      REPL:   <span class="hljs-number">47</span>  core/validate-config
                      REPL:   <span class="hljs-number">43</span>  core/validate-config
                      REPL:   <span class="hljs-number">51</span>  core/eval<span class="hljs-number">17603</span>
                      REPL:   <span class="hljs-number">49</span>  core/eval<span class="hljs-number">17603</span>
</code></pre>
<p>As a developer, I'm already thinking that this approach will be a pain. I'll probably need to add many different configuration parameters, validate for all of them, and then provide descriptive error messages. It'd still be manageable for a small toy application like ours, but it's most likely not the case for larger projects.</p>
<h2 id="heading-validation-with-malli">Validation With Malli</h2>
<p><a target="_blank" href="https://github.com/metosin/malli">Malli</a> to the rescue.</p>
<p>If you're unfamiliar with Malli, refer to my earlier <a target="_blank" href="https://tonitalksdev.com/data-validation-in-clojure">post</a> or the official <a target="_blank" href="https://cljdoc.org/d/metosin/malli/0.16.2/doc/readme">documentation</a> for the basics.</p>
<p>Let's start by defining the expected values and their types. At this point, we'll assume both values as strings and attack the constraints in a moment.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[malli.core <span class="hljs-symbol">:as</span> m])

(<span class="hljs-keyword">def</span> <span class="hljs-title">schema</span>
  [<span class="hljs-symbol">:map</span>
   [<span class="hljs-symbol">:user</span> <span class="hljs-symbol">:string</span>]
   [<span class="hljs-symbol">:shell</span> <span class="hljs-symbol">:string</span>]])

(<span class="hljs-name"><span class="hljs-builtin-name">-&gt;&gt;</span></span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-01.edn"</span>)
     (<span class="hljs-name">aero/read-config</span>)
     (<span class="hljs-name">m/validate</span> schema))
<span class="hljs-comment">;; =&gt; </span>
<span class="hljs-literal">true</span>
</code></pre>
<p>Everything is OK so far. Next, add the constraints from before.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">schema</span>
  [<span class="hljs-symbol">:map</span>
   [<span class="hljs-symbol">:user</span> {<span class="hljs-symbol">:min</span> <span class="hljs-number">5</span>} <span class="hljs-symbol">:string</span>]
   [<span class="hljs-symbol">:shell</span> [<span class="hljs-symbol">:re</span> <span class="hljs-string">"^.*bash$"</span>]]])

(<span class="hljs-name"><span class="hljs-builtin-name">-&gt;&gt;</span></span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-01.edn"</span>)
     (<span class="hljs-name">aero/read-config</span>)
     (<span class="hljs-name">m/validate</span> schema))
<span class="hljs-comment">;; =&gt; </span>
<span class="hljs-literal">false</span>
</code></pre>
<p>Now, we are at the same stage with our <code>validate-config</code> function. Let's take it a step further. Do you remember we talked about the descriptive error messages? Malli provides us with out-of-the-box tools to make this easy for us.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">-&gt;&gt;</span></span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-01.edn"</span>)
     (<span class="hljs-name">aero/read-config</span>)
     (<span class="hljs-name">m/explain</span> schema))
<span class="hljs-comment">;; =&gt;</span>
{<span class="hljs-symbol">:schema</span>
 [<span class="hljs-symbol">:map</span> 
   [<span class="hljs-symbol">:user</span> {<span class="hljs-symbol">:doc</span> <span class="hljs-string">"At least 5 characters long"</span>, <span class="hljs-symbol">:min</span> <span class="hljs-number">5</span>} <span class="hljs-symbol">:string</span>] 
   [<span class="hljs-symbol">:shell</span> {<span class="hljs-symbol">:doc</span> <span class="hljs-string">"String needs to end with `bash`"</span>} [<span class="hljs-symbol">:re</span> <span class="hljs-string">"^.*bash$"</span>]]],
 <span class="hljs-symbol">:value</span> {<span class="hljs-symbol">:user</span> <span class="hljs-string">"tvaisanen"</span>, <span class="hljs-symbol">:shell</span> <span class="hljs-string">"/bin/zsh"</span>},
 <span class="hljs-symbol">:errors</span>
 ({<span class="hljs-symbol">:path</span> [<span class="hljs-symbol">:shell</span>], 
   <span class="hljs-symbol">:in</span> [<span class="hljs-symbol">:shell</span>], 
   <span class="hljs-symbol">:schema</span> [<span class="hljs-symbol">:re</span> <span class="hljs-string">"^.*bash$"</span>], 
   <span class="hljs-symbol">:value</span> <span class="hljs-string">"/bin/zsh"</span>})}
</code></pre>
<p>The output is still somewhat cryptic for the casual reader. We are not satisfied until the resulting error reads like English.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[malli.error <span class="hljs-symbol">:as</span> me])

(<span class="hljs-name"><span class="hljs-builtin-name">-&gt;&gt;</span></span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-01.edn"</span>)
     (<span class="hljs-name">aero/read-config</span>)
     (<span class="hljs-name">m/explain</span> schema)
     (<span class="hljs-name">me/humanize</span>))
<span class="hljs-comment">;; =&gt; </span>
{<span class="hljs-symbol">:shell</span> [<span class="hljs-string">"should match regex"</span>]}
</code></pre>
<p>Now we have the error in English, but we still don't know what the <code>regex</code> part contains.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">schema</span>
  [<span class="hljs-symbol">:map</span>
   [<span class="hljs-symbol">:user</span>
    {<span class="hljs-symbol">:min</span> <span class="hljs-number">5</span>}
    <span class="hljs-symbol">:string</span>]
   [<span class="hljs-symbol">:shell</span>
    [<span class="hljs-symbol">:re</span>
     {<span class="hljs-symbol">:error/message</span> <span class="hljs-string">"String needs to end with `bash`"</span>}
     <span class="hljs-string">"^.*bash$"</span>]]])

(<span class="hljs-name"><span class="hljs-builtin-name">-&gt;&gt;</span></span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-01.edn"</span>)
     (<span class="hljs-name">aero/read-config</span>)
     (<span class="hljs-name">m/explain</span> schema)
     (<span class="hljs-name">me/humanize</span>))
<span class="hljs-comment">;; </span>
=&gt; {<span class="hljs-symbol">:shell</span> [<span class="hljs-string">"String needs to end with `bash`"</span>]}
</code></pre>
<p>One last thing before we move forward.</p>
<p>Usually, we need to read some values as specific types.</p>
<p>Assume that we expect a comma-separated string for the <code>features</code> application and want to use it as a set of strings. Again, we can use Malli decode for this at load time.</p>
<p>Let's see how decoding works first.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[malli.transformer <span class="hljs-symbol">:as</span> mt])

(<span class="hljs-name">m/decode</span> [<span class="hljs-symbol">:map</span> [<span class="hljs-symbol">:int</span> int?]]
          {<span class="hljs-symbol">:int</span> <span class="hljs-string">"1"</span>}
          mt/string-transformer)
<span class="hljs-comment">;; </span>
=&gt; {<span class="hljs-symbol">:int</span> <span class="hljs-number">1</span>}
</code></pre>
<p>Then, we adapt the example to our use case.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">m/decode</span> [<span class="hljs-symbol">:map</span> [<span class="hljs-symbol">:features</span> [<span class="hljs-symbol">:set</span> <span class="hljs-symbol">:string</span>]]]
          {<span class="hljs-symbol">:features</span> <span class="hljs-string">"foo,bar,fizz"</span>}
          mt/string-transformer)
<span class="hljs-comment">;; </span>
=&gt; {<span class="hljs-symbol">:features</span> <span class="hljs-string">"foo,bar,fizz"</span>}
</code></pre>
<p>It didn't work as expected! But of course, how could Malli know what string encoding we use? We can fix this with custom decoders.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">m/decode</span> 
  [<span class="hljs-symbol">:map</span> [<span class="hljs-symbol">:features</span>
          {<span class="hljs-symbol">:decode/string</span> (<span class="hljs-name"><span class="hljs-builtin-name">fn</span></span> [v] 
                            (<span class="hljs-name">set</span> (<span class="hljs-name">clojure.string/split</span> v #<span class="hljs-string">","</span>)))}
          [<span class="hljs-symbol">:set</span> <span class="hljs-symbol">:string</span>]]]
          {<span class="hljs-symbol">:features</span> <span class="hljs-string">"foo,bar,fizz"</span>}
          mt/string-transformer)
<span class="hljs-comment">;; </span>
=&gt; {<span class="hljs-symbol">:features</span> #{<span class="hljs-string">"foo"</span> <span class="hljs-string">"bar"</span> <span class="hljs-string">"fizz"</span>}}
</code></pre>
<p>Let's wrap all of this in a function to load our configuration. I also updated the expected shell to be <code>zsh</code> so the validation step won't throw.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">load-config</span> [filename]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [config (<span class="hljs-name">aero/read-config</span> (<span class="hljs-name">io/resource</span> filename))
        decoded (<span class="hljs-name">m/decode</span> schema config mt/string-transformer)]
    (<span class="hljs-name"><span class="hljs-builtin-name">when-not</span></span> (<span class="hljs-name">m/validate</span> schema decoded)
      (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"invalid schema"</span>
                      (<span class="hljs-name"><span class="hljs-builtin-name">-&gt;&gt;</span></span> decoded
                           (<span class="hljs-name">m/explain</span> schema)
                           (<span class="hljs-name">me/humanize</span>)))))
    decoded))

<span class="hljs-comment">;; let's see what happens</span>
(<span class="hljs-name">load-config</span> <span class="hljs-string">"sample-01.edn"</span>)
</code></pre>
<pre><code class="lang-clojure"><span class="hljs-number">1</span>. Unhandled clojure.lang.ExceptionInfo
   invalid schema
   {<span class="hljs-symbol">:features</span> [<span class="hljs-string">"missing required key"</span>]}
                      REPL:  <span class="hljs-number">132</span>  core/load-config
                      REPL:  <span class="hljs-number">128</span>  core/load-config
                      REPL:  <span class="hljs-number">138</span>  core/eval<span class="hljs-number">17775</span>
</code></pre>
<p>The error shows that we are missing the <code>:features</code> key. Update the configuration file to fix this.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:user</span> #env USER
 <span class="hljs-symbol">:shell</span> #env SHELL
 <span class="hljs-symbol">:features</span> <span class="hljs-string">"foo,bar,fizz"</span>}
</code></pre>
<p>And try again.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">load-config</span> <span class="hljs-string">"sample-01.edn"</span>)
<span class="hljs-comment">;; =&gt;</span>
{<span class="hljs-symbol">:user</span> <span class="hljs-string">"tvaisanen"</span>
 <span class="hljs-symbol">:shell</span> <span class="hljs-string">"/bin/zsh"</span>
 <span class="hljs-symbol">:features</span> #{<span class="hljs-string">"foo"</span> <span class="hljs-string">"bar"</span> <span class="hljs-string">"fizz"</span>}}
</code></pre>
<p>It works as expected! We save a lot of work when dealing with configuration updates. In the future, we'll keep the schema updated with the configuration file itself. Simple.</p>
<h2 id="heading-keep-schema-up-to-date">Keep Schema Up-To-Date</h2>
<p>Keeping the config and schema up to date is easier said than done. We are people, after all. If we want to ensure that the schema is updated when the configuration changes, we most likely need to add a step to remove all undefined keys from the loaded configuration to force each developer to update the schema on changes.</p>
<p>We can do this by adding a <code>mt/strip-extra-keys-transformer</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">transformer</span>
  (<span class="hljs-name">mt/transformer</span> mt/strip-extra-keys-transformer
                  mt/string-transformer))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">load-config</span> [filename]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [config (<span class="hljs-name">aero/read-config</span> (<span class="hljs-name">io/resource</span> filename))
        decoded (<span class="hljs-name">m/decode</span> schema config transformer)]
    (<span class="hljs-name"><span class="hljs-builtin-name">when-not</span></span> (<span class="hljs-name">m/validate</span> schema decoded)
      (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"invalid schema"</span>
                      (<span class="hljs-name"><span class="hljs-builtin-name">-&gt;&gt;</span></span> decoded
                           (<span class="hljs-name">m/explain</span> schema)
                           (<span class="hljs-name">me/humanize</span>)))))
    decoded))
</code></pre>
<p>Now, we force developers to add their changes to the schema configuration since if they don't, the <code>strip-extra-keys-transformer</code> drops the undefined keys, and it'll throw if an update doesn't match the schema.</p>
<p>But why stop here?</p>
<h2 id="heading-configuration-file-as-malli-schema">Configuration File as Malli Schema</h2>
<p>We can define Malli schemas as EDN files.</p>
<p>Aero reads EDN files.</p>
<p>We can write our configurations and type definitions in the same file. As long as we reference the decoder functions in the EDN, if we have any, we should be good to go. Let's update our config file.</p>
<pre><code class="lang-clojure"><span class="hljs-comment">;; sample-02.edn</span>
[<span class="hljs-symbol">:map</span>
 [<span class="hljs-symbol">:user</span>
  {<span class="hljs-symbol">:error/message</span> <span class="hljs-string">"At least 5 characters long"</span>
   <span class="hljs-symbol">:default</span> #env USER
   <span class="hljs-symbol">:min</span> <span class="hljs-number">5</span>}
  <span class="hljs-symbol">:string</span>]
 [<span class="hljs-symbol">:shell</span>
  [<span class="hljs-symbol">:re</span>
   {<span class="hljs-symbol">:error/message</span> <span class="hljs-string">"String needs to end with `zsh`"</span>
    <span class="hljs-symbol">:default</span> #env SHELL}
   <span class="hljs-string">"^.*zsh$"</span>]]
 [<span class="hljs-symbol">:features</span>
  {<span class="hljs-symbol">:default</span> <span class="hljs-string">"foo,bar,bizz"</span>}
  [<span class="hljs-symbol">:set</span> <span class="hljs-symbol">:string</span>]]]
</code></pre>
<p>Now, we have the schema with default values.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">aero/read-config</span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"sample-02.edn"</span>))
<span class="hljs-comment">;; =&gt; </span>
[<span class="hljs-symbol">:map</span>
 [<span class="hljs-symbol">:user</span>
  {<span class="hljs-symbol">:error/message</span> <span class="hljs-string">"At least 5 characters long"</span>
   <span class="hljs-symbol">:default</span> <span class="hljs-string">"tvaisanen"</span>
   <span class="hljs-symbol">:min</span> <span class="hljs-number">5</span>}
  <span class="hljs-symbol">:string</span>]
 [<span class="hljs-symbol">:shell</span>
  [<span class="hljs-symbol">:re</span>
   {<span class="hljs-symbol">:error/message</span> <span class="hljs-string">"String needs to end with `zsh`"</span>
    <span class="hljs-symbol">:default</span> <span class="hljs-string">"/bin/zsh"</span>}
   <span class="hljs-string">"^.*zsh$"</span>]]
 [<span class="hljs-symbol">:features</span> {<span class="hljs-symbol">:default</span> <span class="hljs-string">"foo,bar,bizz"</span>} [<span class="hljs-symbol">:set</span> <span class="hljs-symbol">:string</span>]]]
</code></pre>
<p>The last remaining step is to transform this into a configuration map. We'll do this by creating a transformer using the <code>default-value-transformer</code> .</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">defaults-transformer</span>
  (<span class="hljs-name">mt/default-value-transformer</span>
   {<span class="hljs-symbol">:key</span> <span class="hljs-symbol">:value</span>
    <span class="hljs-symbol">:defaults</span> {<span class="hljs-symbol">:map</span>    (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> {})
               <span class="hljs-symbol">:string</span> (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> <span class="hljs-string">""</span>)
               <span class="hljs-symbol">:vector</span> (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> [])}}))
</code></pre>
<p>We must define defaults for maps to populate nested maps in the configuration. Depending on your case, doing the same for vectors or even strings might be necessary.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">m/decode</span> [<span class="hljs-symbol">:map</span>
           [<span class="hljs-symbol">:int</span> {<span class="hljs-symbol">:value</span> <span class="hljs-number">1</span>} <span class="hljs-symbol">:int</span>]
           [<span class="hljs-symbol">:map</span> <span class="hljs-symbol">:map</span>]
           [<span class="hljs-symbol">:nested-map</span> [<span class="hljs-symbol">:map</span> [<span class="hljs-symbol">:a</span> <span class="hljs-symbol">:string</span>]]]
           [<span class="hljs-symbol">:vector</span> [<span class="hljs-symbol">:vector</span> <span class="hljs-symbol">:any</span>]]
           [<span class="hljs-symbol">:string</span> <span class="hljs-symbol">:string</span>]]
          <span class="hljs-literal">nil</span>
          defaults-transformer)
<span class="hljs-comment">;; =&gt;</span>
{<span class="hljs-symbol">:int</span> <span class="hljs-number">1</span>
 <span class="hljs-symbol">:map</span> {}
 <span class="hljs-symbol">:nested-map</span> {<span class="hljs-symbol">:a</span> <span class="hljs-string">""</span>}
 <span class="hljs-symbol">:vector</span> []
 <span class="hljs-symbol">:string</span> <span class="hljs-string">""</span>}
</code></pre>
<p>Then, we use this to load our configuration schema with the defaults.</p>
<pre><code class="lang-clojure">
(<span class="hljs-keyword">defn</span> <span class="hljs-title">read-config</span>
  ([filename]
   (<span class="hljs-name">read-config</span> filename {<span class="hljs-symbol">:transformer</span> transformer}))
  ([filename {<span class="hljs-symbol">:keys</span> [transformer]}]
   (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [schema-data (<span class="hljs-name">aero/read-config</span> (<span class="hljs-name">io/resource</span> filename))
         schema (<span class="hljs-name"><span class="hljs-builtin-name">try</span></span> (<span class="hljs-name">m/schema</span> schema-data)
                     (<span class="hljs-name">catch</span> Exception e
                       (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"Invalid configuration schema"</span>
                                       {<span class="hljs-symbol">:malli/error</span> (<span class="hljs-name">ex-data</span> e)}))))
         config (<span class="hljs-name">m/decode</span> schema <span class="hljs-literal">nil</span> defaults-transformer)]
     (<span class="hljs-name"><span class="hljs-builtin-name">when-not</span></span> (<span class="hljs-name">m/validate</span> schema config)
       (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"Invalid configuration"</span>
                       {<span class="hljs-symbol">:value</span> config
                        <span class="hljs-symbol">:error</span>  (<span class="hljs-name">me/humanize</span> (<span class="hljs-name">m/explain</span> schema config))})))
     config)))
</code></pre>
<p>The last problem we need to solve is including the decoder functions.</p>
<pre><code class="lang-clojure"><span class="hljs-number">1</span>. Unhandled clojure.lang.ExceptionInfo
   Invalid configuration
   {<span class="hljs-symbol">:value</span> {<span class="hljs-symbol">:user</span> <span class="hljs-string">"tvaisanen"</span>, <span class="hljs-symbol">:shell</span> <span class="hljs-string">"/bin/zsh"</span>, <span class="hljs-symbol">:features</span> <span class="hljs-string">"foo,bar,bizz"</span>},
    <span class="hljs-symbol">:error</span> {<span class="hljs-symbol">:features</span> [<span class="hljs-string">"invalid type"</span>]}}
                      REPL:  <span class="hljs-number">158</span>  core/read-config
                      REPL:  <span class="hljs-number">148</span>  core/read-config
                      REPL:  <span class="hljs-number">167</span>  core/eval<span class="hljs-number">17934</span>
</code></pre>
<h2 id="heading-extend-aero-readers">Extend Aero Readers</h2>
<p>Let's create our reader <code>#decoder ...</code> to reference the custom decoder functions.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[aero.core <span class="hljs-symbol">:as</span> aero]

(<span class="hljs-keyword">defmethod</span> <span class="hljs-title">aero/reader</span> 'decoder
  [opts tag symbol-pointing-to-a-fn]
  (<span class="hljs-name"><span class="hljs-builtin-name">if-let</span></span> [decoder-fn (<span class="hljs-name"><span class="hljs-builtin-name">resolve</span></span> symbol-pointing-to-a-fn)]
    decoder-fn
    (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"Can't load decoder function"</span>
                    {<span class="hljs-symbol">:fn</span> symbol-pointing-to-a-fn}))))
</code></pre>
<p>Now, we can add <code>#decoder config/decode-set-of-strings</code> to the schema file.</p>
<pre><code class="lang-clojure">[<span class="hljs-symbol">:map</span>
 [<span class="hljs-symbol">:user</span>
  {<span class="hljs-symbol">:doc</span> <span class="hljs-string">"At least 5 characters long"</span>
   <span class="hljs-symbol">:value</span> #env USER
   <span class="hljs-symbol">:min</span> <span class="hljs-number">5</span>}
  <span class="hljs-symbol">:string</span>]
 [<span class="hljs-symbol">:shell</span>
  [<span class="hljs-symbol">:re</span>
   {<span class="hljs-symbol">:error/message</span> <span class="hljs-string">"String needs to end with `zsh`"</span>
    <span class="hljs-symbol">:value</span> #env SHELL}
   <span class="hljs-string">"^.*zsh$"</span>]]
 [<span class="hljs-symbol">:features</span>
  {<span class="hljs-symbol">:value</span> <span class="hljs-string">"foo,bar,bizz"</span>
   <span class="hljs-symbol">:decode/string</span> #decoder config/decode-set-of-strings}
  [<span class="hljs-symbol">:set</span> <span class="hljs-symbol">:string</span>]]]
</code></pre>
<p>And that's it! We can now load the configuration from the schema file by decoding the values.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">read-config</span> <span class="hljs-string">"sample-02.edn"</span>)
<span class="hljs-comment">;; =&gt; </span>
{<span class="hljs-symbol">:user</span> <span class="hljs-string">"tvaisanen"</span>
 <span class="hljs-symbol">:shell</span> <span class="hljs-string">"/bin/zsh"</span>
 <span class="hljs-symbol">:features</span> #{<span class="hljs-string">"foo"</span> <span class="hljs-string">"bar"</span> <span class="hljs-string">"bizz"</span>}}
</code></pre>
<h2 id="heading-the-final-result">The Final Result</h2>
<p>Here's everything put together.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> config
  (<span class="hljs-symbol">:require</span> [aero.core <span class="hljs-symbol">:as</span> aero]
            [malli.core <span class="hljs-symbol">:as</span> m]
            [malli.transform <span class="hljs-symbol">:as</span> mt]
            [malli.error <span class="hljs-symbol">:as</span> me]
            [clojure.java.io <span class="hljs-symbol">:as</span> io]))

(<span class="hljs-keyword">def</span> <span class="hljs-title">defaults-transformer</span>
  (<span class="hljs-name">mt/transformer</span>
   (<span class="hljs-name">mt/default-value-transformer</span>
    <span class="hljs-comment">;; look default value under the key `value`</span>
    {<span class="hljs-symbol">:key</span> <span class="hljs-symbol">:value</span>
     <span class="hljs-symbol">:defaults</span> {<span class="hljs-symbol">:map</span>    (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> {})
                <span class="hljs-symbol">:string</span> (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> <span class="hljs-string">""</span>)
                <span class="hljs-symbol">:vector</span> (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> [])}})
   <span class="hljs-comment">;; use string-transformer to "enable" string decoders</span>
   (<span class="hljs-name">mt/string-transformer</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">decode-set-of-strings</span> [string-value]
  (<span class="hljs-name">set</span> (<span class="hljs-name">clojure.string/split</span> string-value #<span class="hljs-string">","</span>)))

(<span class="hljs-keyword">defmethod</span> <span class="hljs-title">aero/reader</span> 'decoder
  [opts tag symbol-pointing-to-a-fn]
  (<span class="hljs-name"><span class="hljs-builtin-name">if-let</span></span> [decoder-fn (<span class="hljs-name"><span class="hljs-builtin-name">resolve</span></span> symbol-pointing-to-a-fn)]
    decoder-fn
    (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"Can't load decoder function"</span>
                    {<span class="hljs-symbol">:fn</span> symbol-pointing-to-a-fn}))))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">read-config</span>
  ([filename]
   (<span class="hljs-name">read-config</span> filename {<span class="hljs-symbol">:transformer</span> defaults-transformer}))
  ([filename {<span class="hljs-symbol">:keys</span> [transformer]}]
   (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [schema-data (<span class="hljs-name"><span class="hljs-builtin-name">eval</span></span> (<span class="hljs-name">aero/read-config</span> (<span class="hljs-name">io/resource</span> filename)))
         schema (<span class="hljs-name"><span class="hljs-builtin-name">try</span></span> (<span class="hljs-name">m/schema</span> schema-data)
                     (<span class="hljs-name">catch</span> Exception e
                       (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"Invalid configuration schema"</span>
                                       {<span class="hljs-symbol">:malli/error</span> (<span class="hljs-name">ex-data</span> e)}))))
         config (<span class="hljs-name">m/decode</span> schema <span class="hljs-literal">nil</span> transformer)]
     (<span class="hljs-name"><span class="hljs-builtin-name">when-not</span></span> (<span class="hljs-name">m/validate</span> schema config)
       (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"Invalid configuration"</span>
                       {<span class="hljs-symbol">:value</span> config
                        <span class="hljs-symbol">:error</span>  (<span class="hljs-name">me/humanize</span> (<span class="hljs-name">m/explain</span> schema config))})))
     config)))

(<span class="hljs-name">read-config</span> <span class="hljs-string">"sample-02.edn"</span>)
<span class="hljs-comment">;; =&gt; </span>
{<span class="hljs-symbol">:user</span> <span class="hljs-string">"tvaisanen"</span> 
 <span class="hljs-symbol">:shell</span> <span class="hljs-string">"/bin/zsh"</span> 
 <span class="hljs-symbol">:features</span> #{<span class="hljs-string">"foo"</span> <span class="hljs-string">"bar"</span> <span class="hljs-string">"bizz"</span>}}
</code></pre>
<h2 id="heading-conclusions">Conclusions</h2>
<p>The benefits of having the validation and the configuration in the same file are that everything is in the same place, including the definition and the validation. It also makes it easier to maintain consistent validation practices since Malli takes care of the validation process. But all of this can make the configurations more difficult to read, which might be a problem, especially if the files need to be understood by people who don't know Clojure, for example, in DevOps.</p>
<p>If your project is already all in on Malli, this makes sense. The developers should already be familiar with the schema syntax, so there shouldn't be too much adoption friction.</p>
<p>I've never used this approach in production, but I'll try it out next time I need to configure a new project.</p>
<p>Once again, thanks for reading.</p>
<p>Feel free to reach out and let me know what you think—social links in the menu.</p>
]]></content:encoded></item><item><title><![CDATA[Why Clojure, From Principal Scientist to a Clojure Consultant]]></title><description><![CDATA[Starting a podcast has been on my to-do list for a while, and I'm happy to announce that I've finally taken the first steps.
My friend and colleague from Metosin, Martin Varela, was happy to jump in front of the camera to capture the following conver...]]></description><link>https://tonitalksdev.com/podcast-why-clojure-with-martin-varela</link><guid isPermaLink="true">https://tonitalksdev.com/podcast-why-clojure-with-martin-varela</guid><category><![CDATA[Clojure]]></category><category><![CDATA[software development]]></category><category><![CDATA[podcast]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Wed, 24 Jul 2024 09:31:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726322027623/c59392ff-44fb-48cd-aab9-57845a9e8b81.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Starting a podcast has been on my to-do list for a while, and I'm happy to announce that I've finally taken the first steps.</p>
<p>My friend and colleague from Metosin, Martin Varela, was happy to jump in front of the camera to capture the following conversation without hesitation.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/TpG6EMZH6JA?si=WhahDUCWr3aYgnQ1">https://youtu.be/TpG6EMZH6JA?si=WhahDUCWr3aYgnQ1</a></div>
<p> </p>
<p>I hope you found this interesting, and thank you for reading, watching, and listening.</p>
<p>Feel free to reach out and let me know what you think—social links in the menu.</p>
<p><a target="_blank" href="https://open.spotify.com/episode/4AMgfAzB36eMWfG4LEWibX?si=IxuTC0upRMmkBkPM31jsFw">Listen on Spotify</a></p>
<p>Podcast: <a target="_blank" href="https://www.youtube.com/channel/UCeeNFX11N2yq8--N_fNRZHw">YouTube</a> / <a target="_blank" href="https://open.spotify.com/show/0qR3Ho0NGO6MjPVngZK6ET?si=5a93f275e40b4ce5">Spotify</a></p>
]]></content:encoded></item><item><title><![CDATA[How to Use Test Mocks and Fixtures In Clojure]]></title><description><![CDATA[After we've learned unit testing, at some point, the complexity of the software grows enough for us to reach out to mock functions and set up test fixtures. Mocks are often needed when you have external services that are unavailable under your contro...]]></description><link>https://tonitalksdev.com/how-to-use-test-mocks-and-fixtures-in-clojure</link><guid isPermaLink="true">https://tonitalksdev.com/how-to-use-test-mocks-and-fixtures-in-clojure</guid><category><![CDATA[Clojure]]></category><category><![CDATA[Testing]]></category><category><![CDATA[TDD (Test-driven development)]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Fri, 28 Jun 2024 07:11:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713465357948/8999aa7d-1675-4b60-b84f-5190ef2bca53.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After we've learned unit testing, at some point, the complexity of the software grows enough for us to reach out to mock functions and set up test fixtures. Mocks are often needed when you have external services that are unavailable under your control during the testing, or they are not practical to run locally for the tests. One example could be an authentication service that returns a user profile.</p>
<p>This post is a follow-up to an earlier post where we set up a project for testing. Follow up on the project setup from <a target="_blank" href="https://tonitalksdev.com/how-to-get-started-with-tdd-in-clojure">here</a> if you haven't already done so. Once again, this post is not about what you should test but to show you the bare minimum of creating mocks and fixtures to get you started.</p>
<p>We'll cover some simple examples to get you started <code>with-redefs</code> to mock functions and variables and <code>use-fixtures</code> to configure test fixtures. Let's start with the first one.</p>
<h2 id="heading-mocking-functions-and-variables">Mocking Functions and Variables</h2>
<p>Clojure has a core feature macro <code>with-redefs</code> that can temporarily override a var (function or variable). Within the <code>with-redefs</code> closure, the program will interpret the var as we've defined locally, and this is useful when mocking some functions or variables in tests.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">with-redefs</span> [var-to-override mock]
    <span class="hljs-comment">;; var-to-override is replaced with the mock</span>
    )
<span class="hljs-comment">;; var-to-override is interpreted normally again</span>
</code></pre>
<p>Let's see how to use this in a test setup by mocking the <code>multiply</code> function from the previous post as an example. Let's return the operation as a string instead of computing the value.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[clojure.test <span class="hljs-symbol">:as</span> t])

(<span class="hljs-keyword">defn</span> <span class="hljs-title">multiply-mock</span> [a b]
  (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> a <span class="hljs-string">" * "</span>  b <span class="hljs-string">" = ?"</span>))

(<span class="hljs-name">t/deftest</span> using-with-redefs
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"we can override multiply implementation"</span>
    (<span class="hljs-name">with-redefs</span> [sut/multiply multiply-mock]
      (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-string">"2 * 2 = ?"</span> (<span class="hljs-name">sut/multiply</span> <span class="hljs-number">2</span> <span class="hljs-number">2</span>))))))
</code></pre>
<p>The same approach works with other namespaces, not just with functions we have defined. For example, we could temporarily change the implementation of <code>clojure.edn/read-string</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">read-string-mock</span> []
  <span class="hljs-string">"are you trying to read EDN?"</span>)

(<span class="hljs-name">t/deftest</span> mock-edn
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"we can override multiply implementation"</span>
    (<span class="hljs-name">with-redefs</span> [clojure.edn/read-string read-string-mock]
      (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> {<span class="hljs-symbol">:a</span> <span class="hljs-number">1</span>} (<span class="hljs-name">clojure.edn/read-string</span> <span class="hljs-string">"{:a 1}"</span>))))))
</code></pre>
<pre><code class="lang-clojure">Fail in mock-edn
we can override multiply implementation

expected: {<span class="hljs-symbol">:a</span> <span class="hljs-number">1</span>}
  actual: <span class="hljs-string">"are you trying to read EDN?"</span>
</code></pre>
<p>In a real-world scenario, we might want to replace a function in a test that calls to an external service that we don't have a test environment or a process that takes time to complete. And that's pretty much what we need to know to get started with mocking using <code>with-redefs</code>. Let's take a look at setting up fixtures next.</p>
<h2 id="heading-test-fixtures">Test Fixtures</h2>
<p>What are text fixtures? Wikipedia defines them as follows.</p>
<blockquote>
<p>A test fixture is a device used to consistently test some item, device, or piece of software. Test fixtures are used in the testing of electronics, software and physical devices.</p>
</blockquote>
<p>In software, this usually means setting up an API, a database, or both so that the tests have a consistent environment in which to run.</p>
<blockquote>
<p>Fixtures allow you to run code before and after tests, to set up the context in which tests should be run.</p>
<p><a target="_blank" href="https://clojuredocs.org/clojure.test">ClojureDocs</a></p>
</blockquote>
<p>In Clojure, a fixture is just a function that we "connect" to the test execution with <code>clojure.test/use-fixtures</code> macro. We can wrap setup and teardown logic in this fixture function.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">logger-fixture</span> [test-fn]
  (<span class="hljs-name"><span class="hljs-builtin-name">prn</span></span> <span class="hljs-string">"start test"</span>)
  (<span class="hljs-name">test-fn</span>)
  (<span class="hljs-name"><span class="hljs-builtin-name">prn</span></span> <span class="hljs-string">"end test"</span>))

<span class="hljs-comment">; run once per namespace</span>
(<span class="hljs-name">t/use-fixtures</span> <span class="hljs-symbol">:once</span> logger-fixture)
<span class="hljs-comment">; run once pre deftest, with-test</span>
(<span class="hljs-name">t/use-fixtures</span> <span class="hljs-symbol">:each</span> logger-fixture)
</code></pre>
<p>Let's see how this works with an HTTP API.</p>
<p>We'll continue the setup from the last post and create a new namespace <code>api</code> for the API code to start testing with the tools we just learned.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> api
  (<span class="hljs-symbol">:require</span> [ring.adapter.jetty <span class="hljs-symbol">:as</span> jetty]))

(<span class="hljs-keyword">def</span> <span class="hljs-title">api-state</span> (<span class="hljs-name"><span class="hljs-builtin-name">atom</span></span> {}))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">read-database</span> []
  @api-state)

(<span class="hljs-keyword">defn</span> <span class="hljs-title">ring-handler</span> [{<span class="hljs-symbol">:keys</span> [method] <span class="hljs-symbol">:as</span> req}]
  {<span class="hljs-symbol">:status</span>  <span class="hljs-number">200</span>
   <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"Content-Type"</span> <span class="hljs-string">"application/edn"</span>}
   <span class="hljs-symbol">:body</span>    (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> (<span class="hljs-name">read-database</span>))})

(<span class="hljs-keyword">defn</span> <span class="hljs-title">start!</span> [opts]
  (<span class="hljs-name">jetty/run-jetty</span> #'ring-handler opts))
</code></pre>
<p>To test the API over HTTP, we need an HTTP client. Add <a target="_blank" href="https://clojars.org/hato">Hato</a> to your <code>deps.edn</code> as the client, we are ready to start testing. Let's write the first test without setting up a fixture.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> api-test
  (<span class="hljs-symbol">:require</span> [clojure.test <span class="hljs-symbol">:as</span> t]
            [api <span class="hljs-symbol">:as</span> sut]
            [hato.client <span class="hljs-symbol">:as</span> http]))

(<span class="hljs-name">t/deftest</span> test-api
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"API works"</span>
    (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-number">200</span> (<span class="hljs-symbol">:status</span> (<span class="hljs-name">http/get</span> <span class="hljs-string">"localhost:8080"</span>))))))
</code></pre>
<p>If we run the test before setting up the fixture, we should get an error since we are trying to call an HTTP server that is not running.</p>
<pre><code class="lang-clojure">Error in test-api
API works

expected: (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-number">200</span> (<span class="hljs-symbol">:status</span> (<span class="hljs-name">http/get</span> <span class="hljs-string">"http://localhost:8080"</span>)))
   error: java.net.ConnectException
</code></pre>
<p>To fix the problem, let's create a fixture around the tests that ensures the server is running and ready to serve requests when we run our tests.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> api-test
  (<span class="hljs-symbol">:require</span> [clojure.test <span class="hljs-symbol">:as</span> t]
            [api <span class="hljs-symbol">:as</span> sut]
            [hato.client <span class="hljs-symbol">:as</span> http]))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">api-fixture</span> [test-fn]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [<span class="hljs-comment">;; store the handle to the server to be able</span>
        <span class="hljs-comment">;; to stop the server after the test is run</span>
        server (<span class="hljs-name"><span class="hljs-builtin-name">atom</span></span> <span class="hljs-literal">nil</span>)]
    <span class="hljs-comment">;; setup test fixture by starting the server</span>
    (<span class="hljs-name"><span class="hljs-builtin-name">reset!</span></span> server (<span class="hljs-name">sut/start!</span> {<span class="hljs-symbol">:port</span> <span class="hljs-number">8080</span> <span class="hljs-symbol">:join?</span> <span class="hljs-literal">false</span>}))

    <span class="hljs-comment">;; run the test </span>
    (<span class="hljs-name">test-fn</span>)

    <span class="hljs-comment">;; teardown test fixture by stopping the server</span>
    (<span class="hljs-name">.stop</span> @server)))

(<span class="hljs-name">t/use-fixtures</span> <span class="hljs-symbol">:once</span> api-fixture)

(<span class="hljs-name">t/deftest</span> test-api
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"API works"</span>
    (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-number">200</span> (<span class="hljs-symbol">:status</span> (<span class="hljs-name">http/get</span> <span class="hljs-string">"localhost:8080"</span>))))))
</code></pre>
<p>And now we should have a passing test!</p>
<pre><code class="lang-clojure">Tested <span class="hljs-number">1</span> namespaces in <span class="hljs-number">61</span> ms
Ran <span class="hljs-number">1</span> assertions, in <span class="hljs-number">1</span> test functions
<span class="hljs-number">1</span> passed
cider-test-fail-fast: t
</code></pre>
<p>And that's it. Now, we have a working API fixture. The same logic applies to databases and other services.</p>
<h2 id="heading-combine-both-mocks-and-fixtures">Combine Both Mocks and Fixtures</h2>
<p>We can mix and match these tools to meet our needs. Let's continue on the API tests by validating that the API returns the expected value, which is an empty map.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">t/deftest</span> test-api
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"API works"</span>
    (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-number">200</span> (<span class="hljs-symbol">:status</span> (<span class="hljs-name">http/get</span> <span class="hljs-string">"http://localhost:8080"</span>)))))

  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"API state is initalized as an empty map"</span>
    (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [response (<span class="hljs-name">http/get</span> <span class="hljs-string">"http://localhost:8080"</span>
                             {<span class="hljs-symbol">:as</span> <span class="hljs-symbol">:clojure</span>})]
      (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> {} (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> response <span class="hljs-symbol">:body</span> ))))))
</code></pre>
<pre><code class="lang-clojure">Tested <span class="hljs-number">1</span> namespaces in <span class="hljs-number">13</span> ms
Ran <span class="hljs-number">2</span> assertions, in <span class="hljs-number">1</span> test functions
<span class="hljs-number">2</span> passed
cider-test-fail-fast: t
</code></pre>
<p>Let's say we wanted to replace the "database state." We could mock the value by using <code>with-redefs</code> by referring to it with <code>sut/api-state</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">t/testing</span> <span class="hljs-string">"API response is altered by with-redefs"</span>
    (<span class="hljs-name">with-redefs</span> [sut/api-state (<span class="hljs-name"><span class="hljs-builtin-name">atom</span></span> {<span class="hljs-symbol">:new</span> <span class="hljs-string">"state"</span>})]
      (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [response (<span class="hljs-name">http/get</span> <span class="hljs-string">"http://localhost:8080"</span>
                               {<span class="hljs-symbol">:as</span> <span class="hljs-symbol">:clojure</span>})]
        (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> {} (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> response <span class="hljs-symbol">:body</span>))))))
</code></pre>
<p>This time, running the test, we can see that the returning value is indeed the one defined in the <code>with-redefs</code> closure.</p>
<pre><code class="lang-clojure">api-test
<span class="hljs-number">1</span> non-passing tests:

Fail in test-api
API response is altered by with-redefs

expected: {}
  actual: {<span class="hljs-symbol">:new</span> <span class="hljs-string">"state"</span>}          

    diff: - <span class="hljs-literal">nil</span>          
          + {<span class="hljs-symbol">:new</span> <span class="hljs-string">"state"</span>}
</code></pre>
<p>Or, in case we wanted to mock the function reading the database, we could do the same for the <code>read-database</code> function.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">t/testing</span> <span class="hljs-string">"read-database handler is mocked by with-redefs"</span>
    (<span class="hljs-name">with-redefs</span> [sut/read-database (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> {<span class="hljs-symbol">:mocked</span> <span class="hljs-string">"value"</span>})]
      (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [response (<span class="hljs-name">http/get</span> api-url {<span class="hljs-symbol">:as</span> <span class="hljs-symbol">:clojure</span>})]
        (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> {} (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> response <span class="hljs-symbol">:body</span> ))))))
</code></pre>
<p>This time, we've successfully mocked the function that returns the state.</p>
<pre><code class="lang-clojure">Fail in test-api
read-database handler is mocked by with-redefs

expected: {}            
  actual: {<span class="hljs-symbol">:mocked</span> <span class="hljs-string">"value"</span>}          

    diff: - <span class="hljs-literal">nil</span>          
          + {<span class="hljs-symbol">:mocked</span> <span class="hljs-string">"value"</span>}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Testing in Clojure is simple and relatively painless. This is because the language provides the essential tools without the need to write boilerplate code to set up mocks and fixtures. The examples here are just toy examples. Still, they show how these tools work and how to get started.</p>
<p>I hope you found this helpful, and thank you for reading.</p>
<p>Feel free to reach out and let me know what you think—social links in the menu.</p>
]]></content:encoded></item><item><title><![CDATA[The Simplest Way to Deploy ClojureScript with Your API]]></title><description><![CDATA[This time, we'll take the API we built and set up a frontend React application with Helix in ClojureScript to be deployed with the API. We'll restructure the backend code and folder structure to keep the code base cleaner. So, bear with me first with...]]></description><link>https://tonitalksdev.com/the-simplest-way-to-deploy-clojurescript-with-your-api</link><guid isPermaLink="true">https://tonitalksdev.com/the-simplest-way-to-deploy-clojurescript-with-your-api</guid><category><![CDATA[Shadow CLJS]]></category><category><![CDATA[Clojure]]></category><category><![CDATA[ClojureScript]]></category><category><![CDATA[Docker]]></category><category><![CDATA[DigitalOcean]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Thu, 04 Jan 2024 17:46:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704390261789/986eacc6-d383-4545-9728-bf8e1ceb6a46.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This time, we'll take the API we built and set up a frontend React application with Helix in ClojureScript to be deployed with the API. We'll restructure the backend code and folder structure to keep the code base cleaner. So, bear with me first with the minor refactoring and setting up the scene on the API side. After this step, we can add server-side routing to serve the API and the static files. SPAs do routing in the front end, so we need to default to the <code>index.html</code> to enable refreshing the site and successfully loading the page. This also allows direct links to frontend paths to be handled correctly. We won't be touching on frontend routing this time around.</p>
<p>You can find the previous steps from the earlier posts in the <a target="_blank" href="https://tonitalksdev.com/series/digitalocean-app-build">Building on DigitalOcean App Platform</a> series.</p>
<h2 id="heading-refactoring-the-api">Refactoring the API</h2>
<p>First, let's update the source file structure from this</p>
<pre><code class="lang-bash">❯ tree src
src
└── main.clj
</code></pre>
<p>to this.</p>
<pre><code class="lang-bash">❯ tree src
src
└── clj
   └── api
      ├── db.clj
      └── main.clj
</code></pre>
<p>We'll add a new path <code>src/cljs</code> for the frontend-related code in a moment.</p>
<p>Let's continue where we left off last time after we created the migrations and moved the database-related code from the original namespace <code>main</code> to <code>api.db</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> api.db
  (<span class="hljs-symbol">:require</span> [migratus.core <span class="hljs-symbol">:as</span> migratus]
            [next.jdbc <span class="hljs-symbol">:as</span> jdbc]
            [next.jdbc.date-time]))

<span class="hljs-comment">;; Use hardcoded default for now.</span>
<span class="hljs-comment">;; Configuration management needs to dealt with later.</span>
(<span class="hljs-keyword">def</span> <span class="hljs-title">dev-jdbc-url</span>
  <span class="hljs-string">"jdbc:postgresql://localhost:5432/db?user=user&amp;password=password"</span>)

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-db-conf</span> []
  {<span class="hljs-symbol">:dbtype</span>  <span class="hljs-string">"postgres"</span>
   <span class="hljs-symbol">:jdbcUrl</span> (<span class="hljs-name"><span class="hljs-builtin-name">or</span></span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"JDBC_DATABASE_URL"</span>)
                dev-jdbc-url)})

(<span class="hljs-keyword">defn</span> <span class="hljs-title">migrations-config</span> []
  {<span class="hljs-symbol">:db</span>                   (<span class="hljs-name">get-db-conf</span>)
   <span class="hljs-symbol">:store</span>                <span class="hljs-symbol">:database</span>
   <span class="hljs-symbol">:migration-dir</span>        <span class="hljs-string">"migrations/"</span>
   <span class="hljs-symbol">:migration-table-name</span> <span class="hljs-string">"migrations"</span>
   <span class="hljs-symbol">:init-in-transaction?</span> <span class="hljs-literal">false</span>})

(<span class="hljs-keyword">defn</span> <span class="hljs-title">create-migration</span> [{<span class="hljs-symbol">:keys</span> [name]}]
  (<span class="hljs-name">migratus.core/create</span> (<span class="hljs-name">migrations-config</span>)
                        name))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">datasource</span> []
  (<span class="hljs-name">jdbc/get-datasource</span> (<span class="hljs-name">get-db-conf</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-migrations</span> []
  (<span class="hljs-name">jdbc/execute!</span> (<span class="hljs-name">datasource</span>) [<span class="hljs-string">"SELECT * FROM migrations"</span>]))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">run-migrations</span> []
  (<span class="hljs-name">migratus/migrate</span> (<span class="hljs-name">db/migrations-config</span>)))
</code></pre>
<p>And then use the <code>api.db</code> in <code>api.main</code> and migrate the rest of the code.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> api.main
  (<span class="hljs-symbol">:require</span> [ring.adapter.jetty <span class="hljs-symbol">:as</span> jetty]
            [reitit.ring <span class="hljs-symbol">:as</span> ring]
            [clojure.java.io <span class="hljs-symbol">:as</span> io]
            [clojure.string <span class="hljs-symbol">:as</span> str]
            [api.db <span class="hljs-symbol">:as</span> db])
  (<span class="hljs-symbol">:gen-class</span>))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-port</span> []
  (<span class="hljs-name">Integer/parseInt</span> (<span class="hljs-name"><span class="hljs-builtin-name">or</span></span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"PORT"</span>)
                        <span class="hljs-string">"8000"</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">app</span> [_request]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [migrations (<span class="hljs-name">db/get-migrations</span>)]
    {<span class="hljs-symbol">:status</span>  <span class="hljs-number">200</span>
     <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"Content-Type"</span> <span class="hljs-string">"application/edn"</span>}
     <span class="hljs-symbol">:body</span>    (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> migrations)}))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">-main</span> [&amp; _args]
  (<span class="hljs-name">db/run-migrations</span>)
  (<span class="hljs-name">jetty/run-jetty</span> #'app {<span class="hljs-symbol">:port</span> (<span class="hljs-name">get-port</span>)}))
</code></pre>
<p>One last thing: update the paths in <code>deps.edn</code> and the <code>:exec-fn</code> for <code>run</code> and <code>create-migration</code>.</p>
<pre><code class="lang-diff">modified   api/deps.edn
<span class="hljs-meta">@@ -1,4 +1,4 @@</span>
<span class="hljs-deletion">-{:paths ["src" "resources"]</span>
<span class="hljs-addition">+{:paths ["src/clj" "resources" "compiled-resources"]</span>

  :deps {org.clojure/clojure {:mvn/version "1.11.0"}
         ring/ring-core {:mvn/version "1.6.3"}
<span class="hljs-meta">@@ -10,7 +10,7 @@</span>

  :aliases {:run
            {:main-opts ["-m" "main"]
<span class="hljs-deletion">-            :exec-fn   main/-main}</span>
<span class="hljs-addition">+            :exec-fn   api.main/-main}</span>
<span class="hljs-meta">@@ -24,4 +25,4 @@</span>

            :create-migration
            {:exec-args {:name nil}
<span class="hljs-deletion">-            :exec-fn db/create-migration}}}</span>
<span class="hljs-addition">+            :exec-fn api.db/create-migration}}}</span>
</code></pre>
<p>Now, we are good to continue on the CLJS side of things.</p>
<h2 id="heading-configure-shadow-cljs">Configure Shadow CLJS</h2>
<p>We'll be using Shadow CLJS to compile the ClojureScript.</p>
<blockquote>
<p><code>shadow-cljs</code> <strong>provides everything you need to compile your ClojureScript projects with a focus on simplicity and ease of use. The provided build targets abstract away most of the manual configuration so that you only have to configure the essentials for your build. Each target provides optimal defaults for each environment and get an optimized experience during development and in release builds.</strong></p>
<p><a target="_blank" href="https://shadow-cljs.github.io/docs/UsersGuide.html#_introduction">Shadow-CLJS: UserGuide</a></p>
</blockquote>
<p>Shadow CLJS has a built-in development time HTTP server to serve the files, but we won't use it this time since we want to serve the files from our backend to keep the development setup as close as possible to the deployment configuration. But before getting there, we must first configure the build process, which has three steps:</p>
<ol>
<li><p>Configure Shadow CLJS</p>
</li>
<li><p>Define NPM dependencies</p>
</li>
<li><p>Write ClojureScript code</p>
</li>
</ol>
<h3 id="heading-1-configure-shadow-cljs">1. Configure Shadow CLJS</h3>
<p>Shadow CLJS is configured with <code>shadow-cljs.edn</code> file. It contains all of the ClojureScript-related configuration, dependencies, source file paths, build configurations, etc. I have added the <a target="_blank" href="https://docs.cider.mx/cider-nrepl/">cider/cider-nrepl</a> and <a target="_blank" href="https://github.com/binaryage/cljs-devtools">binaryage/devtools</a> for convenience, but they are not required for the build.</p>
<p>The configuration is minimal, with only the browser build target defined. This means that when we run <code>shadow-cljs compile/release app</code> we'll get a Javascript bundle meant to be used in a browser.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:source-paths</span> [<span class="hljs-string">"src/cljs"</span>]

 <span class="hljs-symbol">:dependencies</span> [[binaryage/devtools <span class="hljs-string">"0.9.7"</span>]
                [cider/cider-nrepl <span class="hljs-string">"0.36.0"</span>]
                <span class="hljs-comment">;; React wrapper</span>
                [lilactown/helix <span class="hljs-string">"0.1.5"</span>]]

 <span class="hljs-symbol">:builds</span> {<span class="hljs-symbol">:app</span> {<span class="hljs-symbol">:target</span>     <span class="hljs-symbol">:browser</span>
                <span class="hljs-symbol">:output-dir</span> <span class="hljs-string">"compiled-resources/public/js"</span>
                <span class="hljs-symbol">:asset-path</span> <span class="hljs-string">"/js"</span>
                <span class="hljs-symbol">:modules</span>    {<span class="hljs-symbol">:main</span> {<span class="hljs-symbol">:entries</span> [app.main]
                                    <span class="hljs-symbol">:init-fn</span> app.main/init}}
                <span class="hljs-symbol">:devtools</span>   {<span class="hljs-symbol">:preloads</span> [devtools.preload]
                             <span class="hljs-symbol">:after-load</span> app.main/init}}}}
</code></pre>
<p>The application <code>output-dir</code> is where the results will be compiled.</p>
<h3 id="heading-2-define-npm-dependencies">2. Define NPM Dependencies</h3>
<p>NPM dependencies are defined in the <code>package.json</code> file. It has the npm dependencies the project uses, and since we are configuring a Helix app, we'll need the <code>react</code> and <code>react-dom</code> as dependencies.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"web"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.1"</span>,
  <span class="hljs-attr">"private"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"shadow-cljs"</span>: <span class="hljs-string">"2.26.2"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"react"</span>: <span class="hljs-string">"^18.2.0"</span>,
    <span class="hljs-attr">"react-dom"</span>: <span class="hljs-string">"^18.2.0"</span>
  }
}
</code></pre>
<h3 id="heading-3-clojurescript-source">3. ClojureScript Source</h3>
<p>For this step, we only need some ClojureScript code to get started. Let's start with something simple and return to the application code later.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> app.main)

(<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> js/console log  <span class="hljs-string">"Hello, app.main!"</span>)
</code></pre>
<h3 id="heading-confirm-the-build-works">Confirm the Build Works</h3>
<p>Now that we have all the required steps completed let's compile the release build to see if we set up everything correctly.</p>
<pre><code class="lang-bash">❯ npx shadow-cljs release app

shadow-cljs - config: /home/tvaisanen/projects/digitalocean-clojure-minimal/api/shadow-cljs.edn
shadow-cljs - connected to server
[:app] Compiling ...
[:app] Build completed. (45 files, 1 compiled, 0 warnings, 1.25s)
</code></pre>
<pre><code class="lang-bash">❯ ls -ul compiled-resources/public/js

.rw-r--r--  175 tvaisanen  2 Jan 11:58 main.js
.rw-r--r-- 1.3k tvaisanen  2 Jan 11:58 manifest.edn
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> shadow$provide = {};
(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>)</span>{
<span class="hljs-meta">'use strict'</span>;<span class="hljs-comment">/*

 Copyright The Closure Library Authors.
 SPDX-License-Identifier: Apache-2.0
*/</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Hello, app.core!"</span>);
}).call(<span class="hljs-built_in">this</span>);
</code></pre>
<p>It looks like it's working as expected. We are ready to continue to the next phase of serving the files from our backend.</p>
<h2 id="heading-serving-the-spa">Serving the SPA</h2>
<p>In the previous step, we did not yet configure a way for the browser to load the built files. For this, we need an HTML file that loads the Javascript. So, let's create the file <code>resources/public/index.html</code> to do just that.</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/js/main.js"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>As you might have noticed, the index file has a relative path to the compiled resources, but our files are in <code>resources</code> and <code>compiled-resources</code>. We want to keep the static files separate from the build artifacts. This helps keep the build process cleaner since we do not need to think whether the compile-resources folder has development files. It'll be created in the build time and kept from version control.</p>
<pre><code class="lang-xml">❯ tree resources

resources
├── migrations
│  ├── 20231208151434-test.down.sql
│  └── 20231208151434-test.up.sql
└── public
   └── index.html

❯ tree compiled-resources

compiled-resources
└── public
   └── js
      ├── main.js
      └── manifest.edn
</code></pre>
<p>Effectively, we want to merge the <code>resources</code> and <code>compiled-resources</code> in the backend, when the files are served, their respective paths are seen by the browser like they are coming from the same path. We can do this by adding both <code>resources</code> and <code>compiled-resources</code> to the paths in <code>deps.edn</code>.</p>
<pre><code class="lang-diff">modified   api/deps.edn
<span class="hljs-meta">@@ -1,4 +1,4 @@</span>
<span class="hljs-deletion">-{:paths ["src/clj" "resources"]</span>
<span class="hljs-addition">+{:paths ["src/clj" "resources" "compiled-resources"]</span>
</code></pre>
<p>Then, we look at resources by their names in the backend.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[clojure.java.io <span class="hljs-symbol">:as</span> io])

(<span class="hljs-name"><span class="hljs-builtin-name">for</span></span> [resource (<span class="hljs-name"><span class="hljs-builtin-name">concat</span></span> (<span class="hljs-name">file-seq</span> (<span class="hljs-name">io/file</span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"public"</span>)))
                       (<span class="hljs-name">file-seq</span> (<span class="hljs-name">io/file</span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"public/js"</span>))))]
   (<span class="hljs-name">.getName</span> resource))
<span class="hljs-comment">;; =&gt; ("public" "index.html" "js" "main.js" "manifest.edn")</span>
</code></pre>
<p>As expected, we can find all of the files with their names. This enables us to create a resource handler for the API to serve both resource directories. Next, let's jump to the backend routing.</p>
<h3 id="heading-backend-routing">Backend Routing</h3>
<p>We'll be using <a target="_blank" href="https://cljdoc.org/d/metosin/reitit/0.7.0-alpha7/doc/introduction">metosin/reitit</a> for the backend routing. It also provides functions to serve the resources.</p>
<pre><code class="lang-diff">modified   api/deps.edn
<span class="hljs-meta">@@ -3,9 +3,10 @@</span>
<span class="hljs-deletion">-        migratus/migratus {:mvn/version "1.5.4"}}</span>
<span class="hljs-addition">+        migratus/migratus {:mvn/version "1.5.4"}</span>
<span class="hljs-addition">+        metosin/reitit {:mvn/version "0.7.0-alpha7"}}</span>
</code></pre>
<p>We need a couple of updates on the API code.</p>
<ol>
<li><p>Refactor the previous <code>app</code> to <code>/api/*</code> <code>handler</code></p>
</li>
<li><p>Configure the router to serve both the <code>api</code> and the static files.</p>
</li>
</ol>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> api.main
  (<span class="hljs-symbol">:require</span> [api.db <span class="hljs-symbol">:as</span> db]
            [ring.adapter.jetty <span class="hljs-symbol">:as</span> jetty]
            [reitit.ring <span class="hljs-symbol">:as</span> ring]
            [clojure.java.io <span class="hljs-symbol">:as</span> io]
            [clojure.string <span class="hljs-symbol">:as</span> str])
  (<span class="hljs-symbol">:gen-class</span>))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-port</span> []
  (<span class="hljs-name">Integer/parseInt</span> (<span class="hljs-name"><span class="hljs-builtin-name">or</span></span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"PORT"</span>)
                        <span class="hljs-string">"8000"</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">handler</span> [_request]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [migrations (<span class="hljs-name">db/get-migrations</span>)]
    {<span class="hljs-symbol">:status</span>  <span class="hljs-number">200</span>
     <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"Content-Type"</span> <span class="hljs-string">"application/edn"</span>}
     <span class="hljs-symbol">:body</span>    (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> migrations)}))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">index-handler</span>
  [_]
  {<span class="hljs-symbol">:status</span>  <span class="hljs-number">200</span>
   <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"content-type"</span> <span class="hljs-string">"text/html"</span>}
   <span class="hljs-symbol">:body</span>    (<span class="hljs-name">io/file</span> (<span class="hljs-name">io/resource</span> <span class="hljs-string">"public/index.html"</span>))})

(<span class="hljs-keyword">def</span> <span class="hljs-title">app</span>
  (<span class="hljs-name">ring/ring-handler</span>
   (<span class="hljs-name">ring/router</span>
    [[<span class="hljs-string">"/api"</span>
      [<span class="hljs-string">"*"</span> {<span class="hljs-symbol">:get</span> handler}]]
     [<span class="hljs-string">"/"</span> index-handler]
     [<span class="hljs-string">"/*"</span> (<span class="hljs-name">ring/create-resource-handler</span>
            {<span class="hljs-symbol">:not-found-handler</span>
             (<span class="hljs-name"><span class="hljs-builtin-name">fn</span></span> [{<span class="hljs-symbol">:keys</span> [uri] <span class="hljs-symbol">:as</span> r}]
               (<span class="hljs-name"><span class="hljs-builtin-name">if</span></span> (<span class="hljs-name">str/starts-with?</span> uri <span class="hljs-string">"/api"</span>)
                 {<span class="hljs-symbol">:status</span> <span class="hljs-number">404</span>}
                 (<span class="hljs-name">index-handler</span> r)))})]]
    {<span class="hljs-symbol">:conflicts</span> (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> <span class="hljs-literal">nil</span>)})
   (<span class="hljs-name">ring/create-default-handler</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">-main</span> [&amp; _args]
  (<span class="hljs-name">db/run-migrations</span>)
  (<span class="hljs-name">jetty/run-jetty</span> #'app {<span class="hljs-symbol">:port</span> (<span class="hljs-name">get-port</span>)}))
</code></pre>
<p>Let's break the router definition into parts to see what's happening. Our requirements, at the time being, for the routing are:</p>
<ol>
<li><p>to be able to serve both <code>api</code> and static assets.</p>
</li>
<li><p>return index.html if the static asset is not found</p>
</li>
<li><p>serve the App from the root</p>
</li>
</ol>
<pre><code class="lang-clojure">(<span class="hljs-name">ring/router</span>
    [<span class="hljs-comment">;; serve API</span>
     [<span class="hljs-string">"/api"</span> 
      [<span class="hljs-string">"*"</span> handler]]
     <span class="hljs-comment">;; serve app from root</span>
     [<span class="hljs-string">"/"</span> index-handler]
     <span class="hljs-comment">;; in other case serve static</span>
     [<span class="hljs-string">"/*"</span> (<span class="hljs-name">ring/create-resource-handler</span>
            {<span class="hljs-symbol">:not-found-handler</span>
             (<span class="hljs-name"><span class="hljs-builtin-name">fn</span></span> [{<span class="hljs-symbol">:keys</span> [uri] <span class="hljs-symbol">:as</span> r}]
               (<span class="hljs-name"><span class="hljs-builtin-name">if</span></span> (<span class="hljs-name">str/starts-with?</span> uri <span class="hljs-string">"/api"</span>)
                 <span class="hljs-comment">;; if the uri is an API path</span>
                 <span class="hljs-comment">;; serve NOT FOUND</span>
                 {<span class="hljs-symbol">:status</span> <span class="hljs-number">404</span>}
                 <span class="hljs-comment">;; every other case serve the app</span>
                 <span class="hljs-comment">;; so that FE routing can try to </span>
                 <span class="hljs-comment">;; resolve the path</span>
                 (<span class="hljs-name">index-handler</span> r)))})]]
    {<span class="hljs-symbol">:conflicts</span> (<span class="hljs-name"><span class="hljs-builtin-name">constantly</span></span> <span class="hljs-literal">nil</span>)})
</code></pre>
<p>Let's add a <code>dev/clj/user.clj</code> namespace to start the server via REPL</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> user
  (<span class="hljs-symbol">:require</span> [api.main <span class="hljs-symbol">:as</span> main]))

(<span class="hljs-keyword">def</span> <span class="hljs-title">server</span> (<span class="hljs-name"><span class="hljs-builtin-name">atom</span></span> <span class="hljs-literal">nil</span>))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">start!</span> []
  (<span class="hljs-name"><span class="hljs-builtin-name">reset!</span></span> server
          (<span class="hljs-name">main/start!</span> {<span class="hljs-symbol">:port</span>  <span class="hljs-number">8000</span>
                        <span class="hljs-symbol">:join?</span> <span class="hljs-literal">false</span>})))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">stop!</span> []
  (<span class="hljs-name">.stop</span> @server))

(<span class="hljs-comment">comment</span>
  (<span class="hljs-name">start!</span>)
  (<span class="hljs-name">stop!</span>))
</code></pre>
<p>And create an alias to load the <code>user</code> namespace by default.</p>
<pre><code class="lang-diff">modified   api/deps.edn
<span class="hljs-deletion">- :aliases {:run</span>
<span class="hljs-addition">+ :aliases {:dev</span>
<span class="hljs-addition">+           {:extra-paths ["env/clj"]</span>
<span class="hljs-addition">+            :ns-default user}</span>
</code></pre>
<pre><code class="lang-bash">❯ clj -M:dev
2024-01-02 13:41:03.950:INFO::main: Logging initialized @670ms
Clojure 1.11.0

user=&gt; (start!)

Jan 02, 2024 1:41:07 PM clojure.tools.logging<span class="hljs-variable">$eval2887</span><span class="hljs-variable">$fn__2890</span> invoke
INFO: Starting migrations
Jan 02, 2024 1:41:07 PM clojure.tools.logging<span class="hljs-variable">$eval2887</span><span class="hljs-variable">$fn__2890</span> invoke
INFO: Ending migrations
2024-01-02 13:41:07.296:INFO:oejs.Server:main: jetty-9.2.21.v20170120
2024-01-02 13:41:07.305:INFO:oejs.ServerConnector:main: Started ServerConnector@2c6f022d{HTTP/1.1}{0.0.0.0:8000}
2024-01-02 13:41:07.306:INFO:oejs.Server:main: Started @4026ms
<span class="hljs-comment">#object[org.eclipse.jetty.server.Server 0x2e7e4480 "org.eclipse.jetty.server.Server@2e7e4480"]</span>
</code></pre>
<p>Now, we should be able to fetch the static resources</p>
<pre><code class="lang-http">❯ http :8000/
HTTP/1.1 <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Length</span>: 307
<span class="hljs-attribute">Content-Type</span>: text/html
<span class="hljs-attribute">Date</span>: Tue, 02 Jan 2024 11:44:41 GMT
<span class="hljs-attribute">Server</span>: Jetty(9.2.21.v20170120)

<span class="solidity"><span class="hljs-operator">&lt;</span><span class="hljs-operator">!</span>DOCTYPE html<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span>html<span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span>head<span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>meta charset<span class="hljs-operator">=</span><span class="hljs-string">"UTF-8"</span> <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>meta name<span class="hljs-operator">=</span><span class="hljs-string">"viewport"</span> content<span class="hljs-operator">=</span><span class="hljs-string">"width=device-width, initial-scale=1"</span> <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>head<span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span>body<span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>div id<span class="hljs-operator">=</span><span class="hljs-string">"app"</span> <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>script src<span class="hljs-operator">=</span><span class="hljs-string">"/js/main.js"</span> <span class="hljs-keyword">type</span><span class="hljs-operator">=</span><span class="hljs-string">"text/javascript"</span><span class="hljs-operator">&gt;</span><span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>script<span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>script<span class="hljs-operator">&gt;</span>
      app.core.init();
    <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>script<span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>body<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>html<span class="hljs-operator">&gt;</span></span>
</code></pre>
<p>and access the API endpoints via HTTP.</p>
<pre><code class="lang-http">❯ http :8000/api/
HTTP/1.1 <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span>: application/edn
<span class="hljs-attribute">Date</span>: Tue, 02 Jan 2024 11:46:00 GMT
<span class="hljs-attribute">Server</span>: Jetty(9.2.21.v20170120)
<span class="hljs-attribute">Transfer-Encoding</span>: chunked

<span class="ruby">[{<span class="hljs-symbol">:migrations/id</span> <span class="hljs-number">20231208151434</span>, 
  <span class="hljs-symbol">:migrations/applied</span> <span class="hljs-comment">#inst "2024-01-01T09:17:26.936000000-00:00", </span>
  <span class="hljs-symbol">:migrations/description</span> <span class="hljs-string">"test"</span>}]</span>
</code></pre>
<p>And that's a wrap on the backend side.</p>
<h3 id="heading-frontend-code">Frontend Code</h3>
<p>Finally, we can set up the React side of the application. Let's start by adding a <code>utils</code> namespace and define some utilities for rendering the application.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> app.utils
  (<span class="hljs-symbol">:require</span> [helix.core <span class="hljs-symbol">:refer</span> [$]]
            [<span class="hljs-string">"react"</span> <span class="hljs-symbol">:as</span> react]
            [<span class="hljs-string">"react-dom/client"</span> <span class="hljs-symbol">:as</span> rdom]))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">app-container</span> []
  (<span class="hljs-name">js/document.getElementById</span> <span class="hljs-string">"app"</span>))

(<span class="hljs-keyword">defonce</span> <span class="hljs-title">root</span> (<span class="hljs-name"><span class="hljs-builtin-name">atom</span></span> <span class="hljs-literal">nil</span>))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">react-root</span> []
  (<span class="hljs-name"><span class="hljs-builtin-name">when-not</span></span> @root
    (<span class="hljs-name"><span class="hljs-builtin-name">reset!</span></span> root (<span class="hljs-name">rdom/createRoot</span> (<span class="hljs-name">app-container</span>))))
  @root)

(<span class="hljs-keyword">defn</span> <span class="hljs-title">render</span>
  [App]
  (<span class="hljs-name">.render</span> (<span class="hljs-name">react-root</span>)
           ($ react/StrictMode
              ($ App))))
</code></pre>
<p>Next, use the utils to render an App that renders the API response in the DOM.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> app.main
  (<span class="hljs-symbol">:require</span> [app.utils <span class="hljs-symbol">:as</span> utils]
            [cljs.pprint <span class="hljs-symbol">:refer</span> [pprint]]
            [clojure.edn <span class="hljs-symbol">:as</span> edn]
            [helix.core <span class="hljs-symbol">:refer</span> [defnc]]
            [helix.dom <span class="hljs-symbol">:as</span> d]
            [helix.hooks <span class="hljs-symbol">:as</span> hooks]))

(<span class="hljs-name">defnc</span> App []
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [[data set-data] (<span class="hljs-name">hooks/use-state</span> {})]
    (<span class="hljs-name">hooks/use-effect</span>
     <span class="hljs-symbol">:once</span>
     (<span class="hljs-name"><span class="hljs-builtin-name">-&gt;</span></span> (<span class="hljs-name">js/fetch</span> <span class="hljs-string">"/api"</span>)
         (<span class="hljs-name">.then</span> (<span class="hljs-name"><span class="hljs-builtin-name">fn</span></span> [res] (<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> res text)))
         (<span class="hljs-name">.then</span> (<span class="hljs-name"><span class="hljs-builtin-name">fn</span></span> [response-string]
                  (<span class="hljs-name">set-data</span> (<span class="hljs-name">edn/read-string</span> response-string))))))
    (<span class="hljs-name">d/div</span>
     (<span class="hljs-name">d/div</span> <span class="hljs-string">"App Here"</span>)
     (<span class="hljs-name">d/pre</span> (<span class="hljs-name">with-out-str</span>
              (<span class="hljs-name">pprint</span> data))))))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">init</span> []
  (<span class="hljs-name">utils/render</span> App))
</code></pre>
<h2 id="heading-development-setup">Development Setup</h2>
<p>For development time, we can start the CLJS compiler in watch mode to get the updated code in the browser whenever the source code changes.</p>
<pre><code class="lang-clojure">npx shadow-cljs watch app
</code></pre>
<p>After this step, start the API with the <code>:dev</code> profile.</p>
<pre><code class="lang-clojure">❯ clj -M<span class="hljs-symbol">:dev</span>
<span class="hljs-number">2024</span><span class="hljs-number">-01</span><span class="hljs-number">-02</span> <span class="hljs-number">13</span>:<span class="hljs-number">41</span>:<span class="hljs-number">03.950</span><span class="hljs-symbol">:INFO::main:</span> Logging initialized @<span class="hljs-number">670</span>ms
Clojure <span class="hljs-number">1.11</span>.<span class="hljs-number">0</span>
user=&gt; (<span class="hljs-name">start!</span>)
</code></pre>
<p>This should be enough to get us started on writing code for the frontend. Shadow CLJS also provides a CLJS REPL in the browser for us to connect to, but that's out of the scope of this post. Read how to do this from <a target="_blank" href="https://shadow-cljs.github.io/docs/UsersGuide.html#_repl_2">the docs</a>, or let me know if that'd be something you're interested in learning.</p>
<h2 id="heading-deployment">Deployment</h2>
<p>We'll package the CLJS build into our Dockerfile so that the deployment process will be the same as previously. The only change is that we use the new Dockerfile for the build.</p>
<p>Let's create a new <code>api/Dockerfile</code> to build the frontend code as a separate step,</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> clojure:openjdk-<span class="hljs-number">17</span>-tools-deps-alpine AS frontend-build
<span class="hljs-keyword">RUN</span><span class="bash"> apk add --update nodejs npm</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash">  . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npx shadow-cljs release app</span>

<span class="hljs-comment"># Use a base without node dependencies for serving</span>
<span class="hljs-keyword">FROM</span> clojure:openjdk-<span class="hljs-number">17</span>-tools-deps-alpine
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> ls /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=frontend-build /app/compiled-resources /app/compiled-resources</span>
<span class="hljs-keyword">RUN</span><span class="bash"> clojure -P</span>
<span class="hljs-keyword">CMD</span><span class="bash"> clojure -X:run</span>
</code></pre>
<p>update the <code>api/docker-compose.yml</code>,</p>
<pre><code class="lang-diff">modified   api/docker-compose.yml
@@ -5,7 +5,7 @@ services:
   api:
     build:
       context: .
<span class="hljs-deletion">-      dockerfile: docker/dev.Dockerfile</span>
<span class="hljs-addition">+      dockerfile: Dockerfile</span>
       tags:
         - "registry.digitalocean.com/clojure-sample-app/dev"
     environment:
</code></pre>
<p>and then build, start the service, and confirm the build works as expected.</p>
<pre><code class="lang-clojure">docker-compose up --build
</code></pre>
<p>Navigate to <code>localhost:8000</code> to ensure the app is rendering. You can also see the migrations vector rendered on the screen.</p>
<p>If you haven't followed along with the previous posts, you can find the deployment instructions <a target="_blank" href="https://tonitalksdev.com/deploying-clojure-like-a-seasoned-hobbyist?source=more_series_bottom_blogs#heading-digitalocean-and-terraform">here</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>There are multiple ways to deploy frontend applications. You could bake the static files into a container with a web server like NGINX, serve the files from a bucket with CDN, use the DigitalOcean static App, use a service like Netlify, or host the static files from your GitHub pages. But if you have an API component in your App. It makes sense to pair the frontend code tightly with the API to make the deployment phase more robust. What do I mean by that in this context? If we have the exact version of the frontend shipped with the API itself, there's no room for version mismatch.</p>
<p>Once again, thanks for reading; I hope you found this helpful. Here's the accompanying GitHub <a target="_blank" href="https://github.com/tvaisanen/digitalocean-terraform-clojure-template/tree/blog-post-series-04">repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[DigitalOcean App Platform and Database Migrations]]></title><description><![CDATA[This time, we'll continue working on the application from the previous posts by configuring PostgreSQL database migrations. We are not going to go into data modeling just yet. Instead, we are making sure that everything is ready for the time we're go...]]></description><link>https://tonitalksdev.com/digitalocean-app-platform-database-migrations</link><guid isPermaLink="true">https://tonitalksdev.com/digitalocean-app-platform-database-migrations</guid><category><![CDATA[Clojure]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[DigitalOcean]]></category><category><![CDATA[database migrations]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Wed, 13 Dec 2023 05:07:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1702408148869/c3b7c7c0-3029-4ad1-8618-7855f5b3d3ab.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This time, we'll continue working on the application from the previous posts by configuring PostgreSQL database migrations. We are not going to go into data modeling just yet. Instead, we are making sure that everything is ready for the time we're going to start working on the data schemas. By the end of all of this, we should have a deployed API that can fetch the content of the migrations table from the DigitalOcean apps development database.</p>
<p>This is the third post on a series building on DigitalOcean. Read the previous posts in the <a target="_blank" href="https://tonitalksdev.com/series/digitalocean-app-build">series</a> to catch up on the context if you haven't done so already.</p>
<p>So, without further ado, let's get to the topic and configure <a target="_blank" href="https://github.com/yogthos/migratus">Migratus</a> for running the database migrations.</p>
<h2 id="heading-configure-migrations">Configure Migrations</h2>
<p>To use Migratus, first, we must add the dependency to our <code>deps.edn</code> file and get the latest version from <a target="_blank" href="https://clojars.org/migratus">Clojars</a>. I also like to create an alias into the file to have a command line invocation to generate the migration files that we can later use with <code>clj -X:create-migration &lt;migration-name&gt;</code>.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:paths</span> [<span class="hljs-string">"src"</span> <span class="hljs-string">"resources"</span>]

 <span class="hljs-symbol">:deps</span> {...
        migratus/migratus {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.5.4"</span>}}

 <span class="hljs-symbol">:aliases</span> {...
           <span class="hljs-symbol">:create-migration</span>
           {<span class="hljs-symbol">:exec-args</span> {<span class="hljs-symbol">:name</span> <span class="hljs-literal">nil</span>}
            <span class="hljs-symbol">:exec-fn</span> main/create-migration}}}
</code></pre>
<p>The next step is to run the migrations on application startup. Let's do this by updating the <code>main.clj</code> by first requiring <code>migratus</code>, creating a configuration and the <code>create-migration-function</code> configured for the <code>create-migration</code> alias.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> main
  (<span class="hljs-symbol">:require</span> [ring.adapter.jetty <span class="hljs-symbol">:as</span> jetty]
            [migratus.core <span class="hljs-symbol">:as</span> migratus] <span class="hljs-comment">;; Add migratus</span>
            [next.jdbc <span class="hljs-symbol">:as</span> jdbc])
  (<span class="hljs-symbol">:gen-class</span>))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-port</span> []
  (<span class="hljs-name">Integer/parseInt</span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"PORT"</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-db-conf</span> []
  {<span class="hljs-symbol">:dbtype</span>  <span class="hljs-string">"postgres"</span>
   <span class="hljs-symbol">:jdbcUrl</span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"JDBC_DATABASE_URL"</span>)})

<span class="hljs-comment">;; THIS IS NEW ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;</span>

(<span class="hljs-keyword">defn</span> <span class="hljs-title">migrations-config</span> []
  {<span class="hljs-symbol">:db</span>                   (<span class="hljs-name">get-db-conf</span>)
   <span class="hljs-symbol">:store</span>                <span class="hljs-symbol">:database</span>
   <span class="hljs-symbol">:migration-dir</span>        <span class="hljs-string">"migrations/"</span>
   <span class="hljs-symbol">:migration-table-name</span> <span class="hljs-string">"migrations"</span>
   <span class="hljs-symbol">:init-in-transaction?</span> <span class="hljs-literal">false</span>})

(<span class="hljs-keyword">defn</span> <span class="hljs-title">create-migration</span> [{<span class="hljs-symbol">:keys</span> [name]}]
  (<span class="hljs-name">migratus.core/create</span> (<span class="hljs-name">migrations-config</span>)
                        name))

<span class="hljs-comment">;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;</span>

(<span class="hljs-keyword">defn</span> <span class="hljs-title">datasource</span> []
  (<span class="hljs-name">jdbc/get-datasource</span> (<span class="hljs-name">get-db-conf</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">app</span> [_request]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [migrations (<span class="hljs-name">jdbc/execute!</span> (<span class="hljs-name">datasource</span>) 
                                  <span class="hljs-comment">;; Get migrations instead of version</span>
                                  [<span class="hljs-string">"SELECT * FROM migrations"</span>])]
    {<span class="hljs-symbol">:status</span>  <span class="hljs-number">200</span>
     <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"Content-Type"</span> <span class="hljs-string">"application/edn"</span>}
     <span class="hljs-symbol">:body</span>    (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> migrations)}))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">-main</span> [&amp; _args]
  <span class="hljs-comment">;; Run migrations everytime the application starts!</span>
  (<span class="hljs-name">migratus/migrate</span> (<span class="hljs-name">migrations-config</span>))
  (<span class="hljs-name">jetty/run-jetty</span> #'app {<span class="hljs-symbol">:port</span> (<span class="hljs-name">get-port</span>)}))
</code></pre>
<p>We create a test migration with the alias we created in the first step.</p>
<pre><code class="lang-bash">❯ clj -X:create-migration :name <span class="hljs-built_in">test</span>
❯ tree resources
resources
└── migrations
   ├── 20231208151434-test.down.sql
   └── 20231208151434-test.up.sql
</code></pre>
<p>And that's it for the configuration. If you're going to add PostgreSQL schemas in your test migration, note that if you use <a target="_blank" href="https://cljdoc.org/d/migratus/migratus/1.5.4/doc/readme#multiple-statements">multiple SQL statements</a>, you need to separate each of them with <code>--;;</code>.</p>
<h2 id="heading-test-the-migrations-locally">Test the Migrations Locally</h2>
<p>Now that we have the application updated and migrations configured, let's validate that everything works as expected (If you haven't already tested all of this in the REPL). But before that, let's create another Dockerfile for the development build and add the PostgreSQL package to have it available in the DigitalOcean application console. This will allow us to connect to the development database from DigitalOcean with a command-line interface.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> clojure:openjdk-<span class="hljs-number">17</span>-tools-deps-alpine

<span class="hljs-keyword">RUN</span><span class="bash"> apk update; apk add postgresql</span>

<span class="hljs-keyword">WORKDIR</span><span class="bash"> app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> clojure -P</span>
<span class="hljs-keyword">CMD</span><span class="bash"> clojure -X:run</span>
</code></pre>
<p>And next, update the <code>docker-compose.yml</code> to use the development Dockerfile.</p>
<pre><code class="lang-dockerfile">services:

  api:
    build:
      context: .
      dockerfile: docker/dev.Dockerfile <span class="hljs-comment"># &lt;-- use the new file</span>
</code></pre>
<p>After executing <code>docker-compose up --build</code> we should have the updated application running in a container with the database. Use another terminal window from the same directory to test that we can use the <code>psql</code> client from within our application container.</p>
<pre><code class="lang-bash">❯ docker-compose run api psql -h postgres -U user -d db
</code></pre>
<p>The password is defined in the <code>docker-compose.yml</code>'s <code>JDBC_DATABASE_URL</code>. Now that we have successfully connected to the database, let's see if we have the expected table <code>migrations</code> listed with <code>\dt</code> and that we have the expected <code>test</code> migration applied.</p>
<pre><code class="lang-pgsql">db=# \dt
          List <span class="hljs-keyword">of</span> relations
 <span class="hljs-keyword">Schema</span> |    <span class="hljs-type">Name</span>    | <span class="hljs-keyword">Type</span>  | <span class="hljs-keyword">Owner</span>
<span class="hljs-comment">--------+------------+-------+-------</span>
 <span class="hljs-built_in">public</span> | migrations | <span class="hljs-keyword">table</span> | <span class="hljs-keyword">user</span>
(<span class="hljs-number">1</span> <span class="hljs-keyword">row</span>)

db=# <span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> migrations;
       id       |        applied         | description
<span class="hljs-comment">----------------+------------------------+-------------</span>
 <span class="hljs-number">20231208151434</span> | <span class="hljs-number">2023</span><span class="hljs-number">-12</span><span class="hljs-number">-09</span> <span class="hljs-number">08</span>:<span class="hljs-number">29</span>:<span class="hljs-number">05.82</span> | test
(<span class="hljs-number">1</span> <span class="hljs-keyword">row</span>)
</code></pre>
<p>It's looking good. As a last step, let's try to retrieve the same content via the HTTP API.</p>
<pre><code class="lang-clojure">❯ http :<span class="hljs-number">8000</span>
HTTP/<span class="hljs-number">1.1</span> <span class="hljs-number">200</span> OK
Content-Type: application/edn
Date: Sat, <span class="hljs-number">09</span> Dec <span class="hljs-number">2023</span> <span class="hljs-number">08</span>:<span class="hljs-number">33</span>:<span class="hljs-number">08</span> GMT
Server: Jetty(<span class="hljs-number">9.2</span>.21.v20170120)
Transfer-Encoding: chunked

[{<span class="hljs-symbol">:migrations/id</span> <span class="hljs-number">20231208151434</span>, 
  <span class="hljs-symbol">:migrations/applied</span> #inst <span class="hljs-string">"2023-12-09T08:29:05.820000000-00:00"</span>, 
  <span class="hljs-symbol">:migrations/description</span> <span class="hljs-string">"test"</span>}]
</code></pre>
<p>Everything is working as expected. Let's see how we can follow the same steps on the deployed application.</p>
<h2 id="heading-deploy-the-changes-to-digitalocean">Deploy the Changes to DigitalOcean</h2>
<p>Before deploying the changes, we need to update the applications <code>run_command</code> since the Alpine image doesn't have <a target="_blank" href="https://github.com/hanslub42/rlwrap">rlwrap</a> installed.</p>
<pre><code class="lang-diff">@@ -41,7 +41,7 @@ resource "digitalocean_app" "app" {
       source_dir = "api/"
       http_port  = 8000

<span class="hljs-deletion">-      run_command = "clj -X:run"</span>
<span class="hljs-addition">+      run_command = "clojure -X:run"</span>
     }

     database {
</code></pre>
<p>First, push the image to your DigitalOcean Container Registry (DOCR) and wait for the deployment to finish. You can find the instructions for the deployment from the previous posts. Refer to the first post on how to set up the DigitalOcean project and push images to the DigitalOcean Container Registry and the second post to update the application to read the environment variables dynamically.</p>
<p>Now that the changes are deployed, we can visit our DigitalOcean dashboard and find the application console. This provides access similar to what we did earlier with the <code>docker-compose exec ...</code>, which is why we installed the Postgres client in the image itself. I wanted to show that it is possible to connect to the database for debugging, maintenance, or whatever reasons when using the application platform.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702057944192/8b30653f-c621-486f-810f-412fbf86d397.png" alt class="image--center mx-auto" /></p>
<p>Finally, as a last step, let's call the endpoint to see how the API returns the migrations table data.</p>
<pre><code class="lang-clojure">❯ http https://sample-app-mffks.ondigitalocean.app
HTTP/<span class="hljs-number">1.1</span> <span class="hljs-number">200</span> OK
CF-Cache-Status: MISS
CF-RAY: <span class="hljs-number">8326</span>da<span class="hljs-number">43</span>ccded<span class="hljs-number">93</span>f-HEL
Connection: keep-alive
Content-Type: application/edn
Date: Fri, <span class="hljs-number">08</span> Dec <span class="hljs-number">2023</span> <span class="hljs-number">17</span>:<span class="hljs-number">50</span>:<span class="hljs-number">46</span> GMT
Last-Modified: Fri, <span class="hljs-number">08</span> Dec <span class="hljs-number">2023</span> <span class="hljs-number">17</span>:<span class="hljs-number">50</span>:<span class="hljs-number">46</span> GMT
Server: cloudflare
Set-Cookie: __cf_bm=eZ<span class="hljs-number">9</span>Xa<span class="hljs-number">2</span>czsbrF<span class="hljs-number">7</span>O<span class="hljs-number">71</span>GudfKdryjh.VD<span class="hljs-number">8</span>bBKAG<span class="hljs-number">5</span>EcXof<span class="hljs-number">88</span><span class="hljs-number">-1702057846</span><span class="hljs-number">-0</span>-Ack<span class="hljs-number">9</span>rJbWGn<span class="hljs-number">1</span>PH<span class="hljs-number">2</span>yEzcqHNxJjqfzvEX<span class="hljs-number">2</span>n<span class="hljs-number">8</span>nWJybWQpIaP<span class="hljs-number">63</span>X/LFMEaTMazT<span class="hljs-number">1</span>dEjpfdcVYFd<span class="hljs-number">30</span>zmZCkZ<span class="hljs-number">78</span>wART<span class="hljs-number">9</span>yM=<span class="hljs-comment">; path=/; expires=Fri, 08-Dec-23 18:20:46 GMT; domain=.ondigitalocean.app; HttpOnly; Secure; SameSite=None</span>
Transfer-Encoding: chunked
Vary: Accept-Encoding
cache-control: private
x-do-app-origin: <span class="hljs-number">1</span>a<span class="hljs-number">36</span>f<span class="hljs-number">444</span><span class="hljs-number">-619</span>d<span class="hljs-number">-4979</span><span class="hljs-number">-9436</span>-c<span class="hljs-number">8765</span>b<span class="hljs-number">32</span>e<span class="hljs-number">6</span>f<span class="hljs-number">6</span>
x-do-orig-status: <span class="hljs-number">200</span>

[{<span class="hljs-symbol">:migrations/id</span> <span class="hljs-number">20231208151434</span>, 
  <span class="hljs-symbol">:migrations/applied</span> #inst <span class="hljs-string">"2023-12-08T17:46:28.392000000-00:00"</span>, 
  <span class="hljs-symbol">:migrations/description</span> <span class="hljs-string">"test"</span>}]
</code></pre>
<p>It works!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>To run migrations, you can just run them on the application startup. Migratus is an excellent lightweight option for Clojure. Set up the migration files, create the configuration, and you're ready.</p>
<p>To my knowledge, the DigitalOcean app platform doesn't provide a direct way to connect to the development database from the command line. Still, we can use the app console as a bastion if we've installed <code>psql</code> in the container image. Even if you haven't installed it, you can do that in the console, but you'd need to do this again after each deployment.</p>
<p>Thank you for reading. I hope you found this helpful.</p>
<p>Feel free to reach out and let me know what you think—social links in the menu.</p>
]]></content:encoded></item><item><title><![CDATA[Smaller and Safer Clojure Containers: Minimizing the Software Bill of Materials]]></title><description><![CDATA[We are exposed to supply chain security vulnerabilities whenever we use containers (or almost any software). This can be problematic because our goal is to offer a dependable and secure service to our users, which these vulnerabilities can disrupt as...]]></description><link>https://tonitalksdev.com/smaller-and-safer-clojure-containers-minimizing-the-software-bill-of-materials</link><guid isPermaLink="true">https://tonitalksdev.com/smaller-and-safer-clojure-containers-minimizing-the-software-bill-of-materials</guid><category><![CDATA[Clojure]]></category><category><![CDATA[Docker]]></category><category><![CDATA[distroless]]></category><category><![CDATA[infosec]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Wed, 06 Dec 2023 07:37:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701848087303/d89ddade-9188-4e44-886e-3b7338b0b9ac.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We are exposed to supply chain security vulnerabilities whenever we use containers (or almost any software). This can be problematic because our goal is to offer a dependable and secure service to our users, which these vulnerabilities can disrupt as they heighten our risk of being hacked. We don't need to take this as it is—we can act and mitigate these risks proactively. Learn how to reduce your Software Bill of Materials and help your SRE engineers sleep better.</p>
<blockquote>
<p>A software bill of materials is a list of all the open source and third-party components present in a codebase. An SBOM also lists the licenses that govern those components, the versions of the components used in the codebase, and their patch status, which allows security teams to quickly identify any associated security or license risks.</p>
<p><a target="_blank" href="https://www.synopsys.com/blogs/software-security/software-bill-of-materials-bom.html">Synopsys: What is a Software Bill of Materials</a></p>
</blockquote>
<p>When we use public container registries like the docker hub, some bad actors may have sneaked their malware into the image. If this piece of software becomes part of our SBOM, it can compromise the application. We should include only the essential dependencies in our containers to minimize the risk.</p>
<p>Let's take the Docker configuration used in the previous post, <a target="_blank" href="https://blog.tvaisanen.com/deploying-clojure-like-a-seasoned-hobbyist">Deploying Clojure Like a Seasoned Hobbyist</a>, as an example and use it as our vehicle to explore this topic further from the perspective of how much extra is included in the container and what known vulnerabilities we can find with a security scan, try reducing the image size and SBOM, and compare the final artifact sizes between the different approaches.</p>
<p>Examples are available in the <a target="_blank" href="https://github.com/tvaisanen/digitalocean-terraform-clojure-template/tree/blog-post-series-02">repository</a>.</p>
<h2 id="heading-inspecting-the-image">Inspecting The Image</h2>
<p>I'll tag the build (from the previous post) <code>clj-tools-deps-buster</code> to have a shorter descriptive name for this context.</p>
<pre><code class="lang-bash">❯ docker tag \
    registry.digitalocean.com/clojure-sample-app/dev \
    clj-tools-deps-buster
</code></pre>
<p>First, we want access to the manifest to find the container configuration, so let's start by creating an archive from all of the files in the container, including the manifest.</p>
<pre><code class="lang-bash">❯ docker save clj-tools-deps-buster &gt; clj-tools-deps-buster.tar
</code></pre>
<p>Then, let's unpack the archive to get access to the files.</p>
<pre><code class="lang-bash">❯ tar -xvf clj-tools-deps-buster.tar
</code></pre>
<p>Now, we can find the configuration file from the <code>manifest.json</code>.</p>
<pre><code class="lang-json">❯ cat manifest.json| jq <span class="hljs-string">".[0].Config"</span>
<span class="hljs-string">"3085b45633b1c0f87a3ea67000d7d77e2f14a74d1b2968966f866c7c3531f745.json"</span>
</code></pre>
<p>The configuration file is too large to add here, so let's fire up a Babashka REPL and investigate the configuration file a bit further.</p>
<p>If we look at this container's history, the last four steps come from the Dockerfile we used, and the rest already exist in the container.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[cheshire.core <span class="hljs-symbol">:as</span> json])

(<span class="hljs-keyword">def</span> <span class="hljs-title">config</span>
  (<span class="hljs-name"><span class="hljs-builtin-name">-&gt;</span></span>  <span class="hljs-string">"3085b45633b1c0f87a3ea67000d7d77e2f14a74d1b2968966f866c7c3531f745.json"</span>
       slurp
       (<span class="hljs-name">json/parse-string</span> <span class="hljs-literal">true</span>)))

(<span class="hljs-name"><span class="hljs-builtin-name">keys</span></span> config)
<span class="hljs-comment">;; =&gt; (:architecture :config :created :history :os :rootfs)</span>

(<span class="hljs-name"><span class="hljs-builtin-name">count</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> config <span class="hljs-symbol">:history</span>))
<span class="hljs-comment">;; =&gt; 23</span>

(<span class="hljs-name"><span class="hljs-builtin-name">for</span></span> [{<span class="hljs-symbol">:keys</span> [created_by]} (<span class="hljs-name"><span class="hljs-builtin-name">take-last</span></span> <span class="hljs-number">4</span> (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> config <span class="hljs-symbol">:history</span>))]
   created_by)
<span class="hljs-comment">;; =&gt;</span>
<span class="hljs-comment">;; ("WORKDIR /tmp/app"</span>
<span class="hljs-comment">;;  "COPY . . # buildkit"</span>
<span class="hljs-comment">;;  "RUN /bin/sh -c clojure -P # buildkit"</span>
<span class="hljs-comment">;;  "CMD [\"/bin/sh\" \"-c\" \"clj -X:run\"]")</span>
</code></pre>
<p>We can see that the commands are the same as configured in the previous post's Dockerfile. Other authors did all the previous steps, so we might not know what they did. Let's take the third entry from the history as an example.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">print</span> (<span class="hljs-name"><span class="hljs-builtin-name">get-in</span></span> config [<span class="hljs-symbol">:history</span> <span class="hljs-number">2</span> <span class="hljs-symbol">:created_by</span>]))
<span class="hljs-comment">;; /bin/sh -c set -eux; apt-get update; \</span>
<span class="hljs-comment">;; apt-get install -y --no-install-recommends -certificates curl netbase wget; \</span>
<span class="hljs-comment">;; rm -rf /var/lib/apt/lists/*</span>
</code></pre>
<p>The original author of the base image had decided to install curl, netbase, and wget, which all ended up in our application image.</p>
<p>Let's take a look at what commands we have pre-installed on the container by first finding all <code>apt-get install</code> commands from the image history.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">def</span> <span class="hljs-title">install-commands</span>
  (<span class="hljs-name"><span class="hljs-builtin-name">for</span></span> [{<span class="hljs-symbol">:keys</span> [created_by]} (<span class="hljs-name"><span class="hljs-builtin-name">get</span></span> config <span class="hljs-symbol">:history</span>)
        <span class="hljs-symbol">:when</span> (<span class="hljs-name">str/includes?</span> created_by <span class="hljs-string">"apt-get install"</span>)]
    created_by))

(<span class="hljs-name"><span class="hljs-builtin-name">count</span></span> install-commands)
<span class="hljs-comment">;; =&gt; 5</span>
</code></pre>
<p>It looks like we have five different steps using the <code>apt-get install</code>. Let's see what these are all about.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">for</span></span> [command install-commands]
  (<span class="hljs-name"><span class="hljs-builtin-name">into</span></span> []
        (<span class="hljs-name"><span class="hljs-builtin-name">comp</span></span> 
         (<span class="hljs-name"><span class="hljs-builtin-name">filter</span></span> #(<span class="hljs-name">str/includes?</span> % <span class="hljs-string">"apt-get install"</span>))
         (<span class="hljs-name"><span class="hljs-builtin-name">map</span></span> str/trim))
        (<span class="hljs-name"><span class="hljs-builtin-name">-&gt;</span></span> command
            (<span class="hljs-name">str/replace</span> #<span class="hljs-string">"\t"</span> <span class="hljs-string">""</span>)
            (<span class="hljs-name">str/split</span> #<span class="hljs-string">";"</span>))))

<span class="hljs-comment">;; (["apt-get install -y --no-install-recommends ca-certificates curl netbase wget"]</span>
<span class="hljs-comment">;;  ["apt-get install -y --no-install-recommends gnupg dirmngr"]</span>
<span class="hljs-comment">;;  ["/bin/sh -c apt-get update &amp;&amp; apt-get install -y --no-install-recommends git mercurial openssh-client subversion procps &amp;&amp; rm -rf /var/lib/apt/lists/*"]</span>
<span class="hljs-comment">;;  ["apt-get install -y --no-install-recommends bzip2 unzip xz-utils binutils fontconfig libfreetype6 ca-certificates p11-kit"]</span>
<span class="hljs-comment">;;  ["/bin/sh -c apt-get update &amp;&amp; apt-get install -y make rlwrap &amp;&amp; rm -rf /var/lib/apt/lists/* &amp;&amp; wget https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh &amp;&amp; sha256sum linux-install-$CLOJURE_VERSION.sh &amp;&amp; echo \"7677bb1179ebb15ebf954a87bd1078f1c547673d946dadafd23ece8cd61f5a9f *linux-install-$CLOJURE_VERSION.sh\" | sha256sum -c - &amp;&amp; chmod +x linux-install-$CLOJURE_VERSION.sh &amp;&amp; ./linux-install-$CLOJURE_VERSION.sh &amp;&amp; rm linux-install-$CLOJURE_VERSION.sh &amp;&amp; clojure -e \"(clojure-version)\""])</span>
</code></pre>
<p>The result is that we have a bunch of unnecessary executables for the final container. On top of these, to get the complete picture of what is installed, we would still need to go through the Clojure install scripts. But we're not going to go there this time around. Let's continue with the image itself.</p>
<p>Before moving forward, let's see the size of the image and if the base image has any known vulnerabilities.</p>
<pre><code class="lang-bash">❯ docker image ls | grep clj-tools-deps-buster
clj-tools-deps-buster ...  725MB
</code></pre>
<p>The image size is 725MB. A screenshot from the docker hub shows that the base image we used has 17 critical and 27 high-priority vulnerabilities. Let's get back to these numbers a bit later.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701506293800/9a127ea4-036e-4efa-ac53-cb0e298796cc.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-reduce-the-image-size">Reduce the Image Size</h2>
<p>Next, let's see if we can reduce the image size and vulnerabilities by changing to smaller base images.</p>
<h3 id="heading-using-slim-buster">Using Slim Buster</h3>
<p>Create a new file <code>slim.Dockerfile</code></p>
<pre><code class="lang-bash">FROM clojure:openjdk-17-tools-deps-slim-buster
WORKDIR app
COPY . .

RUN clojure -P

CMD clj -X:run
</code></pre>
<p>Build the image.</p>
<pre><code class="lang-bash">❯ docker build \
  --file=slim.Dockerfile . \
  -t clj-tools-deps-slim --no-cache
</code></pre>
<p>Check the size of the container.</p>
<pre><code class="lang-bash">❯ docker image ls | grep clj-tools-deps-slim
clj-tools-deps-slim ... 553MB
</code></pre>
<p>We see that by using the <code>clojure:openjdk-17-tools-deps-slim-buster</code> , we successfully reduced the image size by 172 MB and made minor improvements in vulnerability counts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701507293943/ee97b907-4283-4bfa-90ee-9af7afa12879.png" alt class="image--center mx-auto" /></p>
<p>And let's do the same once more with the Alpine version.</p>
<h3 id="heading-using-alpine-linux-image">Using Alpine Linux Image</h3>
<p>Once again, create a new docker file <code>alpine.Dockerfile</code>.</p>
<pre><code class="lang-bash">FROM clojure:openjdk-17-tools-deps-alpine
WORKDIR app
COPY . .

RUN clojure -P

CMD clj -X:run
</code></pre>
<p>Build the image and check the size.</p>
<pre><code class="lang-bash">❯ docker image ls | grep clj-tools-deps-alpine
clj-tools-deps-alpine ... 356MB
</code></pre>
<p>This is good progress. We went from 725MBs to 356MBs. The net total is -369MBs; we can see improvements with vulnerability counts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701507396211/bac743ea-1537-44e9-b2dd-fe2906f55020.png" alt class="image--center mx-auto" /></p>
<p>We can still improve from here by going distroless. Yes, you read correctly, distroless.</p>
<h3 id="heading-distroless-containers">Distroless Containers</h3>
<p>Google provides language-specific base containers to run application code without a Linux distribution on the image. These base images don't even have shells. When using a language like Rust, You could have only the kernel and include the standard C-libraries in your binary.</p>
<blockquote>
<p>🥑 Language focused docker images, minus the operating system.</p>
<p><a target="_blank" href="https://github.com/GoogleContainerTools/distroless">Github: Google Container Tools</a></p>
</blockquote>
<p>For Clojure code, we'll use the Java image <a target="_blank" href="https://github.com/GoogleContainerTools/distroless/tree/main/java">gcr.io/distroless/java17-debian12</a>. We must build a JAR file for the containers to run the application code without the Clojure dependencies.</p>
<p>So, let's get to it.</p>
<h4 id="heading-build-jar">Build JAR</h4>
<p>First, let's create a JAR build of the application following the instructions in the <a target="_blank" href="https://clojure.org/guides/tools_build">Clojure build.tools reference</a>.</p>
<p>Let's start by updating the application to dynamically read the env variables since they are unavailable during the build time.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> main
  (<span class="hljs-symbol">:require</span> [ring.adapter.jetty <span class="hljs-symbol">:as</span> jetty]
            [next.jdbc <span class="hljs-symbol">:as</span> jdbc])
  (<span class="hljs-symbol">:gen-class</span>))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-port</span> []
  (<span class="hljs-name">Integer/parseInt</span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"PORT"</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">get-db-conf</span> []
  {<span class="hljs-symbol">:dbtype</span> <span class="hljs-string">"postgres"</span>
   <span class="hljs-symbol">:jdbcUrl</span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"JDBC_DATABASE_URL"</span>)})

(<span class="hljs-keyword">defn</span> <span class="hljs-title">datasource</span> []
  (<span class="hljs-name">jdbc/get-datasource</span> (<span class="hljs-name">get-db-conf</span>)))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">app</span> [_request]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [db-version (<span class="hljs-name">jdbc/execute!</span> (<span class="hljs-name">datasource</span>) [<span class="hljs-string">"SELECT version()"</span>])]
    {<span class="hljs-symbol">:status</span>  <span class="hljs-number">200</span>
     <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"Content-Type"</span> <span class="hljs-string">"application/edn"</span>}
     <span class="hljs-symbol">:body</span>    (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> db-version)}))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">-main</span> [&amp; _args]
  (<span class="hljs-name">jetty/run-jetty</span> #'app {<span class="hljs-symbol">:port</span> (<span class="hljs-name">get-port</span>)}))
</code></pre>
<p>Add new alias <code>build</code> into <code>deps.edn</code></p>
<pre><code class="lang-clojure">{...
 <span class="hljs-symbol">:aliases</span> 
  {....
   <span class="hljs-symbol">:build</span> {<span class="hljs-symbol">:deps</span> {io.github.clojure/tools.build
                   {<span class="hljs-symbol">:git/tag</span> <span class="hljs-string">"v0.9.6"</span> <span class="hljs-symbol">:git/sha</span> <span class="hljs-string">"8e78bcc"</span>}}
           <span class="hljs-symbol">:ns-default</span> build}}}
</code></pre>
<p>And then create a <code>build.clj</code> file.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> build
  (<span class="hljs-symbol">:require</span> [clojure.tools.build.api <span class="hljs-symbol">:as</span> b]))

(<span class="hljs-keyword">def</span> <span class="hljs-title">class-dir</span> <span class="hljs-string">"target/classes"</span>)
(<span class="hljs-keyword">def</span> <span class="hljs-title">basis</span> (<span class="hljs-name">b/create-basis</span> {<span class="hljs-symbol">:project</span> <span class="hljs-string">"deps.edn"</span>}))
(<span class="hljs-keyword">def</span> <span class="hljs-title">uber-file</span> <span class="hljs-string">"target/standalone.jar"</span>)

(<span class="hljs-keyword">defn</span> <span class="hljs-title">clean</span> [_]
  (<span class="hljs-name">b/delete</span> {<span class="hljs-symbol">:path</span> <span class="hljs-string">"target"</span>}))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">uber</span> [_]
  (<span class="hljs-name">clean</span> <span class="hljs-literal">nil</span>)
  (<span class="hljs-name">b/copy-dir</span> {<span class="hljs-symbol">:src-dirs</span>   [<span class="hljs-string">"src"</span> <span class="hljs-string">"resources"</span>]
               <span class="hljs-symbol">:target-dir</span> class-dir})
  (<span class="hljs-name">b/compile-clj</span> {<span class="hljs-symbol">:basis</span>      basis
                  <span class="hljs-symbol">:ns-compile</span> '[main]
                  <span class="hljs-symbol">:class-dir</span>  class-dir})
  (<span class="hljs-name">b/uber</span> {<span class="hljs-symbol">:class-dir</span> class-dir
           <span class="hljs-symbol">:uber-file</span> uber-file
           <span class="hljs-symbol">:basis</span>     basis
           <span class="hljs-symbol">:main</span>      'main}))
</code></pre>
<p>Now, we should be able to build the JAR file by running:</p>
<pre><code class="lang-bash">❯ clj -T:build uber
</code></pre>
<p>After this step, we should have the following files in the <code>target</code> folder.</p>
<pre><code class="lang-bash">❯ ls target
classes  standalone.jar
</code></pre>
<h4 id="heading-using-distroless-java">Using Distroless Java</h4>
<p>Lastly, let's create a distroless image to run the application JAR.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> clojure:openjdk-<span class="hljs-number">17</span>-tools-deps-buster as base

<span class="hljs-keyword">WORKDIR</span><span class="bash"> app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> clj -T:build uber</span>

<span class="hljs-keyword">FROM</span> gcr.io/distroless/java17-debian12
<span class="hljs-keyword">COPY</span><span class="bash"> --from=base /tmp/app/target/standalone.jar /tmp/app/standalone.jar</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"/tmp/app/standalone.jar"</span>]</span>
</code></pre>
<blockquote>
<p>As Timo Kramer <a target="_blank" href="https://x.com/timokramer/status/1731958595846013089?s=20">pointed out</a>, we can also create the container with our build script using Google's Jib library. <a target="_blank" href="https://github.com/replikativ/datahike-server/blob/main/build.clj#L114">Take a look at how Datahike uses it</a>.</p>
</blockquote>
<p>Build your image and check the image size.</p>
<pre><code class="lang-bash">❯ docker image ls | grep jvm-distroless
jvm-distroless ... 234MB
</code></pre>
<p>Let's run a security scan for all the images and see how the numbers compare.</p>
<h2 id="heading-comparing-image-sizes-and-known-vulnerabilities">Comparing Image Sizes and Known Vulnerabilities</h2>
<p>For this step, I'll be using Trivy since the docker hub doesn't provide the numbers for the distroless images.</p>
<blockquote>
<p>Trivy is a comprehensive and versatile security scanner. Trivy has <em>scanners</em> that look for security issues, and <em>targets</em> where it can find those issues.</p>
<p><a target="_blank" href="https://github.com/aquasecurity/trivy">Github: Trivy</a></p>
</blockquote>
<p>Here's an example of a partial output. See the <a target="_blank" href="https://github.com/tvaisanen/digitalocean-terraform-clojure-template/tree/blog-post-series-02/container-info/trivy-reports">complete reports here</a>.</p>
<pre><code class="lang-bash">❯ trivy image clj-tools-deps-alpine

clj-tools-deps-buster (debian 10.12)
======
Total: 1016 (UNKNOWN: 9, LOW: 611, MEDIUM: 170, HIGH: 177, CRITICAL: 49)

....

Java (jar)
=====
Total: 48 (UNKNOWN: 0, LOW: 10, MEDIUM: 20, HIGH: 14, CRITICAL: 4)
</code></pre>
<p>Trivy scans the system and the JAR vulnerabilities in one go, so I'll add both separately to the graphs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701847338103/25df54df-6feb-4b3c-bd2b-22a5d63cc0e2.png" alt class="image--center mx-auto" /></p>
<p>We can see clearly that in the case of system vulnerabilities, the amount goes down with the container size. The image size doesn't affect JAR vulnerabilities significantly. Based on this, the <code>clj-tools-deps-buster</code> probably has some extra development time dependencies that are not required for the final image.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Distroless images might not suit your use case, and going distroless doesn't mean the container will be bulletproof. But it's still good to know that it's an option. There's much more to container security than just the packages installed on the image. Minimizing the SBOM is one more trick on your sleeve. I recommend reading OWASP's docker security cheatsheet for additional security steps.</p>
<p>I hope you found this helpful. Thank you for reading.</p>
<p>Feel free to reach out and let me know what you think—social links in the menu.</p>
]]></content:encoded></item><item><title><![CDATA[Getting Started with Clojure Unit Testing: A Simple Tutorial]]></title><description><![CDATA[Testing is one of the aspects of software development that you can not hide yourself from. At least if you work in the industry, you shouldn't. Testing is vital to battle against regression in the code, meaning that when you add new features, you are...]]></description><link>https://tonitalksdev.com/how-to-get-started-with-tdd-in-clojure</link><guid isPermaLink="true">https://tonitalksdev.com/how-to-get-started-with-tdd-in-clojure</guid><category><![CDATA[Clojure]]></category><category><![CDATA[Testing]]></category><category><![CDATA[test driven development]]></category><category><![CDATA[software development]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Thu, 30 Nov 2023 06:41:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701324964331/f81a8aa1-86b0-4293-9484-a44ab30f0b0c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Testing is one of the aspects of software development that you can not hide yourself from. At least if you work in the industry, you shouldn't. Testing is vital to battle against regression in the code, meaning that when you add new features, you are not breaking the previous functionality. Of course, over-testing can be harmful and slow you down if you need to spend more time updating tests than the feature code itself.</p>
<p>This post is not about what, when, and if you should test but a tutorial on getting started when you've decided that you need them and how to integrate the testing into your workflow.</p>
<h2 id="heading-how-clojure-is-different-from-the-mainstream">How Clojure is Different From the Mainstream</h2>
<p>In other dynamic languages, you often run tests in watch mode so that your test runner triggers a new run whenever you save the file. This offers a fast feedback loop on your changes. Clojure differs from the others in that it has the REPL that can be used to evaluate the code while developing and testing it without running all of the tests every time. Let's get back to the topic of running the tests after we've gone through how to write tests.</p>
<h2 id="heading-writing-tests">Writing Tests</h2>
<p>Clojure has its unit testing framework <a target="_blank" href="https://clojure.github.io/clojure/clojure.test-api.html#clojure.test.tap"><code>core.test</code></a> ; therefore, extra dependencies are unnecessary when writing the tests. At the core of testing are assertions, and we use the macro <code>clojure.test/is</code> that takes any predicate with an optional message describing it.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[clojure.test <span class="hljs-symbol">:as</span> t])

(<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">true?</span></span> <span class="hljs-literal">true</span>) <span class="hljs-string">"true is true"</span>)
<span class="hljs-comment">;; =&gt; true</span>

(<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-literal">true</span> <span class="hljs-literal">false</span>) <span class="hljs-string">"true equals false"</span>)
<span class="hljs-comment">;; =&gt; false</span>

<span class="hljs-comment">;; REPL output</span>
<span class="hljs-comment">;;</span>
<span class="hljs-comment">;; FAIL in () (NO_SOURCE_FILE:4)</span>
<span class="hljs-comment">;; true equals false</span>
<span class="hljs-comment">;; expected: (= true false)</span>
<span class="hljs-comment">;;  actual: (not (= true false))</span>
</code></pre>
<p>You can think of this as a function call to evaluate whether a given predicate (boolean or a function that produces a boolean) is true.</p>
<p>Assertions are used within <code>deftests</code> macro that returns a function to evaluate the assertions inside.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">t/deftest</span> test-assertion
  (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-literal">true</span> <span class="hljs-literal">false</span>) <span class="hljs-string">"true equals false"</span>)
  (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">true?</span></span> <span class="hljs-literal">true</span>) <span class="hljs-string">"true is true"</span>))
<span class="hljs-comment">;; =&gt; #'core-test/test-assertion</span>

(<span class="hljs-name">test-assertion</span>)
<span class="hljs-comment">;; REPL output</span>
<span class="hljs-comment">;;</span>
<span class="hljs-comment">;; FAIL in (test-assertion) (NO_SOURCE_FILE:17)</span>
<span class="hljs-comment">;; expected: (= true false)</span>
<span class="hljs-comment">;;  actual: (not (= true false))</span>
</code></pre>
<blockquote>
<p>Read the <a target="_blank" href="https://guide.clojure.style/#testing">Clojure style guide</a> on naming conventions</p>
</blockquote>
<p>If you read the REPL outputs, you might have noticed we lost the message attached to our failing assertion. With <code>deftests</code> we can use <code>testing</code> to define the context for the test.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">t/deftest</span> test-assertion
  (<span class="hljs-name">t/testing</span>  <span class="hljs-string">"true equals false"</span>
    (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-literal">true</span> <span class="hljs-literal">false</span>)))
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"true is true"</span>
      (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">true?</span></span> <span class="hljs-literal">true</span>))))
<span class="hljs-comment">;; =&gt; #'core-test/test-assertion</span>

(<span class="hljs-name">test-assertion</span>)

<span class="hljs-comment">;; REPL output</span>
<span class="hljs-comment">;;</span>
<span class="hljs-comment">;; FAIL in (test-assertion) (NO_SOURCE_FILE:25)</span>
<span class="hljs-comment">;; true equals false</span>
<span class="hljs-comment">;; expected: (= true false)</span>
<span class="hljs-comment">;;   actual: (not (= true false))</span>
</code></pre>
<p>You can see that we got our descriptions back. And that's pretty much the basics of testing in Clojure. There are more tools in <code>clojure.test</code> like <code>are</code> and <code>use-fixture</code>. Let me know if you want to learn more about them. But now, back to test runners.</p>
<p>There are several test runner options to choose from. You could use the functions provided by <code>core.test</code>, Cognitect's <a target="_blank" href="https://github.com/cognitect-labs/test-runner">test-runner</a>, or <a target="_blank" href="https://cljdoc.org/d/lambdaisland/kaocha/1.87.1366/doc/1-introduction">Kaocha</a>, to name a few. We'll be using Kaocha this time. It is an extendable, build-tool agnostic test runner.</p>
<h2 id="heading-setup-the-project">Setup the project</h2>
<p>Let's set up a project with Kaocha testing in mind so that we have a <code>deps.edn</code> alias for running the tests once and in watch mode.</p>
<p>Here's the file structure</p>
<pre><code class="lang-bash">├── deps.edn
├── src
│   └── core.clj
└── <span class="hljs-built_in">test</span>
    └── core_test.clj
</code></pre>
<p>Let's fill in the <code>deps.edn</code> first and add Kaocha as a test dependency.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:paths</span> [<span class="hljs-string">"src"</span>]
 <span class="hljs-symbol">:deps</span> {org.clojure/clojure {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.11.1"</span>}}

 <span class="hljs-symbol">:aliases</span>
 {<span class="hljs-symbol">:test</span>
  {<span class="hljs-symbol">:extra-paths</span> [<span class="hljs-string">"test"</span>]
   <span class="hljs-symbol">:extra-deps</span>  {lambdaisland/kaocha {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.77.1236"</span>}}
   <span class="hljs-symbol">:exec-fn</span>     kaocha.runner/exec-fn
   <span class="hljs-symbol">:exec-args</span>   {<span class="hljs-symbol">:skip-meta</span> <span class="hljs-symbol">:slow</span>}}

  <span class="hljs-symbol">:watch</span>
  {<span class="hljs-symbol">:exec-args</span>   {<span class="hljs-symbol">:watch?</span>     <span class="hljs-literal">true</span>
                 <span class="hljs-symbol">:skip-meta</span>  <span class="hljs-symbol">:slow</span>
                 <span class="hljs-symbol">:fail-fast?</span> <span class="hljs-literal">true</span>}}}}
</code></pre>
<p>Under aliases, we have keys <code>:test</code> and <code>:watch</code> and these work together. With the Clojure CLI tool, we can execute the <code>:exec-fn</code> from the alias configuration by using the <code>-X</code> command line option</p>
<blockquote>
<p>-X is configured with an arg map with :exec-fn and :exec-args keys, and stored under an alias in deps.edn:</p>
<p><a target="_blank" href="https://clojure.org/reference/deps_and_cli#_execute_a_function">Deps and CLI Reference: Execute a Functio</a>n</p>
</blockquote>
<p>so by running <code>clj -X:test</code> we can run the tests once, and with <code>clj -X:test:watch</code> we can continuously run the tests whenever we save the source code file.</p>
<p>Let's write something to test in <code>core.clj</code> namespace.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> core)

(<span class="hljs-keyword">defn</span> <span class="hljs-title">multiply</span> [x y]
  (<span class="hljs-name"><span class="hljs-builtin-name">*</span></span> x y))
</code></pre>
<p>And write the tests we saw earlier, plus a test for multiplication in <code>core-test</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> core-test
  (<span class="hljs-symbol">:require</span> [clojure.test <span class="hljs-symbol">:as</span> t]
            [core <span class="hljs-symbol">:as</span> sut]))

(<span class="hljs-name">t/deftest</span> test-assertion
  (<span class="hljs-name">t/testing</span>  <span class="hljs-string">"true equals false"</span>
    (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-literal">true</span> <span class="hljs-literal">false</span>)))
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"true is true"</span>
      (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">true?</span></span> <span class="hljs-literal">true</span>))))

(<span class="hljs-name">t/deftest</span> multiply-test
  (<span class="hljs-name">t/testing</span> <span class="hljs-string">"multiplication works as expected"</span>
    (<span class="hljs-name">t/is</span> (<span class="hljs-name"><span class="hljs-builtin-name">=</span></span> <span class="hljs-number">4</span> (<span class="hljs-name">sut/multiply</span> <span class="hljs-number">2</span> <span class="hljs-number">2</span>)))))
</code></pre>
<blockquote>
<p>SUT (Software Under Test) I picked up this from <a target="_blank" href="https://practical.li/clojure/testing/unit-testing/">Practicalli</a> years ago and it stuck with me.</p>
</blockquote>
<p>And now we are ready to test.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701320325456/999b9dd6-b503-44e0-971d-156bb744f15c.png" alt class="image--center mx-auto" /></p>
<p>By default, Kaocha uses the <code>src</code> and <code>tests</code> paths so we can run the tests without explicit configuration. There are various configuration options with plugins, so check out the <a target="_blank" href="https://cljdoc.org/d/lambdaisland/kaocha/1.87.1366/doc/3-configuration">docs</a> to see what's possible.</p>
<h2 id="heading-testing-in-the-repl">Testing in the REPL</h2>
<p>Clojure developers usually use their REPL to evaluate the tests, and in my case, it would be in Emacs with CIDER using one of the <code>cider-test-*</code> commands that I have behind keyboard shortcuts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701320657464/2eb1a348-e359-4514-a8c4-18691b585688.png" alt class="image--center mx-auto" /></p>
<p>The same can be done with VS code and Calva. Read from the <a target="_blank" href="https://calva.io/test-runner/">docs</a> how to run tests with Calva.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Testing in Clojure is pretty effortless. We do not need much ceremony to start testing; we need a namespace for the tests and a test runner of our choice.</p>
<p>I hope you found this helpful, and thank you for reading.</p>
<p>Feel free to reach out and let me know what you think—social links in the menu.</p>
]]></content:encoded></item><item><title><![CDATA[Clojure File System Essentials for Node.js Developers]]></title><description><![CDATA[This will be a straightforward post with Javascript to Clojure (on JVM) examples of filesystem operations: listing, reading, writing, renaming, and deleting files. Let me know if you're interested in how to use Node packages from Clojurescript.
So, l...]]></description><link>https://tonitalksdev.com/clojure-for-node-developers-file-system</link><guid isPermaLink="true">https://tonitalksdev.com/clojure-for-node-developers-file-system</guid><category><![CDATA[Clojure]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Thu, 23 Nov 2023 06:10:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700679210959/7f5208cf-93c2-4283-8f02-71a2eb784ea9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This will be a straightforward post with Javascript to Clojure (on <strong>JVM</strong>) examples of filesystem operations: listing, reading, writing, renaming, and deleting files. Let me know if you're interested in how to use Node packages from Clojurescript.</p>
<p>So, let's get to it.</p>
<p>In Javascript, we'd import the <code>fs</code>-module.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'node:fs/promises'</span>)
</code></pre>
<blockquote>
<p>I'll be running all of the Javascript blocks inside a <code>(async () =&gt; { ...code... })()</code> closure.</p>
</blockquote>
<p>In Clojure, we'd reach out for <code>clojure.java.io</code></p>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[clojure.java.io <span class="hljs-symbol">:as</span> io])
</code></pre>
<h2 id="heading-node-filehandle-and-java-file">Node Filehandle and Java File</h2>
<p>In Javascript, we can get a reference to a file with a <code>Filehandle</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> directory = <span class="hljs-keyword">await</span> fs.open(<span class="hljs-string">"."</span>);

<span class="hljs-built_in">console</span>.log(directory)

<span class="hljs-comment">// FileHandle {</span>
<span class="hljs-comment">//     close: [Function: close],</span>
<span class="hljs-comment">//     [Symbol(kHandle)]: FileHandle {},</span>
<span class="hljs-comment">//     [Symbol(kFd)]: 20,</span>
<span class="hljs-comment">//     [Symbol(kRefs)]: 1,</span>
<span class="hljs-comment">//     [Symbol(kClosePromise)]: null</span>
<span class="hljs-comment">// }</span>
</code></pre>
<p><code>FileHandle</code> can be either a directory or a file.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> directoryStats = <span class="hljs-keyword">await</span> fs.stat(<span class="hljs-string">"."</span>);
directoryStats.isFile(); <span class="hljs-comment">// false</span>
directoryStats.isDirectory(); <span class="hljs-comment">// true</span>

<span class="hljs-keyword">let</span> fileStats = <span class="hljs-keyword">await</span> fs.stat(<span class="hljs-string">"files/a.txt"</span>);
fileStats.isFile()); <span class="hljs-comment">// true</span>
fileStats.isDirectory(); <span class="hljs-comment">// false</span>
</code></pre>
<p>The basic building block in Clojure is <code>java.io.File</code>.</p>
<blockquote>
<p>User interfaces and operating systems use system-dependent <em>pathname strings</em> to name files and directories. This class presents an abstract, system-independent view of hierarchical pathnames.</p>
<p><a target="_blank" href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/File.html">Docs: java.io.File</a></p>
</blockquote>
<pre><code class="lang-clojure">(<span class="hljs-name">io/file</span> <span class="hljs-string">"."</span>)
<span class="hljs-comment">;; =&gt; #object[java.io.File 0x54162701 "."]</span>
</code></pre>
<p>Similarly to <code>Filehandle</code> in JS, <code>java.io.File</code> can be either a directory or a file. The <code>File</code> has <code>isDirectory</code> and <code>isFile</code> methods that can be called via Java-interop.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">.isDirectory</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"."</span>))
<span class="hljs-comment">;; =&gt; true</span>

(<span class="hljs-name">.isFile</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"."</span>))
<span class="hljs-comment">;; =&gt; false</span>

(<span class="hljs-name">.isDirectory</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"deps.edn"</span>))
<span class="hljs-comment">;; =&gt; false</span>

(<span class="hljs-name">.isFile</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"deps.edn"</span>))
<span class="hljs-comment">;; =&gt; true</span>
</code></pre>
<p>Let's see what other properties the <code>java.io.File</code> has with the <a target="_blank" href="https://clojuredocs.org/clojure.core/bean"><code>bean</code></a> function.</p>
<blockquote>
<p>bean</p>
<p>Takes a Java object and returns a read-only implementation of the map abstraction based upon its JavaBean properties.</p>
<p><a target="_blank" href="https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core_proxy.clj#L403">Source</a></p>
</blockquote>
<pre><code class="lang-clojure">(<span class="hljs-name">bean</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"."</span>))
<span class="hljs-comment">;; =&gt; </span>
{<span class="hljs-symbol">:path</span> <span class="hljs-string">"."</span>,
 <span class="hljs-symbol">:freeSpace</span> <span class="hljs-number">146245873664</span>,
 <span class="hljs-symbol">:parent</span> <span class="hljs-literal">nil</span>,
 <span class="hljs-symbol">:directory</span> <span class="hljs-literal">true</span>,
 <span class="hljs-symbol">:parentFile</span> <span class="hljs-literal">nil</span>,
 <span class="hljs-symbol">:name</span> <span class="hljs-string">"."</span>,
 <span class="hljs-symbol">:file</span> <span class="hljs-literal">false</span>,
 <span class="hljs-symbol">:canonicalFile</span>
 #object[java.io.File <span class="hljs-number">0</span>x4f2abe7e <span class="hljs-string">"/home/tvaisanen/projects/tmp"</span>],
 <span class="hljs-symbol">:absolute</span> <span class="hljs-literal">false</span>,
 <span class="hljs-symbol">:absoluteFile</span>
 #object[java.io.File <span class="hljs-number">0</span>x5594224b <span class="hljs-string">"/home/tvaisanen/projects/tmp/."</span>],
 <span class="hljs-symbol">:hidden</span> <span class="hljs-literal">true</span>,
 <span class="hljs-symbol">:class</span> java.io.File,
 <span class="hljs-symbol">:canonicalPath</span> <span class="hljs-string">"/home/tvaisanen/projects/tmp"</span>,
 <span class="hljs-symbol">:usableSpace</span> <span class="hljs-number">124331769856</span>,
 <span class="hljs-symbol">:totalSpace</span> <span class="hljs-number">429923737600</span>,
 <span class="hljs-symbol">:absolutePath</span> <span class="hljs-string">"/home/tvaisanen/projects/tmp/."</span>}
</code></pre>
<h2 id="heading-listing-files">Listing Files</h2>
<p>Given we have the following file structure.</p>
<pre><code class="lang-bash">❯ tree files
files
├── a.txt
├── b.txt
├── c.txt
└── nest
    └── a.txt
</code></pre>
<p>In Javascript, we'd typically write.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> dir = <span class="hljs-keyword">await</span> fs.readdir(<span class="hljs-string">"files"</span>);
<span class="hljs-built_in">console</span>.log(dir);
<span class="hljs-comment">// [ 'a.txt', 'b.txt', 'c.txt', 'nest' ]</span>
</code></pre>
<p>And in Clojure, we'll take the folder we want to list as <code>java.io.File</code> and list all the files with <a target="_blank" href="https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core.clj#L4973"><code>file-seq</code></a>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">file-seq</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files"</span>))
<span class="hljs-comment">;; =&gt;</span>
(<span class="hljs-name">#object</span>[java.io.File <span class="hljs-number">0</span>x7e2b40d7 <span class="hljs-string">"files"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x22aaad8f <span class="hljs-string">"files/a.txt"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x3730fe92 <span class="hljs-string">"files/b.txt"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x62aaf1b <span class="hljs-string">"files/c.txt"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x22236af1 <span class="hljs-string">"files/nest"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x55126def <span class="hljs-string">"files/nest/a.txt"</span>])
</code></pre>
<p>I'd expect this to return files similarly <code>fs.readdir</code>, but now we have the nested files in the listing. Let's see why that happens and if we could get it to work similarly to the listing in Javascript.</p>
<p>If we take a look at the <code>file-seq</code> <a target="_blank" href="https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core.clj#L4973">source code</a></p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">file-seq</span>
  <span class="hljs-string">"A tree seq on java.io.Files"</span>
  {<span class="hljs-symbol">:added</span> <span class="hljs-string">"1.0"</span>
   <span class="hljs-symbol">:static</span> <span class="hljs-literal">true</span>}
  [dir]
    (<span class="hljs-name">tree-seq</span>
     (<span class="hljs-name"><span class="hljs-builtin-name">fn</span></span> [<span class="hljs-comment">^java.io.File</span> f] (<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> f (<span class="hljs-name">isDirectory</span>)))
     (<span class="hljs-name"><span class="hljs-builtin-name">fn</span></span> [<span class="hljs-comment">^java.io.File</span> d] (<span class="hljs-name"><span class="hljs-builtin-name">seq</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> d (<span class="hljs-name">listFiles</span>))))
     dir))
</code></pre>
<p>The docstring says that the function returns a tree seq, which sounds like a non-flat structure. And if we look deeper into <code>tree-seq</code> function and get the docstring.</p>
<blockquote>
<p>Returns a lazy sequence of the nodes in a tree, via a depth-first walk. ...</p>
</blockquote>
<p>And here's the explanation of what is happening: "a depth-first walk." This function will go first till the end of the file tree and start coming back towards the root directory. The first argument is a function that returns a boolean value to decide whether the node (read file or directory) needs to be traversed (read looked into). If it returns true, the same file is passed to the second function to list its nodes (read files and subdirectories). Based on this, the tree-seq will do a depth-first search and, therefore, read all the subdirectories.</p>
<p>You might have already noticed that the second function argument calls <code>(. d (listFiles))</code> to get the files for our directory <code>d</code>. Let's use this to mimic the JS version <code>readdir</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">seq</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files"</span>) listFiles))
<span class="hljs-comment">;; =&gt;</span>
(<span class="hljs-name">#object</span>[java.io.File <span class="hljs-number">0</span>x356bf4d3 <span class="hljs-string">"files/a.txt"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x13c28931 <span class="hljs-string">"files/b.txt"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x444349ab <span class="hljs-string">"files/c.txt"</span>]
 #object[java.io.File <span class="hljs-number">0</span>x2e64f09c <span class="hljs-string">"files/nest"</span>])
</code></pre>
<p>At this point, the expected result is almost identical to the Javascript's <code>fs.readdir</code>. We have the files listed in the directory, but instead of file names, we have <code>java.io.File</code> instances. Let's iterate over the files and once again use Java-interop with the <code>getName</code> method.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">for</span></span> [file (<span class="hljs-name"><span class="hljs-builtin-name">seq</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files"</span>) listFiles))]
  (<span class="hljs-name">.getName</span> file))
<span class="hljs-comment">;; =&gt; ("a.txt" "b.txt" "c.txt" "nest")</span>
</code></pre>
<p>The <code>fs.readdir</code> function will throw if called with a filename that is not a directory. If we try to create a file sequence from a <code>java.io.File</code> that is not a directory, we get only the file itself in a list.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">file-seq</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files/a.txt"</span>))
<span class="hljs-comment">;; =&gt; (#object[java.io.File 0x37eeb5e7 "files/a.txt"])</span>
</code></pre>
<p>Let's combine what we've learned from the previous examples into a new function <code>read-dir</code> that takes a path, checks that it is not a file, and then lists the files and directories in the folder but not the subpages.</p>
<pre><code class="lang-clojure">(<span class="hljs-keyword">defn</span> <span class="hljs-title">read-dir</span> [d]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [directory (<span class="hljs-name">io/file</span> d)]
    (<span class="hljs-name"><span class="hljs-builtin-name">when</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> directory isFile)
      (<span class="hljs-name"><span class="hljs-builtin-name">throw</span></span> (<span class="hljs-name">ex-info</span> <span class="hljs-string">"Path is not a directory"</span> {<span class="hljs-symbol">:path</span> directory})))
    (<span class="hljs-name"><span class="hljs-builtin-name">doall</span></span>
     (<span class="hljs-name"><span class="hljs-builtin-name">for</span></span> [file (<span class="hljs-name"><span class="hljs-builtin-name">seq</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">.</span></span> directory listFiles))]
       (<span class="hljs-name">.getName</span> file)))))

(<span class="hljs-name">read-dir</span> <span class="hljs-string">"files"</span>)
<span class="hljs-comment">;; =&gt; ("a.txt" "b.txt" "c.txt" "nest")</span>

(<span class="hljs-name">read-dir</span> <span class="hljs-string">"files/a.txt"</span>)
<span class="hljs-comment">;; Unhandled clojure.lang.ExceptionInfo</span>
<span class="hljs-comment">;; Path is not a directory</span>
<span class="hljs-comment">;; {:path #object[java.io.File 0x7c7a2da9 "files/a.txt"]}</span>
</code></pre>
<p>That's enough on listing files for now. Next, let's look into reading the files.</p>
<h2 id="heading-read-files">Read Files</h2>
<p>Let's see the content for a couple of the example files.</p>
<pre><code class="lang-bash">❯ cat files/a.txt
AAAAA
❯ cat files/b.txt
BBBB
1111
2222
3333
</code></pre>
<p>In Javascript, we'd typically use the <code>fs.readFile</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> content = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">"files/a.txt"</span>, {<span class="hljs-attr">encoding</span>: <span class="hljs-string">"utf8"</span>});
<span class="hljs-built_in">console</span>.log(content);
<span class="hljs-comment">// AAAAA</span>
</code></pre>
<p>In Clojure, we default to <code>slurp</code>.</p>
<pre><code class="lang-javascript">(slurp (io/file <span class="hljs-string">"files/a.txt"</span>))
;; =&gt; <span class="hljs-string">"AAAAA\n"</span>
</code></pre>
<p>Slurp also works with the filename since if the argument is a string; it's first coerced (read interpreted as) as a URI and second as a local file.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">slurp</span></span> <span class="hljs-string">"files/a.txt"</span>)
<span class="hljs-comment">;; =&gt; "AAAAA\n"</span>
</code></pre>
<p>Both <code>fs.readdir</code> and <code>slurp</code> read the file into memory at once; therefore, you might want to avoid it in production!</p>
<p>The more responsible way is to read the files as streams line by line.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> file = <span class="hljs-keyword">await</span> fs.open(<span class="hljs-string">'files/b.txt'</span>,);

<span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> file.readLines()) {
    <span class="hljs-built_in">console</span>.log(line);
}
<span class="hljs-comment">// BBBB</span>
<span class="hljs-comment">// 1111</span>
<span class="hljs-comment">// 2222</span>
<span class="hljs-comment">// 3333</span>
</code></pre>
<p>In Clojure, we can use the <code>java.io.Reader</code> to create a buffered reader for streaming over the files. <code>slurp</code> uses the same mechanism, but instead of reading a file line by line, it reads it all simultaneously.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">io/reader</span>  <span class="hljs-string">"files/a.txt"</span>)
<span class="hljs-comment">;; =&gt; #object[java.io.BufferedReader 0x4754cc18 "java.io.BufferedReader@4754cc18"]</span>
</code></pre>
<p>We can open this stream (read file) to create a lazy line sequence that can be processed one line at a time.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">with-open</span></span> [reader (<span class="hljs-name">io/reader</span>  <span class="hljs-string">"files/b.txt"</span>)]
  (<span class="hljs-name"><span class="hljs-builtin-name">doall</span></span> (<span class="hljs-name"><span class="hljs-builtin-name">for</span></span> [line (<span class="hljs-name"><span class="hljs-builtin-name">line-seq</span></span> reader)]
           (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> <span class="hljs-string">"read: "</span> line))))
<span class="hljs-comment">;; =&gt; ("read: BBBB" "read: 1111" "read: 2222" "read: 3333")</span>
</code></pre>
<p>Next, to writing files.</p>
<h2 id="heading-writing-files">Writing Files</h2>
<p>With Javascript, we'd typically do this with <code>fs.writeFile</code> or use <code>fs.createFileStream</code> for extra control.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> fs.writeFile(<span class="hljs-string">"files/write-with-js.txt"</span>, <span class="hljs-string">"writing from JS"</span>);

<span class="hljs-keyword">let</span> content_01 = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">"files/write-with-js.txt"</span>, 
                                   {<span class="hljs-attr">encoding</span>: <span class="hljs-string">"utf8"</span>});
<span class="hljs-built_in">console</span>.log(content); 
<span class="hljs-comment">// writing from JS</span>
</code></pre>
<p>In Clojure, similarly to <code>slurp</code> we have <code>spit</code> to write data.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">spit</span> <span class="hljs-string">"files/new.txt"</span> <span class="hljs-string">"New content here"</span>)
<span class="hljs-comment">;; =&gt; nil</span>
(<span class="hljs-name"><span class="hljs-builtin-name">slurp</span></span> <span class="hljs-string">"files/new.txt"</span>)
<span class="hljs-comment">;; =&gt; "New content here"</span>
</code></pre>
<p>By default, <code>spit</code> overrides the file with the given content. To add to the file, use arguments <code>:append true</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">spit</span> <span class="hljs-string">"files/new.txt"</span> <span class="hljs-string">"Append to file"</span>)
<span class="hljs-comment">;; =&gt; nil</span>
(<span class="hljs-name"><span class="hljs-builtin-name">slurp</span></span> <span class="hljs-string">"files/new.txt"</span>)
<span class="hljs-comment">;; =&gt; "Append to file"</span>
(<span class="hljs-name">spit</span> <span class="hljs-string">"files/new.txt"</span> <span class="hljs-string">" Try again"</span> <span class="hljs-symbol">:append</span> <span class="hljs-literal">true</span>)
<span class="hljs-comment">;; =&gt; nil</span>
(<span class="hljs-name"><span class="hljs-builtin-name">slurp</span></span> <span class="hljs-string">"files/new.txt"</span>)
<span class="hljs-comment">;; =&gt; "Append to file Try again"</span>
</code></pre>
<p><code>spit</code> wraps the <code>java.io.writer</code>, which can be used directly. Similarly to <code>slurp</code> <code>java.io.writer</code> replaces the file content if <code>:append true</code> is not passed as an option.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">with-open</span></span> [writer (<span class="hljs-name">io/writer</span> <span class="hljs-string">"files/other.txt"</span>)]
  (<span class="hljs-name">.write</span> writer <span class="hljs-string">"text here"</span>))

(<span class="hljs-name"><span class="hljs-builtin-name">slurp</span></span> <span class="hljs-string">"files/other.txt"</span>)
<span class="hljs-comment">;; =&gt; "text here"</span>

(<span class="hljs-name"><span class="hljs-builtin-name">with-open</span></span> [writer (<span class="hljs-name">io/writer</span> <span class="hljs-string">"files/other.txt"</span> <span class="hljs-symbol">:append</span> <span class="hljs-literal">true</span>)]
  (<span class="hljs-name">.write</span> writer <span class="hljs-string">" more text"</span>))
<span class="hljs-comment">;; =&gt; nil</span>

(<span class="hljs-name"><span class="hljs-builtin-name">slurp</span></span> <span class="hljs-string">"files/other.txt"</span>)
<span class="hljs-comment">;; =&gt; "text here more text"</span>
</code></pre>
<p>Let's go through a couple of more use cases.</p>
<h2 id="heading-renaming-files">Renaming Files</h2>
<p>In Javascript, we rename files with <code>fs.rename</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> fs.rename(<span class="hljs-string">"files/c.txt"</span>, <span class="hljs-string">"files/d.txt"</span>);
</code></pre>
<p>In Clojure, we use the <code>renameTo</code> method.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">.renameTo</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files/new.txt"</span>)
           (<span class="hljs-name">io/file</span> <span class="hljs-string">"files/newer.txt"</span>))
<span class="hljs-comment">;; =&gt; true</span>
(<span class="hljs-name">.isFile</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files/new.txt"</span>))
<span class="hljs-comment">;; =&gt; false</span>
(<span class="hljs-name">.isFile</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files/newer.txt"</span>))
<span class="hljs-comment">;; =&gt; true</span>
</code></pre>
<h2 id="heading-deleting-files">Deleting Files</h2>
<p>In Javascript, we delete files with <code>fs.unlink</code></p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">await</span> fs.unlink(<span class="hljs-string">"files/d.txt"</span>);
</code></pre>
<p>In Clojure, we have <code>clojure.java.io/delete-file</code> for the same task.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">.isFile</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files/other.txt"</span>))
<span class="hljs-comment">;; =&gt; true</span>
(<span class="hljs-name">io/delete-file</span> <span class="hljs-string">"files/other.txt"</span>)
<span class="hljs-comment">;; =&gt; true</span>
(<span class="hljs-name">.isFile</span> (<span class="hljs-name">io/file</span> <span class="hljs-string">"files/other.txt"</span>))
<span class="hljs-comment">;; =&gt; false</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Clojure can do the same tasks as Javascript on the filesystem level since <code>clojure.java.io</code> provides utility functions as a layer on top of <code>java.io</code> classes. If there's no function matching your need, you can use Java-interop by, for example, using the <code>java.io.File</code> methods. If you hit a roadblock, check the following resources for more hints.</p>
<ul>
<li><p><a target="_blank" href="https://clojuredocs.org/clojure.java.io">ClojureDocs: clojure.java.io</a></p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/File.html">JavaDocs: java.io.File</a></p>
</li>
</ul>
<p>Once again, I hope you found this helpful. Feel free to reach out and let me know what pain points you might have encountered when learning Clojure and what type of posts would help make the jump—social links in the menu.</p>
<p>Thank you for reading.</p>
]]></content:encoded></item><item><title><![CDATA[Take Your Linting Game to the Next Level!]]></title><description><![CDATA[TLDR: It is possible to declare types for Clojure functions and make them available to the library users by creating clj-kondo configs (type exports). The configs can be created with Malli by collecting function schemas and then transforming these in...]]></description><link>https://tonitalksdev.com/take-your-linting-game-to-the-next-level</link><guid isPermaLink="true">https://tonitalksdev.com/take-your-linting-game-to-the-next-level</guid><category><![CDATA[Clojure]]></category><category><![CDATA[clj-kondo]]></category><category><![CDATA[Malli]]></category><category><![CDATA[static code analysis]]></category><category><![CDATA[Linter]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Tue, 14 Nov 2023 18:25:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699987420311/cb1460cc-85fd-4c05-aa92-56fbeaac2e62.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>TLDR: It is possible to declare types for Clojure functions and make them available to the library users by creating clj-kondo configs (type exports). The configs can be created with Malli by collecting function schemas and then transforming these into clj-kondo types in a config file. If the config files are committed into resources, they can be imported by the host project when used as a dependency. Having Malli schemas has the added benefit of enabling the run-time checks via Malli instrumentation.</p>
</blockquote>
<p>I like imagining an ecosystem where there'd be a clj-kondo config available for the most used libraries. I thought I would chime in my two cents on spreading the know-how to spread the word on how to do this. I'm by no means an expert on the topic, but I've learned a thing or two that might also be helpful to others.</p>
<p>Suppose you are unfamiliar with Malli and clj-kondo and how they can help you with static code analysis. In that case, I recommend first reading <a target="_blank" href="https://blog.tvaisanen.com/data-validation-in-clojure">Data Validation in Clojure</a> and <a target="_blank" href="https://blog.tvaisanen.com/typescript-like-intellisense-for-clojure-functions-with-malli">Typescript Like Intellisense for Clojure Functions With Malli</a> for background.</p>
<p>This time, I want to dig deeper into how these tools can improve the developer experience by making the types available to our code's human and CI consumers. I don't know if this is already yesterday's news, but this trick is worth knowing.</p>
<p>Let's explore the topic by creating a library, publishing it to Github, then using it as a dependency for another project, and making the library types available to benefit from the extended clj-kondo features.</p>
<h2 id="heading-publish-library-with-types">Publish Library With Types</h2>
<p>The only required dependency is Malli, which will be used to generate the clj-kondo types.</p>
<blockquote>
<p>Note that in a "real" project Malli would be added as an extra dependency for an alias when creating the types if the library itself is not depending on Malli. If this is something that doesn't make sense to you let me know in the comments.</p>
</blockquote>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:paths</span> [<span class="hljs-string">"src"</span>]
 <span class="hljs-symbol">:deps</span> {org.clojure/clojure {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.11.1"</span>}
        metosin/malli {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"0.13.0"</span>}}}
</code></pre>
<p>My library will have two functions: adding (x,y) coordinate points and rendering points as a string. If you need help defining the schemas for your use case, check out <a target="_blank" href="https://cljdoc.org/d/metosin/malli/0.13.0/doc/function-schemas">Malli function schema docs</a> for reference.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> tvaisanen.types-export-sample.core
  <span class="hljs-string">"Example on how to create and export type clj-kondo
  type definitions that can be installed in a similar way
  that @types/lib-name is used in Typescript."</span>)

(<span class="hljs-keyword">def</span> <span class="hljs-title">Point</span> [<span class="hljs-symbol">:map</span>
            [<span class="hljs-symbol">:x</span> <span class="hljs-symbol">:int</span>]
            [<span class="hljs-symbol">:y</span> <span class="hljs-symbol">:int</span>]])

(<span class="hljs-keyword">defn</span> <span class="hljs-title">add-points</span>
  {<span class="hljs-symbol">:malli/schema</span> [<span class="hljs-symbol">:=&gt;</span> [<span class="hljs-symbol">:cat</span> Point Point] Point]}
  [p1 p2]
  (<span class="hljs-name">merge-with</span> + p1 p2))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">render-point</span>
  {<span class="hljs-symbol">:malli/schema</span> [<span class="hljs-symbol">:=&gt;</span> [<span class="hljs-symbol">:cat</span> Point] <span class="hljs-symbol">:string</span>]}
  [p]
  (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> p))
</code></pre>
<p>The only difference from what you'd typically write is the added metadata key pair. This will be enough for us to get the benefits in later stages when we want to provide the type hints for the library users. Let's take a look at how to make that happen next.</p>
<h3 id="heading-export-configuration">Export Configuration</h3>
<p>Exporting is the process of creating the library's clj-kondo configuration under the resources folded as the <a target="_blank" href="https://cljdoc.org/d/clj-kondo/clj-kondo/2023.10.20/doc/configuration#exporting">documentation</a> instructs. To do this, I created a naive exporting function that:</p>
<ol>
<li><p>Also collects the <code>malli/schema</code> schemas from the functions,</p>
</li>
<li><p>generates the type definitions under the Malli <code>clj-kondo-types</code>,</p>
</li>
<li><p>copies the <code>config.edn</code> files into resources,</p>
</li>
<li><p>and cleans the <code>clj-kondo-types</code> cache.</p>
</li>
</ol>
<pre><code class="lang-clojure">(<span class="hljs-name">require</span> '[malli.dev <span class="hljs-symbol">:as</span> dev]
         '[clojure.java.io. <span class="hljs-symbol">:as</span> io])

(<span class="hljs-keyword">defn</span> <span class="hljs-title">export-types</span> []
  <span class="hljs-comment">;; collect schemas and start instrumentation</span>
  (<span class="hljs-name">dev/start!</span>)

  <span class="hljs-comment">;; create export file</span>
  (<span class="hljs-keyword">def</span> <span class="hljs-title">export-file</span>
    (<span class="hljs-name">io/file</span> <span class="hljs-string">"resources/clj-kondo/clj-kondo.exports/tvaisanen/export-types-sample/config.edn"</span>))

  <span class="hljs-comment">;; make parents if not exist</span>
  (<span class="hljs-name">io/make-parents</span> export-file)

  <span class="hljs-comment">;; copy the configs</span>
  (<span class="hljs-name">io/copy</span>
   (<span class="hljs-name">io/file</span> <span class="hljs-string">".clj-kondo/metosin/malli-types-clj/config.edn"</span>)
   export-file)

  <span class="hljs-comment">;; clear the cache and stop instrumentation</span>
  (<span class="hljs-name">dev/stop!</span>))
</code></pre>
<p>If we inspect the <code>.clj-kondo</code> cache folder, we can see that there's a config for <code>malli-types-clj</code> which is used to store the temporary cache while the Malli instrumentation is active <code>(dev/start!)</code> and it's cleared on <code>(dev/stop!)</code>.</p>
<pre><code class="lang-bash">❯ tree .clj-kondo
.clj-kondo
└── metosin
    └── malli-types-clj
        └── config.edn
</code></pre>
<p>What we just did with the export function was copy this <code>config.edn</code> file from the clj-kondo cache into our libs resources for persistence.</p>
<pre><code class="lang-clojure">❯ tree resources
resources
└── clj-kondo
    └── clj-kondo.exports
        └── tvaisanen
            └── export-types-sample
                └── config.edn

<span class="hljs-number">5</span> directories, <span class="hljs-number">1</span> file
</code></pre>
<p>This is the folder structure that clj-kondo expects to see when it's looking for available configurations to be imported. In my case the <code>config.edn</code> looks like this.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:linters</span>
 {<span class="hljs-symbol">:unresolved-symbol</span> {<span class="hljs-symbol">:exclude</span> [(<span class="hljs-name">malli.core/=&gt;</span>)]},
  <span class="hljs-symbol">:type-mismatch</span>
  {<span class="hljs-symbol">:namespaces</span>
   {export-types-sample.core

    {render-point
     {<span class="hljs-symbol">:arities</span>
      {<span class="hljs-number">1</span> {<span class="hljs-symbol">:args</span> [{<span class="hljs-symbol">:op</span>  <span class="hljs-symbol">:keys</span>,
                  <span class="hljs-symbol">:req</span> {<span class="hljs-symbol">:x</span> <span class="hljs-symbol">:int</span>,
                        <span class="hljs-symbol">:y</span> <span class="hljs-symbol">:int</span>}}],
          <span class="hljs-symbol">:ret</span>  <span class="hljs-symbol">:string</span>}}},

     add-points
     {<span class="hljs-symbol">:arities</span>
      {<span class="hljs-number">2</span> {<span class="hljs-symbol">:args</span> [{<span class="hljs-symbol">:op</span>  <span class="hljs-symbol">:keys</span>,
                  <span class="hljs-symbol">:req</span> {<span class="hljs-symbol">:x</span> <span class="hljs-symbol">:int</span>,
                        <span class="hljs-symbol">:y</span> <span class="hljs-symbol">:int</span>}}
                 {<span class="hljs-symbol">:op</span>  <span class="hljs-symbol">:keys</span>,
                  <span class="hljs-symbol">:req</span> {<span class="hljs-symbol">:x</span> <span class="hljs-symbol">:int</span>,
                        <span class="hljs-symbol">:y</span> <span class="hljs-symbol">:int</span>}}],
          <span class="hljs-symbol">:ret</span>  {<span class="hljs-symbol">:op</span>  <span class="hljs-symbol">:keys</span>,
                 <span class="hljs-symbol">:req</span> {<span class="hljs-symbol">:x</span> <span class="hljs-symbol">:int</span>, <span class="hljs-symbol">:y</span> <span class="hljs-symbol">:int</span>}}}}}}}}}}
</code></pre>
<p>From here, we can see that argument and return types are defined for two functions <code>render-point</code> and <code>add-points</code> that have. This tells clj-kondo how the function arguments and return value should look when it's doing the static code analysis.</p>
<p>We are almost done with the first step: creating our library with typehints. Next, commit and push the changes to your Git repo. <a target="_blank" href="https://github.com/tvaisanen/export-types-sample">Here's mine</a> for a reference.</p>
<h2 id="heading-load-the-published-library-as-a-dependency">Load the Published Library as a Dependency</h2>
<p>Now that we have "published" a library with clj-kondo configs, we can use this as a dependency for another project.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:paths</span> [<span class="hljs-string">"src"</span>]
 <span class="hljs-symbol">:deps</span> {org.clojure/clojure {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.11.1"</span>}

        metosin/malli {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"0.13.0"</span>}

        tvaisanen/export-types-sample
        {<span class="hljs-symbol">:git/sha</span> <span class="hljs-string">"29f9b259975bde304300e4ca69c70d61622cabab"</span>
         <span class="hljs-symbol">:git/url</span> <span class="hljs-string">"https://github.com/tvaisanen/export-types-sample.git"</span>}}}
</code></pre>
<blockquote>
<p>If you didn't use Github see the available gitlib config options from the <a target="_blank" href="https://clojure.org/reference/deps_and_cli#_coord_attributes">docs</a>.</p>
</blockquote>
<p>When we fetch the source code for the dependencies, we receive the resources (read configs) in our local cache and the malli function schemas within the source code. We don't need to install additional libs or types to access them. Next, we must collect these configs to our project's <code>.clj-kondo</code> cache. <a target="_blank" href="https://cljdoc.org/d/clj-kondo/clj-kondo/2023.10.20/doc/configuration#importing">Clj-kondo docs</a> provide the steps on how to do this. Remember to create the <code>.clj-kondo</code> folder if it doesn't exist before copying the configs.</p>
<p>First, we copy the configs.</p>
<pre><code class="lang-bash">❯ clj-kondo --lint <span class="hljs-string">"<span class="hljs-subst">$(clojure -Spath)</span>"</span> --copy-configs --skip-lint

Imported config to .clj-kondo/tvaisanen/export-types-sample. 
To activate, add <span class="hljs-string">"tvaisanen/export-types-sample"</span> to 
:config-paths <span class="hljs-keyword">in</span> .clj-kondo/config.edn.
</code></pre>
<p>Then update the <code>.clj-kondo/config.edn</code> (you might need to create the file) as instructed by clj-kondo output.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:config-paths</span> [<span class="hljs-string">"tvaisanen/export-types-sample"</span>]}
</code></pre>
<p>And I am then populating the cache with.</p>
<pre><code class="lang-bash">$ clj-kondo --lint $(clojure -Spath) --dependencies --parallel
</code></pre>
<p>After this, we should be good to jump into the editor and see if everything worked as expected.</p>
<h3 id="heading-benefits-of-types-in-the-editor">Benefits of Types in the Editor</h3>
<p>I created a <code>core</code> namespace for the new project where I've required the library I set up. You can see that when <code>add-points</code> is called with an invalid <code>sample/Point</code> it tells us that we are missing a required key <code>:y</code>. Without the extra type config, we should have an issue with "duplicate key :x", so it looks like the lib configs trump the default linting issues in priority.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699694923692/377935a6-3770-4122-9f74-9a06a471bfcb.png" alt class="image--center mx-auto" /></p>
<p>As we extend the code, we can see that <code>add-points</code> return a value that the <code>render-point</code> is okay with according to clj-kondo, but if we pass a map that is not a <code>sample/Point</code> we get another linting error on the screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699695090293/ad1b0e9b-6d63-4782-a361-042a89cbc512.png" alt class="image--center mx-auto" /></p>
<p>These red squiggly lines can save you a lot of headaches by letting you know where the types don't match the expected value. But this is just the first benefit that comes from using the types. So far, we haven't looked into how Malli expands on this.</p>
<h3 id="heading-benefits-of-types-in-the-repl">Benefits of Types in the REPL</h3>
<p>If a library has Malli schema function definitions, we can start instrumentation (monitoring with what values the functions are called and what they return) and get run-time type checking. In practice, this means we get a report on "invalid function call" in the REPL every time a function is called with unexpected arguments.</p>
<p>Let me demonstrate this.</p>
<p>We can use this to our advantage since we created the library with the malli schemas instead of writing the type hints manually into the clj-kondo config. Let's require <code>malli.dev</code> in our namespace and run <code>dev/start!</code> as we did when we first exported the types. Now, try running the <code>sample/render-point</code> with an invalid point argument.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699696127891/c9b38664-c10c-4d03-b52c-71bf5c6f1ceb.png" alt class="image--center mx-auto" /></p>
<p>When we evaluate line 19 while the malli instrumentation is active, we can find something new in the REPL, a schema error nicely documenting what is happening. The REPL provides helpful information about what went wrong with the function call.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699696129542/1365c14a-43fb-479f-990b-5b0b811aca00.png" alt class="image--center mx-auto" /></p>
<p>Of course, we must remember that if there are a lot of functions being called, having the instrumentation on all the time can have a performance penalty.</p>
<p>Just keep in mind when running <code>dev/start!</code> the types are created into <code>.clj-kondo/metosin/malli-types-clj/config.edn</code> and when the <code>dev/stop!</code> is run. These types will be cleaned up.</p>
<h3 id="heading-benefit-of-types-in-the-terminal-ci">Benefit of Types in the Terminal / CI</h3>
<p>Last but least, we can also use the generated types outside our editor by using <code>clj-kondo</code> from the command line.</p>
<pre><code class="lang-clojure">❯ clj-kondo --lint <span class="hljs-string">"src"</span>
src/core.clj:<span class="hljs-number">6</span>:<span class="hljs-number">2</span>: error: Missing required key: <span class="hljs-symbol">:y</span>
src/core.clj:<span class="hljs-number">6</span>:<span class="hljs-number">8</span>: error: duplicate key <span class="hljs-symbol">:x</span>
src/core.clj:<span class="hljs-number">13</span>:<span class="hljs-number">22</span>: error: Missing required key: <span class="hljs-symbol">:x</span>
src/core.clj:<span class="hljs-number">13</span>:<span class="hljs-number">22</span>: error: Missing required key: <span class="hljs-symbol">:y</span>
linting took <span class="hljs-number">5</span>ms, errors: <span class="hljs-number">4</span>, warnings: <span class="hljs-number">0</span>
</code></pre>
<p>Adding this step can help keep many unnecessary bugs out of production when added to the continuous integration checks.</p>
<h2 id="heading-where-to-contribute">Where to Contribute</h2>
<p>If you find that your particular use case is not supported for some reason. Providing patches is encouraged. Malli defines the <code>malli-&gt;clj-kondo</code> transformations in <a target="_blank" href="https://github.com/metosin/malli/blob/master/src/malli/clj_kondo.cljc"><code>malli.clj-kondo</code></a> namespace, which is where you want to look if you want to learn how the transformation process works or if you're going to extend the functionality. On the clj-kondo side, read the <a target="_blank" href="https://github.com/clj-kondo/clj-kondo/blob/master/doc/types.md">status of types</a> and this <a target="_blank" href="https://github.com/clj-kondo/clj-kondo/blob/d9fca2705863e3e604e004ccb942e0b3d2e268ec/src/clj_kondo/impl/types.clj#L18-L51">source code file</a> listing the available type mappings.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That was probably a lot to digest in one go. It took me some time to understand the relationship between clj-kondo and Malli, but slowly and surely, it's making more sense. I wanted to write about this topic because I think if more people knew how to add the types to their projects, it would improve the developer experience across the whole ecosystem (at least, this is what I'd like to imagine).</p>
<p>This is somewhat what happened with Javascript and Typescript. Little by little, the majority of the libs started offering type definitions. I don't know about you, but I'd appreciate a little extra help from my editor in debugging the expected types for a given function and having the change occasionally to turn on the Malli instrumentation to provide some run-time info on what's going wrong.</p>
<p>Some tools can provide the observability to run-time values like <a target="_blank" href="https://www.flow-storm.org/">Flowstrom</a> or just good old <a target="_blank" href="https://github.com/clojure/tools.trace">clojure/tools.trace</a>. However, these tools do not tell me what type of data the original author had intended to receive.</p>
<p>Once again, I hope you found this helpful. Feel free to reach out and let me know what you think—social links in the menu.</p>
<p>Thank you for reading.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying Clojure Like a Seasoned Hobbyist]]></title><description><![CDATA[This is a tutorial on how to set up a minimal Clojure app development environment with Postgres on DigitalOcean using Terraform. Although there will be room for further optimization and improvement in the implementation, this post focuses on providin...]]></description><link>https://tonitalksdev.com/deploying-clojure-like-a-seasoned-hobbyist</link><guid isPermaLink="true">https://tonitalksdev.com/deploying-clojure-like-a-seasoned-hobbyist</guid><category><![CDATA[Clojure]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[DigitalOcean]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Tue, 07 Nov 2023 18:05:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699339847852/983e28d8-3c99-4527-a13a-6cdce7d73dfa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a tutorial on how to set up a minimal Clojure app development environment with Postgres on DigitalOcean using Terraform. Although there will be room for further optimization and improvement in the implementation, this post focuses on providing the most straightforward template for setting up a project.</p>
<p>I prefer configuring deployments in declarative Terraform files over clicking through dashboards. This helps me to get back on track when I return to my abandoned side projects.</p>
<p>It's worth mentioning that I will be using a local terraform state, although I don't recommend it. Lately, I have been using Terraform Cloud for my projects. If you want to learn how to set up a Github CI with Terraform Cloud for this project, please let me know in the comments.</p>
<p>Most platforms like AWS, GCP, Azure, Linode, Fly, and Digitalocean support Terraform. The big three, AWS, GCP, and Azure, feel excessive for hobby projects. Additionally, managing virtual servers or utilizing Kubernetes is not something I want to deal with, so that crosses Linode off the list, leaving me with Fly and Digitalocean as the options. While I haven't tested Fly yet, DigitalOcean's app platform has become my go-to choice out of habit and because the development experience feels suitable.</p>
<h2 id="heading-requirements">Requirements</h2>
<p>We need <a target="_blank" href="https://developer.hashicorp.com/terraform/downloads">Terraform</a>, <a target="_blank" href="https://docs.digitalocean.com/reference/doctl/how-to/install/">Doctl</a>, <a target="_blank" href="https://docs.docker.com/engine/install/">Docker</a>, and <a target="_blank" href="https://docs.docker.com/compose/install/">Docker Compose</a> installed on our development machine and a <a target="_blank" href="https://www.digitalocean.com/">DigitalOcean</a> account with an <a target="_blank" href="https://docs.digitalocean.com/reference/api/create-personal-access-token/">API token</a> with write permissions that we can use to authenticate ourselves from the command line.</p>
<p>You should be able to follow up on the examples without prior knowledge of Terraform. Still, if you're interested in learning more, the <a target="_blank" href="https://developer.hashicorp.com/terraform/intro">official website</a> is an excellent place to start.</p>
<h2 id="heading-application">Application</h2>
<p>To get started, create the following folder structure for the project. Clojure-related code and required Docker files are in the <code>api</code> folder, where <code>docker-compose.yml</code> is for running the application locally with a Postgres database, and the DigitalOcean-related Terraform code will be in the <code>terraform</code> folder.</p>
<pre><code class="lang-bash">project-root
├── api
│   ├── deps.edn
│   ├── docker-compose.yml
│   ├── Dockerfile
│   └── src
│       └── main.clj
└── terraform
    ├── main.tf
    ├── output.tf
    └── setup.tf
</code></pre>
<h3 id="heading-clojure">Clojure</h3>
<p>First, let's start by defining the Clojure dependencies for the application in the <code>api/deps.edn</code>. We need <code>ring</code> dependencies for serving the API and <code>next.jdbc</code> and <code>postgresql</code> for the database access.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:paths</span> [<span class="hljs-string">"src"</span>]

 <span class="hljs-symbol">:deps</span> {org.clojure/clojure {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.11.0"</span>}
        ring/ring-core {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.6.3"</span>}
        ring/ring-jetty-adapter {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.6.3"</span>}
        com.github.seancorfield/next.jdbc {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.2.659"</span>}
        org.postgresql/postgresql {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"42.2.10"</span>}}

 <span class="hljs-symbol">:aliases</span> {<span class="hljs-symbol">:run</span> {<span class="hljs-symbol">:main-opts</span> [<span class="hljs-string">"-m"</span> <span class="hljs-string">"main"</span>]
                 <span class="hljs-symbol">:exec-fn</span>   main/run!}}}
</code></pre>
<p>Next, create the code to serve the API, connect to the database, and make a database query in <code>api/src/main.clj</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> main
  (<span class="hljs-symbol">:require</span> [ring.adapter.jetty <span class="hljs-symbol">:as</span> jetty]
            [next.jdbc <span class="hljs-symbol">:as</span> jdbc])
  (<span class="hljs-symbol">:gen-class</span>))

(<span class="hljs-keyword">def</span> <span class="hljs-title">port</span> (<span class="hljs-name">Integer/parseInt</span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"PORT"</span>)))

(<span class="hljs-keyword">def</span> <span class="hljs-title">db</span> {<span class="hljs-symbol">:dbtype</span> <span class="hljs-string">"postgres"</span>
         <span class="hljs-symbol">:jdbcUrl</span> (<span class="hljs-name">System/getenv</span> <span class="hljs-string">"JDBC_DATABASE_URL"</span>)})

(<span class="hljs-keyword">def</span> <span class="hljs-title">ds</span> (<span class="hljs-name">jdbc/get-datasource</span> db))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">app</span> [_request]
  (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> [db-version (<span class="hljs-name">jdbc/execute!</span> ds [<span class="hljs-string">"SELECT version()"</span>])]
    {<span class="hljs-symbol">:status</span>  <span class="hljs-number">200</span>
     <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"Content-Type"</span> <span class="hljs-string">"application/edn"</span>}
     <span class="hljs-symbol">:body</span>    (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> db-version)}))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">run!</span> [&amp; _args]
  (<span class="hljs-name">jetty/run-jetty</span> #'app {<span class="hljs-symbol">:port</span> port}))
</code></pre>
<p>This is all of the application code. The next step is to create a Dockerfile and set up the local development environment to validate that the database connection and the API are working as expected when provided with the expected environment variables.</p>
<h3 id="heading-dockerfile">Dockerfile</h3>
<p>Let's keep the <code>api/Dockerfile</code> as simple as possible for now.</p>
<pre><code class="lang-bash">FROM clojure:openjdk-17-tools-deps-buster

WORKDIR app
COPY . .

RUN clojure -P

CMD clj -X:run
</code></pre>
<blockquote>
<p>In general it's better to create the Dockerfile in stages to avoid unnecessary build delay and minimize the image size. If you're interested in learning more about that let me know in the comments. You can also read more <a target="_blank" href="https://docs.docker.com/build/building/multi-stage/">Docker: Multi-Stage Builds</a> for further information.</p>
</blockquote>
<p>Finally, let's set up a <code>api/docker-compose.yml</code> file for mimicking our deployment environment to test that the API is working as expected.</p>
<pre><code class="lang-dockerfile">version: <span class="hljs-string">'3.1'</span>

services:

  api:
    build: .
    environment:
      JDBC_DATABASE_URL: <span class="hljs-string">"jdbc:postgresql://postgres:5432/db?user=user&amp;password=password"</span>
      PORT: <span class="hljs-number">8000</span>
    ports:
      - <span class="hljs-number">8000</span>:<span class="hljs-number">8000</span>

  postgres:
    image: postgres
    environment:
      POSTGRES_DB: db
      POSTGRES_USER: <span class="hljs-keyword">user</span>
      POSTGRES_PASSWORD: password
    ports:
      - <span class="hljs-number">5432</span>:<span class="hljs-number">5432</span>
</code></pre>
<h3 id="heading-validate-application-code">Validate Application Code</h3>
<p>Start the API and the database with <code>docker-compose up</code> and <code>HTTP GET localhost:8000</code>.</p>
<pre><code class="lang-bash">❯ http localhost:8000
HTTP/1.1 200 OK
Content-Type: application/edn
Date: Mon, 30 Oct 2023 16:50:53 GMT
Server: Jetty(9.2.21.v20170120)
Transfer-Encoding: chunked

[{:version <span class="hljs-string">"PostgreSQL 14.2 (Debian 14.2-1.pgdg110+1) 
            on x86_64-pc-linux-gnu, compiled by gcc 
            (Debian 10.2.1-6) 10.2.1 20210110, 64-bit"</span>}]
</code></pre>
<p>If you did get something similar to the above, we are good to continue forward. The next step is to set up the application to run in DigitalOcean.</p>
<blockquote>
<p>In the case you're wondering what's the <code>http</code> command. It is <a target="_blank" href="https://httpie.io/cli">httpie</a>.</p>
</blockquote>
<h2 id="heading-digitalocean-and-terraform">DigitalOcean and Terraform</h2>
<p>The first thing we must do is to set up the DigitalOcean Terraform provider. This is analogous to adding a Clojure dependency in a <code>deps.edn</code> file. Open <code>terraform/setup.tf</code> and add the following content to it.</p>
<pre><code class="lang-go">terraform {
  required_providers {
    digitalocean = {
      source  = <span class="hljs-string">"digitalocean/digitalocean"</span>
      version = <span class="hljs-string">"~&gt; 2.0"</span>
    }
  }
}

provider <span class="hljs-string">"digitalocean"</span> {}
</code></pre>
<p>When a new provider is added, Terraform needs to be explicitly initialized with <code>terraform init</code>. After running the command, you should see the following message with additional output.</p>
<pre><code class="lang-bash">Terraform has been successfully initialized!
</code></pre>
<p>So far, we have downloaded the provider but have not yet configured the authentication. We'll get there in a moment.</p>
<h3 id="heading-project">Project</h3>
<p>Now it's time to set up the project to which we'll assign all our resources. Add the following content to <code>terraform/main.tf</code> and feel free to name your project however you wish.</p>
<pre><code class="lang-bash">resource <span class="hljs-string">"digitalocean_project"</span> <span class="hljs-string">"project"</span> {
  name        = <span class="hljs-string">"my-sample-project"</span>
  description = <span class="hljs-string">"description"</span>
  environment = <span class="hljs-string">"development"</span>
  purpose     = <span class="hljs-string">"template-app"</span>
}
</code></pre>
<p>If we try to apply the changes before configuring the API token, we should see something like this:</p>
<pre><code class="lang-bash">❯ terraform apply

...

digitalocean_project.project: Creating...
╷
│ Error: Error creating Project: POST https://api.digitalocean.com/v2/projects: 
|        401 (request <span class="hljs-string">"f2774cfc-66fc-4f34-98d9-0935f9dcd33d"</span>) 
|        Unable to authenticate you with digitalocean_project.project,
│        on main.tf line 1, <span class="hljs-keyword">in</span> resource <span class="hljs-string">"digitalocean_project"</span> <span class="hljs-string">"project"</span>:
│        1: resource <span class="hljs-string">"digitalocean_project"</span> <span class="hljs-string">"project"</span> {
│
╵
</code></pre>
<p>To apply the changes, we must authenticate ourselves with a <code>token</code>. There are a couple of different options on how to do this.</p>
<blockquote>
<p><a target="_blank" href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs#token"><code>token</code></a> - (Required) This is the DO API token. Alternatively, this can also be specified using environment variables ordered by precedence:</p>
<ul>
<li><p><a target="_blank" href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs#DIGITALOCEAN_TOKEN"><code>DIGITALOCEAN_TOKEN</code></a></p>
</li>
<li><p><a target="_blank" href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs#DIGITALOCEAN_ACCESS_TOKEN"><code>DIGITALOCEAN_ACCESS_TOKEN</code></a></p>
</li>
</ul>
<p><a target="_blank" href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs">DigitalOcean Terraform Provider</a></p>
</blockquote>
<p>I'll do this step by exporting my API token in an environment variable in the terminal.</p>
<pre><code class="lang-bash">❯ <span class="hljs-built_in">export</span> DIGITALOCEAN_TOKEN=<span class="hljs-variable">${YOUR_API_TOKEN}</span>
</code></pre>
<p>After this, we should be able to create the project by running <code>terraform apply</code>.</p>
<pre><code class="lang-bash">❯ terraform apply

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  <span class="hljs-comment"># digitalocean_project.project will be created</span>
  + resource <span class="hljs-string">"digitalocean_project"</span> <span class="hljs-string">"project"</span> {
      + created_at  = (known after apply)
      + description = <span class="hljs-string">"description"</span>
      + environment = <span class="hljs-string">"development"</span>
      + id          = (known after apply)
      + is_default  = <span class="hljs-literal">false</span>
      + name        = <span class="hljs-string">"my-sample-project"</span>
      + owner_id    = (known after apply)
      + owner_uuid  = (known after apply)
      + purpose     = <span class="hljs-string">"template-app"</span>
      + resources   = (known after apply)
      + updated_at  = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only <span class="hljs-string">'yes'</span> will be accepted to approve.

  Enter a value: yes

digitalocean_project.project: Creating...
digitalocean_project.project: Creation complete after 1s [id=22276173-77de-4e41-ab47-fae4a86ec414]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
</code></pre>
<p>And confirm that the project was created by logging in to the DigitalOcean dashboard.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698853736510/172d417b-8394-47c0-bc63-78ea8cb4caf5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-container-registry">Container Registry</h3>
<p>The next step is to set up the container registry to publish the application image. Add the following to <code>terraform/main.tf</code>.</p>
<pre><code class="lang-go"> resource <span class="hljs-string">"digitalocean_container_registry"</span> <span class="hljs-string">"app_registry"</span> {
  name                   = <span class="hljs-string">"clojure-sample-app"</span>
  subscription_tier_slug = <span class="hljs-string">"starter"</span>
}
</code></pre>
<p>After applying the configuration, confirm that the registry was created. Let's do it with the <code>doctl</code> command line tool this time around.</p>
<pre><code class="lang-bash">❯ doctl registry get clojure-sample-app
Name                  Endpoint                                        Region slug
clojure-sample-app    registry.digitalocean.com/clojure-sample-app    blr1
</code></pre>
<p>Here, we need to pick up the endpoint value <code>clojure-sample-app</code> and use that to tag the application image. Add the <code>build</code> section to the <code>api/docker-compose.yml</code> file and run <code>docker-compose build</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">api:</span>
    <span class="hljs-attr">build:</span>
      <span class="hljs-attr">context:</span> <span class="hljs-string">.</span>
      <span class="hljs-attr">tags:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"registry.digitalocean.com/clojure-sample-app/dev"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">JDBC_DATABASE_URL:</span> <span class="hljs-string">"jdbc:postgresql://postgres:5432/db?user=user&amp;password=password"</span>
      <span class="hljs-attr">PORT:</span> <span class="hljs-number">8000</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-number">8000</span><span class="hljs-string">:8000</span>
</code></pre>
<p>After building, confirm that the image exists with the expected name.</p>
<pre><code class="lang-bash">❯ docker image ls | grep clojure-sample-app
registry.digitalocean.com/clojure-sample-app/dev   latest 20266c1dfcda   12 seconds ago   715MB
</code></pre>
<p>It looks like the build produced the expected outcome. Next, we must <a target="_blank" href="https://docs.digitalocean.com/products/container-registry/how-to/use-registry-docker-kubernetes/#docker-integration">log in</a> to the DigitalOcean container registry to publish the Docker image.</p>
<pre><code class="lang-bash">❯ doctl registry login
Logging Docker <span class="hljs-keyword">in</span> to registry.digitalocean.com
</code></pre>
<p>And then push the image to the registry.</p>
<pre><code class="lang-bash">❯ docker push registry.digitalocean.com/clojure-sample-app/dev
...
latest: digest: sha256:d8a5c01cd95ab5c0981c454866ec52203feba529f169e2da93cb6a99f3af5d88 size: 2840
</code></pre>
<p>Let's confirm with <code>doctl</code> that the image is available in DigitalOcean.</p>
<pre><code class="lang-bash">❯ doctl registry repository list-v2
Name    Latest Manifest                                                            Latest Tag    Tag Count    Manifest Count    Updated At
dev     sha256:d8a5c01cd95ab5c0981c454866ec52203feba529f169e2da93cb6a99f3af5d88    latest        1            1                 2023-11-01 16:07:59 +0000 UTC
</code></pre>
<p>At this point, we have created everything required for the app deployment. Let's do that next.</p>
<h3 id="heading-application-1">Application</h3>
<p>Create a <code>digitalocean_app resource</code> and update the <code>digitalocean_project</code> from before by adding the app to the resources. This way, DigitalOcean can group the resources (in this case, just the app) under the project.</p>
<p>Let's use the least expensive configuration for the dev environment by utilizing a <code>basic-xss</code> instance with a development database. You can modify these based on your needs. See the rest of the available configuration options from the <a target="_blank" href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/app#argument-reference">docs</a>.</p>
<pre><code class="lang-go">resource <span class="hljs-string">"digitalocean_project"</span> <span class="hljs-string">"project"</span> {
  ...
  # Add this line
  resources   = [digitalocean_app.app.urn]
}

resource <span class="hljs-string">"digitalocean_app"</span> <span class="hljs-string">"app"</span> {
  spec {
    name   = <span class="hljs-string">"sample-app"</span>
    region = <span class="hljs-string">"ams"</span>

    alert {
      rule = <span class="hljs-string">"DEPLOYMENT_FAILED"</span>
    }

    service {
      name               = <span class="hljs-string">"api"</span>
      instance_count     = <span class="hljs-number">1</span>
      instance_size_slug = <span class="hljs-string">"basic-xxs"</span>

      image {
        registry_type = <span class="hljs-string">"DOCR"</span>
        repository    = <span class="hljs-string">"dev"</span>
        tag           = <span class="hljs-string">"latest"</span>
        deploy_on_push {
          enabled = <span class="hljs-literal">true</span>
        }
      }

      env {
        key   = <span class="hljs-string">"JDBC_DATABASE_URL"</span>
        value = <span class="hljs-string">"$${starter-db.JDBC_DATABASE_URL}"</span>
      }

      source_dir = <span class="hljs-string">"api/"</span>
      http_port  = <span class="hljs-number">8000</span>

      run_command = <span class="hljs-string">"clj -X:run"</span>
    }

    database {
      name       = <span class="hljs-string">"starter-db"</span>
      engine     = <span class="hljs-string">"PG"</span>
      production = <span class="hljs-literal">false</span>
    }
  }
}
</code></pre>
<p>You probably noticed that we don't have a <code>PORT</code> variable for the application. This is because, in this case, we had configured it. Digitalocean would override it with the <code>http_port</code> variable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698857182445/16813ef2-1b72-48cc-bac1-918c59607c1a.png" alt class="image--center mx-auto" /></p>
<p>The <code>image</code> block of the configuration ties the container registry we created earlier to the application. After, whenever we push a new image with the <code>dev</code> tag, the application will be redeployed.</p>
<h3 id="heading-validate">Validate</h3>
<p>Finally, it's time to check if we did everything correctly. Let's fetch the live URL for our new application. We can do this with <code>doctl apps list</code>.</p>
<pre><code class="lang-bash">❯ doctl apps list -o json | jq <span class="hljs-string">".[0].live_url"</span>
<span class="hljs-string">"https://sample-app-xeoo8.ondigitalocean.app"</span>
</code></pre>
<p>However, I prefer using the resource <a target="_blank" href="https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/app#attributes-reference">available attributes</a> via <a target="_blank" href="https://developer.hashicorp.com/terraform/language/values/outputs">Terraform outputs</a>. These are usually declared in the <code>output.tf</code> file, but I'll skip this step for the example and leave it in <code>terraform/main.tf</code>. Add the following to the file and run <code>terraform apply</code> once more.</p>
<pre><code class="lang-bash">output <span class="hljs-string">"app_url"</span> {
  value = digitalocean_app.app.live_url
}
</code></pre>
<p>Afterwards, the outputs can be viewed with <code>terraform output</code></p>
<pre><code class="lang-bash">❯ terraform output
app_url = <span class="hljs-string">"https://sample-app-xeoo8.ondigitalocean.app"</span>
</code></pre>
<p>Now, we are ready for the last step. Validate that we get the Postgres version as a result, as we did with the local setup.</p>
<pre><code class="lang-http">❯ http https://sample-app-xeoo8.ondigitalocean.app

<span class="yaml"><span class="hljs-string">HTTP/1.1</span> <span class="hljs-number">200</span> <span class="hljs-string">OK</span>
<span class="hljs-attr">CF-Cache-Status:</span> <span class="hljs-string">MISS</span>
<span class="hljs-attr">CF-RAY:</span> <span class="hljs-string">81f5801c8898d92e-HEL</span>
<span class="hljs-attr">Connection:</span> <span class="hljs-string">keep-alive</span>
<span class="hljs-attr">Content-Encoding:</span> <span class="hljs-string">br</span>
<span class="hljs-attr">Content-Type:</span> <span class="hljs-string">application/edn</span>
<span class="hljs-attr">Date:</span> <span class="hljs-string">Wed,</span> <span class="hljs-number">01</span> <span class="hljs-string">Nov</span> <span class="hljs-number">2023 16:26:37 </span><span class="hljs-string">GMT</span>
<span class="hljs-attr">Last-Modified:</span> <span class="hljs-string">Wed,</span> <span class="hljs-number">01</span> <span class="hljs-string">Nov</span> <span class="hljs-number">2023 16:26:36 </span><span class="hljs-string">GMT</span>
<span class="hljs-attr">Server:</span> <span class="hljs-string">cloudflare</span>
<span class="hljs-attr">Set-Cookie:</span> <span class="hljs-string">__cf_bm=SHREkz9ddLOs1EoTzuNj7DSPYfazEy_bpdi7y_Kb9BE-1698855996-0-AVgy3TNvUDITYonrci23K+dW1BHozbNCLeNX0FMHMpI3miRJBq8I4HlQE5nMA5YjoZ4dEXLatcef+AE+gjq4xeQ=;</span> <span class="hljs-string">path=/;</span> <span class="hljs-string">expires=Wed,</span> <span class="hljs-number">01</span><span class="hljs-string">-Nov-23</span> <span class="hljs-number">16</span><span class="hljs-string">:56:36</span> <span class="hljs-string">GMT;</span> <span class="hljs-string">domain=.ondigitalocean.app;</span> <span class="hljs-string">HttpOnly;</span> <span class="hljs-string">Secure;</span> <span class="hljs-string">SameSite=None</span>
<span class="hljs-attr">Transfer-Encoding:</span> <span class="hljs-string">chunked</span>
<span class="hljs-attr">Vary:</span> <span class="hljs-string">Accept-Encoding</span>
<span class="hljs-attr">cache-control:</span> <span class="hljs-string">private</span>
<span class="hljs-attr">x-do-app-origin:</span> <span class="hljs-string">35eedf3b-e0a9-4376-863e-c5ba59ef5d6a</span>
<span class="hljs-attr">x-do-orig-status:</span> <span class="hljs-number">200</span>

[{<span class="hljs-string">:version</span> <span class="hljs-string">"PostgreSQL 12.16 on x86_64-pc-linux-gnu, 
            compiled by gcc, a 47816671df p 0b9793bd75, 64-bit"</span>}]</span>
</code></pre>
<p>Looks like everything is working as expected.</p>
<h3 id="heading-destroy">Destroy</h3>
<p>If you don't want to keep the application running, remember to destroy all of the created resources with <code>terraform destroy</code>. The expected monthly cost with this setup with the <code>basic-xxs</code> instance size and development database is around $12 per month, but the starter subscription tier of DOCR is free of charge.</p>
<p>You can read more about the pricing models from the DigitalOcean docs: <a target="_blank" href="https://docs.digitalocean.com/products/app-platform/details/pricing/">App platform</a>, <a target="_blank" href="https://docs.digitalocean.com/products/container-registry/details/pricing/">DOCR pricing</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I think that DigitalOcean is an excellent lightweight alternative for smaller projects. It does have all the expected features for general app development, and it can be extended with the add-ons found in the <a target="_blank" href="https://marketplace.digitalocean.com/">marketplace</a>. I have not yet used it for long-term projects since my side projects don't last long enough, but so far, I've been happy with it from the tinkerer's perspective. I would rather not spend my leisure coding time dealing with infrastructure.</p>
<p>Once again, thanks for reading, and I hope you found this helpful. Here's the accompanying GitHub <a target="_blank" href="https://github.com/tvaisanen/digitalocean-terraform-clojure-template/tree/blog-post-series-01">repository</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Show Me The Javascript (Clojurescript)]]></title><description><![CDATA[When I first started learning Clojurescript, I often thought about how the code I'm writing translates to Javascript. This was because I had spent a few years prior primarily working on Typescript; hence, it was the reference I had at the time.
I fig...]]></description><link>https://tonitalksdev.com/show-me-the-javascript</link><guid isPermaLink="true">https://tonitalksdev.com/show-me-the-javascript</guid><category><![CDATA[Clojure]]></category><category><![CDATA[ClojureScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Fri, 27 Oct 2023 05:32:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698333021856/beea364c-6391-4714-a322-00328ec4da4a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I first started learning Clojurescript, I often thought about how the code I'm writing translates to Javascript. This was because I had spent a few years prior primarily working on Typescript; hence, it was the reference I had at the time.</p>
<p>I figured there must be an easy way to see the translation result, and I asked around what's the default way to do this in the REPL. As far as I know, I might as well have been the first one asking about this since there was no quick answer available (usually there is).</p>
<p>Since Clojurescript is compiled into Javascript, I thought the most straightforward way to do this would be to use the plain JS interop.</p>
<h2 id="heading-plain-js-interop">Plain JS Interop</h2>
<blockquote>
<p>The toString() method of Function instances returns a string representing the source code of this function.</p>
<p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString">MDN Docs</a></p>
</blockquote>
<p>Knowing that <code>toString</code> method prints out the source code for functions. We can view the compiled source by simply calling the <code>.toString</code>. In Javascript, it'd be as simple as:</p>
<pre><code class="lang-javascript">&gt;&gt; <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">piece_of_code</span>(<span class="hljs-params"></span>)</span>{ 
&gt;&gt;  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"hello"</span>)
&gt;&gt; };
<span class="hljs-literal">undefined</span>
&gt;&gt; piece_of_code.toString()
<span class="hljs-string">'function piece_of_code(){ 
  console.log("hello")
}'</span>
</code></pre>
<p>The same approach works in the CLJS context, and we can make the result read better by just printing out the value.</p>
<pre><code class="lang-clojure">(<span class="hljs-name">.toString</span> piece-of-code)
<span class="hljs-string">"function cljs$user$piece_of_code(){\nreturn console.log(\"hello\");\n}"</span>

(<span class="hljs-keyword">defn</span> <span class="hljs-title">print-src</span> [f]
  (<span class="hljs-name">println</span> (<span class="hljs-name"><span class="hljs-builtin-name">str</span></span> f)))

(<span class="hljs-name">print-src</span> piece-of-code)
<span class="hljs-comment">;; =&gt; </span>
<span class="hljs-comment">;; function app$core$piece_of_code(){</span>
<span class="hljs-comment">;;   return console.log("hello");</span>
<span class="hljs-comment">;; }</span>
</code></pre>
<h2 id="heading-cljs-compiler-option">CLJS Compiler Option</h2>
<p>But after some digging, I found the fantastic video: "<a target="_blank" href="https://www.youtube.com/watch?v=kBKIGj1_WAo&amp;t=10124s">Mike Fikes explains the ClojureScript Compile</a>r" and learned that a dynamic CLJS compiler variable <code>*print-fn-bodies*</code> does the same thing without the extra work. Later, I discovered that Mike also has a <a target="_blank" href="https://blog.fikesfarm.com/posts/2017-07-29-improved-function-printing.html">blog post</a> explaining how to do this.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">set!</span></span> *print-fn-bodies* <span class="hljs-literal">true</span>)

(<span class="hljs-name"><span class="hljs-builtin-name">do</span></span> piece-of-code)
<span class="hljs-comment">;; =&gt; #object[app$core$piece_of_code "function app$core$piece_of_code(){</span>
<span class="hljs-comment">;;   return console.log("hello");</span>
<span class="hljs-comment">;; }"]</span>
</code></pre>
<p>With this approach, it is enough to set the variable, and you're good to go.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I think it is good to know how to do this when you're working with CLJS. You might not need to drop into the JS level that often, but knowing how to do it quickly without using external tools makes all the difference in the dev flow, and even if you don't need to, it's good to know how to do it just for curiosity's sake.</p>
<p>Ultimately, the code we are dealing with is just JavaScript, and we can use the same functions we'd be using on the JS development. On top of that, if you're working in a browser context, it's also possible to view and interact with the compiled code in the browser console.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698332312812/acf8f8dc-2780-4a4d-adcc-a8da542cecd3.png" alt class="image--center mx-auto" /></p>
<p>Thanks for reading. I hope you found this helpful. I recommend taking a look at <a target="_blank" href="https://blog.fikesfarm.com/tags/ClojureScript.html">Mike's blog</a> to learn more about Clojurescript.</p>
]]></content:encoded></item><item><title><![CDATA[Clojure and Cross Origin Resource Sharing (CORS)]]></title><description><![CDATA[Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8000/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.

If you're dealing with the above error with your Clo...]]></description><link>https://tonitalksdev.com/clojure-and-cross-origin-resource-sharing-cors</link><guid isPermaLink="true">https://tonitalksdev.com/clojure-and-cross-origin-resource-sharing-cors</guid><category><![CDATA[Clojure]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[CORS]]></category><category><![CDATA[Ring]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Toni Väisänen]]></dc:creator><pubDate>Sat, 21 Oct 2023 07:48:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697873860221/9e9024ed-f28b-43b1-9c86-0047c4750d1e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at <a target="_blank" href="http://localhost:8000/">http://localhost:8000/</a>. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.</p>
</blockquote>
<p>If you're dealing with the above error with your Clojure web app you've come to the right place. This is a common problem when fetching data from a different origin (read URL) for single-page web applications (SPA). A common scenario is that an SPA is served from AWS S3 and it is fetching data from different servers.</p>
<h2 id="heading-how-to-fix-the-same-origin-policy-disallows-reading-the-remote-resource">How to Fix "The Same Origin Policy disallows reading the remote resource"</h2>
<p>A common fix is to add a backend middleware to deal with the CORS headers such as <code>ring-cors</code> and this is what this post is about. Alternatively, you can serve the web app from the API's origin if possible or write your logic (i.e. middleware) to handle the CORS requests or if you're dealing with a third-party API there is likely an option to configure your allowed web origins.</p>
<h2 id="heading-configure-cors-middleware">Configure CORS Middleware</h2>
<p>First, add the latest version of <a target="_blank" href="https://clojars.org/ring-cors">ring-cors</a> to your dependencies.</p>
<pre><code class="lang-clojure">{<span class="hljs-symbol">:paths</span> [<span class="hljs-string">"src"</span> <span class="hljs-string">"resources"</span>]
 <span class="hljs-symbol">:deps</span> {org.clojure/clojure {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.11.1"</span>}
        ring/ring-core {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.6.3"</span>}
        ring/ring-jetty-adapter {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"1.6.3"</span>}
        ring-cors/ring-cors {<span class="hljs-symbol">:mvn/version</span> <span class="hljs-string">"0.1.13"</span>}}}
</code></pre>
<p>Require the dependency and wrap the application handler with <code>ring.middleware.cors/wrap.cors</code>.</p>
<pre><code class="lang-clojure">(<span class="hljs-name"><span class="hljs-builtin-name">ns</span></span> tvaisanen.cors
  (<span class="hljs-symbol">:require</span> [ring.middleware.cors <span class="hljs-symbol">:refer</span> [wrap-cors]]
            [ring.adapter.jetty <span class="hljs-symbol">:as</span> jetty]))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">app</span> [_request]
  {<span class="hljs-symbol">:status</span> <span class="hljs-number">200</span>
   <span class="hljs-symbol">:headers</span> {<span class="hljs-string">"Content-Type"</span> <span class="hljs-string">"text/html"</span>}
   <span class="hljs-symbol">:body</span> <span class="hljs-string">"OK"</span>})

(<span class="hljs-keyword">defonce</span> <span class="hljs-title">server</span> (<span class="hljs-name"><span class="hljs-builtin-name">atom</span></span> <span class="hljs-literal">nil</span>))

(<span class="hljs-keyword">defn</span> <span class="hljs-title">start!</span> []
  (<span class="hljs-name"><span class="hljs-builtin-name">reset!</span></span> server
          (<span class="hljs-name">jetty/run-jetty</span>
           (<span class="hljs-name">wrap-cors</span> #'app
                      <span class="hljs-symbol">:access-control-allow-origin</span> #<span class="hljs-string">"https://tvaisanen.com"</span>
                      <span class="hljs-symbol">:access-control-allow-methods</span> [<span class="hljs-symbol">:get</span> <span class="hljs-symbol">:put</span> <span class="hljs-symbol">:post</span> <span class="hljs-symbol">:delete</span>])
           {<span class="hljs-symbol">:port</span> <span class="hljs-number">8000</span> <span class="hljs-symbol">:join?</span> <span class="hljs-literal">false</span>})))

(<span class="hljs-comment">comment</span>
  (<span class="hljs-name">.stop</span> @server)
  (<span class="hljs-name">start!</span>))
</code></pre>
<p>Let's run the server and validate that it is working as expected.</p>
<h2 id="heading-verify-the-configuration">Verify the Configuration</h2>
<p>Verify the configuration by making an HTTP request from the browser console and inspecting the headers from the network tab.</p>
<p><strong>Send the Request</strong></p>
<p>Type this in your browser's dev tools console (from the origin configured for CORS).</p>
<pre><code class="lang-javascript">fetch(<span class="hljs-string">"http://localhost:8000"</span>).then(<span class="hljs-built_in">console</span>.log)
</code></pre>
<p><strong>Request Headers</strong></p>
<p>You should see something similar to this in your request headers when you inspect the network tab.</p>
<pre><code class="lang-javascript">GET / HTTP/<span class="hljs-number">1.1</span>
<span class="hljs-attr">Host</span>: localhost:<span class="hljs-number">8000</span>
<span class="hljs-attr">Origin</span>: https:<span class="hljs-comment">//tvaisanen.com</span>
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
</code></pre>
<p><strong>Response Headers</strong></p>
<p>And the response headers are similar to this.</p>
<pre><code class="lang-javascript">HTTP/<span class="hljs-number">1.1</span> <span class="hljs-number">200</span> OK
<span class="hljs-attr">Date</span>: Thu, <span class="hljs-number">19</span> Oct <span class="hljs-number">2023</span> <span class="hljs-number">05</span>:<span class="hljs-number">53</span>:<span class="hljs-number">16</span> GMT
Content-Type: text/html
Access-Control-Allow-Methods: DELETE, GET, POST, PUT
Access-Control-Allow-Origin: https:<span class="hljs-comment">//tvaisanen.com</span>
Transfer-Encoding: chunked
<span class="hljs-attr">Server</span>: Jetty(<span class="hljs-number">9.2</span><span class="hljs-number">.21</span>.v20170120)
</code></pre>
<p>What is important to notice here is that the response header <code>Access-Control-Allow-Origin</code> has the value from the requests <code>Origin</code> header. Having this with the <code>Access-Control-Allow-Methods</code> header tells the browser that it is okay to use the data they are asking for.</p>
<p>Let's double-check that this is indeed what is happening from the command line.</p>
<p><strong>CORS Headers Included</strong></p>
<p>When the origin header is passed and the value matches the one that is configured on the server side we get the expected <code>Access-Control-Headers</code>.</p>
<pre><code class="lang-http">❯ http localhost:8000 'origin:https://tvaisanen.com'

<span class="apache"><span class="hljs-attribute">HTTP</span>/<span class="hljs-number">1</span>.<span class="hljs-number">1</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Access</span>-Control-<span class="hljs-literal">Allow</span>-Methods: DELETE, GET, POST, PUT
<span class="hljs-attribute">Access</span>-Control-<span class="hljs-literal">Allow</span>-Origin: https://tvaisanen.com
<span class="hljs-attribute">Content</span>-Type: text/html
<span class="hljs-attribute">Date</span>: Thu, <span class="hljs-number">19</span> Oct <span class="hljs-number">2023</span> <span class="hljs-number">06</span>:<span class="hljs-number">02</span>:<span class="hljs-number">10</span> GMT
<span class="hljs-attribute">Server</span>: Jetty(<span class="hljs-number">9</span>.<span class="hljs-number">2</span>.<span class="hljs-number">21</span>.v<span class="hljs-number">20170120</span>)
<span class="hljs-attribute">Transfer</span>-Encoding: chunked

<span class="hljs-attribute">OK</span></span>
</code></pre>
<p><strong>CORS Headers Missing</strong></p>
<p>If the value of the origin header does not match the configured value the access control headers should not be in the response.</p>
<pre><code class="lang-bash">❯ http localhost:8000 <span class="hljs-string">'origin:https://not-tvaisanen.com'</span>

HTTP/1.1 200 OK
Content-Type: text/html
Date: Thu, 19 Oct 2023 06:02:18 GMT
Server: Jetty(9.2.21.v20170120)
Transfer-Encoding: chunked

OK
</code></pre>
<p>Looks like everything is working as expected!</p>
<p>There's more to cross-origin resource sharing but this should be enough to get you over the initial problem. I recommend reading related MDN Docs to learn more about the topic.</p>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Cross-Origin Resource Sharing (CORS)</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors#cors_error_messages">CORS Errors</a></p>
</li>
</ul>
<p>Thanks again for reading, I hope you found this useful.</p>
]]></content:encoded></item></channel></rss>