From 431db3070b044559495e6734a50b526b3f3c3b6d Mon Sep 17 00:00:00 2001 From: Sagar Maheshwary Date: Sun, 17 Dec 2023 16:39:04 +0500 Subject: [PATCH] Created articles (CRUD) api using fiber and postgresql --- .gitignore | 3 + articles-app/.env.example | 8 ++ articles-app/README.md | 49 +++++++++ articles-app/main.go | 218 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 articles-app/.env.example create mode 100644 articles-app/README.md create mode 100644 articles-app/main.go diff --git a/.gitignore b/.gitignore index 17f8f13..18d6a10 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ go.work # Generated by "air" **/tmp/* + +# env files +**/.env diff --git a/articles-app/.env.example b/articles-app/.env.example new file mode 100644 index 0000000..9847ebc --- /dev/null +++ b/articles-app/.env.example @@ -0,0 +1,8 @@ +APP_PORT=8000 + +DB_HOST=localhost +DB_PORT=5432 +DB_DATABASE= +DB_USERNAME=postgres +DB_PASSWORD= +DB_SSLMODE=disable diff --git a/articles-app/README.md b/articles-app/README.md new file mode 100644 index 0000000..5584780 --- /dev/null +++ b/articles-app/README.md @@ -0,0 +1,49 @@ +# ARTICLES APP + +CRUD api build using Fiber framework and PostgreSQL database. + +### DEPENDENCIES + +```bash +go get github.com/gofiber/fiber/v2 +``` + +```bash +go get github.com/lib/pq +``` + +```bash +go get github.com/gofor-little/env +``` + +### TABLES + +```sql +CREATE TABLE users_1 ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NULL +); +``` + +```sql +CREATE TABLE articles_1 ( + id BIGSERIAL PRIMARY KEY, + slug VARCHAR(120) NOT NULL UNIQUE, + title VARCHAR(120) NOT NULL, + body TEXT NOT NULL, + user_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +); +``` + +### RUNNING THE APPLICATION + +```bash + go run main.go +``` diff --git a/articles-app/main.go b/articles-app/main.go new file mode 100644 index 0000000..90d97df --- /dev/null +++ b/articles-app/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/gofor-little/env" + _ "github.com/lib/pq" +) + +// Not in use, just for reference +type User struct { + Id string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + CreatedAt string `json:"created_at"` + UpdatedAt *string `json:"updated_at"` +} + +type Article struct { + Id string `json:"id"` + Slug string `json:"slug"` + Title string `json:"title"` + Body string `json:"body"` + UserId string `json:"user_id"` + CreatedAt string `json:"created_at"` + UpdatedAt *string `json:"updated_at"` +} + +func main() { + if err := env.Load(".env"); err != nil { + panic(err) + } + + db, err := getDatabaseConnection() + + if err != nil { + log.Fatalln(err) + } + + app := fiber.New() + + app.Get("/articles", func(c *fiber.Ctx) error { + return getArticles(c, db) + }) + app.Post("/articles", func(c *fiber.Ctx) error { + return storeArticle(c, db) + }) + app.Get("/articles/:id", func(c *fiber.Ctx) error { + return getArticle(c, db) + }) + app.Put("/articles/:id", func(c *fiber.Ctx) error { + return updateArticle(c, db) + }) + app.Delete("/articles/:id", func(c *fiber.Ctx) error { + return deleteArticle(c, db) + }) + + port := env.Get("APP_PORT", "8000") + + log.Fatalln(app.Listen(fmt.Sprintf(":%v", port))) + +} + +func getArticles(c *fiber.Ctx, db *sql.DB) error { + rows, err := db.Query("SELECT * FROM articles") + + if err != nil { + log.Fatalln(err) + return c.JSON("An error occurred.") + } + + //Docs: This releases any resources held by the rows no matter how the function returns. + //Looping all the way through the rows also closes it implicitly, but it is better + //to use defer to make sure rows is closed no matter what. + defer rows.Close() + + var articles []Article + + for rows.Next() { + var article Article + + rows.Scan( + &article.Id, + &article.Slug, + &article.Title, + &article.Body, + &article.UserId, + &article.CreatedAt, + &article.UpdatedAt, + ) + + articles = append(articles, article) + } + + return c.JSON(fiber.Map{ + "articles": articles, + }) +} + +func storeArticle(c *fiber.Ctx, db *sql.DB) error { + var article Article + c.BodyParser(&article) + + article.Slug = strings.ReplaceAll(strings.ToLower(article.Title), " ", "-") + + err := db.QueryRow( + "INSERT INTO articles (slug, title, body, user_id) VALUES ($1, $2, $3, $4) returning id, created_at", + article.Slug, + article.Title, + article.Body, + article.UserId, + ).Scan(&article.Id, &article.CreatedAt) + + if err != nil { + log.Fatalln(err) + return c.JSON("An error occurred.") + } + + return c.JSON(fiber.Map{ + "message": "Created a new article.", + "article": article, + }) +} + +func getArticle(c *fiber.Ctx, db *sql.DB) error { + id := c.Params("id") + var article Article + + err := db.QueryRow("SELECT * FROM articles WHERE id = $1", id).Scan( + &article.Id, + &article.Slug, + &article.Title, + &article.Body, + &article.UserId, + &article.CreatedAt, + &article.UpdatedAt, + ) + + if err != nil { + log.Fatalln(err) + return c.JSON("An error occurred.") + } + + fmt.Println(article.Id, err) + + return c.JSON(fiber.Map{ + "article": article, + }) +} + +func updateArticle(c *fiber.Ctx, db *sql.DB) error { + id := c.Params("id") + var article Article + c.BodyParser(&article) + + article.Slug = strings.ReplaceAll(strings.ToLower(article.Title), " ", "-") + + err := db.QueryRow( + "UPDATE articles SET slug = $1, title = $2, body = $3, updated_at = now() WHERE id = $4 returning id, user_id, created_at, updated_at", + article.Slug, + article.Title, + article.Body, + id, + ).Scan(&article.Id, &article.UserId, &article.CreatedAt, &article.UpdatedAt) + + if err != nil { + log.Fatalln(err) + return c.JSON("An error occurred.") + } + + return c.JSON(fiber.Map{ + "message": "Selected article has been updated.", + "article": article, + }) +} + +func deleteArticle(c *fiber.Ctx, db *sql.DB) error { + id := c.Params("id") + + _, err := db.Exec("DELETE FROM articles WHERE id = $1", id) + + if err != nil { + log.Fatalln(err) + return c.JSON("An error occurred.") + } + + return c.JSON(fiber.Map{ + "message": "Selected article has been deleted.", + }) +} + +func getDatabaseConnection() (*sql.DB, error) { + dbHost := env.Get("DB_HOST", "localhost") + dbPort := env.Get("DB_PORT", "5432") + dbDatabase := env.Get("DB_DATABASE", "articles_app") + dbUsername := env.Get("DB_USERNAME", "postgres") + dbPassword := env.Get("DB_PASSWORD", "password") + dbSSLMode := env.Get("DB_SSLMODE", "disable") + + connectionURL := fmt.Sprintf( + "postgresql://%s:%s@%s:%s/%s?sslmode=%s", + dbUsername, + dbPassword, + dbHost, + dbPort, + dbDatabase, + dbSSLMode, + ) + + db, err := sql.Open("postgres", connectionURL) + + return db, err +}