Go with Echo

Cover Page

Back-end Page

handlers module

Change to your chatterd directory and edit handlers.go:

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

First add the following imports to the import() block at the top of the file:

	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"regexp"
	"strings"

Next define these three structs to help llmchat() deserialize JSON received from clients. Add these lines right below the import block:

type OllamaMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type OllamaRequest struct {
	AppID    string    `json:"appID"`
	Model    string    `json:"model"`
	Messages []OllamaMessage `json:"messages"`
	Stream   bool      `json:"stream"`
}

type OllamaResponse struct {
	Model     string  `json:"model"`
	Message   OllamaMessage `json:"message"`
}

To store the client’s conversation context/history with Ollama in the PostgreSQL database, llmchat() first confirms that the client has sent an appID that can be used to tag its entries in the database. Here’s the signature of llmchat() along with its check for client’s appID:

func llmchat(c echo.Context) error {
	err := errors.New("")
	var ollamaRequest OllamaRequest

	if err = c.Bind(&ollamaRequest); err != nil {
		return logClientErr(c, http.StatusUnprocessableEntity, err)
	}

	if len(ollamaRequest.AppID) == 0 {
		return logClientErr(c, http.StatusUnprocessableEntity,
			fmt.Errorf("invalid appID: %s", ollamaRequest.AppID))
	}

	// insert into DB

	logOk(c)
	return nil
}

Once we confirm that the client has an appID, we insert its current prompt into the database, adding to its conversation history with Ollama. Replace the comment // insert into DB with the following code:

	// insert each message into the database
	for _, msg := range ollamaRequest.Messages {
		_, err = chatterDB.Exec(background, `INSERT INTO chatts (name, message, id, appid) VALUES ($1, $2, gen_random_uuid(), $3)`,
			msg.Role, msg.Content, ollamaRequest.AppID)
		if err != nil {
			return logClientErr(c, http.StatusBadRequest, err)
		}
	}

	// retrieve history

Then we retrieve the client’s conversation history chronologically by timestamp, including the just inserted, current prompt, as the last entry, and put them in a JSON format expected by Ollama’s chat API. Replace // retrieve history with:

	// reconstruct ollamaRequest to be sent to Ollama:
	// - add context: retrieve all past messages by appID,
	//   incl. the one just received,
	// - convert each back to OllamaMessage, and
	// - insert it into ollamaRequest
	req := c.Request()
	reqCtx := req.Context()
	rows, err := chatterDB.Query(reqCtx, `SELECT name, message FROM chatts WHERE appid = $1 ORDER BY time ASC`, ollamaRequest.AppID)
	if err != nil {
		if rows != nil {
			rows.Close()
		}
		return logServerErr(c, err)
	}

	ollamaRequest.Messages = nil
	var msg OllamaMessage
	for rows.Next() {
		err = rows.Scan(&msg.Role, &msg.Content)
		if err != nil {
			rows.Close()
			return logServerErr(c, err)
		}
		ollamaRequest.Messages = append(ollamaRequest.Messages, msg)
	}

	// send request to Ollama

We create a HTTP request packet with ollamaRequest above as its payload and send it to Ollama. Then we declare an accumulator variable, tokens, to accumulate the reply tokens Ollama streams back. To reduce storage requirement, we compact multiple consecutive whitespaces into one during the token accumulation process. The regular expression used for the compaction is stored in the wsRegex variable. We also prime the response stream to the client by preparing a response header to be sent with each SSE event to the client. Replace // send request to Ollama with:

	requestBody, err := json.Marshal(&ollamaRequest) // convert the request to JSON
	if err != nil {
	    return logServerErr(c, err)
	}
	ollama_url := OLLAMA_BASE_URL.String() + "/chat"
	request, _ := http.NewRequestWithContext(reqCtx, req.Method, ollama_url, bytes.NewReader(requestBody))
	
	response, err := http.DefaultClient.Do(request)
	if err != nil {
	    return logServerErr(c, err)
	}
	defer func() {
	    _ = response.Body.Close()
	}()
	
	var tokens []string
	wsRegex := regexp.MustCompile("\\s+") 
	
	res := c.Response()
	res.Header().Set(echo.HeaderContentType, "text/event-stream")
	res.Header().Set(echo.HeaderCacheControl, "no-cache")

	// accumulate tokens and forward data lines

For each incoming NDJSON element, we convert it into an OllamaResponse type. If the conversion is unsuccessful, we return an SSE error event and move on to the next NDJSON line. Otherwise, we append the content in the OllamaResponse to the tokens variable, after removing duplicated whitespaces, and yield the line as an SSE Message data line. Replace // accumulate tokens and yield data lines with:

	reader := bufio.NewReader(response.Body)
	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			if err != io.EOF {
				err_msg, _ := json.Marshal(err.Error())
				_, _ = fmt.Fprintf(res, "event: error\ndata: { \"error\": %s }\n\n", string(err_msg))
				res.Flush()
			}
			break
		}

		var ollamaResponse OllamaResponse
		// deserialize each line into OllamaResponse
		if err = json.Unmarshal([]byte(line), &ollamaResponse); err != nil {
			err_msg, _ := json.Marshal(err.Error())
			_, _ = fmt.Fprintf(res, "event: error\ndata: { \"error\": %s }\n\n", string(err_msg))
			res.Flush()
		} else {
			// append response token to full assistant message
			tokens = append(tokens, wsRegex.ReplaceAllString(ollamaResponse.Message.Content, " "))
			// send NDJSON line as SSE line
			_, _ = fmt.Fprintf(res, "data: %s\n\n", line)
			res.Flush()
		}
	}

	// insert full response into database

When we reach the end of the NDJSON stream, we insert the full Ollama response into PostgreSQL database as the assistant’s reply. It will later be sent back to Ollama as part of subsequent prompts’ context. Replace // insert full response into database with:

	if len(tokens) != 0 {
		var completion = strings.Join(tokens, " ")
		
		// save full response to db, to form part of next prompt's history
		_, err = chatterDB.Exec(background, `INSERT INTO chatts (name, message, id, appid) VALUES ('assistant', $1, gen_random_uuid(), $2)`,
			completion, ollamaRequest.AppID)
		// replace 'assistant' with NULL to test error event

		if err != nil {
			jsonErrMsg, _ := json.Marshal(fmt.Sprintf("%s", err))
			_, _ = fmt.Fprintf(res, "event: error\ndata: { \"error\": %s }\n\n", string(jsonErrMsg))
			res.Flush()
		}
	} // completion
	

If we encountered any error in the insertion above, we send an SSE error event to the client.

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 this route right after the route for /llmprompt/:

	{"POST", "/llmchat/", llmchat},

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

Build and test run

To build your server:

server$ go get   # -u  # to upgrade all packages to the latest version
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 back-end spec provides instructions on Testing llmChat API and SSE error handling.

References


Prepared by Xin Jie ‘Joyce’ Liu, Chenglin Li, and Sugih Jamin Last updated January 18th, 2026