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
![]()
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 |