<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: spice</title>
    <description>The latest articles on DEV Community by spice (@rabspice).</description>
    <link>https://dev.to/rabspice</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F307900%2Fb869af61-8f49-45a5-aaa7-30236c2885e0.PNG</url>
      <title>DEV Community: spice</title>
      <link>https://dev.to/rabspice</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rabspice"/>
    <language>en</language>
    <item>
      <title>I used Cloudflare Workers and R2 as HTML generating service. It was so easy!</title>
      <dc:creator>spice</dc:creator>
      <pubDate>Fri, 26 Apr 2024 23:40:15 +0000</pubDate>
      <link>https://dev.to/rabspice/i-used-cloudflare-workers-and-r2-as-html-generating-service-it-was-so-easy-2n8d</link>
      <guid>https://dev.to/rabspice/i-used-cloudflare-workers-and-r2-as-html-generating-service-it-was-so-easy-2n8d</guid>
      <description>&lt;p&gt;This month, I added a HTML generate feature in my web app with Cloudflare Workers and R2.&lt;/p&gt;

&lt;p&gt;I was very surprised at workflow of them, it's so easy!&lt;/p&gt;

&lt;p&gt;I will explain why and how I implemented it.&lt;/p&gt;

&lt;h2&gt;
  
  
  About my web app
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cora-pic.com/en/"&gt;CoraPic&lt;/a&gt; is a easy meme generator web app.&lt;br&gt;
You can create a meme just in 10 seconds only on your browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fik4ph8y6mjuhbgnnxvbz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fik4ph8y6mjuhbgnnxvbz.gif" alt="example of CoraPic" width="582" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(this meme is &lt;a href="https://cora-pic.com/en/meme-generator/love-triangle"&gt;here&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This web app is just hosted on AWS with Amplify.&lt;br&gt;
It didn't have any API server, because all image processing is done with Canvas API on browser.&lt;/p&gt;

&lt;p&gt;But...&lt;br&gt;
I wanted to make it easy to share on any social media without downloading image.&lt;br&gt;
Easy sharing feature will help app to be more popular.&lt;/p&gt;

&lt;p&gt;That's why I decided to add a feature to generate a HTML file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Cloudflare? Even though I'm using AWS
&lt;/h2&gt;

&lt;p&gt;I choose Cloudflare because of the price.&lt;/p&gt;

&lt;p&gt;Cloudflare Workers have &lt;a href="https://developers.cloudflare.com/workers/platform/limits/#worker-limits"&gt;free plan&lt;/a&gt;.&lt;br&gt;
It's enough for me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnt5g09lrnahhotodqrc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnt5g09lrnahhotodqrc.png" alt="Cloudflare Workers free limit" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About Cloudflare R2, they also offers free capacity:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqnfvxbwk8tztivr6lb8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqnfvxbwk8tztivr6lb8.png" alt="Cloudflare R2 pricing" width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I love the most about　&lt;a href="https://developers.cloudflare.com/r2/pricing/"&gt;R2 pricing&lt;/a&gt; is, "Zero egress fee"!&lt;br&gt;
It means we don't have to pay for &lt;strong&gt;data transfer fee&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuw3fo2d6z59x4vi84pd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuw3fo2d6z59x4vi84pd.png" alt="Screenshot of Cloudflare R2 official site" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even if we used AWS' CDN service CloudFront, we have to pay the cost of data transfer.&lt;/p&gt;

&lt;p&gt;But using Cloudflare R2 let us release from this cost!&lt;/p&gt;

&lt;p&gt;It's super cool for me.&lt;br&gt;
I already have $4 / month cost for AWS Amplify, mainly because of the data transfer fee, even my app only has 10k PV / month.&lt;/p&gt;

&lt;p&gt;I don't want to pay such a stupid fee anymore.&lt;/p&gt;

&lt;p&gt;So I decided to use Cloudflare Workers and R2.&lt;/p&gt;

&lt;p&gt;(I definitely have to quit AWS Amplify if I want to reduce the cost. I know.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiit9nvn5t4ndbgr5cbqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiit9nvn5t4ndbgr5cbqx.png" alt="diagram of data transfer fee" width="483" height="510"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The architecture of the HTML generator
&lt;/h2&gt;

&lt;p&gt;Here is the architecture:&lt;/p&gt;

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

&lt;p&gt;As I mentioned, my web app creates the meme image on the browser.&lt;br&gt;
So, what I had to do is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save the image on server&lt;/li&gt;
&lt;li&gt;Generate a HTML file with the image URL&lt;/li&gt;
&lt;li&gt;Enable HTML file to be accessed from the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By saving HTML file and enabling public access to R2 bucket, we don't have to care how to serve it.&lt;/p&gt;

&lt;p&gt;I used Cloudflare Workers to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;receive the image data from the browser&lt;/li&gt;
&lt;li&gt;save the image on R2&lt;/li&gt;
&lt;li&gt;generate a HTML file with the image URL&lt;/li&gt;
&lt;li&gt;return the HTML access URL to the browser&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  About Worker's code
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://hono.dev/"&gt;hono&lt;/a&gt; to implement worker.&lt;/p&gt;

&lt;p&gt;They have &lt;a href="https://hono.dev/getting-started/cloudflare-workers"&gt;Cloudflare Workers tutrial&lt;/a&gt; in their document.&lt;br&gt;
It covers how to write code and deploy it with &lt;a href="https://developers.cloudflare.com/workers/wrangler/"&gt;Wrangler&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My main code is like &lt;a href="https://github.com/Spice-Z/cora-pic-g/blob/37e6300d53383df594afd492d561958ea636a44e/src/gen.ts#L20C1-L32C78"&gt;this&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;genHTML&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;GenHtmlArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;R2Bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bucketPreviewUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base64Image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;;base64,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;base64Image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid base64 image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detectType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64Image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;imageType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid image type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Convert base64 to binary&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageBinary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64Image&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`gen/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;imageType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Save image&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageBinary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;httpMetadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error saving image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bucketPreviewUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;imageKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="c1"&gt;// generate html from template with saved image&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;genFromTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;imageSrc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`gen/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.html`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Save html&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;httpMetadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read all of my source code in &lt;a href="https://github.com/Spice-Z/cora-pic-g"&gt;GitHub&lt;/a&gt; if you are interested in.&lt;/p&gt;

&lt;p&gt;Anyway, it was very easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  About other trivial matters
&lt;/h2&gt;

&lt;p&gt;I had to setup the R2 bucket.&lt;br&gt;
It was very straightforward and Cloudflare offers good &lt;a href="https://developers.cloudflare.com/r2/get-started/"&gt;getting started document&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also, I moved my domain (cora-pic.com) from &lt;a href="https://aws.amazon.com/route53/"&gt;Amazon Route 53&lt;/a&gt; to &lt;a href="https://www.cloudflare.com/products/registrar/"&gt;Cloudflare Registrar&lt;/a&gt; to use custom domain for Worker and R2.&lt;/p&gt;

&lt;p&gt;Both of them have document about domain transfer.&lt;br&gt;
We can follow these document to move the domain.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-transfer-from-route-53.html"&gt;Transferring a domain from Amazon Route 53 to another registrar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/registrar/get-started/transfer-domain-to-cloudflare/"&gt;Transfer your domain to Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  That's it !
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cora-pic.com/en"&gt;CoraPic&lt;/a&gt; has a good feature now!&lt;/p&gt;

&lt;p&gt;I'm so happy if you use this feature!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ejztaijatu6fc2tk26j.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ejztaijatu6fc2tk26j.jpeg" alt="Thank you for reading!" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I decided to close my mobile app 😿</title>
      <dc:creator>spice</dc:creator>
      <pubDate>Sat, 30 Mar 2024 20:07:30 +0000</pubDate>
      <link>https://dev.to/rabspice/i-decided-to-close-my-mobile-app-3plg</link>
      <guid>https://dev.to/rabspice/i-decided-to-close-my-mobile-app-3plg</guid>
      <description>&lt;p&gt;Hi! I'm Yugo, a software engineer based in Vancouver.&lt;/p&gt;

&lt;p&gt;I've decided to close one of my indie apps 😿&lt;br&gt;
I'll share reasons and learning in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the app
&lt;/h2&gt;

&lt;p&gt;The app was "Listen", available on App Store and Google Play.&lt;br&gt;
It helps English learners listen to podcasts with many features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real Time Transcription&lt;/li&gt;
&lt;li&gt;Real Time Translation&lt;/li&gt;
&lt;li&gt;Dictation&lt;/li&gt;
&lt;li&gt;Reminder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcarbxpmzonq62hs0z9oz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcarbxpmzonq62hs0z9oz.png" alt="Screenshot of App Store's image" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tech stack is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Native (Expo)&lt;/li&gt;
&lt;li&gt;GraphQL (Apollo)&lt;/li&gt;
&lt;li&gt;GCP (Cloud build, Cloud Run, firestore, etc.)&lt;/li&gt;
&lt;li&gt;OpenAI API (Whisper, GPT-3.5)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I created this app because I wanted to use English podcast as a learning material.&lt;/p&gt;

&lt;p&gt;There are many English learning apps, but many of them aren't using the real English conversation.&lt;br&gt;
It's very hard and boring to learn English with them.&lt;br&gt;
Therefore, I wanted an app that offers authentic English conversations as learning material.&lt;/p&gt;

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

&lt;p&gt;And I want to create a whole service from scratch.&lt;br&gt;
Despite having more than 5 years of experience as a software engineer, most of my time was spent adding features to existing services.&lt;br&gt;
There were few chances to develop a service from scratch.&lt;br&gt;
I believed I could learn a lot by creating an entire service.&lt;/p&gt;

&lt;p&gt;Also, I just like creating something new 😆&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I decided to close my app
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reason 1: Cost
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdre55r3tusut8jkoeexa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdre55r3tusut8jkoeexa.png" alt="flying money" width="668" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About the app and backend, the cost is around $1 per month.&lt;/p&gt;

&lt;p&gt;The problem is the OpenAI API.&lt;br&gt;
I had to pay around $50 per month for using the Whisper and ChatGPT API, even though the app didn't have many users.&lt;br&gt;
It's not so expensive, I can pay from my pocket.&lt;/p&gt;

&lt;p&gt;However, it was enough to demotivate me.&lt;/p&gt;

&lt;p&gt;On March 2024, OpenAI changed their price system to the up-front payment.&lt;br&gt;
I feel this is the change to stop using the API and close the app.&lt;/p&gt;

&lt;p&gt;I should have made a specific plan how to get profits to cover the cost before releasing the app.&lt;br&gt;
Even if you can afford it, ongoing costs can affect your motivation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 2: Busy!
&lt;/h3&gt;

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

&lt;p&gt;I was living in Japan when I started developing the app.&lt;br&gt;
But now, I live in Vancouver and am attending college.&lt;/p&gt;

&lt;p&gt;I realized I'm too busy to grow the app!&lt;/p&gt;

&lt;p&gt;What I'm doing now is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Immigrating to Canada

&lt;ul&gt;
&lt;li&gt;going to college, preparing for the job interview, etc.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Podcast

&lt;ul&gt;
&lt;li&gt;Weekly published. I have to record and edit it.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Youtube (no relevant to the development)

&lt;ul&gt;
&lt;li&gt;Just for fun, but I want to spend time gaining more subscribers...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Other indie app project

&lt;ul&gt;
&lt;li&gt;I have another indie app!&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;And more...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Life is too short to pursue all my interests!&lt;br&gt;
I want to narrow my focus to higher-priority activities.&lt;/p&gt;

&lt;p&gt;We're not elves; our time is limited.&lt;br&gt;
The best we can do is to reduce the number of things we want to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  But... I learned many things!
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Less is more for starting indie app
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hvttrjmkii9ich14h6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hvttrjmkii9ich14h6g.png" alt="eureka!" width="264" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I should have not developed the app with so many features from the beginning.&lt;br&gt;
I had to focus on improving the core feature.&lt;br&gt;
I had been developed this app alone. Resources were limited.&lt;br&gt;
Although I had many ideas and sufficient passion, it was challenging to develop all of them with good quality simultaneously.&lt;/p&gt;

&lt;p&gt;Less is more!&lt;/p&gt;

&lt;h3&gt;
  
  
  New tech skills
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnee9u5ia41yfmw9rgoxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnee9u5ia41yfmw9rgoxe.png" alt="share the knowledge" width="400" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I learned many new tech skills from this app 🔥&lt;/p&gt;

&lt;p&gt;As I showed in the infrastructure image, I used Cloud Run for the backend and GitHub Actions for deploying it.&lt;/p&gt;

&lt;p&gt;This was the first time for me to create a Cloud Run deployment pipeline with GitHub Actions.&lt;/p&gt;

&lt;p&gt;I had to learn about the Artifact Registry, Cloud Build, IAM service, Workload Identity, etc.&lt;/p&gt;

&lt;p&gt;Also, I used OpenAI API for the first time in product.&lt;br&gt;
There were many technics to use the AI effectively, like "Prompt Engineering".&lt;/p&gt;

&lt;p&gt;For me, creating the new things is the best way to learn new tech skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, what's next for my indie hacking?
&lt;/h2&gt;

&lt;p&gt;As I mentioned, I have another indie app, &lt;a href="https://www.cora-pic.com/en"&gt;CoraPic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2nhmjduk93xspr8n9a3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2nhmjduk93xspr8n9a3.png" alt="Screenshot of CoraPic" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I released it 4 years ago and redesigned it 2 years ago.&lt;br&gt;
I haven't updated it since then, but it's still operational and receives 10,000 views per month, primarily from Japanese users.&lt;/p&gt;

&lt;p&gt;I want to update this site and growth the number of users especially in English version.&lt;br&gt;
I believe I can get more profits from this app.🤭&lt;/p&gt;

&lt;p&gt;Also, I wanna make mobile app version with React Native.&lt;br&gt;
It may take time, but I will enjoy the journey.&lt;/p&gt;




&lt;p&gt;If you are interested in me, let's &lt;br&gt;
 connect on LinkedIn!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/yugo-spice-ogura/"&gt;https://www.linkedin.com/in/yugo-spice-ogura/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>gcp</category>
    </item>
  </channel>
</rss>
