Cover Page

Backend Page

Go with Echo

handlers.go

Change to your chatterd folder and edit handlers.go:

server$ cd ~/reactive/chatterd
server$ vi handlers.go

Add the following libraries to the import block at the top of the file:

import (
    //...
    "io"
    "os"
    "path"
    "sync"
    //...
)

Now add two new properties to the Chatt struct in handlers.go:

type Chatt struct {
    // . . .
    ImageUrl  *string    `json:"imageUrl"`
    VideoUrl  *string    `json:"videoUrl"`
}

Since both string optional, we set the type to *string, which can take a null pointer, nil in Go.

Next add the postimages() function along with the saveFormFile() and copyZeroAlloc() helper functions. We also specify our designated media directory to store image/video files.

const MEDIA_ROOT = "/home/ubuntu/reactive/chatterd/media/"

func postimages(c echo.Context) error {
	var err error
	var chatt Chatt

	chatt.Username = string(c.FormValue("username"))
	chatt.Message = string(c.FormValue("message"))
	chatt.ImageUrl, err = saveFormFile(c, "image", chatt.Username, ".jpeg")
	if err == nil {
		chatt.VideoUrl, err = saveFormFile(c, "video", chatt.Username, ".mp4")
	}
	if err != nil {
		return logClientErr(c, http.StatusUnprocessableEntity, err)
	}

	_, err = chatterDB.Exec(background, `INSERT INTO chatts (username, message, id, imageurl, videourl) VALUES ($1, $2, gen_random_uuid(), $3, $4)`, chatt.Username, chatt.Message, chatt.ImageUrl, chatt.VideoUrl)
	if err != nil {
		return logClientErr(c, http.StatusBadRequest, err)
	}

	logOk(c)
	return c.JSON(http.StatusOK, struct{}{})
}

func saveFormFile(c echo.Context, media string, username string, ext string) (*string, error) {
	content, err := c.FormFile(media)
	if err != nil { return nil, nil } // not an error, media not posted

	securename := path.Base(path.Clean(username))
	if securename[0] == '.' || securename == "/" {
		return nil, errors.New("invalid username")
	}
	filename := securename + "-" + strconv.FormatInt(time.Now().Unix(), 10) + ext

	src, err := content.Open()
	if err != nil { return nil, err }
	defer src.Close()

	dst, err := os.Create(MEDIA_ROOT + filename)
	if err != nil { return nil, err }
	defer dst.Close()

	if _, err = copyZeroAlloc(dst, src); err != nil {
		return nil, err
	}

    mediaUrl := "https://" + string(c.Request().Host) + "/media/" + filename
	return &mediaUrl, nil
}

// adapted from "github.com/valyala/fasthttp"
var copyBufPool = sync.Pool{ New: func() any {return make([]byte, 16384) } }
func copyZeroAlloc(w io.Writer, r io.Reader) (int64, error) {
	if directRead, ok := r.(io.WriterTo); ok {
		return directRead.WriteTo(w)
	}
	if directWrite, ok := w.(io.ReaderFrom); ok {
		return directWrite.ReadFrom(r)
	}
	// else copy in chunks of 16 KB.
	vbuf := copyBufPool.Get()
	buf := vbuf.([]byte)
	n, err := io.CopyBuffer(w, r, buf)
	copyBufPool.Put(vbuf)
	return n, err
}

Make a copy of your getchatts() in handlers.go and name the copy getimages(). In getimages(), replace the SELECT statement with the following: `SELECT username, message, id, time, imageurl, videourl FROM chatts`. This statement will retrieve all data, including our new image and video URLs from the PostgreSQL database.

Still in getimages(), replace the rows.Scan() call in the for rows.Next() {} block with:

		err = rows.Scan(&chatt.Username, &chatt.Message, &chatt.Id, &chatt.Timestamp, &chatt.ImageUrl, &chatt.VideoUrl)

and if the returned err is nil:

        chattArr = append(chattArr, []any{chatt.Username, chatt.Message, chatt.Id, chatt.Timestamp, chatt.ImageUrl, chatt.VideoUrl})

In addition to the original columns, we added reading the imageurl and videourl columns and included them in the chatt data returned to the front end.

We’re done with handlers.go. Save and exit the file.

main.go

Edit the file main.go:

server$ vi main.go

Find the global variable router and add the following new routes for the new APIs /getimages and /postimages:

    {"GET", "/getimages/", getimages},
    {"POST", "/postimages/", postimages},

To serve image/video static files, in the main() function add the following before launching the server, e.g., after server.Pre():

	server.Static("/media", MEDIA_ROOT)
	server.Use(middleware.BodyLimit("10M"))

This tells Echo to redirect URL path /media to its static-file server and to serve media files from our designated media directory, whose value is stored in MEDIA_ROOT, and to limit each upload to 10 MB.

We’re done with main.go. Save and exit the file.

Build and test run

To build your server:

server$ go get
server$ go build

:point_right:Go is a compiled language, like C/C++ and unlike Python, which is an interpreted language. This means you must run go build each and every time you made changes to your code, for the changes to show up in your executable.

To run your server:

server$ sudo ./chatterd
# Hit ^C to end the test

The cover backend spec provides instructions for Testing image and video upload.

References


Prepared by Sugih Jamin Last updated August 31st, 2025