I've been using Postman for a number of years now, basically since I first got into web development. It is indeed a fantastic tool, but one part I hate about it is the proprietary nature of the way it stores its data. In the world of development, there is always a push to keep as many components as possible with the repository. It helps with tracking who did what, and keeps a team on the same page. Postman stores its data differently. Instead of keeping it in files that can be stored with a repo, it stores it on a backend service. Now this isn't necessarily a bad thing as it enables some pretty neat collaboration & automation. BUT if all you're after is an easy way to test your APIs, then it can be overkill, and furthermore frustrating as you are locked into their system.

Enter the VS Code Rest Client plugin. It's a pretty straightforward plugin on the service that has a decent feature set the more you dig into it. Ultimately it was what I was after. An easy way to write API tests with the ability to store them with my repos, quickly switch environments, and use variables that I could define myself or chain together between requests. Now all I had to do is find a way to migrate all the work Ive done in Postman over the years into this new plugin, which defines its test in a .http file as shown below.

get http://localhost:3000

###

get http://localhost:3000/294688564132708868

###

post http://localhost:3000
Content-Type: application/json

{
  "another": "test"
}

###

put http://localhost:3000/294688564132708868
Content-Type: application/json

{
  "hello": "chicago!"
}

###
delete http://localhost:3000/294688564132708868

I realized if I exported a Postman collection, it was just a JSON file with the definitions of the requests themselves in there like so.

{
	"info": {
		"_postman_id": "9cf8e206-ea07-4ee9-aae8-19d67537a1d7",
		"name": "Test Collection",
		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
	},
	"item": [
		{
			"name": "Fetch Items",
			"request": {
				"method": "GET",
				"header": [],
				"url": {
					"raw": "localhost:3000",
					"host": [
						"localhost"
					],
					"port": "3000"
				}
			},
			"response": []
		},
		{
			"name": "Get Item",
			"request": {
				"method": "GET",
				"header": [],
				"url": {
					"raw": "localhost:3000/294688583753662980",
					"host": [
						"localhost"
					],
					"port": "3000",
					"path": [
						"294688583753662980"
					]
				}
			},
			"response": []
		},
		{
			"name": "Create Item",
			"request": {
				"method": "POST",
				"header": [],
				"body": {
					"mode": "raw",
					"raw": "{\r\n    \"testKey\": \"testVal\"\r\n}",
					"options": {
						"raw": {
							"language": "json"
						}
					}
				},
				"url": {
					"raw": "localhost:3000",
					"host": [
						"localhost"
					],
					"port": "3000"
				}
			},
			"response": []
		},
		{
			"name": "Update Item",
			"request": {
				"method": "PUT",
				"header": [],
				"body": {
					"mode": "raw",
					"raw": "{\r\n    \"anotherTest\": \"anotherVal\"\r\n}",
					"options": {
						"raw": {
							"language": "json"
						}
					}
				},
				"url": {
					"raw": "localhost:3000/294691575403905538",
					"host": [
						"localhost"
					],
					"port": "3000",
					"path": [
						"294691575403905538"
					]
				}
			},
			"response": []
		},
		{
			"name": "Delete Item",
			"request": {
				"method": "DELETE",
				"header": [],
				"url": {
					"raw": "localhost:3000/294691575403905538",
					"host": [
						"localhost"
					],
					"port": "3000",
					"path": [
						"294691575403905538"
					]
				}
			},
			"response": []
		}
	]
}

As of the writing of this article, I'm practicing with Golang and I was looking for a way to challenge myself with a real world app that I could use. These seem to be the projects that resonate with my learning style the best.

  • Define a problem
  • Break down the problem
  • Solve it with some new fancy utility I can write
  • Win!

So one day, while live streaming on my YouTube channel, I decided to start building. I broke down the workflow of the app to do the following;

  1. I needed a way to read the contents of a file (the exported Postman collection)
  2. Once read, I needed a way to convert the JSON into a format that Go would understand
  3. Then I needed to use that format to create what would essentially be a formatted string.
  4. And finally, a way to save the string to a file on the disk.

I ended up writing everything in a single main.go file, which I break down below. I added numbers in the comments for each respective code block to line up with my outline above.

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	// 1.a. Using command line arguments, I'm able to specify the path to the file I want to import.
	jsonFile, err := os.Open(os.Args[1])
	if err != nil {
		fmt.Println(err)
	}

	// 1.b. Read the contents of the file into a byte array
	byteValue, _ := ioutil.ReadAll(jsonFile)
	defer jsonFile.Close()

	// 2. Create an empty variables and convert the JSON into a Go type that I can work with
	var collection PostmanCollection
	json.Unmarshal(byteValue, &collection)

	// 3.a. Create an empty string to hold the contents of the final file to be created.
	outString := ""
	// 3.b. Loop over the Items in the collection and append certain pieces to 'outString'
	for idx, item := range collection.Items {
		outString += item.Request.Method + " " + item.Request.Url.Raw + "\n"
		for _, header := range item.Request.Headers {
			outString += header.Key + ": " + header.Value + "\n"
		}
		if item.Request.Body.Raw != "" {
			outString += "\n" + item.Request.Body.Raw + "\n"
		}

		if idx+1 != len(collection.Items) {
			outString += "\n###\n\n"
		}
	}

	// 4.a. Create a file on disk to hold the contents of the 'outString'
	f, err := os.Create(collection.Info.Name + ".http")
	if err != nil {
		fmt.Println(err)
		return
	}

	// 4.b. Write 'outString' to that file
	_, err = f.WriteString(outString)
	if err != nil {
		fmt.Println(err)
		return
	}
	f.Close()
}

// Below details out the types used to hold the data. Note the annotations to the right
//    of each field which instruct Go how to read the raw data into the repsective types.
type PostmanCollection struct {
	Info  Info   `json:"info"`
	Items []Item `json:"item"`
}

type Info struct {
	PostmanId string `json:"_postman_id"`
	Name      string `json:"name"`
	Schema    string `json:"schema"`
}

type Item struct {
	Name    string  `json:"name"`
	Request Request `json:"request"`
}

type Request struct {
	Method  string   `json:"method"`
	Headers []Header `json:"header"`
	Url     Url      `json:"url"`
	Body    Body     `json:"body"`
}

type Header struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

type Url struct {
	Raw string `json:"raw"`
}

type Body struct {
	Raw string `json:"raw"`
}

After an hour of coding, I came away with a very basic MVP. I admit, at this stage it still needs alot of work, but it does exactly what I needed it to. Now I need to feed it more collections in order to iron out any bugs!