Skip to content

Instantly share code, notes, and snippets.

@juanpabloaj
Last active May 31, 2023 08:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save juanpabloaj/c024b89f8be5bc8f3d01fde9a9acbad9 to your computer and use it in GitHub Desktop.
Save juanpabloaj/c024b89f8be5bc8f3d01fde9a9acbad9 to your computer and use it in GitHub Desktop.
go api rest example
*.db
api_rest
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
_ "github.com/mattn/go-sqlite3"
)
func respondWithError(w http.ResponseWriter, code int, message string) {
respondWithJSON(w, code, map[string]string{"error": message})
}
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
response, _ := json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(response)
}
type App struct {
Router *mux.Router
DB *sql.DB
}
func (a *App) getUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid user ID")
return
}
u := user{ID: id}
if err := u.getUser(a.DB); err != nil {
switch err {
case sql.ErrNoRows:
respondWithError(w, http.StatusNotFound, "User not found")
default:
respondWithError(w, http.StatusInternalServerError, err.Error())
}
return
}
respondWithJSON(w, http.StatusOK, u)
}
func (a *App) getUsers(w http.ResponseWriter, r *http.Request) {
count, _ := strconv.Atoi(r.FormValue("count"))
start, _ := strconv.Atoi(r.FormValue("start"))
if count > 10 || count < 1 {
count = 10
}
if start < 10 {
start = 0
}
users, err := getUsers(a.DB, start, count)
if err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
respondWithJSON(w, http.StatusOK, users)
}
func (a *App) createUser(w http.ResponseWriter, r *http.Request) {
var u user
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&u); err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
defer r.Body.Close()
if err := u.createUser(a.DB); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
respondWithJSON(w, http.StatusCreated, u)
}
func (a *App) updateUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid user ID")
return
}
var u user
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&u); err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
defer r.Body.Close()
u.ID = id
if err := u.updateUser(a.DB); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
respondWithJSON(w, http.StatusOK, u)
}
func (a *App) deleteUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid User ID")
return
}
u := user{ID: id}
if err := u.deleteUser(a.DB); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"})
}
func (a *App) Initialize(user, password, dbname string) {
var err error
a.DB, err = sql.Open("sqlite3", "./app.db")
if err != nil {
log.Fatal(err)
}
a.Router = mux.NewRouter()
a.initializeRouters()
}
func (a *App) initializeRouters() {
a.Router.HandleFunc("/users", a.getUsers).Methods("GET")
a.Router.HandleFunc("/user", a.createUser).Methods("POST")
a.Router.HandleFunc("/user/{id:[0-9]+}", a.getUser).Methods("GET")
a.Router.HandleFunc("/user/{id:[0-9]+}", a.updateUser).Methods("PUT")
a.Router.HandleFunc("/user/{id:[0-9]+}", a.deleteUser).Methods("DELETE")
}
func (a *App) Run(addr string) {
log.Fatal(http.ListenAndServe(addr, a.Router))
}
package main
func main() {
a := App{}
a.Initialize("BD_USERNAME", "DB_PASSWORD", "rest_api_example")
a.Run(":8080")
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"strconv"
"testing"
)
var a App
func TestMain(m *testing.M) {
a = App{}
a.Initialize("BD_USERNAME", "DB_PASSWORD", "rest_api_example")
ensureTableExists()
code := m.Run()
clearTable()
os.Exit(code)
}
func ensureTableExists() {
if _, err := a.DB.Exec(tableCreationQuery); err != nil {
log.Fatal(err)
}
}
func clearTable() {
a.DB.Exec("DELETE FROM users")
a.DB.Exec("ALTER TABLE users AUTO_INCREMENT = 1")
}
const tableCreationQuery = `
CREATE TABLE IF NOT EXISTS users
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT NOT NULL
)`
func executeRequest(req *http.Request) *httptest.ResponseRecorder {
rr := httptest.NewRecorder()
a.Router.ServeHTTP(rr, req)
return rr
}
func checkResponseCode(t *testing.T, expected, actual int) {
if expected != actual {
t.Errorf("Expected response code %d. Got %d\n", expected, actual)
}
}
func addUsers(count int) {
if count < 1 {
count = 1
}
for i := 0; i < count; i++ {
statement := fmt.Sprintf("INSERT INTO users(name, age) VALUES('%s', %d)", ("User " + strconv.Itoa(i+1)), ((i + 1) * 10))
a.DB.Exec(statement)
}
}
func TestEmptyTable(t *testing.T) {
clearTable()
req, _ := http.NewRequest("GET", "/users", nil)
response := executeRequest(req)
checkResponseCode(t, http.StatusOK, response.Code)
if body := response.Body.String(); body != "[]" {
t.Errorf("Expected an empty array. Got %s", body)
}
}
func TestGetNonExistentUser(t *testing.T) {
clearTable()
req, _ := http.NewRequest("GET", "/user/45", nil)
response := executeRequest(req)
checkResponseCode(t, http.StatusNotFound, response.Code)
var m map[string]string
json.Unmarshal(response.Body.Bytes(), &m)
if m["error"] != "User not found" {
t.Errorf("Expected the 'error' key of the response to be set to 'User not found'. Got '%s'", m["error"])
}
}
func TestCreateUser(t *testing.T) {
clearTable()
payload := []byte(`{"name": "test user", "age": 30}`)
req, _ := http.NewRequest("POST", "/user", bytes.NewBuffer(payload))
response := executeRequest(req)
checkResponseCode(t, http.StatusCreated, response.Code)
var m map[string]interface{}
json.Unmarshal(response.Body.Bytes(), &m)
if m["name"] != "test user" {
t.Errorf("Expected user name to be 'test user'. Got '%v'", m["name"])
}
if m["age"] != 30.0 {
t.Errorf("Expected user name to be '30'. Got '%v'", m["age"])
}
if m["id"] != 1.0 {
t.Errorf("Expected user ID to be '1'. Got '%v'", m["id"])
}
}
func TestGetUser(t *testing.T) {
clearTable()
addUsers(1)
req, _ := http.NewRequest("GET", "/user/1", nil)
response := executeRequest(req)
checkResponseCode(t, http.StatusOK, response.Code)
}
func TestUpdateUser(t *testing.T) {
clearTable()
addUsers(1)
req, _ := http.NewRequest("GET", "/user/1", nil)
response := executeRequest(req)
var originalUser map[string]interface{}
json.Unmarshal(response.Body.Bytes(), &originalUser)
payload := []byte(`{"name": "test user - updated name", "age": 21}`)
req, _ = http.NewRequest("PUT", "/user/1", bytes.NewBuffer(payload))
response = executeRequest(req)
checkResponseCode(t, http.StatusOK, response.Code)
var m map[string]interface{}
json.Unmarshal(response.Body.Bytes(), &m)
if m["id"] != originalUser["id"] {
t.Errorf("Expected the id to remain the same (%v). Got %v", originalUser["id"], m["id"])
}
if m["name"] == originalUser["name"] {
t.Errorf("Expected the name to change from '%v' to '%v'. Got '%v'", originalUser["name"], m["name"], m["name"])
}
if m["age"] == originalUser["age"] {
t.Errorf("Expected the age to change from '%v' to '%v'. Got '%v'", originalUser["age"], m["age"], m["age"])
}
}
func TestDeleteUser(t *testing.T) {
clearTable()
addUsers(1)
req, _ := http.NewRequest("GET", "/user/1", nil)
response := executeRequest(req)
checkResponseCode(t, http.StatusOK, response.Code)
req, _ = http.NewRequest("DELETE", "/user/1", nil)
response = executeRequest(req)
checkResponseCode(t, http.StatusOK, response.Code)
req, _ = http.NewRequest("GET", "/user/1", nil)
checkResponseCode(t, http.StatusNotFound, response.Code)
}
**/*.go {
prep: go test -v
}
**/*.go !**/*_test.go {
prep: go build
}
package main
import (
"database/sql"
"fmt"
)
type user struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func (u *user) getUser(db *sql.DB) error {
statement := fmt.Sprintf("SELECT name, age FROM users WHERE id=%d", u.ID)
return db.QueryRow(statement).Scan(&u.Name, &u.Age)
}
func (u *user) updateUser(db *sql.DB) error {
statement := fmt.Sprintf("UPDATE users SET name='%s', age=%d WHERE id=%d", u.Name, u.Age, u.ID)
_, err := db.Exec(statement)
return err
}
func (u *user) deleteUser(db *sql.DB) error {
statement := fmt.Sprintf("DELETE FROM users WHERE id=%d", u.ID)
_, err := db.Exec(statement)
return err
}
func (u *user) createUser(db *sql.DB) error {
statement := fmt.Sprintf("INSERT INTO users(name, age) VALUES('%s', %d)", u.Name, u.Age)
_, err := db.Exec(statement)
if err != nil {
return err
}
err = db.QueryRow("SELECT LAST_INSERT_ID()").Scan(&u.ID)
if err != nil {
return err
}
return nil
}
func getUsers(db *sql.DB, start, count int) ([]user, error) {
statement := fmt.Sprintf("SELECT id, name, age FROM users LIMIT %d OFFSET %d", count, start)
rows, err := db.Query(statement)
if err != nil {
return nil, err
}
defer rows.Close()
users := []user{}
for rows.Next() {
var u user
if err := rows.Scan(&u.ID, &u.Name, &u.Age); err != nil {
return nil, err
}
users = append(users, u)
}
return users, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment