Skip to content

Commit 2d59b80

Browse files
committed
initial implementation of API server from server-database project
1 parent 7a3e796 commit 2d59b80

File tree

6 files changed

+374
-0
lines changed

6 files changed

+374
-0
lines changed

multiple-servers/api/api.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"os"
9+
10+
"github.com/jackc/pgx/v4"
11+
)
12+
13+
type Config struct {
14+
DatabaseURL string
15+
Port int
16+
}
17+
18+
func Run(config Config) {
19+
conn, err := pgx.Connect(context.Background(), config.DatabaseURL)
20+
if err != nil {
21+
log.Fatalf("unable to connect to database: %v\n", err)
22+
}
23+
// Defer closing the connection to when main function exits
24+
defer conn.Close(context.Background())
25+
26+
http.HandleFunc("/images.json", func(w http.ResponseWriter, r *http.Request) {
27+
log.Println(r.Method, r.URL.EscapedPath())
28+
29+
// Grab the indent query param early
30+
indent := r.URL.Query().Get("indent")
31+
32+
var response []byte
33+
var responseErr error
34+
if r.Method == "POST" {
35+
// Add new image to the database
36+
image, err := AddImage(conn, r)
37+
if err != nil {
38+
fmt.Fprintln(os.Stderr, err.Error())
39+
// We don't expose our internal errors (i.e. the contents of err) directly to the user for a few reasons:
40+
// 1. It may leak private information (e.g. a database connection string, which may even include a password!), which may be a security risk.
41+
// 2. It probably isn't useful to them to know.
42+
// 3. It may contain confusing terminology which may be embarrassing or confusing to expose.
43+
http.Error(w, "Something went wrong", http.StatusInternalServerError)
44+
return
45+
}
46+
47+
response, responseErr = MarshalWithIndent(image, indent)
48+
} else {
49+
// Fetch images from the database
50+
images, err := FetchImages(conn)
51+
if err != nil {
52+
fmt.Fprintln(os.Stderr, err.Error())
53+
http.Error(w, "Something went wrong", http.StatusInternalServerError)
54+
return
55+
}
56+
57+
response, responseErr = MarshalWithIndent(images, indent)
58+
}
59+
60+
if responseErr != nil {
61+
fmt.Fprintln(os.Stderr, err.Error())
62+
http.Error(w, "Something went wrong", http.StatusInternalServerError)
63+
return
64+
}
65+
// Indicate that what follows will be JSON
66+
w.Header().Add("Content-Type", "text/json")
67+
// Send it back!
68+
w.Write(response)
69+
})
70+
71+
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil))
72+
}

multiple-servers/api/images.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
10+
"github.com/jackc/pgx/v4"
11+
)
12+
13+
type Image struct {
14+
Title string
15+
AltText string
16+
URL string
17+
}
18+
19+
func (img Image) String() string {
20+
return fmt.Sprintf("%s (%s): %s", img.Title, img.AltText, img.URL)
21+
}
22+
23+
func FetchImages(conn *pgx.Conn) ([]Image, error) {
24+
// Send a query to the database, returning raw rows
25+
rows, err := conn.Query(context.Background(), "SELECT title, url, alt_text FROM public.images")
26+
// Handle query errors
27+
if err != nil {
28+
return nil, fmt.Errorf("unable to query database: [%w]", err)
29+
}
30+
31+
// Create slice to contain the images
32+
var images []Image
33+
// Iterate through each row to extract the data
34+
for rows.Next() {
35+
var title, url, altText string
36+
// Extract the data, passing pointers so the data can be updated in place
37+
err = rows.Scan(&title, &url, &altText)
38+
if err != nil {
39+
return nil, fmt.Errorf("unable to read from database: %w", err)
40+
}
41+
// Append this as a new Image to the images slice
42+
images = append(images, Image{Title: title, URL: url, AltText: altText})
43+
}
44+
45+
return images, nil
46+
}
47+
48+
func AddImage(conn *pgx.Conn, r *http.Request) (*Image, error) {
49+
// Read the request body into a bytes slice
50+
body, err := io.ReadAll(r.Body)
51+
if err != nil {
52+
return nil, fmt.Errorf("could not read request body: [%w]", err)
53+
}
54+
55+
// Parse the body JSON into an image struct
56+
var image Image
57+
err = json.Unmarshal(body, &image)
58+
if err != nil {
59+
return nil, fmt.Errorf("could not parse request body: [%w]", err)
60+
}
61+
62+
// Insert it into the database
63+
_, err = conn.Exec(context.Background(), "INSERT INTO public.images(title, url, alt_text) VALUES ($1, $2, $3)", image.Title, image.URL, image.AltText)
64+
if err != nil {
65+
return nil, fmt.Errorf("could not insert image: [%w]", err)
66+
}
67+
68+
return &image, nil
69+
}

multiple-servers/api/util.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// The special type interface{} allows us to take _any_ value, not just one of a specific type.
11+
// This means we can re-use this function for both a single image _and_ a slice of multiple images.
12+
func MarshalWithIndent(data interface{}, indent string) ([]byte, error) {
13+
// Convert images to a byte-array for writing back in a response
14+
var b []byte
15+
var marshalErr error
16+
// Allow up to 10 characters of indent
17+
if i, err := strconv.Atoi(indent); err == nil && i > 0 && i <= 10 {
18+
b, marshalErr = json.MarshalIndent(data, "", strings.Repeat(" ", i))
19+
} else {
20+
b, marshalErr = json.Marshal(data)
21+
}
22+
if marshalErr != nil {
23+
return nil, fmt.Errorf("could not marshal data: [%w]", marshalErr)
24+
}
25+
return b, nil
26+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
"servers/api"
7+
)
8+
9+
func main() {
10+
// Check that DATABASE_URL is set
11+
if os.Getenv("DATABASE_URL") == "" {
12+
log.Fatalln("DATABASE_URL not set")
13+
}
14+
15+
api.Run(api.Config{
16+
DatabaseURL: os.Getenv("DATABASE_URL"),
17+
Port: 8081,
18+
})
19+
}

multiple-servers/go.mod

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
module servers
22

33
go 1.18
4+
5+
require (
6+
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
7+
github.com/jackc/pgconn v1.13.0 // indirect
8+
github.com/jackc/pgio v1.0.0 // indirect
9+
github.com/jackc/pgpassfile v1.0.0 // indirect
10+
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
11+
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
12+
github.com/jackc/pgtype v1.12.0 // indirect
13+
github.com/jackc/pgx/v4 v4.17.0 // indirect
14+
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
15+
golang.org/x/text v0.3.7 // indirect
16+
)

0 commit comments

Comments
 (0)