package main import ( "net/http" "fmt" "encoding/json" "os" "context" "github.com/jackc/pgx/v5" ) //mixed opinions on global variables online, need production example to verify this practice var conn = ConnectPostgres() //---------------------------------------------------------------------------------------------------- //root that returns a static string func RootHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello World!") } //---------------------------------------------------------------------------------------------------- //json struct for incoming requests type IncomingJson struct { Message string `json:"message"` } //take note that for structs, Capitalised fields indicates exported fields, and vice versa //therefore "name string" will not be decoded/encoded, but "Name string" will //the `json:name` //json struct for api response type OutboundJson struct { Status string `json:"status"` RequestData IncomingJson `json:"request_data"` } //API that accepts and returns a json defined in the struct above func JsonHandler(w http.ResponseWriter, r *http.Request){ //checks if the method is Post, returns error if not if r.Method != http.MethodPost { http.Error(w, "Method not allowed,Only Post Requests", http.StatusMethodNotAllowed) return } //decode Json from request body, returns error if doesnt fit struct var incoming IncomingJson err := json.NewDecoder(r.Body).Decode(&incoming) if err != nil { http.Error(w,"Bad Request, Failed to parse body({message:str})", http.StatusBadRequest) return } //set response header to json w.Header().Set("Content-Type", "application/json") //encode and set response resp := OutboundJson{ Status: "received", RequestData: incoming, } err = json.NewEncoder(w).Encode(resp) if err != nil { fmt.Println("JSON encoding Error:",err) http.Error(w, "Error encoding JSON", http.StatusInternalServerError) return } } //---------------------------------------------------------------------------------------------------- func ConnectPostgres()(*pgx.Conn){ //yes i need to put this in a .env, im just testing for now and this database has nothing important conn, err := pgx.Connect(context.Background(), "postgres://postgres:12345678@localhost:5432/postgres") if err != nil { fmt.Println("Unable to connect to database", err) os.Exit(1) } fmt.Println("Connected to database") return conn } //operation dictates which CRUD operation to perform (c,r,u,d) //identifier dictates which row the operation is performed on (int) //value dictates the actual change of operation type IncomingPostgres struct{ Operation string `json:"operation"` Identifier int `json:identifier",omitempty` Value string `json:"value",omitempty` } type OutgoingPostgres struct{ Identifier int `json:"identifier"` Value string `json:"value"` Status string `json:"status"` } //the pointed table only has two columns: id(int) and value(str) //the following handler provides a simple example on handling CRUD operations via an API using pgx func PostgresHandler(w http.ResponseWriter, r *http.Request){ //checks if the method is Post, returns error if not if r.Method != http.MethodPost { http.Error(w, "Method not allowed,Only Post Requests", http.StatusMethodNotAllowed) return } //decode Json from request body, returns error if doesnt fit struct var incoming IncomingPostgres err := json.NewDecoder(r.Body).Decode(&incoming) if err != nil { http.Error(w,"Bad Request, Failed to parse body({operation:str,identifier[optional]:int,value[optional]:string})", http.StatusBadRequest) return } //map of allowed inputs for the operation field, returns error if doesnt fit allowed_operations := map[string]bool { "c":true, "r":true, "u":true, "d":true, } if !allowed_operations[incoming.Operation] { http.Error(w, "Bad Request, operation field only allows the following inputs: {c,r,u,d}", http.StatusBadRequest) return } //switch case to determine the operation //this method directly checks for zero values, which might not be feasible if zero values are allowed inputs //consider transforming the struct into a map then checking if it exists, that would be a more robust method var id int var value string switch incoming.Operation{ case "c": if incoming.Value != ""{ fmt.Println("create",incoming.Value) //SQL query for the db rows, err := conn.Query(context.Background(),"INSERT INTO test_table (value) VALUES ($1) RETURNING(id)",incoming.Value) if err != nil{ fmt.Println(err) http.Error(w,"Server Error, something is wrong", http.StatusInternalServerError) return } //close the connection when the function is finished defer rows.Close() if rows.Next(){ err = rows.Scan(&id) if err != nil{ fmt.Println(err) http.Error(w,"Server Error, Scanning Failed",http.StatusInternalServerError) return } } value = incoming.Value } else { http.Error(w,"Bad Request, Value Key is needed for create operation",http.StatusBadRequest) return } case "r": if incoming.Identifier != 0{ fmt.Println("read",incoming.Identifier) //SQL query for the db rows, err := conn.Query(context.Background(),"SELECT id,value FROM test_table WHERE id=$1",incoming.Identifier) if err != nil{ fmt.Println(err) http.Error(w, "Server Error, something is wrong", http.StatusInternalServerError) return } //close the connection when the function is finished defer rows.Close() //this is a readone() method, only the first item queried will be considered and returned if rows.Next(){ err = rows.Scan(&id,&value) if err != nil{ http.Error(w,"Server Error, Scanning Failed",http.StatusInternalServerError) return } } else { http.Error(w,"Not Found, identifier does not exist",http.StatusNotFound) return } } else { http.Error(w,"Bad Request, Identifier Key is needed for read operation",http.StatusBadRequest) return } case "u": if incoming.Identifier != 0 && incoming.Value != ""{ fmt.Println("update",incoming.Identifier,incoming.Value) _, err := conn.Query(context.Background(),"UPDATE test_table SET value=$1 WHERE id=$2",incoming.Value,incoming.Identifier) if err != nil{ fmt.Println(err) http.Error(w,"Update Failed, double check if identifier exists",http.StatusBadRequest) return } id = incoming.Identifier value = incoming.Value } else { http.Error(w,"Bad Request, Identifier Key and Value Key is needed for update operation",http.StatusBadRequest) return } case "d": if incoming.Identifier != 0{ fmt.Println("delete",incoming.Identifier) _,err := conn.Query(context.Background(),"DELETE FROM test_table WHERE id=$1",incoming.Identifier) if err != nil{ fmt.Println(err) http.Error(w,"Delete Failed, double check if identifier exists",http.StatusBadRequest) return } value = incoming.Value id = incoming.Identifier } else { http.Error(w,"Bad Request, Value Key is needed for create operation",http.StatusBadRequest) return } } w.Header().Set("Content-Type","application/json") resp := OutgoingPostgres{ Status: "Successful", Identifier: id, Value: value, } json.NewEncoder(w).Encode(resp) } //---------------------------------------------------------------------------------------------------- //main entry function to register routes and serve func main() { defer conn.Close(context.Background()) http.HandleFunc("/pgx",PostgresHandler) http.HandleFunc("/json", JsonHandler) http.HandleFunc("/", RootHandler) fmt.Println("Starting server at http://localhost:8080") err := http.ListenAndServe(":8080", nil) if err != nil { fmt.Println("Error starting server:", err) } }