Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of server-database exercise #3

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions server-database/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Server and database

In this project, you'll build another server. This one will have a simple API that serves data in JSON form. You'll then convert the backend to read from a Postgres database, serving data for the API. You'll then turn off the database and learn how to handle errors correctly.

Timebox: 6 days

Learning objectives:

- Build a simple API server that talks JSON
- Understand how a server and a database work together
- Use SQL to read data from a database
- Accept data over a POST request and write it to the database

## Project

See the `main` branch for the instructions.
12 changes: 0 additions & 12 deletions server-database/README.md

This file was deleted.

16 changes: 16 additions & 0 deletions server-database/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module server-database

go 1.18

require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.0 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/text v0.3.7 // indirect
)
175 changes: 175 additions & 0 deletions server-database/go.sum

Large diffs are not rendered by default.

154 changes: 154 additions & 0 deletions server-database/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"

"github.com/jackc/pgx/v4"
)

type Image struct {
Title string `json:"title"`
AltText string `json:"alt_text"`
URL string `json:"url"`
}

func (img Image) String() string {
return fmt.Sprintf("%s (%s): %s", img.Title, img.AltText, img.URL)
}

func fetchImages(conn *pgx.Conn) ([]Image, error) {
// Send a query to the database, returning raw rows
rows, err := conn.Query(context.Background(), "SELECT title, url, alt_text FROM public.images")
// Handle query errors
if err != nil {
return nil, fmt.Errorf("unable to query database: [%w]", err)
}

// Create slice to contain the images
var images []Image
// Iterate through each row to extract the data
for rows.Next() {
var title, url, altText string
// Extract the data, passing pointers so the data can be updated in place
err = rows.Scan(&title, &url, &altText)
if err != nil {
return nil, fmt.Errorf("unable to read from database: %w", err)
}
// Append this as a new Image to the images slice
images = append(images, Image{Title: title, URL: url, AltText: altText})
}

return images, nil
}

func addImage(conn *pgx.Conn, r *http.Request) (*Image, error) {
// Read the request body into a bytes slice
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("could not read request body: [%w]", err)
}

// Parse the body JSON into an image struct
var image Image
err = json.Unmarshal(body, &image)
if err != nil {
return nil, fmt.Errorf("could not parse request body: [%w]", err)
}

// Insert it into the database
_, err = conn.Exec(context.Background(), "INSERT INTO public.images(title, url, alt_text) VALUES ($1, $2, $3)", image.Title, image.URL, image.AltText)
if err != nil {
return nil, fmt.Errorf("could not insert image: [%w]", err)
}

return &image, nil
}

// The special type interface{} allows us to take _any_ value, not just one of a specific type.
// This means we can re-use this function for both a single image _and_ a slice of multiple images.
func marshalWithIndent(data interface{}, indent string) ([]byte, error) {
// Convert images to a byte-array for writing back in a response
var b []byte
var marshalErr error
// Allow up to 10 characters of indent
if i, err := strconv.Atoi(indent); err == nil && i > 0 && i <= 10 {
b, marshalErr = json.MarshalIndent(data, "", strings.Repeat(" ", i))
} else {
b, marshalErr = json.Marshal(data)
}
if marshalErr != nil {
return nil, fmt.Errorf("could not marshal data: [%w]", marshalErr)
}
return b, nil
}

func main() {
// Check that DATABASE_URL is set
if os.Getenv("DATABASE_URL") == "" {
fmt.Fprintf(os.Stderr, "DATABASE_URL not set\n")
os.Exit(1)
}

// Connect to the database
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
// Handle a possible connection error
if err != nil {
fmt.Fprintf(os.Stderr, "unable to connect to database: %v\n", err)
os.Exit(1)
}
// Defer closing the connection to when main function exits
defer conn.Close(context.Background())

// Create the handler function that serves the images JSON
http.HandleFunc("/images.json", func(w http.ResponseWriter, r *http.Request) {
// Grab the indent query param early
indent := r.URL.Query().Get("indent")

var response []byte
var responseErr error
if r.Method == "POST" {
// Add new image to the database
image, err := addImage(conn, r)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
// We don't expose our internal errors (i.e. the contents of err) directly to the user for a few reasons:
// 1. It may leak private information (e.g. a database connection string, which may even include a password!), which may be a security risk.
// 2. It probably isn't useful to them to know.
// 3. It may contain confusing terminology which may be embarrassing or confusing to expose.
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}

response, responseErr = marshalWithIndent(image, indent)
} else {
// Fetch images from the database
images, err := fetchImages(conn)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}

response, responseErr = marshalWithIndent(images, indent)
}

if responseErr != nil {
fmt.Fprintln(os.Stderr, err.Error())
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
// Indicate that what follows will be JSON
w.Header().Add("Content-Type", "text/json")
// Send it back!
w.Write(response)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Binary file added server-database/readme-assets/create-database.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server-database/readme-assets/create-table.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server-database/readme-assets/insert-script.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.