Part II: Beyond JSON APIs
Uploaded that new picture on your Social Media ? Downloaded that pdf file from your email? Sent a voice note to your friend or maybe a video? Ever filled up a survey form online? Been there done that haven't you?
Notice how you are sending or receiving media over the internet? Its not just text, but files, images, audio, videos, forms and what not! If you see, in a way, aren't you sending media content to a server somewhere via an API. And its the same other way round too, you are receiving media from the server via APIs.
But so far, the APIs we developed have only dealt with text, that too JSON based requests and responses. How do we then send/receive multimedia data? Seems fancy, huh!?
Not really! In fact its really easy to achieve. And we are going to do just that in this post....
Multipart Data
Now of course, all APIs can't always be JSON or text-based. You may want to send files, images, and other content. That's why REST APIs allow a data type called "multipart"
What's multipart? Break it down: "Multi" and "Part". Multi means many and part implies data. So in short, it means that you can send multiple types of data in one request.
A simple example is when you are creating a profile on a site, you send in your details like name, age, bio etc in text format and upload your picture in file or image format. All this data is sent as a single API request to the server as multipart data and the server handles the rest. Let's get into the technicalities now.
Content-type
REST APIs always send some metadata about the request and response in the "Headers". One of the most common headers is "Content-Type". As the name suggests, it's going to tell you the type of data sent in the request or received in the response.
If you're sending json, the value is "application/json" . For plain text, its "text/plain". Ands, for multipart data, the value of the header starts with "multipart/". Of course there can be multiple subtypes within the multipart content type. But the one we are focusing on is the "multipart/form-data" subtype. Mainly because this is the content type that allows us to send files in the request body along with other text data. .
Uploading Files
Creating an endpoint to upload a file is a breezy task using the echo framework. First, we need to define a function which will act as the handler for the route.
func UploadFile(c echo.Context)error{
}
Next, let's look at the steps this function will perform
- Get the file from the request body
file, err := c.FormFile("file")
- Open the file and read the contents
src, err := file.Open()
defer src.Close()
- Save to a destination file path
//define the destination path: name of the file
dstPath := file.Filename
//create the destination path
dst, err := os.Create(dstPath)
defer dst.Close()
// Copy the contents from source
if _, err = io.Copy(dst, src)
We need and example to illustrate upload of files and as usual we are going to use our petstore application! In this, we need an API that will upload a pet's image for a given pet identifier. This is what you'll need to do:
- Accept image file from the API route "/pet/upload" along with the pet identifier
- Save the image in a directory called as "images"
- Additionally, we save the file's location in database table "pet" under the imageURL column
Before, we begin, we need a directory to store the images:
mkdir images
This should create a directory call images in your current working directory.
Now, let's go through each step, one at a time:
Accept an Image file and Store in "images" folder
We'll first create a handler function for the endpoint route "/pet/upload" under the "/internal/petstore/rest/pet.go". Let's call the handler function "UploadPetImage"
func UploadPetImage(c echo.Context) error {}
Now, get the file's data from the form value and read the file:
//Get file from form value
file, err := c.FormFile("file")
if err != nil {
// Handle the error
}
// Read Source file
src, err := file.Open()
if err != nil {
// Handle the error
}
defer src.Close()
To store the file under "images" folder:
// Get destination path
var imageDir string = "images"
dstPath := filepath.Join(imageDir, file.Filename)
dst, err := os.Create(dstPath)
if err != nil {
// Handle error
}
defer dst.Close()
// Copy source contents
if _, err = io.Copy(dst, src); err != nil {
// Handle error
}
Lastly, let's pass a success message
return c.JSON(http.StatusCreated, "File uploaded successfully")
Now notice, how we have left out the error handling? The error handling will be done in the same way as we had illustrated in the previous post on handling API error. Something like this;
if err!=nil {
return echo.NewHTTPError(<STATUS CODE>, <ERROR MESSAGE>)
}
Saving the Image URL to the database
The last thing we need to do is to save the image location in the database. Our table is going to be the "pet" table and we'll be updating the column called "imageURL" for an existing entry.
Now to do this, we'll need the identifier for the pet entry in the database table. We'll accept this id as a form value from the API itself.
id, err := strconv.Atoi(c.FormValue("id"))
Following this, we need to make a few tweaks to our code. Let's start from ground zero.
- Update the models
The first place you'll head over to is the "models" directory under "pkg" and open the pet.go file. Here, we have the model "Pet" and its interface "PetService" defined for the pet object. We'll add another function under the interface called "UpdateImageURL". So your interface is going to look like :
//PetService interface for Pet model
type PetService interface {
CreatePet() error
GetPetByCategory(categoryID int) ([]*Pet, error)
GetPet(id int) (*Pet, error)
DeletePet(id int) error
UpdateImageURL() error
}
Next, we need to map the id that we have obtained from the form data to "ID" field under the Pet model. It's simple. Just like we added json annotations to the fields, we can also add form annotations:
type Pet struct {
ID int `json:"id" form:"id"`
Name string `json:"name"`
......
}
- Create a Repo Function
The next layer we'll target is the repo , or the database layer. (If you're confused about this, we'd recommend that you read the Microservices series).
Navigate to "internal/petstore/repo/pet.go". Here, we'll fill in the actual code to update the image url.
func (p *Pet) UpdateImageURL() error {
stmt, err := client.DbClient.Prepare("UPDATE pet SET image_url=$1 WHERE id=$2;")
if err != nil {
return err
}
//closing the statement to prevent memory leaks
defer stmt.Close()
_, err = stmt.Exec(p.ImageURL, p.ID)
if err != nil {
return err
}
return nil
}
- Create the Service Function
Service layer comes next! Here's how it looks
var ps db.PetService
func UpdateImageURL(p *db.Pet) error {
ps = p
err = ps.UpdateImageURL()
if err != nil {
return err
}
return nil
}
- The Rest layer
Finally, we can call the service layer function in our UploadPetImage handler we created earlier. The entire function will then look like:
- Register the route
The last step is to register the endpoint "/pet/upload" as a POST route. We'll add this to "/cmd/petstore/main.go"
var (
router = echo.New()
)
router.POST("/pet/upload", rest.UploadPetImage)
Test it all out
Time to run the API. Start your application by running:
go run cmd/petstore/main.go
In Postman, select the POST method and under the "Body" tab, select "form-data".
Under the key and value headers, add the id of the pet. Add another key called "file" and select the type to be "File" from the drop down as shown. Upload a file of your choice and hit send!
Just to be sure, check if the image has been saved in the images directory.
There it is! The file's uploaded to your system!
In Summary
What we showed you was just a simple file upload where you can upload a single file to the server. In reality, we can do a lot of things with files like uploading multiple files to the server or downloading files from the server or even serving static files kept on the server. You can experiment with all of these! We're listing down a few link for your reference to give you a head start....