<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>FastAPI on Geoff Westmoreland</title>
        <link>https://gwestmoreland.dev/categories/fastapi/</link>
        <description>Recent content in FastAPI on Geoff Westmoreland</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Sat, 06 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://gwestmoreland.dev/categories/fastapi/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>HEIC to JPG converter in Azure, serverless</title>
        <link>https://gwestmoreland.dev/p/heic-to-jpg-converter-in-azure-serverless/</link>
        <pubDate>Sat, 06 Jun 2026 00:00:00 +0000</pubDate>
        
        <guid>https://gwestmoreland.dev/p/heic-to-jpg-converter-in-azure-serverless/</guid>
        <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I wanted to build an app that would run on Azure&amp;rsquo;s serverless compute, and allow the user to just interact a simple web UI. I figured a file converter would be a simple enough way to make this happen.&lt;/p&gt;
&lt;p&gt;At first I was going to make a .mov to .mp3 converter, but decided against it. .mov files will obviously be larger and would require more compute to convert, and I&amp;rsquo;m trying to keep costs low after all!&lt;/p&gt;
&lt;p&gt;I went with HEIC to JPG because I could actually get some use out of this conversion, and the files will be much smaller.&lt;/p&gt;
&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/G-DoubleU/HEIC-Converter&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Here&amp;rsquo;s a link&lt;/a&gt; to the Github repo with the finished product.&lt;/p&gt;
&lt;h2 id=&#34;architecture--design&#34;&gt;Architecture &amp;amp; Design&lt;/h2&gt;
&lt;p&gt;The API and Azure function are written in Python. I decided to go with FastAPI for the actual API as I had a base level of familiarity with it, and it would suit my simple use case.&lt;/p&gt;
&lt;p&gt;A Quick Breakdown of how this all works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;FastAPI serves a &lt;em&gt;very basic&lt;/em&gt; web UI and prompts for a file upload&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;User uploads a file and the API sends that file to an Azure Storage Account container for uploaded files&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When a new file lands in the &amp;ldquo;Uploads&amp;rdquo; storage container, Azure Event Grid sends a message to our Azure Function to notify it of a new file&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Event Grid message provides details on the name of the file and the file extension&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Function app does some basic input validation, making sure the file is a .heic and was uploaded to the expected container&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Function app converts the .heic to .jpg using the pillow-heif library and uploads it to the &amp;ldquo;Processed&amp;rdquo; container in Azure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FastAPI polls the &amp;ldquo;Processed&amp;rdquo; container every few seconds until it sees a file with the expected name (ex. original_file_name.jpg) and downloads the file&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The converted file is returned to the user&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;fast-api&#34;&gt;Fast API&lt;/h2&gt;
&lt;p&gt;The FastAPI part is the was the newest for me. I had a basic understanding of API&amp;rsquo;s, but had no idea how to build one myself. FastAPI&amp;rsquo;s documentation is great, so I spent some time reading and refreshing my memory on HTTP methods and response codes.&lt;/p&gt;
&lt;p&gt;The API consists of just two routes. The root &amp;lsquo;/&amp;rsquo; route serves the most basic web page with a means of uploading a file.&lt;/p&gt;
&lt;p&gt;The route that does all the work is the &amp;lsquo;/upload&amp;rsquo; route. This builds our connection to the Azure storage account and copies the file into the &amp;ldquo;uploads&amp;rdquo; storage account in Azure.&lt;/p&gt;
&lt;p&gt;While the Azure Function is running, the API will poll the output storage account in regular intervals while it waits for the converted file to appear. Once this file appears, it&amp;rsquo;s downloaded and returned to the user.&lt;/p&gt;
&lt;h2 id=&#34;azure-function&#34;&gt;Azure Function&lt;/h2&gt;
&lt;p&gt;This is where the actual file conversion happens. The Function uses an Event Grid trigger, so it only wakes up when a blob lands in the &amp;ldquo;uploads&amp;rdquo; container.&lt;/p&gt;
&lt;p&gt;When it fires, the Event Grid message gives the Function the name and URL of the blob. The function does a few quick validations to make sure the blob is actually in the correct container, and the file extension is .heic.&lt;/p&gt;
&lt;p&gt;The conversion itself is handled by &lt;a class=&#34;link&#34; href=&#34;https://github.com/bigcat88/pillow_heif&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;pillow-heif&lt;/a&gt;. This library is great, it kept me from having to spend too much time on the technical specifics of the conversion.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;pillow_heif&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;register_heif_opener&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;PIL&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;register_heif_opener&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Image&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;open&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;io&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BytesIO&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;heic_bytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;buffer&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;io&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BytesIO&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;convert&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;RGB&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;save&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;buffer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;format&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;JPEG&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;quality&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;90&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;buffer&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seek&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;From there the JPG gets written to the &amp;ldquo;processed&amp;rdquo; container under the original filename, which is what FastAPI is waiting to see.&lt;/p&gt;
&lt;h2 id=&#34;infrastructure-with-terraform&#34;&gt;Infrastructure with Terraform&lt;/h2&gt;
&lt;p&gt;None of this would be complete without some IaC. All of the infra was written in Terraform. The primary resources are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A storage account with two blob containers: &amp;lsquo;uploads&amp;rsquo; and &amp;lsquo;processed&amp;rsquo;&lt;/li&gt;
&lt;li&gt;A Function App (Linux, Python, consumption plan) runs the conversion&lt;/li&gt;
&lt;li&gt;An App Service that hosts the web UI and FastAPI&lt;/li&gt;
&lt;li&gt;Event Grid wiring that connects the storage account and the function&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;.Zip files containing the actual Function and FastAPI/Python code are pushed via Azure CLI after the main infrastructure is applied with Terraform. In a true prod environment, this is &lt;strong&gt;not&lt;/strong&gt; the way you would go, but it&amp;rsquo;s sufficient for a short lived project.&lt;/p&gt;
&lt;p&gt;I got stuck for a while on the Event Grid aspect of the IaC. I tried to apply the Event Grid in the same set of .tf files that contained everything else, but it failed over and over. It turns out that Event Grid, used in this way, won&amp;rsquo;t initialize until the function it&amp;rsquo;s targeting is up and running. Even with Terraform&amp;rsquo;s smart dependency handling, Event Grid was trying to initialize before the Function was alive.&lt;/p&gt;
&lt;p&gt;I ended up separating out the Event Grid code into a second set of .tf files and running a separate &amp;rsquo;terraform apply&amp;rsquo; once the Function was up. Again, not the smoothest way to do this, but I was able to make it repeatable.&lt;/p&gt;
&lt;h2 id=&#34;final-notes&#34;&gt;Final Notes&lt;/h2&gt;
&lt;p&gt;If I revisit this project in the future, there are several things I will add. I&amp;rsquo;d like add a prettier web UI with additional upload options. I&amp;rsquo;d also like to push the code for the Function and FastAPI using a true CI/CD pipeline, to get away from the manual deploying of the .zip&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;All in all I am satisfied with the way this turned out, and really enjoyed getting to work with Azure&amp;rsquo;s serverless offerings.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
