<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[sebastianroy.de]]></title><description><![CDATA[An early technical founder sharing his experience building machine learning based apps.]]></description><link>https://sebastianroy.de/</link><image><url>https://sebastianroy.de/favicon.png</url><title>sebastianroy.de</title><link>https://sebastianroy.de/</link></image><generator>Ghost 5.2</generator><lastBuildDate>Thu, 19 Mar 2026 06:58:13 GMT</lastBuildDate><atom:link href="https://sebastianroy.de/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Upload an Image via Multiform POST Request in Kotlin using Retrofit2 in 2023]]></title><description><![CDATA[If you're an Android app developer looking to provide users with the ability to upload images or for computer vision tasks, you're in the right place. In this blog post, we'll show you how to send an image via post request in Kotlin using Retrofit2.]]></description><link>https://sebastianroy.de/upload-an-image-via-multiform-post-request-in-kotlin-using-retrofit2-in-2023/</link><guid isPermaLink="false">64249b958a9bb5c076f7b914</guid><category><![CDATA[API]]></category><category><![CDATA[Building an AI SaaS]]></category><category><![CDATA[Coding]]></category><category><![CDATA[JSON]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Wed, 29 Mar 2023 20:38:19 GMT</pubDate><media:content url="https://sebastianroy.de/content/images/2023/03/UploadImageKotlinBlogHeader.png" medium="image"/><content:encoded><![CDATA[<blockquote>Note 8. May 2024: Since Kotlin Multiplattform came out, I would now recommend to use ktor for HTTP requests instead of Retrofit2, since Retrofit2 is limited to Android.</blockquote><img src="https://sebastianroy.de/content/images/2023/03/UploadImageKotlinBlogHeader.png" alt="Upload an Image via Multiform POST Request in Kotlin using Retrofit2 in 2023"><p>In the interface will define URL routes (<code>/v1/uploadImage</code>) that the server can accept and HTTP types (<code>GET</code>, <code>POST</code>, <code>PUT</code>). Retrofit2 uses decorators (<code>@&#x2026;</code>) to achieve this. It also specifies the response type. I use <code>Call&lt;ResponseBody&gt;</code> first to obtain a raw JSON (think curly brackets { &#x2026;}), but you can also define a type in Kotlin and cast the response immediately using <code>Call&lt;List&lt;MyImageObjects&gt;&gt;</code>to retrieve an array of image objects (name, UUID, x-coordinate&#x2026;).</p><p>Since we don&#x2019;t want to send some user information via POST request, but one or several images, our request <em>body</em> will be a MultiPart Form.</p><p>When retrieving more complex objects with metadata and countless arrays, it is difficult to cast the JSON response immediately into a Kotlin object. The structure of this object has to match the JSON response in all types. In that case, it is easier to just display the raw JSON response first and write a custom deserialiser of this JSON response for easier debugging.</p><ol><li>Add Dependencies</li><li>Add Permissions</li><li>Preparing the Interface</li><li>View Model</li><li>Main Activity</li><li>Catch Errors</li><li>Conclusion</li></ol><h2 id="add-dependencies">Add Dependencies</h2><p>First, let&#x2019;s add the dependencies to <code>gradle.build</code></p><pre><code class="language-kt">implementation &quot;com.squareup.retrofit2:retrofit:2.9.0&quot;
implementation &quot;com.squareup.retrofit2:converter-gson:2.6.0&quot;
</code></pre><p>The version might change, you can look it up here for <a href="https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit">retrofit</a> and <a href="https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson">gson</a>.</p><p>To apply changes, hit <code>Sync Now</code>.</p><h2 id="add-permissions">Add Permissions</h2><p>To access the internet, the app needs permission. &#xA0;The user can later see the required permissions in the app store.</p><p>Add Internet Permission to <code>AndroidManifest.xml</code>.</p><pre><code class="language-kt"> &lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&gt;
</code></pre><p>Failing to add this will lead the app to crash when sending an HTTP request.</p><h2 id="prepare-the-interface">Prepare the Interface</h2><p>Creating an interface is not strictly necessary, but good practice for interoperability and testability. However, it does not implement the function specifically, meaning how you handle the response is defined later when creating a class that complies with this interface.</p><p>So let&#x2019;s create a new interface file <code>FileApi.kt</code>.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2023/03/Bildschirmfoto-2023-03-21-um-16.03.50.png" class="kg-image" alt="Upload an Image via Multiform POST Request in Kotlin using Retrofit2 in 2023" loading="lazy"></figure><p>The first line will be your package name, similar to this.</p><pre><code class="language-kt">package com.uploadImage.test
</code></pre><p>When you use a keyword that hasn&#x2019;t been imported, Android Studio will give you a hint to do so. But sometimes there are multiple sources for one keyword. The required imports for this interface will be</p><pre><code class="language-kt">import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
</code></pre><p>The next line has already been created by the IDE</p><pre><code class="language-kt">interface FileApi {
	...
}
</code></pre><p>Now we define the routes. We want to send a POST request (to send instead of GET something) to <code>http://someurl.com/api/v1/uploadimage</code> as a Multipart form (since we upload an image).</p><pre><code class="language-kt">    @Multipart
    @POST(&quot;/api/v1/uploadimage&quot;)
</code></pre><p>Next, we use asynchronous coroutines for networking. We add a function, <code>uploadImage</code>, that can be suspended to the background until a result is received.</p><pre><code class="language-kt">fun uploadImage(
</code></pre><p>We define now the <code>@Part</code> that makes up the <code>@Multipart</code> request.</p><pre><code class="language-kt">	@Part image: MultipartBody.Part
</code></pre><p>The resulting raw JSON can be obtained as a <code>ResponseBody</code>.</p><pre><code class="language-kt"> ): Call&lt;ResponseBody&gt;
</code></pre><p>Alternatively, the resulting JSON can be cast into a custom type, e.g., a list of Words. We will make use of this later by adding a second function.</p><pre><code class="language-kt"> ): Call&lt;List&lt;Word&gt;&gt;
</code></pre><p>So, here we create a companion object, which means that we create only a single instance in the whole application. This is also called a singleton pattern in Kotlin.</p><pre><code class="language-kt">// No need to create an instance of an object, just use this class method
companion object {
     ...   
}
</code></pre><blockquote>A <strong>singleton</strong> is an object that has just one instance and can be accessed globally over the lifetime of the app.<br><strong>Object</strong> is a way to create a singleton. If that object is part of a class or an interface, it is a <strong>companion object</strong>.</blockquote><p>But a companion object of what? A companion object of the <code>OkHttpClient</code>.</p><pre><code class="language-kt">// Create HTTP Client for network request
private val client = OkHttpClient.Builder().build()
</code></pre><pre><code class="language-kt"> val instance by lazy {
	...
 }
</code></pre><p><code>Retrofit</code> acts as a layer above a basic http client, and we could choose different ones, so here we specify the client we want to use for our HTTP requests and the <code>baseUrl</code> to the server we intend to send the image to.</p><pre><code class="language-kt">Retrofit.Builder()
      .baseUrl(&quot;https://testapi.com/&quot;)
      .client(client) // add a client instance here, e.g. OkHttpClient
      .addConverterFactory(GsonConverterFactory.create())
      .build()
      .create(FileApi::class.java)
</code></pre><p>If you would like to use your own local server, you can&#x2019;t use encryption with https because you need a certificate for this. It is easy to get a certificate by <a href="https://letsencrypt.org/docs/challenge-types/">letsencrypt</a>, but you require a domain name and not just a local IP.</p><p>But Android doesn&#x2019;t allow unencrypted traffic per default, so we need to enable it in our testing environment. We do this by going to <code>AndroidManifest.xml</code> and add</p><pre><code class="language-kt"> android:usesCleartextTraffic=&quot;true&quot;
</code></pre><p>in the<code> &lt;application&#x2026;</code> tag.</p><p>Another way would be to deploy your server on a droplet on Digital Ocean and forward a subdomain like <code>api.yourdomain.com</code> to the IP of this droplet. Then you can enable encryption on your server. In go with the echo framework, you could do this by launching the server by using <code>e.StartAutoTLS</code> instead of <code>e.Start</code> and using port 443.</p><pre><code class="language-kt">e.Logger.Fatal(e.StartAutoTLS(&quot;:443&quot;))
</code></pre><p>This generates a Letsencrypt certificate automatically for you.</p><p>Now you should be able to access the routes encrypted from your server using <code>https://api.yourdomain.com/v1/uploadImage</code>.</p><h2 id="the-class">The Class</h2><p>We create the file <code>FileRepository.kt</code>. It will open an image and call the companion object <code>instance</code> defined above. &#xA0;We will also handle the response, which could be either successful or not. If it is successful, we log the raw JSON response and return true, and if it isn&#x2019;t, we deal with the errors and return false.</p><p>So let&#x2019;s start with the imports for logging, the HTTP client okhttp3, retrofit2, file and Input/Output exception.</p><pre><code class="language-kt">import android.util.Log
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.File
import java.io.IOException
</code></pre><p>Then we create a function uploadImage, that gets passed a <code>file</code> of type <code>File</code> which we want to upload.</p><p>It will add a skeleton for a class</p><pre><code class="language-kt">class FileRepository {
	...
}
</code></pre><p>We add a function, uploadImage, that expects a <code>Boolean</code> (<code>true</code> or <code>false</code>) depending on the success of the request.</p><pre><code class="language-kt">fun uploadImage(file: File): Boolean {
	...
}
</code></pre><p>Next, we set the return variable <code>res</code> to false. It will be switched to true later in the callback function when the request was successful.</p><pre><code class="language-kt">	var res = false 
</code></pre><p>Then we access <code>uploadImage</code> function of the <code>companion object</code> in the <code>FileApi</code> interface we created earlier. We enclose it in a <code>try</code> block to catch errors.</p><pre><code class="language-kt">try {
       FileApi.instance.uploadImage(
                ...
            )
    }
</code></pre><p>As defined in the interface, we use image as a <code>MultipartBody.Part</code> an argument to <code>uploadImage()</code>. The image is created from <code>file</code> that was passed to this method by the <code>FileViewModel</code>, which we haven&#x2019;t created yet.</p><pre><code class="language-kt">    image = MultipartBody.Part
             .createFormData(
                  &quot;image&quot;,
                  file.name,
                  file.asRequestBody()
             )
</code></pre><p>Now, <code>enqueue()</code> (as opposed to <code>dequeue()</code>) adds an <code>object</code> to the task queue.</p><pre><code class="language-kt">.enqueue(
  object : Call&lt;ResponseBody&gt; {
		...
	}
)
...
</code></pre><p>The response is handled by <code>onFailure()</code> and <code>onResponse()</code>. In case of a failure, we log an error.</p><pre><code class="language-kt">
    override fun onFailure(call: Call&lt;ResponseBody&gt;, t: Throwable) {
                        Log.e(&quot;API Request&quot;, &quot;I got an error and i don&apos;t know why :(&quot;)
                    }

</code></pre><p><br>In case of success, we read the <code>response.body()</code> and <code>log</code> it as a <code>string</code>.</p><pre><code class="language-kt">    override fun onResponse(call: Call&lt;ResponseBody&gt;, response: Call&lt;ResponseBody&gt;) {
                        val responseBody = response.body()
                        Log.e(&quot;API Request&quot;, responseBody.toString())
     }
</code></pre><p><br>Since the response has been handles successfully, we will set our success switch <code>res</code> to <code>true</code> to <code>return</code> and exit the function.</p><pre><code class="language-kt"> res = true
 return res
</code></pre><p>Otherwise, if we have an <code>IOException</code> or a <code>HttpException</code> we print the error and <code>return</code> false, as <code>res</code> was initially set to false.</p><pre><code class="language-kt">catch (e: IOException) {
            e.printStackTrace()
            return res
} catch (e: HttpException) {
            e.printStackTrace()
            return res
} finally {
            Log.e(&quot;API Request&quot;, res.toString())
}
</code></pre><h2 id="view-model">View Model</h2><p>Next, we create the view Model <code>FileViewModel.kt</code>.</p><p>These are the required imports</p><pre><code class="language-kt">import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.io.File
import kotlinx.coroutines.launch
</code></pre><p>In the <code>class FileViewModel </code>we add an instance <code>repository</code> of a <code>FileRepository</code> we defined in the previous section.</p><pre><code class="language-kt">class FileViewModel(
	private val repository: FileRepository = FileRepository()): ViewModel() {
	...
	}
</code></pre><p>We launch an asynchronous call with <code>viewModelScope.launch</code> to upload the image.</p><pre><code class="language-kt">fun uploadImage(file: File) {
	viewModelScope.launch {
		repository.uploadImage(file)
	}
}
</code></pre><h2 id="mainactivity">MainActivity</h2><p>Now, we add the viewModel of type <code>viewModel&lt;FileViewModel&gt;()</code> to <code>MainActivity.kt</code> (after your apps theme).</p><pre><code class="language-kt">...
setContent {
   UploadImageTheme {
		 val viewModel = FileViewModel()
         // Dependency injection?
         // val viewModel = viewModel&lt;FileViewModel&gt;()
	...

</code></pre><p>Then we add a <code>Box</code> in which other UI elements are contained</p><pre><code class="language-kt">Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
) {
	...
}
</code></pre><p>In this case, we just add an upload button. When it is clicked, it will look for the file <code>img.png</code> in <code>cacheDir</code> which we added to the code repository at the beginning of this tutorial.</p><pre><code class="language-kt">Button(onClick = {
  val file = File(cacheDir, &quot;img.png&quot;)
  file.createNewFile()
  file.outputStream().use {
  assets.open(&quot;img.png&quot;).copyTo(it)
  }
  viewModel.uploadImage(file)
  }) {
  Text(text = &quot;Upload image&quot;)
}
</code></pre><p>Check if file <code>img.png</code> exists in the assets folder.</p><h2 id="catch-errors">Catch Errors</h2><ul><li>API server is not available (retrofit2.HttpException: HTTP 403)</li><li>javax.net.ssl.SSLException: Unable to parse TLS packet header</li></ul><h2 id="conclusion">Conclusion</h2><p>In this post, I described how to send an image as a POST request using retrofit2. First we introduced the dependencies, permissions and talked about pitfalls in encrypted HTTPS requests and Android. We created an interface where we defined the routes. Then we created a class that used this interface and handled the response with success and failure. The file was send from the view model and the view model was accessed after a click on a button in the UI.</p><h2 id="links">Links</h2><ul><li><a href="https://www.geeksforgeeks.org/retrofit-with-kotlin-coroutine-in-android/">https://www.geeksforgeeks.org/retrofit-with-kotlin-coroutine-in-android/</a></li><li><a href="https://stackoverflow.com/questions/74825563/send-an-image-to-an-ocr-api-with-rertofit-and-kotlin">https://stackoverflow.com/questions/74825563/send-an-image-to-an-ocr-api-with-rertofit-and-kotlin</a></li><li><a href="https://trendoceans.com/how-to-fix-cleartext-http-traffic-not-permitted-in-android/">https://trendoceans.com/how-to-fix-cleartext-http-traffic-not-permitted-in-android/</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Sending an Image as POST Request with Swift 5 to Go 1.19 Server]]></title><description><![CDATA[In this post we will upload an image from the user of an iOS app to a server written in Go as an asynchronous multiform POST request. This could be for uploading a profile image or detecting objects.]]></description><link>https://sebastianroy.de/sending-an-image-as-post-request-with-swift-5-to-go-119-server/</link><guid isPermaLink="false">635860488a9bb5c076f7b681</guid><category><![CDATA[Building an AI SaaS]]></category><category><![CDATA[Go]]></category><category><![CDATA[Swift]]></category><category><![CDATA[JSON]]></category><category><![CDATA[Coding]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Wed, 26 Oct 2022 11:33:26 GMT</pubDate><media:content url="https://sebastianroy.de/content/images/2022/10/Cover---Sending-an-Image-as-POST-Request-with-Swift-5-to-Go-1.19-Server-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://sebastianroy.de/content/images/2022/10/Cover---Sending-an-Image-as-POST-Request-with-Swift-5-to-Go-1.19-Server-1.png" alt="Sending an Image as POST Request with Swift 5 to Go 1.19 Server"><p>We will just return file size, image width and height in pixels as a basic JSON object. If you want to learn how to cast more complex JSON objects into structs, checkout <a href="https://sebastianroy.de/relaying-a-microservice-json-response-to-the-client-by-unmarshalling-go-structs/">Relaying a Microservice JSON Response to the Client by Unmarshalling Go Structs</a>.</p><p>First, we need to obtain the image we intend to send. This could be done by taking a photo using the camera, selecting an image from the library, or loading an image from a URL. Loading an image from a URL is the easiest way. Then we need to create the POST request, send it and catch any error if something went wrong.</p><ol><li>Backend in Go 1.19</li><li>Returning a JSON with file name, size, width and height</li><li>Frontend in Swift 5</li><li>Basic Concepts</li><li>Code Skeleton</li><li>Selecting an Image</li><li>Creating and Sending the HTTP Request</li><li>Conclusion</li><li>Links</li></ol><p>If we can&#x2019;t accept POST requests that contain an image, any request will remain unanswered. So let&#x2019;s create a server to do that.</p><h2 id="backend-in-go-119">Backend in Go 1.19</h2><p>First initialise a new Go environment and install the <a href="https://echo.labstack.com/">echo</a> framework.</p><pre><code class="language-bash">$ mkdir ImageHandlerTutorial &amp;&amp; cd ImageHandlerTutorial 
$ go mod init ImageHandlerTutorial 
$ go get github.com/labstack/echo/v4 
$ go get github.com/labstack/echo/v4/middleware  
</code></pre><p>Then create a <code>ImageHandlerTutorial.go</code> file with a basic skeleton for using <a href="https://echo.labstack.com/">echo</a> and other imports for writing files or encoding JSON.</p><pre><code class="language-go">package main

import (
	&quot;bytes&quot;
	// Formating strings
	&quot;fmt&quot;

	&quot;io&quot;
	// Multipart POST request
	&quot;mime/multipart&quot;
	// Opening and closing files
	&quot;os&quot;
	&quot;time&quot;

	&quot;image&quot;
	// Register possible image formats
	_ &quot;image/png&quot;
	_ &quot;image/jpeg&quot;

	&quot;encoding/json&quot;
	&quot;io/ioutil&quot;
	&quot;log&quot;
	&quot;net/http&quot;

	// Echo framework
	&quot;github.com/labstack/echo/v4&quot;
	&quot;github.com/labstack/echo/v4/middleware&quot;
)

func main() {
	e := echo.New()

	// Avoid CORS error when testing with redocly
	e.Use(middleware.CORS())

	// API Versioning
	//...

	// POST request handling
	//...

	e.Logger.Fatal(e.Start(&quot;:1324&quot;))
}
</code></pre><p>To provide some structure for the URLs, we create two groups, such that we can create resources under <code>127.0.0.1:1324/api/v1</code>. The IP will change if it is running on a real server instead of the same local machine the iOS Simulator will run on later.</p><pre><code class="language-go">	// API Versioning
	apiGroup := e.Group(&quot;/api&quot;)
	apiv1Group := apiGroup.Group(&quot;/v1&quot;)
</code></pre><p>Under this group, we now create an endpoint <code>imageanalysis</code>. It will handle the POST request <code>127.0.0.1:1324/api/v1/imageanalysis</code>.</p><p>This also includes a function to handle the image file. We open the data in field <code>file</code> into the source <code>src</code> and write the file to <code>dst</code> on disk.</p><p>The way it is written here, different uploads will overwrite the same file name <code>file</code>. This is a bit sketchy, but also doesn&#x2019;t use much space. Think about deleting the file after analysis, renaming it if you need it later, and what happens when multiple users call the function at the same time.</p><pre><code class="language-go">	apiv1Group.POST(&quot;/imageanalysis&quot;, func(c echo.Context) error {
		// TODO: Catch error when no file is send in POST Request
		// Source
		file, err := c.FormFile(&quot;file&quot;)
		if err != nil {
			return err
		}
		src, err := file.Open()
		if err != nil {
			return err
		}
		defer src.Close()

		// Destination
		dst, err := os.Create(file.Filename)
		if err != nil {
			return err
		}
		defer dst.Close()

		// Copy
		if _, err = io.Copy(dst, src); err != nil {
			return err
		}
		// TODO: Why do i need to open the file here again, instead of just passing the reference from os.Create()?
		// TODO: Maybe don&apos;t create the file on disk at all, just send it from memory
		buf, err := os.Open(dst.Name())
		if err != nil {
			log.Fatal(err)
		}
		defer buf.Close()

		return ImageRequestHandler(c, buf)
	})


</code></pre><p>To finish, we start the echo server (but that part is already in the skeleton above)</p><pre><code class="language-go">e.Logger.Fatal(e.Start(&quot;:1324&quot;))
</code></pre><p>Save the file and to run the server open a terminal and execute.</p><pre><code class="language-bash">$ go run ImageHandlerTutorial.go          
</code></pre><p>This should output</p><pre><code class="language-bash">   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.9.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
&#x21E8; http server started on [::]:1324
</code></pre><p>Go Playground doesn&#x2019;t support anything with networking, so if you want to try it, you need to install <a href="https://go.dev/dl/">Go</a> on your machine. You can see and download the full listing <a href="https://ideone.com/kkPNWW">here</a>.</p><h2 id="returning-a-json-with-file-name-size-width-and-height">Returning a JSON with file name, size, width and height</h2><p>So for practice, let&#x2019;s return a JSON with the file size in kb and image size in pixels. Here we could also send the image for analysis to another server to do object detection. We would need to perform another POST request, this time from Go to an object detection microservice, and I have written about this in <a href="https://sebastianroy.de/sending-images-in-post-request-as-multipart-form-from-go-to-microservice/">Sending Images in POST request as MultiPart Form from Go to Microservice</a>.</p><pre><code class="language-go">// Send an image to microservice for image analysis
func ImageRequestHandler(c echo.Context, img *os.File) error {
	// TODO: Use user image instead
	log.Print(img.Name())

	body := &amp;bytes.Buffer{}
	writer := multipart.NewWriter(body)
	// TODO: Try to change image name here
	fw, err := writer.CreateFormFile(&quot;image&quot;, &quot;image.jpg&quot;)

	_, err = io.Copy(fw, img)
	if err != nil {
		log.Fatal(err)
	}

	writer.Close()

	// Get filesize of image
	stat, err := img.Stat()
	log.Print(&quot;File size: &quot;, stat.Size()/1024, &quot; kb&quot;)

	// Get width and height in px for the image
	im, _, err := image.DecodeConfig(img)
	if err != nil {
		fmt.Fprintf(os.Stderr, &quot;%s: %v\n&quot;, img.Name(), err)
	}
	log.Print(im.Width)

	// Form JSON response that contains name, size, height and width
	jsonStr := fmt.Sprintf(`{ &quot;name&quot;: &quot;%s&quot;,
				 &quot;size&quot;: %d,
				 &quot;width&quot;: %d,
				 &quot;height&quot;: %d
				}`, img.Name(), stat.Size(), im.Width, im.Height)
	log.Print(jsonStr)

	c.Response().Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
	//TODO: Avoid double casting []byte string?!
	c.Response().Write([]byte(string(jsonStr)))

	// TODO: Figure out how to avoid return and just return with c.Response() similar to w.Write
	return err
}
</code></pre><h2 id="basic-concepts">Basic concepts</h2><h3 id="multiformdata-http-requests">Multiform/data http requests</h3><p>A <code>POST</code> request is one type of <em>HTTP request</em> like <code>GET</code> and <code>PUT</code>. There are many types of <code>POST</code> request, which we can specify in the header. There are <code>image/png</code> or <code>text</code> for example. For flexibility, however, we will use <code>multiformat/data</code>. This offers multiple data fields.</p><p>HTTP requests have a <em>boundary</em>. It tells the parser that interesting data is coming and when it sees the same boundary again that this is the end of that data batch. We will define it using a random <code>uuidString</code>. This is just a common way to generate random text to uniquely identify something and can look like <code>633a76d8-87a3-494c-bd1c-7192985e01f5</code>.</p><h3 id="asynchronous-tasks-with-dispatchqueues">Asynchronous tasks with DispatchQueues</h3><p>Like any type of networking activity, it takes some time to execute and might even fail. We want to avoid blocking the user interface waiting for a response. That&#x2019;s why multithreading is used. This is a way to make asynchronous networking requests. There are many ways to do this and it is a complex subject. Here we will use <code>DispatchQueues</code>. This is a way to schedule tasks with the first-in-first-out principle (FIFO) and we let iOS handle thread management.</p><h2 id="swift-code-skeleton">Swift Code Skeleton</h2><p>We will use two functions <code>loadFrom</code> and <code>uploadImage</code> and wrap those up in a class <code>POSTImage</code>. Then we create an object <code>postRequest</code> of our new type <code>POSTImage</code> and execute.</p><pre><code class="language-swift">import UIKit

class POSTImage {
	var image = UIImage()
	
	// Load an image from URL 
	func loadFromURL(URLAddress: String) {
		 
	}

	// Load an image from Library (TODO)
	func loadImageFromLibrary() {}

	// Sending the image to the server
	func uploadImage(fileName: String, image: UIImage) {
	
	}
}


let postRequest = POSTImage()
postRequest.loadFromURL(URLAddress: &quot;https://upload.wikimedia.org/wikipedia/commons/b/b9/Panther_chameleon_%28Furcifer_pardalis%29_male_Montagne_d%E2%80%99Ambre.jpg&quot;)

</code></pre><h2 id="selecting-an-image">Selecting an Image</h2><h3 id="load-an-image-from-url">Load an image from URL</h3><p>So, now let&#x2019;s get an image by loading it from a URL with <code>loadFromURL(...)</code> with an <code>URLAddress</code> of type <code>String</code> as argument. We specified the URL in the last line of our skeleton above already. In a real scenario, this could be using a URL from a s3 object storage. We use <code>guard</code> to catch errors when parsing the URL.</p><pre><code class="language-swift"> guard let url = URL(string: URLAddress) else {
     return
 }
</code></pre><p>Next we will load the image. Loading an image takes some time and can fail. We would rather not block the user interface, therefore handling the task over to a separate thread with <code>DispatchQueue.main.async</code>. First, we try to obtain the <code>Data</code> from the <code>url</code> into <code>imageData</code>. Then the <code>imageData</code> will be cast into <code>loadedImage</code> which can be used for display as <code>UIImage</code>.</p><p>Now we will upload the image. This is a provisional solution, but it works. The issue is that we loaded the image in a separate thread. We can only call <code>uploadImage</code>, once we already got the image. Therefore, our program has to wait or we need a callback function that is called after the image was loaded from URL. We will do that later, but for now, we just call <code>uploadImage</code> from the sub thread in <code>DispatchQueue</code>.</p><pre><code class="language-swift">DispatchQueue.main.async { [weak self] in
    if let imageData = try? Data(contentsOf: url) {
    if let loadedImage = UIImage(data: imageData) {
       self?.image = loadedImage
       // TODO: Remove this and use completion handler instead
       self?.uploadImage(fileName: &quot;file&quot;, image: loadedImage)
    }
}
</code></pre><p>The other issue is that we haven&#x2019;t implemented <code>uploadImage</code> yet, which we will do next. Here is the full listing for the function <code>selectImageFromURL</code>.</p><pre><code class="language-swift">func selectImageFromURL(URLAddress: String) {
     guard let url = URL(string: URLAddress) else {
         return
     }
        
     DispatchQueue.main.async { [weak self] in
        if let imageData = try? Data(contentsOf: url) {
    		if let loadedImage = UIImage(data: imageData) {
    		   self?.image = loadedImage
      		 // TODO: Remove this and use completion handler instead
    		   self?.uploadImage(fileName: &quot;file&quot;, image: loadedImage)
    }
}
</code></pre><h2 id="create-and-send-post-request">Create and send POST request</h2><p>Now we create a <a href="https://developer.apple.com/documentation/foundation/urlsession">URLSession</a> named <code>session</code>. It will be sent to the <code>url</code> specified by <code>URL(...)</code>. This must be an endpoint to your server, that will handle HTTP POST requests. I have written a previous post about how to do this also with <code>URLSession</code> and <code>DispatchQueues</code> at least with JSON objects (text, no images) in <a href="https://developer.apple.com/documentation/foundation/urlsession">Communications from Go Backend to SwiftUI Frontend with JSON API</a>.</p><p>First, we need to define the server URL and start a <a href="https://developer.apple.com/documentation/foundation/urlsession">URLSession</a> to handle networking.</p><pre><code class="language-swift">let url = URL(string: &quot;http://127.0.0.1:1324/api/v1/imageanalysis&quot;)

let session = URLSession.shared
</code></pre><p>Now we configure the request as <code>POST</code> request and making it a <code>multipart/form-data</code> request in the header, where we configure how the <code>boundary</code> looks like.</p><pre><code class="language-swift">let boundary = UUID().uuidString
var urlRequest = URLRequest(url: url!)
urlRequest.httpMethod = &quot;POST&quot;
urlRequest.setValue(&quot;multipart/form-data; boundary=\(boundary)&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)
</code></pre><p>After the header we need to set the actual data. We create <code>data</code> of type <code>Data</code>. Then we append the boundary and add the parameter &#x201D;file&#x201D;. The <code>Content-Type</code> of this parameter is <code>image/png</code>. And finally we have to append the actual file data <code>image.pngData</code>. Finally, we close the requests boundary.</p><pre><code class="language-swift">var data = Data()
        
// Add the image data to the raw http request data
data.append(&quot;\r\n--\(boundary)\r\n&quot;.data(using: .utf8)!)

let paramName = &quot;file&quot;
data.append(&quot;Content-Disposition: form-data; name=\&quot;\(paramName)\&quot;; filename=\&quot;\(fileName)\&quot;\r\n&quot;.data(using: .utf8)!)
data.append(&quot;Content-Type: image/png\r\n\r\n&quot;.data(using: .utf8)!)
data.append(image.pngData()!)
        
data.append(&quot;\r\n--\(boundary)--\r\n&quot;.data(using: .utf8)!)
</code></pre><p>Now that we defined the request, it is time to execute it. We use the <code>session.uploadTask</code> with our created <code>urlRequest</code> and <code>data</code> as arguments. Then we handle the response in the <code>completionHandler</code>. There we convert the JSON response into a Go struct.</p><pre><code class="language-swift">session.uploadTask(with: urlRequest, from: data, completionHandler: { responseData, response, error in
                if error == nil {
                    let jsonData = try?JSONSerialization.jsonObject(with: responseData!, options: .allowFragments)
                    if let json = jsonData as? [String: Any] {
                        print(json)
                    }
                }
	}).resume()
</code></pre><p>Now let&#x2019;s look at the full listing of the function <code>uploadImage</code>.</p><pre><code class="language-swift">func uploadImage(fileName: String, image: UIImage) {
        let url = URL(string: &quot;http://127.0.0.1:1324/api/v1/electrophoresis/imageanalysis&quot;)
        let boundary = UUID().uuidString

        let session = URLSession.shared

        var urlRequest = URLRequest(url: url!)
        urlRequest.httpMethod = &quot;POST&quot;

        urlRequest.setValue(&quot;multipart/form-data; boundary=\(boundary)&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)

        var data = Data()
        
        // Add the image data to the raw http request data
        data.append(&quot;\r\n--\(boundary)\r\n&quot;.data(using: .utf8)!)
        let paramName = &quot;file&quot;
        data.append(&quot;Content-Disposition: form-data; name=\&quot;\(paramName)\&quot;; filename=\&quot;\(fileName)\&quot;\r\n&quot;.data(using: .utf8)!)
        data.append(&quot;Content-Type: image/png\r\n\r\n&quot;.data(using: .utf8)!)
        data.append(image.pngData()!)
        
        data.append(&quot;\r\n--\(boundary)--\r\n&quot;.data(using: .utf8)!)
        
        session.uploadTask(with: urlRequest, from: data, completionHandler: { responseData, response, error in
                if error == nil {
                    let jsonData = try? JSONSerialization.jsonObject(with: responseData!, options: .allowFragments)
                    if let json = jsonData as? [String: Any] {
                        print(json)
                    }
                }
            }).resume()
    }
</code></pre><p>Finally, we get to see the result of all the hard work so far:</p><pre><code class="language-swift">[&quot;size&quot;: 5451466, &quot;name&quot;: file, &quot;height&quot;: 1396, &quot;width&quot;: 2872]
</code></pre><p>This is the analysis done by the server, shown on the client. Real world analysis would be more complex and the client would display that data on the UI instead of just printing it to the console.</p><h2 id="bonus-some-curious-errors">Bonus: Some curious errors</h2><p>One issue I had was that data function wasn&#x2019;t found. This was because I created my own data type that was called <code>data</code>. This has overridden the native function. So give your types new names that are not used in iOS. iOS libraries prefix any new class or struct to avoid it.</p><h2 id="full-listing">Full Listing</h2><pre><code class="language-swift">import UIKit


class POSTImage {
    var image = UIImage()
    
    func loadFrom(URLAddress: String) {
        guard let url = URL(string: URLAddress) else {
            return
        }
        
        DispatchQueue.main.async { [weak self] in
            if let imageData = try? Data(contentsOf: url) {
                if let loadedImage = UIImage(data: imageData) {
                     // Why is self optional here?
                     self?.image = loadedImage
                     self?.uploadImage(fileName: &quot;file&quot;, image: loadedImage)
                }
            }
        }
    }
    
    func uploadImage(fileName: String, image: UIImage) {
        let url = URL(string: &quot;http://127.0.0.1:1324/api/v1/imageanalysis&quot;)
        let boundary = UUID().uuidString

        let session = URLSession.shared

        var urlRequest = URLRequest(url: url!)
        urlRequest.httpMethod = &quot;POST&quot;

        urlRequest.setValue(&quot;multipart/form-data; boundary=\(boundary)&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)

        var data = Data()
        
        // Add the image data to the raw http request data
        data.append(&quot;\r\n--\(boundary)\r\n&quot;.data(using: .utf8)!)
        let paramName = &quot;file&quot;
        data.append(&quot;Content-Disposition: form-data; name=\&quot;\(paramName)\&quot;; filename=\&quot;\(fileName)\&quot;\r\n&quot;.data(using: .utf8)!)
        data.append(&quot;Content-Type: image/png\r\n\r\n&quot;.data(using: .utf8)!)
        data.append(image.pngData()!)
        
        data.append(&quot;\r\n--\(boundary)--\r\n&quot;.data(using: .utf8)!)
        
        session.uploadTask(with: urlRequest, from: data, completionHandler: { responseData, response, error in
                if error == nil {
                    let jsonData = try? JSONSerialization.jsonObject(with: responseData!, options: .allowFragments)
                    if let json = jsonData as? [String: Any] {
                        print(json)
                    }
                }
            }).resume()
    }
}

let postRequest = POSTImage()
postRequest.loadFrom(URLAddress: &quot;https://upload.wikimedia.org/wikipedia/commons/b/b9/Panther_chameleon_%28Furcifer_pardalis%29_male_Montagne_d%E2%80%99Ambre.jpg&quot;)

// TODO: wait for myimageview to load
// uploadImage(fileName: &quot;file&quot;, image: myImageView.image)

</code></pre><h2 id="conclusion">Conclusion</h2><p>To sum things up, we discussed the server and client side of sending images over HTTP with POST requests, doing simple analysis on the server and returning the result as JSON to the client. I hope I could cover this subject in a useful way with most of the puzzle pieces.</p><h2 id="links">Links</h2><p><a href="https://orjpap.github.io/swift/http/ios/urlsession/2021/04/26/Multipart-Form-Requests.html">https://orjpap.github.io/swift/http/ios/urlsession/2021/04/26/Multipart-Form-Requests.html</a></p><p><a href="https://stackoverflow.com/questions/29623187/upload-image-with-multipart-form-data-ios-in-swift">https://stackoverflow.com/questions/29623187/upload-image-with-multipart-form-data-ios-in-swift</a></p><p><a href="https://www.hackingwithswift.com/books/ios-swiftui/sending-and-receiving-orders-over-the-internet">https://www.hackingwithswift.com/books/ios-swiftui/sending-and-receiving-orders-over-the-internet</a></p><!--kg-card-begin: html--><div id="hyvor-talk-view"></div>
<script type="text/javascript">
    var HYVOR_TALK_WEBSITE = 8174;
    var HYVOR_TALK_CONFIG = {
        url: false,
        id: false
    };
</script>
<script async type="text/javascript" src="//talk.hyvor.com/web-api/embed.js"></script><!--kg-card-end: html-->]]></content:encoded></item><item><title><![CDATA[How to Document Code with Swimm.io]]></title><description><![CDATA[As a founder I spend the last couple of weeks working on documenting the code base to be employee ready. That way when capital comes and new people join we can press the accelerator without spilling the energy all over the place.]]></description><link>https://sebastianroy.de/how-to-document-code-with-swimmio/</link><guid isPermaLink="false">63010f138a9bb5c076f7b5f8</guid><category><![CDATA[Documentation]]></category><category><![CDATA[Onboarding]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Tue, 23 Aug 2022 20:28:39 GMT</pubDate><content:encoded><![CDATA[<p>Code should be documented to avoid building technical debt. Even if other developers join a team with poorly documented code, they tend to document the code less themselves, worsening the problem. It also avoids an unique individual(the founder for example) being the king over a part of a codebase (which may be why some people don&#x2019;t document).</p><p>The code itself is kind of a documentation already. While not being a complete documentation, lacking insights and reasons a particular choice was made, it does show a whole lot. That means writing documentation would have to <strong>copy a lot of the ideas</strong> that are visible in code. That could be the structure of objects and classes. On the other hand it is cumbersome to go to a separate site for documentation, simply because it is a pain just to <strong>find the right spot in the documentation</strong> that refers to the section in code I am interested in. And I haven&#x2019;t even talked about keeping code and documentation in <strong>sync</strong>.</p><p><strong>Swimm.io </strong>offers a nice solution to all of this. It references documentation right within VSCode and in the documentation you can quote the code, which is automatically updated if any changes are committed to the repository or it at least tells you if any manual adjustment is needed.</p><p>So in the last post I have written about <a href="https://sebastianroy.de/designing-the-glue-between-server-and-client/">documenting APIs with Insomnia and Redocly</a>, now let&#x2019;s look at documenting the code itself. The setup we will be using VSCode as an editor, a GitHub and <a href="https://swimm.io/">swimm.io</a> account (one private repo free, unlimited repos 29 $ per user per month).</p><h2 id="connect-a-repository">Connect a Repository</h2><figure class="kg-card kg-embed-card"><iframe width="1236" height="390" style="border:0;" scrolling="no" src="https://screencast-o-matic.com/player/c3jYbGVTbX6?title=0&amp;a=1&amp;overlays=1" allowfullscreen="true"></iframe></figure><p>First we need to <a href="https://app.swimm.io/register">create an account</a> and connect it to a GitHub repository.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-18-um-18.21.26.png" class="kg-image" alt loading="lazy"></figure><h2 id="create-a-new-document">Create a new Document</h2><figure class="kg-card kg-embed-card"><iframe width="1236" height="390" style="border:0;" scrolling="no" src="https://screencast-o-matic.com/player/c3jY2pVTDWp?title=0&amp;a=1&amp;overlays=1" allowfullscreen="true"></iframe></figure><p>Now we have access to the codebase from out documentation, we can create a new document and directly select code as a quote in it.</p><p>So create a new document and name the document. Then press <code>/</code> to open option menu and select <code>Code Snippet</code>.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-20-um-15.32.36.png" class="kg-image" alt loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-20-um-15.35.43.png" class="kg-image" alt loading="lazy"></figure><p>This will let you choose a file from your code. Use your courser to select the code you want to refer in the documentation and klick <code>Add &amp; Exit</code>.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-20-um-15.36.44.png" class="kg-image" alt loading="lazy"></figure><p>So lets say we want to refer to a variable name in the code. We could just do that, but what happens when someone changes the name? Then the documentation and the code is out of sync. To avoid that we can link variables to the code. When you select a keyword and convert the style into code by klicking <code>&lt;/&gt;</code>. It then opens a suggestion from which line in the code you wan to link the variable to.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-20-um-15.38.02.png" class="kg-image" alt loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-20-um-15.38.36.png" class="kg-image" alt loading="lazy"></figure><p>After Linking you can see green checkmarks next to the keyword</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-20-um-15.40.06.png" class="kg-image" alt loading="lazy"></figure><p>Now that we created our first document, we will have to commit it to the repository by clicking <code>&#x201E;Create a pull request&#x201C;</code> and select <code>&#x201E;Push to master&#x201C;</code> and hit <code>Save</code>.</p><p><br>Now that the documentation is stored in our GitHub repository, next we will add the Swimm extension to VSCode and see how our code now looks from our IDE.</p><h2 id="view-documentation-from-vscode">View Documentation from VSCode</h2><p>Open VSCode. In the extension tab search for <code>Swimm</code> and install it.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-16.13.41.png" class="kg-image" alt loading="lazy"></figure><p>Then we login to our Swimm account from the Swimm extension tab and make sure to sync the git repository and checkout the main branch (or the one you commited the Swimm documentation to).</p><p>This is where the work pays off. You note an additional line above the <code>user</code> struct. If we hover over it we get the caption we set for our code previously.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-16.33.03.png" class="kg-image" alt loading="lazy"></figure><p>Now if we click on it, we directly jump to the documentation section within VSCode.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-16.36.15.png" class="kg-image" alt loading="lazy"></figure><p>This is so cool. If we double click we can even edit the documentation and save it on the next commit, all without leaving VSCode.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-16.37.39.png" class="kg-image" alt loading="lazy"></figure><h2 id="keeping-documentation-and-code-in-sync">Keeping Documentation and Code in Sync.</h2><p>Now what happens when we rename the field <code>Email</code> to <code>PrimaryEmail</code>.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-20.23.36.png" class="kg-image" alt loading="lazy"></figure><p>When refactoring this quoted section, Swimm will issue a warning.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-20.27.59.png" class="kg-image" alt loading="lazy"></figure><p>We will not have to <code>Edit Doc</code> and <code>Reselect</code> the code of interest. Then hit <code>Update Selection</code> and <code>Create Pull Request</code> to push to main. All issues should be resolved again.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-20.30.40.png" class="kg-image" alt loading="lazy"></figure><p>And this is basically all we need to know on how to document code with Swimm.io.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-23-um-20.32.03.png" class="kg-image" alt loading="lazy"></figure><h2 id="bonus-playlists">Bonus: Playlists</h2><p>Going back to the beginning of this post, regarding onboarding new developers. Playlist give new developers an ordered way to dig into your codebase.</p><h2 id="conclusion">Conclusion</h2><p>Although we started by documenting code we discussed the importance of that in the context of building a team to develop a product. I am myself not up to speed yet, but will spend the next couple of days integrating Swimm.io in my routine. Do you have any war stories come to mind? Would love to read them in the comments.</p><h2 id="links">Links</h2><ul><li>Avoiding Technical Debt <a href="https://rubenscheedler.hashnode.dev/a-technical-debt-crisis-is-brewing-in-the-land-of-software-and-heres-why">https://rubenscheedler.hashnode.dev/a-technical-debt-crisis-is-brewing-in-the-land-of-software-and-heres-why</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Designing the Glue between Server and Client]]></title><description><![CDATA[Documenting APIs with Insomnia and Redoc using OpenAPI 3.0.4]]></description><link>https://sebastianroy.de/designing-the-glue-between-server-and-client/</link><guid isPermaLink="false">62f930518a9bb5c076f7b5df</guid><category><![CDATA[API]]></category><category><![CDATA[Documentation]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Tue, 16 Aug 2022 22:35:16 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sebastianroy.de/content/images/2022/08/image1.jpeg" class="kg-image" alt loading="lazy" width="1175" height="783" srcset="https://sebastianroy.de/content/images/size/w600/2022/08/image1.jpeg 600w, https://sebastianroy.de/content/images/size/w1000/2022/08/image1.jpeg 1000w, https://sebastianroy.de/content/images/2022/08/image1.jpeg 1175w" sizes="(min-width: 720px) 720px"><figcaption>Preview of documentation created with Redocly from OpenAPI YAML file.</figcaption></figure><p>When working on a project for 3 months and then abandoning it, documentation is a bit of a pain. But for anything more than that, documenting the software structure is important, especially when working in a team or when planning to work in a team someday. Even when working alone I don&#x2019;t remember what I thought 6 months ago, which functions should be deleted and out commented code can be removed.</p><p>While I have written about documenting actual code with <a href="https://swimm.io/">swimm.io</a> in a recent post, now I will go into looking at a micro service from a networking perspective by designing and testing Application Programmable Interfaces or APIs. Actually the world today is run using APIs.</p><p>Writing software documentation I often do after the code is written, but for deciding how an API call should behave and which routes should exist, it makes sense to design an API first before writing any code. There are different conventions how an API should look like, e.g. OpenAPI, and then a file syntax like YAML (maybe JSON or XML would be possible as well) that describes how to specify an API according to <a href="https://swagger.io/specification/">OpenAPI</a> standard. Then there are <a href="https://restfulapi.net/resource-naming/">RESTful naming conventions</a> for the URL routes. I know, but it sounds more complicated than those fancy abbreviations make it seem. Learning those conventions and standards enables you to harness the experience and mistakes of decades of development without having to reinvent the wheel.</p><p>While OpenAPI version 3.1.0 is released, the tool Insomnia, which helps us write a OpenAPI specification in YAML, does only support version 3.0.4, so we have to stay with that for now. The new version makes working with files in POST request a little bit easier.</p><p>At the end of this post we will have written a long list as YAML file from which we can generate a website that will be our documentation with redoc. Using the tool <a href="https://insomnia.rest/download">Insomnia</a> (alternative to Postman) gives a live preview of this documentation website and later we can test the actual API with it. This is more convenient than just using VSCode.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-17-um-15.38.17.png" class="kg-image" alt loading="lazy"></figure><p>So first lets create a new API in Insomnia or if you decide for another way create an <code>api.yml</code> file and insert the following line (without the <code>...</code>).</p><pre><code class="language-yaml">openapi: 3.0.4
...
</code></pre><p>The current version of OpenAPI is <code>3.1.0</code>. Insomnia unfortunately doesn&#x2019;t support that, that results in some inconvenient syntax when uploading files as POST request, so we are going with <code>3.0.4</code>.</p><p>Next we are extending the document step by step until we developed a full API definition.</p><ol><li>Set general information</li><li>Set API server URL</li><li>Define routes for HTTP requests</li><li>Add Parameters and Send Files</li><li>Define expected responses</li><li>Define a schema for an object</li><li>Publish the documentation with Redoc</li><li>Bonus: Dealing with CORS Error in Go Echo</li><li>Conclusion</li></ol><h2 id="set-general-information">Set general information</h2><p>So let&#x2019;s start with adding general information.</p><pre><code class="language-yaml">...
info:
  title: Pantera API
  description: &quot;This is a animal identification API. The goal is to implement this in Go.&quot;
  version: 0.0.1
  termsOfService: http://swagger.io/terms/
  contact:
    email: info@pantera.io
  license:
    name: Commercial
    url: http://localhost
...
</code></pre><p><code>title</code> is a self assigned name of the API.</p><p><code>description</code> should describe the purpose of the API, well duh, obviously. But don&#x2019;t take this lightly. Before writing a single line of code it makes you think and clearly state your intentions. This serves as a guide which functions the API should provide and maybe even more important which functions it should not provide, because they are part of a different service. Doesn&#x2019;t have to be perfect starting out though, it evolves over time.</p><p><code>Version</code> serves to denote the Version of your API. The first digit 1.x.x usually indicates major changes, that can be incompatible with earlier versions. Often functions are set <code>deprecated</code> when updating to one major release and then phased of with the release after that.</p><p>The <code>termsOfService</code> link to a legal document that provides terms under which the API can be used. You might want to restrict use of an API only to your company or limit commercial use or enforce rate limits, actually I am not sure if this is done in the terms, but in any case rate limits should also be enforced in the server code.</p><p><code>contact</code> allows to provide an email or other contact information to the person responsible for the API.</p><p><code>license</code> let&#x2019;s you choose a license like CreativeCommons, MIT.... &#xA0;and link to the terms with a <code>url</code>.</p><pre><code class="language-yaml">...
externalDocs:
  description: Find out more about Swagger
  url: http://swagger.io
...
</code></pre><pre><code class="language-yaml">...
tags:
  - name: Animals
    description: Provides a list of Animals with key datapoints and an image analysis tool to identify those animals.
  - name: Zoos
    description: Returns a list of Zoos
...
</code></pre><h2 id="set-api-server-url">Set API server URL</h2><p>Next we set the server base URL. In the beginning this can be you own computer and later it will be your domain <code>pantera.io</code>. You can define multiple URLs as well, one for production, one for testing. Now, I have set a version <code>v1</code>, but it might be better to set it in an <code>Accept-Version</code> header.</p><pre><code class="language-yaml">...
servers:
  - url: http://127.0.0.1:1324/api/v1
...
</code></pre><p>This was the warmup. Next we will define our actual API paths and how the possible requests (think <code>GET</code>, <code>POST</code>, <code>PUT</code>) as well as responses looks like.</p><h2 id="define-routes-for-http-requests">Define routes for HTTP requests</h2><h3 id="animalsimageanalysis">/animals/imageanalysis</h3><p>Our first path is lets say <code>/animals/imageanalysis</code></p><pre><code class="language-yaml">...
paths: 
	/animals/imageanalysis:
		post:
...
</code></pre><p>Then we add a tag under which this path should be categorised like <code>animals</code>, which we have defined earlier, but you can set new tags, they just won&#x2019;t have any description then.</p><pre><code class="language-yaml">...
       tags:
       - Animals		
...
</code></pre><p>The route has it&#x2019;s own summary and description. There is also a optional field <code>operationId</code> which has to be unique and refers to the function name in server code that handles that route.</p><pre><code class="language-yaml">...
       summary: &quot;Object detection for animals&quot;
       description: &quot;Accepts an image as multiform/form-data and returns coordinates of objects detected in that image&quot;
	   operationId: parseImageAnalysisResponse
...
</code></pre><p>and maybe let&#x2019;s add another tag for information about zoos.</p><pre><code class="language-yaml">...  
	- name: Zoos
   	 description: Manage a list of Zoos
...
</code></pre><h2 id="add-parameters-and-send-files">Add Parameters and Send Files</h2><p>This is now the interesting part, a form with a file to upload. There is a simpler way, which is just uploading one file or image. We go for a way to upload multiple files and parameters using <code>multiparty/form-data</code>. It is an object with multiple properties, let&#x2019;s call the first <code>file1</code>. Although we talk about an image, it is still of type <code>string</code> and has format <code>binary</code>. Then we set the encoding of the field <code>file1</code> as <code>contentType: image/png, image/jpeg</code>. I think about this as a snippet that I can copy and paste.</p><pre><code class="language-yaml">	...
	requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file1:
                  type: string
                  format: binary
            encoding:
              file1:
                contentType: image/png, image/jpeg`
	...
</code></pre><p>This works so far, but for further reading you can look into <code>format: base64</code>and the general <code>Content-Type: application/octet-stream</code>.</p><p>If we could work with OpenAPI <code>3.1.0</code>, then it will look a little clearer.</p><pre><code class="language-yaml"># OpenAPI v3.1
requestBody:
  content:
    multipart/form-data:
  	schema:
       type: object
       properties:
        orderId:
          type: integer
        fileName:
          type: string
          contentMediaType: application/octet-stream
</code></pre><p>Now after discussing how a request looks like, let&#x2019;s move forward to HTTP responses.</p><h2 id="define-expected-responses">Define expected responses</h2><p>The most famous HTTP response code will probably be 404 - not found. This isn&#x2019;t the most common one, since most of the time the internet works, we get 200 - success. Those codes are grouped 1xx, 2xx, 3xx, 4xx or 5xx and a list of available <a href="https://en.m.wikipedia.org/wiki/List_of_HTTP_status_codes">HTTP responses can be found here on Wikipedia</a>.</p><pre><code class="language-yaml">...
responses:
        &quot;405&quot;:
          description: Invalid input
        &quot;200&quot;:
           description: JSON Response
           content:
             application/json:
              schema:
                  $ref: &quot;#/components/schemas/AnimalAnalysis&quot;
...
</code></pre><p>Ok, so first we open the section <code>responses:</code>. Then we decide which codes we want to use, in this case <code>405</code> and <code>200</code>. Then we use <code>description:</code> for a summary on when this response will occur. If it is successful, we return <code>application/json</code>, since this about an API that uses JSON, but could also use XML. If it were a website it would be <code>text/html</code> and an image is <code>image/jpg</code>, you can find more <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types">here</a>. Now we define the structure of that JSON response under <code>schema:</code>. This can look like this</p><pre><code class="language-yaml">examples:
    application/json:
      id: 38
      title: T-Shirt
      image:
         url: images/38.png
</code></pre><p>for the following JSON</p><pre><code class="language-yaml">{
	id: 38
	&quot;title&quot;: T-Shirt
	&quot;image&quot;: {
		url: image/38.png
	}
}
</code></pre><p>For more involved types that should be defined as structs later, we can <em>refer</em> to a schema and define it in a different section. Since we might expect multiple animals in one image we might obtain an array, this is denoted by using <code>items</code> instead of just adding the reference to the schema.</p><pre><code class="language-yaml">application/json:
    schema:
	  items:
        $ref: &apos;#/components/schemas/AnimalObject&apos;
</code></pre><p>This will result in an error, since that object isn&#x2019;t defined yet.</p><h2 id="define-a-schema-for-an-object">Define a schema for an object</h2><p>Now we have to define the object we referred to in the last section.</p><p>So we give the type a name <code>Bands</code>. It is of type <code>object</code>. Then we define its <code>properties</code>.</p><p>Properties also have a name <code>column</code> and a <code>description</code>. This column has again a type which could be <code>number</code>, <code>string</code> or <code>object</code>. The format further specifies the type of string which could be an <code>xid</code> or a <code>date</code>. If a string just has a few possibilities, you can define those as a list in <code>enum</code>. And last we give a sample value under <code>example</code>.</p><pre><code class="language-yaml">...
components:
   schemas:
     AnimalAnalysis:
      type: object
      properties:
        detection:
           type: string
           enum:
             - auto
             - manual
           description: &quot;Describes if an animal was detected by the object detection algorithm or edited manual by the user.&quot;
        detectedAnimal:
          type: string
          example: &quot;Lion&quot;
        xid:
          type: string
          format: xid
          example: &quot;9m4e2mr0ui3e8a215n4g&quot;
        xMin:
          type: number
          format: int64
          example: 45
          description: x-Coordinate of upper left corner.
        yMin:
          type: number
          format: int64
          example: 20
          description: y-Coordinate of upper left corner.
        xMax:
          type: number
          format: int64
          example: 80
          description: x-Coordinate of lower right corner.
        yMax:
          type: number
          format: int64
          example: 40
          description: y-Coordinate of lower right corner.
...
</code></pre><p>And that&#x2019;s how we define the response for our API.</p><p>A object detection microservice will not deliver the response in a format that is convenient for us for use in the client. We have to adapt the data first. Checkout my post about <a href="https://sebastianroy.de/relaying-a-microservice-json-response-to-the-client-by-unmarshalling-go-structs/">Relaying a Microservice JSON Response to the Client by Unmarshalling Go Structs</a> on how to implement this in Go.</p><h2 id="publish-the-documentation-with-redoc">Publish the documentation with Redoc</h2><p>Redoc.ly is a service for hosting OpenAPI based documentations and make them accessible for third parties. They have a premium hosting service and an open source CLI tool. You can install it with</p><pre><code class="language-yaml">$ npm i -g redoc-cli 
</code></pre><p>Then copy the YAML file we created above and store it under <code>api.yml</code>.</p><p>You can preview the documentation with</p><pre><code class="language-yaml">$ redocly preview-docs api.yml   
  &#x1F50E;  Preview server running at http://127.0.0.1:8080
</code></pre><p>and open a browser on this URL.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-17-um-00.21.42.png" class="kg-image" alt loading="lazy"></figure><p>If you upload the file to a GitHub repository you can use <a href="redocly.com">Redocly</a> to host your documentation and also keep it updated when you commit changes to your repository.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-16-um-23.27.38.png" class="kg-image" alt loading="lazy"></figure><h2 id="bonus-dealing-with-cors-error-in-go-echo">Bonus: Dealing with CORS Error in Go Echo</h2><p>When you use Insomnia to access the API you can use the try function to make API calls, but not from a browser with Redocly for browser security reasons (CORS).</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/08/Bildschirmfoto-2022-08-17-um-14.14.53.png" class="kg-image" alt loading="lazy"></figure><p>To avoid this install the echo middleware</p><pre><code class="language-yaml">$ go get github.com/labstack/echo/v4/middleware 
</code></pre><p>and then in the code add the following line after <code>e := echo.New()</code></p><pre><code class="language-yaml">e.Use(middleware.CORS())
</code></pre><h2 id="conclusion">Conclusion</h2><p>Great, I really appreciate staying for so long and if you made it this far that should be enough to design your own APIs. Leave a comment with any questions, suggestions, things not working or words of encouragement.</p><h2 id="links">Links</h2><ul><li>Migrating from OpenAPI 3.0.4 to 3.1.0 <a href="https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0">https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0</a></li><li>A similar tutorial on DigitalOcean <a href="https://www.digitalocean.com/community/tutorials/how-to-create-documentation-for-your-rest-api-with-insomnia">https://www.digitalocean.com/community/tutorials/how-to-create-documentation-for-your-rest-api-with-insomnia</a></li><li>Go Echo CORS settings <a href="https://echo.labstack.com/middleware/cors/">https://echo.labstack.com/middleware/cors/</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Relaying a Microservice JSON Response to the Client by Unmarshalling Go Structs]]></title><description><![CDATA[In this post we will transform a JSON response from a microservice by adding metadata like a name, date or a unique identifier and relaying it to the user client. This is part of my journey in building AI SaaS app on iOS for researchers, as an example we will pretend to send data to an AI object ide]]></description><link>https://sebastianroy.de/relaying-a-microservice-json-response-to-the-client-by-unmarshalling-go-structs/</link><guid isPermaLink="false">62e913cf8a9bb5c076f7b59f</guid><category><![CDATA[Go]]></category><category><![CDATA[Coding]]></category><category><![CDATA[JSON]]></category><category><![CDATA[Building an AI SaaS]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Fri, 05 Aug 2022 14:35:40 GMT</pubDate><media:content url="https://sebastianroy.de/content/images/2022/08/PreandAfterJSON.png" medium="image"/><content:encoded><![CDATA[<img src="https://sebastianroy.de/content/images/2022/08/PreandAfterJSON.png" alt="Relaying a Microservice JSON Response to the Client by Unmarshalling Go Structs"><p>The past week I have been working on connecting an image analysis microservice to the user frontend. We can send an image via post request &#xA0;and retrieve an array of x and y-coordinates as well as a label for the identified object and a confidence parameter.</p><pre><code class="language-go">{&quot;success&quot;: true,
 &quot;predictions&quot;: [
		{
			&quot;confidence&quot;: 0.69146144,
			&quot;label&quot;: &quot;Lion&quot;,
			&quot;y_min&quot;: 682,
			&quot;x_min&quot;: 109,
			&quot;y_max&quot;: 707,
			&quot;x_max&quot;: 186
		},
		{
			&quot;confidence&quot;: 0.7,
			&quot;label&quot;: &quot;Pinguin&quot;,
			&quot;y_min&quot;: 782,
			&quot;x_min&quot;: 209,
			&quot;y_max&quot;: 717,
			&quot;x_max&quot;: 196
		}]
}

</code></pre><p>In this post I will talk about transforming the JSON response and adding metadata like a name, date or a unique identifier. You could also convert the data, so instead of forwarding <code>x_min</code> and <code>x_max</code> coordinates, we could relay X-Min as coordinate and <code>width</code> instead of <code>x_max</code>. In order to distinguish on the client if an object was detected by an algorithm or a human we add a new field <code>method</code>. This could also be a more complicated object to store the version of the AI algorithm. The relayed response should look like this</p><pre><code class="language-go">{&quot;name&quot;: &quot;Zoo Image Analysis&quot;,
 &quot;xid&quot;: &quot;9m4e2mr0ui3e8a215n4g&quot;
 &quot;predictions&quot;: [
		{
			&quot;confidence&quot;: 0.69146144,
			&quot;label&quot;: &quot;Lion&quot;,
			&quot;x&quot;: 682,
			&quot;y&quot;: 109,
			&quot;width&quot;: 77, 
			&quot;height&quot;: 25,
			&quot;method&quot;: &quot;AI&quot;
		},
		{
			&quot;confidence&quot;: 0.7,
			&quot;label&quot;: &quot;Penguin&quot;,
			&quot;x&quot;: 782,
			&quot;y&quot;: 209,
			&quot;width&quot;:  -13,
			&quot;height&quot;: -65,
			&quot;method&quot;: &quot;AI&quot;
		}]
}
</code></pre><p>To do this we will create a <code>ResponseAdapter</code> struct that mimics the response structure of the AI microservice as well as a <code>PredictionAdapter</code>. Then we create a <code>Response</code> and a <code>Prediction</code> struct that contains the fields and methods we want to relay to our client.</p><ol><li>Print unknown JSON data with an empty interface</li><li>Convert JSON data into a usable struct to access data fields</li><li>Rewrite into testable functions</li><li>Implementing custom UnmarshalJSON</li><li>Conclusion</li></ol><p>When retrieving some JSON data from an API, we cannot call those properties like with <code>data.name</code>. Converting this data into structs takes some unmarshalling, mapping and interfacing. So let&#x2019;s play with some JSON handling examples to step by step arrive at our goal, which is making data from an API call usable.</p><p>You can follow these steps using your local tooling in VS Code and go-tools or just use <a href="https://play.golang.com/">Go Playground</a> without any installation.</p><h2 id="printing-unknown-json-data-with-an-empty-interface">Printing unknown JSON data with an empty interface</h2><p>To have some sample data, we read some JSON data from an AI microservice as a multiline string into a byte array.</p><pre><code class="language-go">var data = []byte(`{&quot;success&quot;: true,
	&quot;predictions&quot;: [
		{
			&quot;confidence&quot;: 0.69146144,
			&quot;label&quot;: &quot;Lion&quot;,
			&quot;y_min&quot;: 682,
			&quot;x_min&quot;: 109,
			&quot;y_max&quot;: 707,
			&quot;x_max&quot;: 186
		},
		{
			&quot;confidence&quot;: 0.7,
			&quot;label&quot;: &quot;Penguin&quot;,
			&quot;y_min&quot;: 782,
			&quot;x_min&quot;: 209,
			&quot;y_max&quot;: 717,
			&quot;x_max&quot;: 196
		}]
	}`)

</code></pre><p>Next we create a temporary empty interface</p><pre><code class="language-go">var temp map[string]interface{}
</code></pre><p>Then wir unmarshall the data into this variable temp. This requires an import of <code>&#x201E;encoding/json&#x201C;</code>.</p><pre><code class="language-go">json.Unmarshal(data, &amp;temp)
</code></pre><p>Catch any unmarshalling errors</p><pre><code class="language-go">if err != nil {
		fmt.Println(err)
}
</code></pre><p>Then we can print out temp.</p><pre><code class="language-go">fmt.Println(temp)
</code></pre><p>The result should be</p><pre><code class="language-go">map[predictions:[map[confidence:0.69146144 label:Lion x_max:186 x_min:109 y_max:707 y_min:682] map[confidence:0.7 label:Penguin x_max:196 x_min:209 y_max:717 y_min:782]] success:true]
</code></pre><p>Great. If we want to access just the title</p><pre><code class="language-go">fmt.Println(temp.predictions[0])
</code></pre><p>we get an error.</p><pre><code class="language-go">./prog.go:40:19: temp.predictions undefined (type map[string]interface{} has no field or method predictions)
</code></pre><p>Ok, let&#x2019;s take care about this next. The listing for this section.</p><pre><code class="language-go">package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
)

func main() {

	var data = []byte(`{&quot;success&quot;: true,
	&quot;predictions&quot;: [
		{
			&quot;confidence&quot;: 0.69146144,
			&quot;label&quot;: &quot;Lion&quot;,
			&quot;y_min&quot;: 682,
			&quot;x_min&quot;: 109,
			&quot;y_max&quot;: 707,
			&quot;x_max&quot;: 186
		},
		{
			&quot;confidence&quot;: 0.7,
			&quot;label&quot;: &quot;Penguin&quot;,
			&quot;y_min&quot;: 782,
			&quot;x_min&quot;: 209,
			&quot;y_max&quot;: 717,
			&quot;x_max&quot;: 196
		}]
	}`)

	var temp map[string]interface{}

	//var prediction PredictionsAdapter

	err := json.Unmarshal(data, &amp;temp)

	if err != nil {
		fmt.Println(err)
	}

	//fmt.Println(temp.predictions[0])
}
</code></pre><h2 id="accessing-data-fields">Accessing data fields</h2><p>Assuming we know the expected data structure in advance, lets create a struct.</p><pre><code class="language-go">type ResponseAdapter struct {
	Success     bool                 `json:&quot;success&quot;`
	Predictions []PredictionsAdapter `json:&quot;predictions&quot;`
}
</code></pre><p>Scroll up to the first JSON response to see where this is coming from. This is mirroring the response we get from the AI microservice. To be exported, the names must start with a capital letter. <code>[]PredictionsAdapter</code> means that we expect multiple objects or an array of type PredictionsAdapter.</p><pre><code class="language-go">type PredictionsAdapter struct {
	Confidence float64 `json:&quot;confidence&quot;`
	Label      string  `json:&quot;label&quot;`
	YMin       int     `json:&quot;y_min&quot;`
	XMin       int     `json:&quot;x_min&quot;`
	YMax       int     `json:&quot;y_max&quot;`
	XMax       int     `json:&quot;x_max&quot;`
}
</code></pre><p>We extend the <code>PredictionsAdapter</code> with two functions <code>Width()</code> and <code>Height()</code>, since we need this information later for our client response with <code>Predictions</code>.</p><pre><code class="language-go">func (p PredictionsAdapter) Width() int {

	return p.XMax - p.XMin
}

func (p PredictionsAdapter) Height() int {

	return p.YMax - p.YMin
}
</code></pre><p>Instead of unmarshalling it into an empty interface, we use a variable of type <code>ResponseAdapter</code>.</p><pre><code class="language-go">var responseAdapter ResponseAdapter

json.Unmarshall(data, &amp;responseAdapter)

fmt.Println(responseAdapter.Predictions[0])
</code></pre><p>The <code>&amp;</code> implies that we are referring to the address in memory. And thats how we can access JSON data from go as structs :).</p><pre><code class="language-go">{0.69146144 Lion 682 109 707 186}
</code></pre><p>The full listing</p><pre><code class="language-go">package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
)

type ResponseAdapter struct {
	Success     bool                 `json:&quot;success&quot;`
	Predictions []PredictionsAdapter `json:&quot;predictions&quot;`
}

type PredictionsAdapter struct {
	Confidence float64 `json:&quot;confidence&quot;`
	Label      string  `json:&quot;label&quot;`
	YMin       int     `json:&quot;y_min&quot;`
	XMin       int     `json:&quot;x_min&quot;`
	YMax       int     `json:&quot;y_max&quot;`
	XMax       int     `json:&quot;x_max&quot;`
}

func (p PredictionsAdapter) Width() int {

	return p.XMax - p.XMin
}

func (p PredictionsAdapter) Height() int {

	return p.YMax - p.YMin
}

func main() {

	var data = []byte(`{&quot;success&quot;: true,
	&quot;predictions&quot;: [
		{
			&quot;confidence&quot;: 0.69146144,
			&quot;label&quot;: &quot;Lion&quot;,
			&quot;y_min&quot;: 682,
			&quot;x_min&quot;: 109,
			&quot;y_max&quot;: 707,
			&quot;x_max&quot;: 186
		},
		{
			&quot;confidence&quot;: 0.7,
			&quot;label&quot;: &quot;Penguin&quot;,
			&quot;y_min&quot;: 782,
			&quot;x_min&quot;: 209,
			&quot;y_max&quot;: 717,
			&quot;x_max&quot;: 196
		}]
	}`)

	//var temp map[string]interface{}

	var responseAdapter ResponseAdapter

	err := json.Unmarshal(data, &amp;responseAdapter)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(responseAdapter.Predictions[0])
}
</code></pre><h2 id="testing">Testing</h2><p>I find it always motivating to write something that works and see some result first. To adapt good practices let&#x2019;s now rewrite everything into a testable function and get rid of <code>main()</code>. If you use Go Playground, it will automatically run the test functions in absence of <code>main()</code>.</p><p>First import <code>&quot;testing&quot;</code> and <code>&quot;github.com/google/go-cmp/cmp&quot;</code>. It is a library to compare expectations <code>want</code> with actual result <code>got</code> of a function. It checks if both are equal with <code>if !cmp.Equal(want, got)</code> and returns an error with the differences otherwise.</p><p>The declaration of <code>data</code>we plug out of the functional context such that it is accessible from each testing function.</p><p>Most of what has been happening in <code>main</code>we will rename to <code>ParseImageAnalysisResponse</code> which will receive the JSON content of type <code>[]byte</code> and return a <code>ResponseAdapter</code> and an <code>error</code>.</p><p>The test function has the same name with prefix <code>TestXxx</code> and a reference to <code>*testing.T</code> which enables automated testing in go with <code>go test</code>. The first line then enables parallel test execution with <code>t.Parallel()</code>.</p><p>Then we declare our expected result. This time we don&#x2019;t use the JSON format, since we expect data as a go struct. Thats why we state the type before opening curly parenthesises <code>want := ResponseAdapter{... .}</code></p><pre><code class="language-go">want := ResponseAdapter{
		Success: true,
		Predictions: []PredictionsAdapter{
			{
				Confidence: 0.69146144,
				Label:      &quot;Lion&quot;,
				YMin:       682,
				XMin:       109,
				YMax:       707,
				XMax:       186,
			},
			{
				Confidence: 0.7,
				Label:      &quot;Penguin&quot;,
				YMin:       782,
				XMin:       209,
				YMax:       717,
				XMax:       196,
			},
		},
}
</code></pre><p>Then we have to call the function with our <code>data</code> and catch possible errors. <code>got</code> will be of type <code>ResponseAdapter</code>, since this is what <code>ParseImageAnalysisResponseAdapter(&#x2026;)</code> returns.</p><pre><code class="language-go">got, err := ParseImageAnalysisResponseAdapter(data)
if err != nil {
		t.Fatal(err)
}
</code></pre><p>And last we compare expectation and result.</p><pre><code class="language-go">if !cmp.Equal(want, got) {
	t.Error(cmp.Diff(want, got))
}
</code></pre><p>If everything is correct we can use Run on Go Playground or <code>go test</code> in the command line and the test should pass or return an error.</p><pre><code class="language-go">=== RUN   TestParseImageAnalysisResponse
=== PAUSE TestParseImageAnalysisResponse
=== CONT  TestParseImageAnalysisResponse
{0.69146144 Lion 682 109 707 186}
--- PASS: TestParseImageAnalysisResponse (0.00s)
PASS
</code></pre><p>Full listing</p><pre><code class="language-go">package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;github.com/google/go-cmp/cmp&quot;
	&quot;testing&quot;
)

type ResponseAdapter struct {
	Success     bool                 `json:&quot;success&quot;`
	Predictions []PredictionsAdapter `json:&quot;predictions&quot;`
}

type PredictionsAdapter struct {
	Confidence float64 `json:&quot;confidence&quot;`
	Label      string  `json:&quot;label&quot;`
	YMin       int     `json:&quot;y_min&quot;`
	XMin       int     `json:&quot;x_min&quot;`
	YMax       int     `json:&quot;y_max&quot;`
	XMax       int     `json:&quot;x_max&quot;`
}

var data = []byte(`{&quot;success&quot;: true,
	&quot;predictions&quot;: [
		{
			&quot;confidence&quot;: 0.69146144,
			&quot;label&quot;: &quot;Lion&quot;,
			&quot;y_min&quot;: 682,
			&quot;x_min&quot;: 109,
			&quot;y_max&quot;: 707,
			&quot;x_max&quot;: 186
		},
		{
			&quot;confidence&quot;: 0.7,
			&quot;label&quot;: &quot;Penguin&quot;,
			&quot;y_min&quot;: 782,
			&quot;x_min&quot;: 209,
			&quot;y_max&quot;: 717,
			&quot;x_max&quot;: 196
		}]
	}`)

func ParseImageAnalysisResponseAdapter(data []byte) (ResponseAdapter, error) {

	var responseAdapter ResponseAdapter

	err := json.Unmarshal(data, &amp;responseAdapter)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(responseAdapter.Predictions[0])
	
	return responseAdapter, nil
}

func TestParseImageAnalysisResponseAdapter(t *testing.T) {
	t.Parallel()

	want := ResponseAdapter{
		Success: true,
		Predictions: []PredictionsAdapter{
			{
				Confidence: 0.69146144,
				Label:      &quot;Lion&quot;,
				YMin:       682,
				XMin:       109,
				YMax:       707,
				XMax:       186,
			},
			{
				Confidence: 0.7,
				Label:      &quot;Penguin&quot;,
				YMin:       782,
				XMin:       209,
				YMax:       717,
				XMax:       196,
			},
		},
	}

	got, err := ParseImageAnalysisResponseAdapter(data)
	if err != nil {
		t.Fatal(err)
	}

	if !cmp.Equal(want, got) {
		t.Error(cmp.Diff(want, got))
	}

}
</code></pre><p>Now we have the data available in such a format that we can actually use it. We could even convert it to JSON and pass it to our user client. But sometimes the implementation logic requires that we add or remove some fields and provide the data in a different format. If that is what makes most sense, we should do it to maintain a well designed data workflow, even if it takes some additional work.</p><p>In the next section we will define the types <code>Response</code> and <code>Predictions</code> in a way we actually need them and transform them from our Adapter types using custom <code>UnmarshallJSON</code>. First we will add some custom metadata. Adding or changing fields in <code>[]PredictionsAdapter</code> however requires looping through each element in the array.</p><h2 id="implementing-custom-unmarshalljson">Implementing custom UnmarshallJSON</h2><p>Let&#x2019;s create the Test for <code>ParseImageAnalysisResponse</code> that we are going to implement next (ignore any errors). Out want will be a Response that contains some metadata like name and Xid and the array of predictions. We use the data to pass it to <code>ParseImageAnalysisResponse</code> and than see if our expectation was met.</p><pre><code class="language-go">func TestParseImageAnalysisResponse(t *testing.T) {
	t.Parallel()

	want := Response{
		Name: &quot;Test Image&quot;,
		Xid:  &quot;9m4e2mr0ui3e8a215n4g&quot;,
		Predictions: []Prediction{
			{
				Confidence: 0.69146144,
				Label:      	&quot;Lion&quot;,
				X:       		109,
				Y:       		682,
				Width:      	77,
				Height:     	25,
				Method:     	&quot;AI&quot;,
			},
			{
				Confidence:	 	0.7,
				Label:      	&quot;Penguin&quot;,
				X:       		209,
				Y:       		782,
				Width:          -13,
				Height:       	-65,
				Method:     	&quot;AI&quot;,
			},		
		},
	}

	got, err := ParseImageAnalysisResponse(data)
	if err != nil {
		t.Fatal(err)
	}

	if !cmp.Equal(want, got) {
		t.Error(cmp.Diff(want, got))
	}

}
</code></pre><p>Next we define types that match the JSON API response we want to relay to our client, that could be a web app or a mobile app for Android or iOS.</p><pre><code class="language-go">type Prediction struct {
	Confidence  float64 `json:&quot;confidence&quot;`
	Label       string  `json:&quot;label&quot;`
	X       	int     `json:&quot;x&quot;`
	Y	        int     `json:&quot;y&quot;`
	Width       int     `json:&quot;width&quot;`
	Height      int     `json:&quot;height&quot;`
	Method      string  `json:&quot;method&quot;` // AI, Manual, Predicted(?)
}
</code></pre><p>The Response will contain many of those predictions, since many objects can be detected in an image. But further more it will include meta data like a <code>Name</code> and an <code>Xid</code>. A Xid is a unique identifier that is a related but shorter version of UUID. We might need to change the type later, but for now let&#x2019;s use <code>string</code>.</p><pre><code class="language-go">type Response struct {
	Name        string       `json:&quot;name&quot;`
	Xid         string       `json:&quot;xid&quot;`
	Predictions []Prediction `json:&quot;predictions&quot;`
}
</code></pre><p>Now we will adapt the <code>ParseImageAnalysisResponse</code> function. It will still use the <code>data</code> of type <code>[]byte</code> as an argument, but instead of a <code>ResponseAdapter</code> it will now return our new <code>Response</code>. Therefore we use a variable <code>response</code> of type <code>Response</code> and use this when unmarshalling. We then check for errors and return the <code>response</code>.</p><pre><code class="language-go">func ParseImageAnalysisResponse(data []byte) (Response, error) {

	var response Response
	// Use custom UnmarshalJSON method to obtain Response
	err := json.Unmarshal(data, &amp;response)

	if err != nil {
		fmt.Println(err)
	}

	return response, nil

}
</code></pre><p>Since there is no easy 1-to-1 mapping from the <code>ResponseAdapter</code> to the <code>Response</code>, we need to do some manual work. Instead of the default <code>UnmarshalJSON</code> which we don&#x2019;t have to specify, we will now use a customized version of it that explicitly states how we can derive our <code>Response</code> to the client from the data in the <code>ResponseAdapter</code>.</p><p>The <code>data</code> we send to <code>ParseImageAnalysisResponse</code> is the one we want to unmarshall. We know that we can unmarshall it into a <code>ResponseAdapter</code>, so thats what we will do first, but not into the pointer of the response object <code>r</code>, but into a temporary variable <code>tmp</code>.</p><pre><code class="language-go">var tmp ResponseAdapter
err := json.Unmarshal(data, &amp;tmp)

if err != nil {
	fmt.Println(err)
}
</code></pre><p>This variable also has the Predictions in form of a <code>PredictionsAdapter</code>. We now cast them into a new Predictions-Array.</p><pre><code class="language-go">var p []Prediction
</code></pre><p>We do this by looping though each element <code>PredictionsAdapter</code></p><pre><code class="language-go">for _, element := range tmp.Predictions {
</code></pre><p>and create a temporary corresponding predictions element.</p><p>This predictions element also defines the data for any added field or it could also leave out fields from <code>PredictionsAdapter</code>. Then we append the Predictions element to the Predictions Array <code>p</code>, that was defined before the loop. I did variable creation and appending kind of in one step here and closed the loop.</p><pre><code class="language-go">p = append(p, Prediction{
			Confidence: element.Confidence,
			Method:     &quot;AI&quot;,
			Label:      element.Label,
			XMin:       element.XMin,
			XMax:       element.XMax,
			YMin:       element.YMin,
			YMax:       element.YMax,
	})
}
</code></pre><p>Then we set the <code>Response</code> fields. That includes metadata like the name and Xid and setting the predictions array p.</p><pre><code class="language-go">r.Name = &quot;Test Image&quot;
r.Xid = &quot;9m4e2mr0ui3e8a215n4g&quot;
r.Predictions = p
</code></pre><p>And last since UnmarshalJSON requires us to return an error, we do that as well.</p><pre><code class="language-go">return err
</code></pre><p>Now lets see how this function looks in its full glory.</p><pre><code class="language-go">func (r *Response) UnmarshalJSON(data []byte) error {


	var tmp ResponseAdapter
	err := json.Unmarshal(data, &amp;tmp)

	if err != nil {
		fmt.Println(err)
	}

	var p []Prediction
	for _, element := range tmp.Predictions {

		p = append(p, Prediction{
			Confidence: element.Confidence,
			Method:     &quot;AI&quot;,
			Label:      element.Label,
			X:	        element.XMin,
			Y:     	    element.YMin,
			Width:      element.Width(),
			Height:     element.Height(),
		})

	}

	r.Name = &quot;Test Image&quot;
	r.Xid = &quot;9m4e2mr0ui3e8a215n4g&quot;
	r.Predictions = p

	return err
}
</code></pre><h2 id="conclusion">Conclusion</h2><p>Great. We can now take an image that was annotated with an AI microservice and make this analysis useful for relaying it the user client by enhancing it with more relevant data for a JSON response. On the networking side how to exactly send JSON responses from a HTTP request to a client was explained . I talked about <a href="Sending%20Images%20in%20POST%20request%20as%20MultiPart%20Form%20from%20Go%20to%20Microservice">how to send POST requests to the AI micrososervice in a previous post</a>.</p><p>Last, let&#x2019;s look at the full listing, which you can find also on <a href="https://play.golang.com/p/vBJPQtDN7pE">Go Playground</a>. The tests could also be written into a separate file of course.</p><pre><code class="language-go">package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;github.com/google/go-cmp/cmp&quot;
	&quot;testing&quot;
)

type ResponseAdapter struct {
	Success     bool                 `json:&quot;success&quot;`
	Predictions []PredictionsAdapter `json:&quot;predictions&quot;`
}

type PredictionsAdapter struct {
	Confidence float64 `json:&quot;confidence&quot;`
	Label      string  `json:&quot;label&quot;`
	YMin       int     `json:&quot;y_min&quot;`
	XMin       int     `json:&quot;x_min&quot;`
	YMax       int     `json:&quot;y_max&quot;`
	XMax       int     `json:&quot;x_max&quot;`
}


func (p PredictionsAdapter) Width() int {

	return p.XMax - p.XMin
}

func (p PredictionsAdapter) Height() int {

	return p.YMax - p.YMin
}


type Prediction struct {
	Confidence float64 `json:&quot;confidence&quot;`
	Label      string  `json:&quot;label&quot;`
	X          int     `json:&quot;x&quot;`
	Y          int     `json:&quot;y&quot;`
	Width      int     `json:&quot;width&quot;`
	Height     int     `json:&quot;height&quot;`
	Method     string  `json:&quot;method&quot;` // AI, Manual, Predicted(?)
}


type Response struct {
	Name        string       `json:&quot;name&quot;`
	Xid         string       `json:&quot;xid&quot;`
	Predictions []Prediction `json:&quot;predictions&quot;`
}


func (r *Response) UnmarshalJSON(data []byte) error {


	var tmp ResponseAdapter
	err := json.Unmarshal(data, &amp;tmp)

	if err != nil {
		fmt.Println(err)
	}

	var p []Prediction
	for _, element := range tmp.Predictions {

		// fmt.Println(&quot;element&quot;, element, &quot;at index&quot;, index)

		p = append(p, Prediction{
			Confidence: element.Confidence,
			Method:     &quot;AI&quot;,
			Label:      element.Label,
			X:          element.XMin,
			Y:          element.YMin,
			Width:      element.Width(),
			Height:     element.Height(),
		})

	}

	r.Name = &quot;Test Image&quot;
	r.Xid = &quot;9m4e2mr0ui3e8a215n4g&quot;
	r.Predictions = p

	return err
}

var data = []byte(`{&quot;success&quot;: true,
	&quot;predictions&quot;: [
		{
			&quot;confidence&quot;: 0.69146144,
			&quot;label&quot;: &quot;Lion&quot;,
			&quot;y_min&quot;: 682,
			&quot;x_min&quot;: 109,
			&quot;y_max&quot;: 707,
			&quot;x_max&quot;: 186
		},
		{
			&quot;confidence&quot;: 0.7,
			&quot;label&quot;: &quot;Penguin&quot;,
			&quot;y_min&quot;: 782,
			&quot;x_min&quot;: 209,
			&quot;y_max&quot;: 717,
			&quot;x_max&quot;: 196
		}]
	}`)

func ParseImageAnalysisResponseAdapter(data []byte) (ResponseAdapter, error) {

	var responseAdapter ResponseAdapter

	err := json.Unmarshal(data, &amp;responseAdapter)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(responseAdapter.Predictions[0])
	
	return responseAdapter, nil
}

func ParseImageAnalysisResponse(data []byte) (Response, error) {

	var response Response
	// Use custom UnmarshalJSON method to obtain Response
	err := json.Unmarshal(data, &amp;response)

	if err != nil {
		fmt.Println(err)
	}
	// fmt.Println(&quot;Response&quot;)
	// fmt.Println(response)

	return response, nil

}


func TestParseImageAnalysisResponseAdapter(t *testing.T) {
	t.Parallel()

	want := ResponseAdapter{
		Success: true,
		Predictions: []PredictionsAdapter{
			{
				Confidence: 0.69146144,
				Label:      &quot;Lion&quot;,
				YMin:       682,
				XMin:       109,
				YMax:       707,
				XMax:       186,
			},
			{
				Confidence: 0.7,
				Label:      &quot;Penguin&quot;,
				YMin:       782,
				XMin:       209,
				YMax:       717,
				XMax:       196,
			},
		},
	}

	got, err := ParseImageAnalysisResponseAdapter(data)
	if err != nil {
		t.Fatal(err)
	}

	if !cmp.Equal(want, got) {
		t.Error(cmp.Diff(want, got))
	}

}

func TestParseImageAnalysisResponse(t *testing.T) {
	t.Parallel()

	want := Response{
		Name: &quot;Test Image&quot;,
		Xid:  &quot;9m4e2mr0ui3e8a215n4g&quot;,
		Predictions: []Prediction{
			{
				Confidence: 0.69146144,
				Label:      	&quot;Lion&quot;,
				X:       		109,
				Y:       		682,
				Width:      	77,
				Height:     	25,
				Method:     	&quot;AI&quot;,
			},
			{
				Confidence:	 0.7,
				Label:      	&quot;Penguin&quot;,
				X:       		209,
				Y:       		782,
				Width:          -13,
				Height:       	-65,
				Method:     	&quot;AI&quot;,
			},
		},
	}

	got, err := ParseImageAnalysisResponse(data)
	if err != nil {
		t.Fatal(err)
	}

	if !cmp.Equal(want, got) {
		t.Error(cmp.Diff(want, got))
	}

}
</code></pre><h2 id="links">Links</h2><p><a href="https://bitfieldconsulting.com/golang/map-string-interface">https://bitfieldconsulting.com/golang/map-string-interface</a></p><p><a href="https://jhall.io/posts/go-json-tricks-array-as-structs/">https://jhall.io/posts/go-json-tricks-array-as-structs/</a></p><p><a href="https://stackoverflow.com/questions/42377989/unmarshal-json-array-of-arrays-in-go">https://stackoverflow.com/questions/42377989/unmarshal-json-array-of-arrays-in-go</a></p><p></p><!--kg-card-begin: html--><div id="hyvor-talk-view"></div>
<script type="text/javascript">
    var HYVOR_TALK_WEBSITE = 8174;
    var HYVOR_TALK_CONFIG = {
        url: false,
        id: false
    };
</script>
<script async type="text/javascript" src="//talk.hyvor.com/web-api/embed.js"></script><!--kg-card-end: html-->]]></content:encoded></item><item><title><![CDATA[Sending Images in POST request as MultiPart Form from Go to Microservice]]></title><description><![CDATA[Today we will learn how to send an image to a microservice for analysis and return the result to the user.]]></description><link>https://sebastianroy.de/sending-images-in-post-request-as-multipart-form-from-go-to-microservice/</link><guid isPermaLink="false">62bc36528a9bb5c076f7b57d</guid><category><![CDATA[Go]]></category><category><![CDATA[Coding]]></category><category><![CDATA[Building an AI SaaS]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Wed, 29 Jun 2022 11:27:18 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1526498460520-4c246339dccb?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGh0dHB8ZW58MHx8fHwxNjU2NTAxOTcx&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1526498460520-4c246339dccb?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGh0dHB8ZW58MHx8fHwxNjU2NTAxOTcx&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Sending Images in POST request as MultiPart Form from Go to Microservice"><p>This problem occurred as I was trying to detect objects in an image with AI. I was able to train a server side object detection algorithm. A client specific solution i.e. with Create ML on iOS would be premature optimisation and makes more sense once the use case is well established. For now I want to be able to update the object detection algorithm anytime with improved training data.</p><p>We will talk about</p><ol><li>Skeleton and imports</li><li>Manipulate binary data from an image</li><li>Multipart Messages</li><li>HTTP Post request</li><li>HTTP Response</li><li>Conclusion</li><li>Full Listing</li></ol><p>There are two ways to handle user images. The first is to upload the image to an image hoster like cloudinary or uploadcare and then pass just the URL to the server. I actually suggest to use this method and I have written a post about this earlier . But sometimes you may want to handle images at the server directly.</p><p>Simple text forms can be send as <code>x-www-form-urlencoded</code>. But we will create a <code>multipart/form-data</code> POST request in Go to forward images.</p><h2 id="skeleton-and-imports">Skeleton and imports</h2><p>Let&#x2019;s start with a skeleton that contains <code>imports</code> and <code>main</code> function. The <code>main</code> function launches a server that listens on port 5000. It will call the function <code>handleRequest</code> once the url <code>http://127.0.0.1:5000/image</code> is called. This function will load an image file from disk in the same folder, send it to an analysis microservice and receives the result.</p><pre><code class="language-go">package main

import (
	&quot;bufio&quot;
	&quot;bytes&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;io/ioutil&quot;
	&quot;log&quot;
	&quot;mime/multipart&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;
	&quot;os&quot;
	&quot;time&quot;
)

func main() {
	handler := http.HandlerFunc(handleRequest)
	http.Handle(&quot;/image&quot;, handler)
	fmt.Println(&quot;Server starts at port 5000&quot;)
	http.ListenAndServe(&quot;:5000&quot;, nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) { 

// TODO: send image to microservice

}
</code></pre><h2 id="manipulate-binary-data-from-an-image">Manipulate binary data from an image</h2><p>In contrast to Python is Go a language that makes memory management and pointer visible. That makes it performant, but also requires some down to the metal thinking. The <code>&amp;</code> operator returns the memory address of a variable. We create a <code>http.Client</code>. Since we don&#x2019;t want to stall our server, just because the microservice is not available, we set a <code>Timeout</code>.</p><p>When dealing with text we use <code>strings</code> to perform operations like split or search. When dealing with data e.g. from images, this is done in a buffer of <code>&amp;bytes</code>, where the &amp; operator refers again to the memory address of that buffer of image bytes.</p><pre><code class="language-go">client := &amp;http.Client{
	Timeout: time.Second * 20,
}
body := &amp;bytes.Buffer{}
</code></pre><h2 id="multipart-messages">Multipart Messages</h2><p>Messages that contain multiple parts like a text message and an image attachment can be encoded with MIME/Multipart, which is also used in e-mails. In this case we will only use one part. We create a multipart <code>writer</code>. The image section of the message with key name &#x201E;image&#x201C; and file name &#x201E;image.jpg&#x201C; is stored in form writer <code>fw</code>. You can choose any name, we are not referring to the filename on disk yet.</p><pre><code class="language-go">writer := multipart.NewWriter(body)
fw, err := writer.CreateFormFile(&quot;image&quot;, &quot;image.jpg&quot;)
</code></pre><p>Next we open the image from disk using the operating system package <code>os</code>. This file is then copied into the multipart message form writer <code>fw</code>. Any error is logged and then the writer needs to be closed.</p><pre><code class="language-go">buf, err := os.Open(&quot;image.jpg&quot;)
_, err = io.Copy(fw, buf)
if err != nil {
	log.Fatal(err)
}
writer.Close()
</code></pre><h2 id="http-post-request">HTTP Post request</h2><p>Next we create a new http POST request, which in contrast to GET is used to send images and not retrieve them. Here we pass a URL and the bytes buffer that contains the image now and set the appropriate content type header.</p><pre><code class="language-go">req, err := http.NewRequest(&quot;POST&quot;, &quot;http://127.0.0.1:80/v1/vision/custom/testmodel&quot;, bytes.NewReader(body.Bytes()))
req.Header.Set(&quot;Content-Type&quot;, writer.FormDataContentType())
</code></pre><p>Then we use the client to perform the request, catch the http status code like 200 for OK or 404 or 400 when there is an issue and store the response in <code>rsp</code>.</p><h2 id="http-response">HTTP Response</h2><pre><code class="language-go">rsp, _ := client.Do(req)

if rsp.StatusCode != http.StatusOK {
	log.Printf(&quot;Request failed with response code: %d&quot;, rsp.StatusCode)
}
log.Print(rsp.StatusCode)
</code></pre><p>Then we read the body section of the response and for testing purposes print it to the command line on the server.</p><pre><code class="language-go">body, err := ioutil.ReadAll(rsp.Body)
if err != nil {
	log.Fatal(err)
}
log.Println(string(body))
</code></pre><p>To return data to the user as JSON we use http response which was passed as an argument in <code>handleRequest</code> function.</p><pre><code class="language-go">w.Header().Set(&quot;Content-Type&quot;, &quot;JSON&quot;)
w.Write(body)
</code></pre><p>This is now the result of the analysis the microservice has performed on the image, like the name of the detected object and its boundaries.</p><h2 id="conclusion">Conclusion</h2><p>In this post we loaded an image from disk, send it to a microservice for analysis and provided the response to the user. Missing is the part, where the user can actually send the image himself from the client to the server. If you like the content and want to be notified about any updates please subscribe to the email list.</p><h2 id="full-listing">Full Listing</h2><p>Here is the full listing. You can also view the code as <a href="https://goplay.tools/snippet/NDgJI6ycF-B">Go Playground</a>, but unfortunately you can&#x2019;t execute it (deadlock error). For it to work you need to actually launch the microservice, which we haven&#x2019;t discussed here.</p><pre><code class="language-go">package main

import (
	&quot;bytes&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;io/ioutil&quot;
	&quot;log&quot;
	&quot;mime/multipart&quot;
	&quot;net/http&quot;
	&quot;os&quot;
	&quot;time&quot;
)

func main() {
	handler := http.HandlerFunc(handleRequest)
	http.Handle(&quot;/image&quot;, handler)
	fmt.Println(&quot;Server startet at port 8080&quot;)
	http.ListenAndServe(&quot;:5000&quot;, nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {

	buf, err := os.Open(&quot;image.jpg&quot;)

	body := &amp;bytes.Buffer{}
	writer := multipart.NewWriter(body)
	fw, err := writer.CreateFormFile(&quot;image&quot;, &quot;image.jpg&quot;)

	_, err = io.Copy(fw, buf)
	if err != nil {
		log.Fatal(err)
	}
	writer.Close()

	req, err := http.NewRequest(&quot;POST&quot;, &quot;http://127.0.0.1:80/v1/vision/custom/testmodel&quot;, bytes.NewReader(body.Bytes()))
	req.Header.Set(&quot;Content-Type&quot;, writer.FormDataContentType())
	client := &amp;http.Client{
		// Set timeout to not be at mercy of microservice to respond and stall the server
		Timeout: time.Second * 20,
	}

	rsp, _ := client.Do(req)

	if rsp.StatusCode != http.StatusOK {
		log.Printf(&quot;Request failed with response code: %d&quot;, rsp.StatusCode)
	}
	log.Print(rsp.StatusCode)

	body2, err := ioutil.ReadAll(rsp.Body)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(string(body2))

	w.Header().Set(&quot;Content-Type&quot;, &quot;JSON&quot;)
	w.Write(body2)
}

</code></pre><h2 id="links">Links</h2><ul><li><a href="https://golangbyexample.com/http-mutipart-form-body-golang/">https://golangbyexample.com/http-mutipart-form-body-golang/</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Communications from Go Backend to SwiftUI Frontend with JSON API]]></title><description><![CDATA[Let’s establish the communication between server API and client.
We will deal with janitor work of programming. It consists of structuring values of data fields into groups, storing them long term and unpack them to work with.]]></description><link>https://sebastianroy.de/communications-from-go-backend-to-swiftui-frontend-with-json-api/</link><guid isPermaLink="false">629e829c8a9bb5c076f7b518</guid><category><![CDATA[Building an AI SaaS]]></category><category><![CDATA[Go]]></category><category><![CDATA[Coding]]></category><category><![CDATA[API]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Mon, 06 Jun 2022 22:56:38 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1593720213428-28a5b9e94613?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIwfHxwcm9ncmFtbWluZ3xlbnwwfHx8fDE2NTQ1NTcyMTk&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1593720213428-28a5b9e94613?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIwfHxwcm9ncmFtbWluZ3xlbnwwfHx8fDE2NTQ1NTcyMTk&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Communications from Go Backend to SwiftUI Frontend with JSON API"><p>This might seem overly complicated. So, why? In a real world applications not everyone should know about all the data, some calculations should be done on a server, some on a client, some data needs to be shared, other needs to be private.</p><p>This post will not limit itself to unwrapping JSON data in Swift and Go, but also discuss tools how to create a more complicated data structure. A founder might not have a team of engineers that each can focus on a single programming language. But it is not necessary to master a programming language with all its glory. It is fine if we can mange the parts we need and learn as we go.</p><p>This post will provide everything you need from planning the communication flow and get it going from backend to frontend. We will not deal with databases though.</p><ol><li>Define JSON Structure</li><li>Create basic backend API server in Go<br>		1. Convert JSON Structure to Golang types</li><li>Test JSON API</li><li>Connect UI to Backend API<br>		1. Convert JSON Structure to Swift structs</li></ol><p>We will use XCode 13.1, Swift 5 and iOS 15.1.</p><h2 id="define-json-structure">Define JSON Structure</h2><p>The way we structure data needs to be consistent along database, backend, JSON API and the front end. Like using a software for UI mockups (Figma, Sketch), it is helpful to have tool to define data structures like jsoneditoronline.org. This will serve as a source of truth for the data structures. I use this because I can save structures. Later we will use <a href="https://app.quicktype.io/">quicktype</a> to convert the JSON structure into structs in Swift and Go.</p><h2 id="create-basic-api-server-in-go">Create basic API server in Go</h2><h3 id="install-go">Install Go</h3><p>Install Go from its website <a href="https://go.dev/doc/install">https://go.dev/doc/install</a>.</p><p>Open the commandline and type <code>go version</code> to check if the installation worked.</p><pre><code class="language-bash">$ go version
go version go1.18.2 darwin/amd64
</code></pre><p>I use Visual Studio Code with the Go extension as code editor and <a href="https://echo.labstack.com/">echo</a> as a framework.</p><p>Create a new project by creating a folder and changing into the new directory. Then initialize the new app and fetch the echo framework from GitHub.</p><pre><code class="language-bash">$ mkdir jsonserver &amp;&amp; cd jsonserver
$ go mod init jsonserver
$ go get github.com/labstack/echo/v4
</code></pre><h3 id="hello-world">Hello World</h3><p>Now write a short server in Go. It will display <code>Hello World</code> when called with a browser. In the next chapter we will output a JSON structure instead.</p><pre><code class="language-Go">package main

import (
	&quot;net/http&quot;
	
	&quot;github.com/labstack/echo/v4&quot;
)

func main() {
	e := echo.New()
	e.GET(&quot;/&quot;, func(c echo.Context) error {
		return c.String(http.StatusOK, &quot;Hello, World!&quot;)
	})
	e.Logger.Fatal(e.Start(&quot;:1323&quot;))
}</code></pre><p>The crucial function is <code>e.GET()</code>. It says wenn we call the root URL <code>/</code> it will call a function that returns a <code>string</code>. We will create a separate function to do this next.</p><p>Save this file as <code>server.go</code> and from the terminal run <code>$ go run server.go</code>.</p><pre><code class="language-go">$ go run server.go
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.7.2
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
&#x21E8; http server started on [::]:1323
</code></pre><p>Success! Open a browser with the URL <code>127.0.0.1:1323/</code>and you should see <code>Hello, World!</code> in the browser window.</p><h2 id="convert-json-structure-to-golang-types">Convert JSON Structure to Golang types</h2><p>First we create a sample file <code>groceries.json</code> where we will coding books.</p><pre><code class="language-json">{
&quot;title&quot; : &quot;Weekend groceries&quot;
}
</code></pre><p><code>{ }</code> in JSON indicates a dictionary and not a function like in Go or Swift. A dictionary consists of key and value pairs. Later we can access the title by printing <code>print(groceries.title)</code>. Parentheseses <code>&quot;&quot;</code>contains text or a <code>string</code>. Numbers on the other hand don&#x2019;t need parentheses and are of type <code>Int</code>. These are basic types. We can also create our own types or structs with multiple fields.</p><p>We will create a struct to load this structure into an object in Go. After the <code>import</code> segment insert the following lines to declare how the structure of an object of type <code>groceries</code> looks like.</p><p>Note: The purpose here is to make programming easier. The code editor will suggest existing fields and give an error when a field doesn&#x2019;t exist. There are other languages like python that are untyped or where types are optional. Working without types, classes, structs and objects is easier to learn, but more error prone later.</p><pre><code class="language-json">type Groceries struct {
	title 	string 	&apos;json:&quot;title&quot;&apos;
}
</code></pre><p>Title is the field name, string is the type and <code>&#x2018;json:&#x201D;title&quot;&#x2019;</code> contains the corresponding json field, which usually is the same, but it doesn&#x2019;t have to be.</p><p>Next we change the URL from root <code>/</code> to <code>/api/v1/groceries</code> using groups calling the function <code>readJSON()</code> that reads and returns the <code>groceries.json</code> file. Add the following in the <code>main()</code> function after initialising the echo <code>e</code> framework.</p><pre><code class="language-go">apiGroup := e.Group(&quot;/api&quot;)
v1 := v1.Group(&quot;/v1&quot;)

v1.GET(&quot;/groceries&quot;, func(c echo.Context) error {
		books := readJSON()

		return c.JSON(http.StatusOK, groceries)
		}
	)
</code></pre><h3 id="return-json-file">Return JSON File</h3><p>We still need to define the function <code>readJSON()</code>.</p><pre><code class="language-go">func readJSON() book {
	file, err := ioutil.ReadFile(&quot;./groceries.json&quot;)
	
	if err != nil {
		log.Fatal(err)
	}

	b := book{}
	err json.Unmarshal(file, &amp;b)

	if err != nil {
		log.Fatal(err)
	}
	return b
}
</code></pre><p>Visual Studio Code automatically adds required imports, but if it doesn&#x2019;t here is the full listing with the added imports.</p><pre><code class="language-go">package main

import (
	&quot;net/http&quot;
	
	&quot;encoding/json&quot;
	&quot;io/ioutil&quot;
	&quot;log&quot;

	&quot;github.com/labstack/echo/v4&quot;
)

type Book struct {
	title 	string 	&apos;json:&quot;title&quot;&apos;
}

func readJSON() book {
	file, err := ioutil.ReadFile(&quot;./groceries.json&quot;)
	
	if err != nil {
		log.Fatal(err)
	}

	b := book{}
	err json.Unmarshal(file, &amp;b)

	if err != nil {
		log.Fatal(err)
	}
	return b
}

func main() {
	e := echo.New()
	e.GET(&quot;/&quot;, func(c echo.Context) error {
		return c.String(http.StatusOK, &quot;Hello, World!&quot;)
	})

	apiGroup := e.Group(&quot;/api&quot;)
	v1 := v1.Group(&quot;/v1&quot;)

	v1.GET(&quot;/books&quot;, func(c echo.Context) error {
		books := readJSON()

		return c.JSON(http.StatusOK, books)
		}
	)
	e.Logger.Fatal(e.Start(&quot;:1323&quot;))
}
</code></pre><p>We run this again with <code>go run server.go</code> and open a browser <code>http://127.0.0.1:1323/api/v1/groceries</code>.</p><h3 id="integrate-an-advanced-json-model">Integrate an advanced JSON Model</h3><p>If everything worked so far (and write a comment below if it doesn&#x2019;t), we can move on to a more advanced data structure. The issue is that the JSON structure and the fields in the Go and Swift structs need to match. When fields are labelled wrong, are missing or have the wrong type the app crashes. Then it is important to have some kind of error message to tell you where to look for any issues.</p><p>Open <a href="https://jsoneditoronline.org/">JSONEditorOnline</a> to create your own data structure or use the following grocery list.</p><pre><code class="language-json">{
  &quot;list&quot;: {
       &quot;title&quot;: &quot;Dinner&quot;,
       &quot;uid&quot;: &quot;wq3413lkj&quot;
  },
 &quot;items&quot;: [{
    &quot;name&quot;: &quot;flour&quot;,
    &quot;uid&quot;: &quot;asd3lpr2j&quot;,
    &quot;quantity&quot;: {
      &quot;value&quot;: 500,
      &quot;unit&quot;: &quot;g&quot;
    },
    &quot;brand&quot;: &quot;Natura&quot;,
    &quot;expired&quot;: &quot;12-12-2022&quot;,
    &quot;image&quot;: &quot;s3dasfsf&quot;
     },
     
    {
    &quot;name&quot;: &quot;potatoes&quot;,
    &quot;uid&quot;: &quot;4h3q4erg&quot;,
    &quot;quantity&quot;: {
      &quot;value&quot;: 2,
      &quot;unit&quot;: &quot;kg&quot; },
    &quot;brand&quot;: &quot;regio&quot;,
    &quot;expired&quot;: &quot;08-12-2022&quot;,
    &quot;image&quot;: &quot;s3/2343gvdv&quot; }]
}
</code></pre><p>Also save this list as <code>groceries.json</code> in the Go folder. Before we will move to the front end, let me introduce a tool to test APIs. When APIs become more complex you don&#x2019;t want to test each request by hand again and again.</p><h2 id="test-json-api">Test JSON API</h2><p>To test APIs I use <a href="https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm">Talend API Tester</a> as a Chrome Browser extension, but there are others available.</p><h1 id="connect-ui-to-backend-api">Connect UI to Backend API</h1><p>The backend now has the most basic functionality. You can expand it with user authentication, or attach other micro services. Now we will move to the front end . It will display the data encoded in JSON to the user. You will need XCode 13.1 or above, some functionality like async is not available in earlier versions and later versions don&#x2019;t work on older macs.</p><p>Open Xcode to create a new project.</p><p>Create a new Folder <code>Services</code> and a SwiftUI File <code>JSONService.swift</code>.</p><p>Create a new <code>struct JSONContentUI: View</code>. To store the data we create a <code>@StateObject</code> and call it <code>groceries</code>. This will automatically update the UI when data is changed.</p><pre><code class="language-swift">struct JSONContentUI: View {
@StateObject var groceries = GroceriesAPI()	
	var body: some View {
	
		List {
			ForEach(groceries.groceries, id: \.self) { grocery in
				HStack {
					Text(grocery.list.title)
			}
		}
		.onAppear {
			groceries.fetchJSON()
		}
	}

}
</code></pre><p>As you can see we use a class <code>GroceriesAPI</code> which has the function <code>fechJSON()</code>. Let&#x2019;s implement this class next.</p><p>First we define the source URL. This might fail because of an invalid URL, so we need to <code>guard</code> it. We then open an <code>URLSession</code>to obtain the JSON as a <code>string</code> and print it.</p><pre><code class="language-swift">class GroceriesAPI: ObservableObject {
	@Published var groceries: [groceries] = []
	
	func fetchJSON() {
		guard let url = URL(string: &quot;http://127.0.0.1:1323/api/v1/groceries&quot;) else { return }
		
		let task URLSession.shared.dataTask(with: url) { data, response, error in
				if let data = data, let string = String(data: data, encoding: .utf8) {
					print(string)
				}
	
		// Decode JSON data
	}

}
</code></pre><h2 id="convert-json-structure-to-swift-structs">Convert JSON Structure to Swift structs</h2><p>Then we add the structs to encode groceries. We paste the JSON structure on <a href="https://app.quicktype.io/#l=swift&amp;s=music">quicktype</a>, name it <code>Groceries</code> and select Swift as a target language and copy the structs to the <code>JSONService.swift</code> file.</p><pre><code class="language-swift">// MARK: - Welcome
struct Welcome: Codable {
    let list: List
    let items: [Item]
}

// MARK: - Item
struct Item: Codable {
    let name, uid: String
    let quantity: Quantity
    let brand, expired, image: String
}

// MARK: - Quantity
struct Quantity: Codable {
    let value: Int
    let unit: String
}

// MARK: - List
struct List: Codable {
    let title, uid: String
}
</code></pre><h2 id="decode-json-to-struct">Decode JSON to struct</h2><p>We then expand <code>fetchJSON()</code> to decode the JSON string.</p><pre><code class="language-swift">let json = try JSONDecoder().decode([user].self, from: data)
</code></pre><p>To catch errors we wrap this in a <code>do {} catch {}</code> block. We then assign the encoded JSON groceries object to the groceries StateObject that is read by the UI.</p><pre><code class="language-swift">do {
    let json = try JSONDecoder().decode([groceries].self, from: data)
    print(json[0])

    DispatchQueue.main.async {
        self.groceries = json
   	}
} catch {
	print(&quot;JSON Format does not comply with struct keys.&quot;)
}
</code></pre><p></p><h2 id="conclusion">Conclusion</h2><p>So congratulations if you made it this far. There was a lot of stuff in here, I am writing these articles as I learn them myself, so please feel free to add any recommendations of issues you had while going through this.</p><!--kg-card-begin: html--><div id="hyvor-talk-view"></div>
<script type="text/javascript">
    var HYVOR_TALK_WEBSITE = 8174;
    var HYVOR_TALK_CONFIG = {
        url: false,
        id: false
    };
</script>
<script async type="text/javascript" src="//talk.hyvor.com/web-api/embed.js"></script><!--kg-card-end: html-->]]></content:encoded></item><item><title><![CDATA[How I use the iPad as a Productivity Device with Craft.do]]></title><description><![CDATA[In this post I will talk about my recent iPad productivity setup as a laptop replacement on the go.]]></description><link>https://sebastianroy.de/how-i-use-the-ipad-as-a-productivity-device-with-craftdo/</link><guid isPermaLink="false">62989045033fc009b1767021</guid><category><![CDATA[Productivity]]></category><category><![CDATA[Tools]]></category><category><![CDATA[iPad]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Thu, 02 Jun 2022 10:47:05 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1618344880247-3969cea5b81e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDI0fHxpcGFkfGVufDB8fHx8MTY1NDE2NjUxMQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1618344880247-3969cea5b81e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDI0fHxpcGFkfGVufDB8fHx8MTY1NDE2NjUxMQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="How I use the iPad as a Productivity Device with Craft.do"><p>Working on a business is similar to working out. Working on it daily leads to results over several months. And we want to get shredded. The best way to get shredded it to setup the environment to make working out easy and even fun.</p><h2 id="why-taking-notes-matters">Why Taking Notes Matters</h2><p>A good tool has to <strong>grow with the project</strong>. It starts with a blank canvas. Then you add a few bullet points. After a while you see the patterns to group similar bullet points under headlines like &#x201C;product features&quot; or &quot;marketing&quot;. But you just have a rough idea to use a blog as a marketing channel, so there is nothing else to write. You don&#x2019;t want to make it a page yet, let alone introduce a new tool just for marketing. It has to grow organically. Especially when you add a team members who needs to dig through your thoughts.</p><h2 id="what-else-is-out-there">What else is out there?</h2><p>For some time I am missing a <strong>tool to launch new projects</strong> weather it is an NGO, an app, a video or a business. I have been using Smartsheet, Google Docs, Almanac and Notion, but haven&#x2019;t been happy so far. </p><p>Don&#x2019;t get me wrong, Smartsheet has amazing GANTT charts for very complex projects with lots of nested tasks and dependencies with multiple teams. </p><p>Google Docs is good for collaborating on simple projects in real time. </p><p>Notion has its database capabilities and I am sure you can organise your life with it, but I haven&#x2019;t become cozy with it yet. Mainly because it is not a native app and therefore feels slow. Almanac is nice but it doesn&#x2019;t have a mobile app at all.</p><h2 id="notes">Notes</h2><p>I recently fell in love with <strong><a href="https://www.craft.do/">Craft.do</a></strong>. The downside, it is iOS first. It has a WebApp, but as of May 2022 it is not nicely usable on an android devices (change to desktop mode on your browser in the craft web app). If you can live with that, it&apos;s a stellar experience so far. Bullet points grow into headlines, headlines can be turned into pages. And before you know, you have a business plan draft. Pages are linked and can be shared with advisors or new team members. They update themselves, so no need to send out a new version of final draft version 2. But most importantly it accommodates the thought process of bringing a new project to life through writing.</p><p>Writing means <strong>clarity of thought</strong>. It avoids repeatedly thinking about the same thing over and over again and getting ideas out of you brain to make room for new ones. It allows you to get back to an idea at any point and reevaluate their validity. This avoids shiny new object syndrome.</p><figure class="kg-card kg-embed-card kg-card-hascaption"><iframe width="1236" height="500" style="border:0;" scrolling="no" src="https://screencast-o-matic.com/player/c31nQ7V3ism?title=0&amp;controls=0&amp;a=1&amp;overlays=1" allowfullscreen="true"></iframe><figcaption>How to setup a new business idea in Craft.do (click to play). Also click <a href="https://www.craft.do/s/W2h13GAE5gFnwF">here</a> to see how a shared notebook looks like.</figcaption></figure><h2 id="keyboard">Keyboard</h2><p>To work on the go, I have been using an iPad for ten years now. It has become a canvas, great for consuming video and for writing - with a pen. But it hasn&#x2019;t replaced my Laptop so far mainly because of the keyboard. There are bluetooth keyboards, which is a second item to carry around, or Apples smart over, which has a terrible writing experience. </p><p>The Apple Magic Keyboard is really nice but 300 bucks for a keyboard which will probably not work for the next generation iPad. Sorry.</p><p>Luckily Logitech has come up with the <strong><a href="https://www.amazon.de/dp/B07W7L9JX9?ref_=cm_sw_r_cp_ud_dp_8DP163P8TGSET423EBVV">Combo Touch Keyboard</a></strong>. Not only does it have a keyboard, it also has a trackpad and it is fully detachable when you want to watch a movie. It costs around 140 EUR but it turns a tablet into a laptop. This is highly valuable for anyone that needs to write long pages of text for a business plan or blog post like this one.</p><h2 id="conclusion">Conclusion</h2><p>The <a href="https://www.craft.do/">Craft</a> app (Free plan, 4 $ personal, 10 $ Business) together with the <a href="https://www.amazon.de/dp/B07W7L9JX9?ref_=cm_sw_r_cp_ud_dp_8DP163P8TGSET423EBVV">Logitech combo touch</a> keyboard (140 EUR) turns the iPad from a consumption into a productivity device. Yes, it works as a laptop replacement if you don&#x2019;t have special needs like virtual machines or XCode. It&#x2019;s fast, it&apos;s quiet and it&apos;s light. And most of all, it&apos;s fun to use. You won&#x2019;t be disappointed.</p><!--kg-card-begin: html--><script defer src="https://cdn.commento.io/js/commento.js"></script>
<div id="commento"></div><!--kg-card-end: html-->]]></content:encoded></item><item><title><![CDATA[12 Steps to Setup an AI SaaS Business]]></title><description><![CDATA[In this post I will explain how to use Mockups, Landing Pages, Surveys, Logo Generators, Prototypes to build a Minimum Viable Product (MVP) and validate customers for a potential SaaS business using AI.]]></description><link>https://sebastianroy.de/12-steps-to-setup-an-ai-saas-business/</link><guid isPermaLink="false">62180cd7b3d690afd4cda228</guid><category><![CDATA[Building an AI SaaS]]></category><dc:creator><![CDATA[Sebastian Roy]]></dc:creator><pubDate>Thu, 24 Feb 2022 22:55:19 GMT</pubDate><media:content url="https://sebastianroy.de/content/images/2022/02/IdeaToMVPnontransparent-1-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://sebastianroy.de/content/images/2022/02/IdeaToMVPnontransparent-1-1.png" alt="12 Steps to Setup an AI SaaS Business"><p><em>Update 05. December 2022: Added how to edit DNS records for custom email domains and updated labeling frameworks for object detection.</em><br><em>TODO: Elaborate on Step 11 and 12.</em></p><p>Ideas are cheap, execution is key - we know that. We also know that we should not build out all kinds of features that nobody wants, but launch a Minimum-Viable-Product (MVP) or better a Minimum-Sellable-Product (MSP) and get it into the hands of users as soon as possible. But at the idea stage, are you sure you <strong>can deliver on your promise</strong>? When should you build a landing page for customer validation? Here is my experience from the past few weeks.</p><p>In this series I will document my <strong>12-step process</strong> as a solo low budget technical SaaS founder from idea to MVP using AI. But what if you are non-technical founder ? Even then you will be able to get to step 10 of 12 and gain credibility with investors, potential technical co-founders and save time when working with freelance developers. I will introduce some tools that I actually use like <a href="https://www.mailerlite.com/invite/21b7b75330a16">Mailerlite</a> (Affiliate) for landing pages or <a href="https://looka.com">Looka</a> to create a logo with AI. The job of a founder is to stich these existing services together in an elegant way such that a product or service appears on the other end. Even as a technical founder, don&#x2019;t waste your time reinventing the wheel.</p><p>It is of course possible to build anything from scratch using open source tools, you can host a website on your laptop and forward ports of your router if you are on a <strong>no budget</strong>, but you get distracted with maintenance and you <strong>loose mental energy</strong> from your core product.</p><p>The 12-step process will cover</p><ol><li>Defining the Target Audience</li><li>Estimate the Market Size</li><li>Product Idea</li><li>Estimate Pricing</li><li>A Proof-of-Concept</li><li>Branding: Name, Logo, Color-Scheme</li><li>Customer Validation with Landing Pages, Email-Lists and Surveys</li><li>Feature Priorisation</li><li>Mockups and Prototype</li><li>User Testing</li><li>Minimal Viable Product</li><li>Beta Program on iOS</li></ol><h2 id="1-defining-the-target-audience">1. Defining the Target Audience</h2><p>A good business <strong>solves a pain point of a large but specific group of people</strong>. Low hanging fruits, problems that basically everyones has, are already harvested, have large barriers to entry or are too competitive. How do you compete if someone is able to sell a product at cost or even at a loss? Therefore it is fine to address issues only a niche group of people has or you had personally in your business or private life.</p><p>By defining your target audience you learn where to find them. Who do they follow on Twitter, which groups do they join on WhatsApp, which conferences do they visit? If you are your target audience, then observe yourself, where do you spend your time on- and offline?</p><p>It further helps to distinguish between your product not being sophisticated enough for a customer and someone who simply doesn&#x2019;t has any demand for your product at this point in time. Try to sell drugs to someone who isn&#x2019;t into drugs, and no matter the quality of your product you will never convince them to buy anything. The same is true for someone who has already bought enough drugs or is currently out of money, no matter what you do, they will not be your customer, maybe later. You will be surprised how many businesses fail to understand this. We will validate assumptions made here in step 7 (don&#x2019;t do drugs tough, they are only useful to illustrate the point).</p><p>Let&#x2019;s pick <strong>people visiting the zoo</strong> with a smartphone as an example.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sebastianroy.de/content/images/2022/02/Okonjima_Lioness.jpg" class="kg-image" alt="12 Steps to Setup an AI SaaS Business" loading="lazy"><figcaption><a href="https://commons.wikimedia.org/wiki/File:Okonjima_Lioness.jpg">Falense</a>, <a href="Falense,%20CC%20BY-SA%203.0%20%3Chttp://creativecommons.org/licenses/by-sa/3.0/%3E,%20via%20Wikimedia%20Commons">CC BY-SA 3.0</a>, via Wikimedia Commons</figcaption></figure><h2 id="2-estimate-the-market-size">2. Estimate the Market Size</h2><p>Starting out it is good to niche down, but here the market size is already determined and this decides if an idea is fundable. No matter how fancy the technology is, if the market is too small you might need to go without funding, also called bootstrapping or worse there is no business. I have worked at an angle funded startup that went out of business for that reason.</p><p>There are <a href="https://en.wikipedia.org/wiki/List_of_zoos_in_Germany">318 zoos in Germany</a>. To sell an app to each german zoo (which is already an illusion) for 10 $ a month, thats 3180 &#x20AC; a month or 38 160 &#x20AC; a year. You can&#x2019;t even hire one engineer from this. BUT if you can run it as a side business, it might be viable.</p><p>Can you sell to each zoo visitor? Different story, <a href="https://fzs.org/en/about-us/organization/fzs-zoo-frankfurt/">800 000 people visit the zoo in Frankfurt each year</a>, well at least when there is no pandemic. So let&#x2019;s say we target 10% of the zoos (sounds low? They won&#x2019;t even respond to your email, I tried.) and 10% of the visitors (because the others don&#x2019;t have a smartphone, were counted twice, are not our target demographic, poor students or simply not interested), that makes 31 zoos with 80 000 convertible visitors each (not every zoo is as large as Frankfurt, make a note, and refine later), so 2.4 million potential customers, if we can sell all of them something for 10 &#x20AC; a month, thats a whooping 288 million &#x20AC; a year. Here we have a business. This is our <strong>Total Addressable Market (TAM)</strong>. If we multiply <a href="https://abovethecrowd.com/2011/05/24/all-revenue-is-not-created-equal-the-keys-to-the-10x-revenue-club/">the estimated yearly revenue by 10</a>, we get a market value of 2.8 billion &#x20AC;. Thats more than a billion and therefore a unicorn (= an investors dream).</p><p>The numbers don&#x2019;t need to be accurate, the goal is to distinguish between business and no business using suitable orders of magnitude. Just don&#x2019;t make the mistake to hire a team of ten people to then figure out later your market can only support one person part time or pitch a startup to investors that you need to bootstrap anyway.</p><p>Side note: The decision to accept venture capital directly leads to the need for an <strong>exit</strong>, so to sell the company to a larger company or go public via an IPO or recently with SPACs. Thats where investors and the founders get their payday.</p><p>Not everyone wants to sell their company though. Mark Zuckerberg didn&#x2019;t sell Facebook to Yahoo for 1 billion $ in 2006, because he said it might be the only idea he will ever have. Brian Acton now works for and donated a billion $ to Signal after selling WhatsApp for 19 billion $ to Facebook. The founder of <a href="https://www.theverge.com/2019/9/8/20855201/wunderlist-buy-back-offer-microsoft-christian-reber">Wonderlist Christian Reber wanted to buy back his company after selling it to Microsoft for 250 million $</a> - who in turn shut it down. Now he is again building his second ToDo-App.</p><h2 id="3-product-idea">3. Product Idea</h2><p>Let&#x2019;s start with a machine learning app to identify large cats (<em>panthera</em>) in the zoo, so it should detect if an animal is a lion, a tiger or a leopard. Does this sound more like a feature or like a long term vision? I think it is good to have both. To just identify lions may be just a <strong>feature</strong>, but it will be the feature we will focus on till MVP stage.</p><p>The long term <strong>vision</strong> might be an Augmented Reality (AR) based zoo guide. Identifying lions will then only be a small, but killer feature. At this stage focus on the feature, but keep the vision in the back of you head, because that will be the business in the end.</p><h2 id="4-estimate-pricing">4. Estimate Pricing</h2><p>Let&#x2019;s assume people value their time around 20 &#x20AC; an hour (got that number from a guy who works in neuroscience). A solution should deliver 10x more value than the customer has to pay. So if someone saves 2 hours a month using the app by not going to the library to identify how a lion looks like or employing a zoo guide, thats 40 &#x20AC;. A tenth of this would be 4 &#x20AC; a month.</p><p>We all undervalue our work, so always try to double the price once in a while, see if something bad happens. When charging too little, it is not possible to deploy enough resources to build a product that satisfies the customers. There is also more than productivity to pricing, like brand, convenience, competition and design.</p><h2 id="5-a-proof-of-concept">5. A Proof-of-Concept</h2><p>Ok, we have an idea, and it is a suitable business, but can we actually deliver? Of course you can skip this and go to customer validation right away. But personally I like to make sure I actually can build the product first. So even with any help available can you actually train and deploy an AI to identify images of lions?</p><p>First we need to label the images that we can then use to train an algorithm width. For labelling <a href="https://labelstud.io/">Label Studio</a> provides a great suite and is free as well.</p><p>Then we need to train an algorithm. Since we will most likely have only few images it is best to use pre-trained models like YOLO. &#xA0;<a href="https://docs.deepstack.cc/object-detection/">DeepStack</a> provides a Docker container to use labeled images for training an algorithm and then launch a server to make predictions.<br>To classify images <a href="https://github.com/alankbi/detecto">Detecto</a> is a good start as well. It is a python framework based on PyTorch, a widely used machine learning framework. The most important and labor intensive part is labelling the images.</p><p>Second you can try to find machine learning services like <a href="https://www.v7labs.com">V7Labs</a>. They are good, but cost around 150 &#x20AC; per month. They have a 14 day free trial though and good customer support. V7 is especially good for labelling (also in groups, so it can be outsourced), training and directly offering an API for your users to classify images. This is the fastest way to get going.</p><h2 id="6-branding-name-logo-domain-and-email">6. Branding: Name, Logo, Domain and Email</h2><p>Now it is time to pick a name. There are awesome AI powered services like <a href="https://namelix.com">Namelix</a> to create name ideas as well as <a href="https://looka.com">Looka</a> to create logo ideas (free to create, 80 $ for commercial use). From the logo generator I also try to get a color scheme to use with the landing page for a recognisable branding.</p><figure class="kg-card kg-embed-card"><iframe width="1236" height="375" style="border:0;" scrolling="no" src="https://screencast-o-matic.com/player/crll3VV2U8v?title=0&amp;controls=0&amp;a=1&amp;bg=transparent&amp;overlays=1" allowfullscreen="true"></iframe></figure><p>Then use the name to get a suitable domain on something like <a href="https://united-domains.com">united-domains.com</a>. .com or .org domains might be hard to get, but there are also new domains like .app or .io that are usually not that crowded (59 $ / year).</p><p>For email boxes like name@domain.app I use the independent swiss provider <a href="https://www.migadu.com">migadu.com</a> (19 $ / year), they also charge based on number of emails send and not artificially limited based on number of domains registered or number of email boxes.</p><p>To do be able to send and receive emails from and @yourdomain.com address the records for the Domain-Name-System (DNS) need to be specified. It is responsible for converting an IP 97.120.21.123 into yourdomain.com. For the email service it requires copying entries from Migadu Setup Instruction page</p><figure class="kg-card kg-image-card"><img src="https://res.cloudinary.com/sebastian-roy2/image/upload/v1670195248/12%20Step%20Process%20From%20Idea%20To%20MVP/MigaduDNSSettings_bftvem.png" class="kg-image" alt="12 Steps to Setup an AI SaaS Business" loading="lazy"></figure><p>to the DNS Entry of United Domains.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://res.cloudinary.com/sebastian-roy2/image/upload/v1670195763/12%20Step%20Process%20From%20Idea%20To%20MVP/SelectDNSUnitedDomains_gorih6.png" class="kg-image" alt="12 Steps to Setup an AI SaaS Business" loading="lazy"><figcaption>Select DNS in the settings page of your domain provider like United Domains.</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://res.cloudinary.com/sebastian-roy2/image/upload/v1670195894/12%20Step%20Process%20From%20Idea%20To%20MVP/UnitedDomainsDNSSettings_zoufpa.png" class="kg-image" alt="12 Steps to Setup an AI SaaS Business" loading="lazy"><figcaption>United Domains DNS Settings Page. This is where the entries from Migadu Setup Instruction need to be entered.</figcaption></figure><p>This process takes some time, first by setting it up but also till changes take effect, the DNS system is slow and it takes time till changes a synced over the internet. In a similar way you need to setup the DNS entries from your website, which talk about next.</p><h2 id="7-customer-validation-landing-pages-and-surveys">7. Customer Validation: Landing pages and Surveys</h2><p>Once we know it can be done and created our brand, it is time to test the market. A good way is to create a landing page with a concise description of our product and make user sign up for a mailing list to get early access. I prefer <a href="https://www.mailerlite.com/invite/21b7b75330a16">Mailerlite</a> (Affiliate, free, 90 $ if you want your own domain) as an all in one solution with landing page, email list and analytics, but <a href="https://webflow.com">Webflow</a> and <a href="https://unbounce.com/">Unbounce.com</a> (90 $/month) and <a href="http://eepurl.com/hPFQ0r">Mailchimp</a> (Affiliate) are also common options.</p><p>Like in step 6. it requires copying DNS settings from Mailerlite (Account settings -&gt; Domains -&gt; View records) or Webflow into the DNS entries page of United Domains.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://res.cloudinary.com/sebastian-roy2/image/upload/v1670198921/12%20Step%20Process%20From%20Idea%20To%20MVP/MailerLiteDNS_i3sun8.png" class="kg-image" alt="12 Steps to Setup an AI SaaS Business" loading="lazy"><figcaption>DNS settings page in Mailerlite.</figcaption></figure><p>Once I created a nice landing page with a concise value proposition, I try to post the link in a group where I know there are interested customers, like a WhatsApp group.</p><p>When I posted the link in a WhatsApp group with 194 members, I got 28 visits and 8 of them subscribed to the newsletter. Thats a conversion rate of 28 %, of course we have small numbers here, but that is at least a signal that demand exists.</p><p>This step is not always easy, be sensible where posting links is appropriate and not spammy. We can also do targeted paid advertising on Facebook, Twitter and LinkedIn, a next step for me to try out. It is always important to target customers that might actually be interested, otherwise we get a lot of visits to our site with no signups and that ruins the conversion rate statistic on Mailerlite.</p><p>To learn more about your potential customers, like how much they would be willing to pay for your product, you can ask them to fill out a <strong>survey</strong>. Now as we all know surveys like Google Forms are an option but are often very annoying, it should be as convenient as possible to fill them out. For example you can ask different questions based on age group or role in a company or ask them if they are willing to pay 50 &#x20AC; a month and only if they answer &#x201E;no&#x201C;, you could again ask if they would be wiling to pay 25 &#x20AC; a month. This can be done by branching questions and only ask certain questions based on already given answers. The #1 tool to do this is <a href="https://www.typeform.com">Typeform.com</a>, but their plan with 21 &#x20AC; per month is also very expensive. Fortunately there is a nice and for now free alternative called <a href="https://tripetto.app">Tripetto.app</a>. Check them out, you won&#x2019;t be disappointed.</p><figure class="kg-card kg-image-card"><img src="https://sebastianroy.de/content/images/2022/02/TripettoImage.png" class="kg-image" alt="12 Steps to Setup an AI SaaS Business" loading="lazy"></figure><p>Example of how a branched questionnaire in Tripetto might look like.</p><h2 id="8-feature-priorisation">8. Feature Priorisation</h2><p>While getting excited about the first subscribers in this phase your mind will probably generate a ton of ideas what your app should be able to do. It could also detect apes or plants, maybe it should display more details about an animal and maybe a small video. Those ideas are helpful, if dealt with responsibly. Most of them should not make it into the MVP, but are ideas for further development later and maybe even much later.</p><p>But to prepare for this phase and maybe include one killer feature into the MVP, it is helpful to sort features by priority with the help of your new subscribers. You can even let them make suggestions for new features themselves. Services that can help with this are <a href="https://www.productboard.com/pricing/">Productboard</a>, <a href="https://www.upvoty.com/features/">Upvoty</a>, <a href="https://hellonext.co/alternative/upvoty">Hellonext</a> and <a href="[https://noorahq.com/compare/canny-alternative/]">Noorahq</a>, which do more or less the same, so pick the one you like most. This will help you develop clarity about what your app should be able to do, which will be the guide for the next step, sketching out a prototype.</p><h2 id="9-mockup-and-prototype">9. Mockup and Prototype</h2><p>Now, we have the idea, we know it is potentially a business, the pain point we want to address and the most important features. Now the solution needs to be translated into text fields, images, boxes and buttons. As a mockup tool I use <a href="proto.io">Proto.io</a> (25 &#x20AC; / month), but <a href="https://www.adobe.com/de/products/xd.html">Adobe XD</a> (11,89 &#x20AC; / month), <a href="https://www.sketch.com/">Sketch</a> (9 $ / month) and <a href="https://www.figma.com">Figma.com</a> (free for three files) are other common tools. If you are not comfortable with technical tools, just use pen and paper and draw out the flow of your app.</p><figure class="kg-card kg-embed-card"><iframe width="1280" height="375" style="border:0;" scrolling="no" src="https://screencast-o-matic.com/player/crllrdV2UNo?title=0&amp;controls=0&amp;a=1&amp;bg=transparent&amp;overlays=1" allowfullscreen="true"></iframe></figure><p>Before you implement the app, you will need to hire an UX Designer who creates a high fidelity version of your prototype, but too get your idea from your head into the real world and to communicate it to users, investors and a potential co-founder being able to draft a prototype is an important skill for any SaaS founder.</p><h2 id="10-user-testing">10. User Testing</h2><p>While landing pages and feature priorisation have to be done with your target audience, you don&#x2019;t need them for user testing. Anyone can test your product. Here it is about, does a normal human use your product in a way you would expect them to do. Do they search for functions where you have hidden them? Just give them a task they should complete and observe how they interact with your app.</p><p>For more professional user testing there are also companies like <a href="https://www.loop11.com/pricing/">Loop11</a> and <a href="https://www.userbrain.com/en/pricing/">Userbrain.com</a> to help with this, but they seem not worth it at early stages. But at least now you know they exist, use them if needed.</p><h2 id="11-minimum-viable-product-mvp-with-in-app-customer-feedback">11. Minimum Viable Product (MVP) with in App Customer Feedback</h2><p>After 10 steps we are finally ready to get our hands dirty. We pretty much determined what we want to build, now it&#x2019;s time to build it. You can do or learn this yourself if you want. I subscribed to <a href="https://www.raywenderlich.com">raywenderlich.com</a> (485 &#x20AC; incl VAT, but Black Friday discount available) for mobile frontend and <a href="https://www.usegolang.com">usegolang.com</a> (220 &#x20AC; life time access, but Black Friday discount available) for backend development, but there are also cheaper courses on Skillshare, Udemy and Youtube especially to just get started. If you need help or are stuck with an error, large companies have senior developers in different departments, but you still can get similar mentoring from experts in their field on <a href="https://www.codementor.io">codementor.io</a> (20 $ per 15 minutes).</p><p>But learning to code while building a startup might not be possible. It is always good to engage a technical <strong>co-founder</strong> and this stage is perfect for this, since now we actually know what we want to build. If you contact a developer at an earlier stage they feel like you had an idea but want them to do all of the work. Developers develop what you ask them according to your specification. But it is not their fault if the specification is wrong in the first place.</p><p>If the app does not demand using specific technology like 3D, Apple Pencil, Augmented Reality or new UI elements it is good to use <strong>cross platform languages</strong> like Flutter, React Native or NativeScript Vue. This enables you to use one codebase for iOS and Android.</p><p>If you need core technologies it is better to develop the iOS version in SwiftUI and Android version in Kotlin. This way you can leverage the strength of each platform. I love NativeScript, it does actually give you access to native APIs, but then you add a new level of complexity, because you need to understand iOS and Android as well as NativeScript and you don&#x2019;t get as many prebuilt libraries.</p><h2 id="12-beta-program-with-user-feedback-and-user-analytics">12. Beta Program with User Feedback and User Analytics</h2><p>You can develop the MVP in the last step in the iOS Simulator for free, but if you want to test on device you need access to the Apple Developer Program, which is 99 $ per year. This also enables you to use Inflight for beta user testing. Now we actually need target customers unlike in step 10.</p><p>Since we collected a few email addresses using the landing page from step 7, this is the time we can leverage them. First contact individual subscribers directly, ask them to test the product and get feedback. Then reach out to groups of subscribers and so on.</p><h1 id="the-bottom-line">The Bottom Line</h1><p>So how much do we have to spend to get here?</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Service</th>
<th>Cost</th>
<th>Trial</th>
<th>Essentials</th>
<th>Full-Speed ahead</th>
</tr>
</thead>
<tbody>
<tr>
<td>V7 Machine Learning</td>
<td>1800 EUR / year</td>
<td>yes</td>
<td></td>
<td>x</td>
</tr>
<tr>
<td>Name and Logo</td>
<td>80 EUR one time</td>
<td>yes, creating is free, using it costs a fee</td>
<td></td>
<td>x</td>
</tr>
<tr>
<td>Domain and E-Mail</td>
<td>30 EUR / Year</td>
<td>no</td>
<td>x</td>
<td>x</td>
</tr>
<tr>
<td>Customer Validation with Landing Pages</td>
<td>90 EUR / Year</td>
<td>yes, 14-day</td>
<td>x</td>
<td>x</td>
</tr>
<tr>
<td>Prototype</td>
<td>300 EUR / Year</td>
<td>yes, 14-day</td>
<td></td>
<td>x</td>
</tr>
<tr>
<td>Learning Ressources and Mentoring</td>
<td>700 EUR / year</td>
<td>no, on demand</td>
<td></td>
<td>x</td>
</tr>
<tr>
<td>iOS Developer Certificate</td>
<td>99 USD / year</td>
<td>no</td>
<td>x</td>
<td>x</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Total</td>
<td></td>
<td></td>
<td>219 EUR</td>
<td>3085 EUR</td>
</tr>
<tr>
<td><strong>Cost per month</strong></td>
<td></td>
<td></td>
<td><strong>18,25 EUR</strong></td>
<td><strong>275 EUR</strong></td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>So for 3085 &#x20AC; we can develop an MVP that leverages the power of machine learning in about a month, sure thats not cheap but definitely also not millions either. But you can also go for just the essentials with 275 &#x20AC; per year. So you are either convinced that your idea is that good to put your money and effort on the line - or you don&#x2019;t and will never know. But let me tell you seeing other people succeed with ideas you never acted on is a painful experience.</p><p>V7 is the largest expense, you can avoid them by using PyTorch or Detecto or using their trial and then wait until you have actual customers waiting in line, but ultimately if your are not a computer vision expert I would not waste time with PyTorch and just use V7s service (while I have some affiliate links in this article, there is unfortunately none for them. V7 was just the best option available to develop a machine learning Proof-of-Concept in 3-4 days without any code or advanced machine learning knowledge.)</p><h1 id="conclusion">Conclusion</h1><p>So there we have it, the hopefully complete step by step process including budget to develop an AI based MVP of a mobile app starting with just an idea. Which tools do you prefere to use? Any suggestions comment below.</p><p>This is the first article on my blog sebastianroy.de and <a href="https://medium.com/@sebastianroy">Medium</a>, so as as sign of interest I would be happy if you subscribe to my newsletter, share it with friends who might benefit from it and give me some feedback of the three items you liked and three items you disagree with. Let me know if this lengthy but conclusive style of article is something you like to see more of.</p><blockquote>Sebastian Roy is a biophysicist from Frankfurt who co-founded the iGEM Team Frankfurt, the NGO Nepal Health e.V. and currently developing <a href="https://gelly.bio">gelly.bio</a>, an app to automatically annotate gel electrophoresis images.</blockquote><!--kg-card-begin: html--><div id="hyvor-talk-view"></div>
<script type="text/javascript">
    var HYVOR_TALK_WEBSITE = 8174;
    var HYVOR_TALK_CONFIG = {
        url: false,
        id: false
    };
</script>
<script async type="text/javascript" src="//talk.hyvor.com/web-api/embed.js"></script><!--kg-card-end: html-->]]></content:encoded></item></channel></rss>