Part - 3
Why WHERE 1=1 in the SQL query
WHERE 1=1 is always true and acts as a placeholder to simplify appending additional conditions dynamically. It allows us to add conditions without worrying about whether it's the first conditions or not, avoiding the need to check if you should start with WHERE or add an AND.
WHERE 1=1 also simplify the code for adding filters. Without WHERE 1=1, you would need to check if the WHERE clause already exists before adding AND for each filter which adds extra complexity. And using this WHERE 1=1, we can actually handle multiple filters.
In conclusion, using WHERE 1=1 is a common practice for making dynamic query buliding more straightformward. It avoids the need for complex conditional logic when appending multiple filter conditions, making your code cleaner and easier to maintain.
Advanced Filtering Technique: GET - Getting entries based on multiple criteria
func addFilters(r *http.Request, query string, args []interface{}) (string, []interface{}) {
params := map[string]string{
"first_name": "first_name",
"last_name": "last_name",
"email": "email",
"class": "class",
"subject": "subject",
}
for param, dbField := range params {
value := r.URL.Query().Get(param)
if value != "" {
query += " AND " + dbField + " = ?"
args = append(args, value)
}
}
return query, args
}
Advanced Sort Order Technique: GET - Sorting nad getting entries based on multiple criteria
.Get() returns a single value. This method returns the first value associated with the sortby key in the query parameters as a string. If there are multiple sortby parameters, only the first one is returned and if the key is not present it returns an empty string.
On the other hand, r.URL.Query()["sortby"] returns a slice of values. This method returns all values associated with the sortby key as a slice of strings. If there are multiple sortby parameters, all of them are returned. And all of them are returned in a slice form. If the key is not present, it returns a nil slice.
So in our case, where we want to handle multiple sorting criteria, r.URL.Query()["sortby"] is more appropriate because it gives us all the sorting parameters as a slice, allowing us to iterate through them and apply multiple sorting conditions.
func addSorting(r *http.Request, query string) string {
sortParams := r.URL.Query()["sortby"]
if len(sortParams) > 0 {
query += " ORDER BY"
for i, param := range sortParams {
parts := strings.Split(param, ":")
if len(parts) != 2 {
continue
}
field, order := parts[0], parts[1]
if !isValidField(field) || !isValidOrder(order) {
continue
}
if i > 0 {
query += ","
}
query += " " + field + " " + order
}
}
return query
}
func isValidOrder(order string) bool {
return order == "asc" || order == "desc"
}
func isValidField(field string) bool {
validFields := map[string]bool{
"first_name": true,
"last_name": true,
"class": true,
"email": true,
"subject": true,
}
return validFields[field]
}
Updating a complete entry - PUT
PUT and PATCH differ from one another in the way that PUT is meant to completely replace the entity. However PATCH is just modifying the entry slightly. If there is a minor modification then in that case we use patch. However, PUT is not used that much.
When we are posting data, we use Exec(). When we are retrieving data, we use Query(). In PUT request, we need to send all the values for all the fields. If we send a blank value, then the black value will be updated.
func updateTeacherHandler(w http.ResponseWriter, r *http.Request){
idStr := strings.TrimPrefix(r.URL.Path, "/teachers/")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
var updatedTeacher models.Teacher
err = json.NewDecoder(r.Body).Decode(&updatedTeacher)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Request Payload", http.StatusBadRequest)
return
}
db, err := sqlconnect.ConnectDb()
if err != nil {
log.Println(err)
http.Error(w, "Unable to connect to database", http.StatusInternalServerError)
return
}
defer db.Close()
var existingTeacher models.Teacher
err = db.QueryRow("SELECT * FROM teachers WHERE id = ?", id).Scan(&existingTeacher.ID, &existingTeacher.Class, &existingTeacher.Email, &existingTeacher.FirstName, &existingTeacher.LastName, &existingTeacher.Subject)
if err != nil {
if err != sql.ErrNoRows {
http.Error(w, "Teacher not found", http.StatusNotFound)
return
}
http.Error(w, "Unable to Retrieve Data", http.StatusInternalServerError)
return
}
updatedTeacher.ID = existingTeacher.ID
_, err = db.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", updatedTeacher.FirstName, updatedTeacher.LastName, updatedTeacher.Email, updatedTeacher.Class, updatedTeacher.Subject, updatedTeacher.ID)
if err != nil {
http.Error(w,"Error Updating teacher", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedTeacher)
}
Modifying an Entry - PATCH
// PATCH /teachers/{id}
func patchTeachersHandler(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/teachers/")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
var updates map[string]interface{}
err = json.NewDecoder(r.Body).Decode(&updates)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Request Payload", http.StatusBadRequest)
return
}
db, err := sqlconnect.ConnectDb()
if err != nil {
log.Println(err)
http.Error(w, "Unable to connect to database", http.StatusInternalServerError)
return
}
defer db.Close()
var existingTeacher models.Teacher
err = db.QueryRow("SELECT id, class, email, first_name, last_name, subject FROM teachers WHERE id = ?", id).Scan(&existingTeacher.ID, &existingTeacher.Class, &existingTeacher.Email, &existingTeacher.FirstName, &existingTeacher.LastName, &existingTeacher.Subject)
if err != nil {
if err != sql.ErrNoRows {
http.Error(w, "Teacher not found", http.StatusNotFound)
return
}
http.Error(w, "Unable to Retrieve Data", http.StatusInternalServerError)
return
}
// Apply updates
for k, v := range updates {
switch k {
case "first_name":
existingTeacher.FirstName = v.(string)
case "last_name":
existingTeacher.LastName = v.(string)
case "email":
existingTeacher.Email = v.(string)
case "class":
existingTeacher.Class = v.(string)
case "subject":
existingTeacher.Subject = v.(string)
}
}
_, err = db.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", existingTeacher.FirstName, existingTeacher.LastName, existingTeacher.Email, existingTeacher.Class, existingTeacher.Subject, existingTeacher.ID)
if err != nil {
http.Error(w, "Error Updating teacher", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(existingTeacher)
}
Improving our PATCH functionality - reflect package
Instead of using switch statements for every field, use the reflect package.
// // Apply updates
// for k, v := range updates {
// switch k {
// case "first_name":
// existingTeacher.FirstName = v.(string)
// case "last_name":
// existingTeacher.LastName = v.(string)
// case "email":
// existingTeacher.Email = v.(string)
// case "class":
// existingTeacher.Class = v.(string)
// case "subject":
// existingTeacher.Subject = v.(string)
// }
// }
// Apply updates using `reflect` package
teacherVal := reflect.ValueOf(&existingTeacher).Elem()
teacherType := teacherVal.Type()
for k, v := range updates {
for i := 0; i < teacherVal.NumField(); i++ {
field := teacherType.Field(i)
if field.Tag.Get("json") == k + ",omitempty" {
if teacherVal.Field(i).CanSet() {
teacherVal.Field(i).Set(reflect.ValueOf(v).Convert(teacherVal.Field(i).Type()))
}
}
}
}
Deleting an entry - DELETE
func deleteTeachersHandler(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/teachers/")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
db, err := sqlconnect.ConnectDb()
if err != nil {
log.Println(err)
http.Error(w, "Unable to connect to database", http.StatusInternalServerError)
return
}
defer db.Close()
result, err := db.Exec("DELETE FROM teachers WHERE id = ?", id)
if err != nil {
http.Error(w, "Error deleting teacher", http.StatusInternalServerError)
return
}
fmt.Println(result.RowsAffected())
rowsAffected, err := result.RowsAffected()
if err != nil {
http.Error(w, "Error retrieving delete result", http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
http.Error(w, "Teacher not found", http.StatusNotFound)
return
}
// Response Body -> Optional
w.Header().Set("Content-Type", "application/json")
response := struct {
Status string `json:"status"`
ID int `json:"id"`
}{
Status: "Teacher deleted successfully",
ID: id,
}
json.NewEncoder(w).Encode(response)
// Return status of NoContent -> Compulsory
w.WriteHeader(http.StatusNoContent)
}
Modernizing Routes:
With Go version 1.22, we can extract path parameters just like we used to extract query parameters. We can just mention the method right before the route and pass it the handler function associates with that HTTP method.
Path with ... with the ellipsis, will include all the values after the intial route as the path parameter associated with the key that we have mentioned before the three dots ....
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
// Method based routing
mux.HandleFunc("POST /items/create", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Item created")
})
// Method based routing
mux.HandleFunc("DELETE /items/create", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Item deleted")
})
// Wildcard in pattern - path parameter
mux.HandleFunc("GET /teachers/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Teacher ID: %s", r.PathValue("id"))
})
// Wildcard with "...."
mux.HandleFunc("/files/{path...}", func (w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Path: %s", r.PathValue("path"))
})
// Confusion when there are two conflicting wildcards
mux.HandleFunc("/path1/{param1}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Param1: %s", r.PathValue("param1"))
})
// mux.HandleFunc("/{param2}/path2", func(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf(w, "Param2: %s", r.PathValue("param2"))
// })
/* Error:
/{param2}/path2 and /files/{path...} both match some paths, like "/files/path2".
But neither is more specific than the other.
/{param2}/path2 matches "/param2/path2", but /files/{path...} doesn't.
/files/{path...} matches "/files/", but /{param2}/path2 doesn't.
*/
mux.HandleFunc("/path2/param2", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Nothing to see here")
})
http.ListenAndServe(":8080", mux)
}
Refactoring MUX
There should me only a single space between the http method and your route. If there is no space or more than one space, our application will not recognize the DELETE method associates with this route.
mux.HandleFunc("DELETE /teachers/", handlers.TeachersHandler)
For now we are focusing on only the /teachers/ route. So the router.go will now look like this:
package router
import (
"net/http"
"school_management_api/internal/api/handlers"
)
func Router() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/", handlers.RootHandler)
mux.HandleFunc("GET /teachers/", handlers.TeachersHandler)
mux.HandleFunc("GET /teachers/{id}", handlers.TeachersHandler)
mux.HandleFunc("POST /teachers/", handlers.TeachersHandler)
mux.HandleFunc("PUT /teachers/", handlers.TeachersHandler)
mux.HandleFunc("PATCH /teachers/", handlers.TeachersHandler)
mux.HandleFunc("PATCH /teachers/{id}", handlers.TeachersHandler)
mux.HandleFunc("DELETE /teachers/", handlers.TeachersHandler)
mux.HandleFunc("DELETE /teachers/{id}", handlers.TeachersHandler)
mux.HandleFunc("/students/", handlers.StudentsHandler)
mux.HandleFunc("/execs/", handlers.ExecsHandler)
return mux
}
Using Path Params for Specific Entry
router/router.go
package router
import (
"net/http"
"school_management_api/internal/api/handlers"
)
func Router() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/", handlers.RootHandler)
mux.HandleFunc("GET /teachers/", handlers.GetTeachersHandler)
mux.HandleFunc("POST /teachers/", handlers.AddTeacherHandler)
mux.HandleFunc("PATCH /teachers/", handlers.PatchTeachersHandler)
mux.HandleFunc("DELETE /teachers/", handlers.DeleteTeachersHandler)
mux.HandleFunc("GET /teachers/{id}", handlers.GetTeacherHandler)
mux.HandleFunc("PUT /teachers/{id}", handlers.UpdateTeacherHandler)
mux.HandleFunc("PATCH /teachers/{id}", handlers.PatchTeachersHandler)
mux.HandleFunc("DELETE /teachers/{id}", handlers.DeleteTeachersHandler)
mux.HandleFunc("/students/", handlers.StudentsHandler)
mux.HandleFunc("/execs/", handlers.ExecsHandler)
return mux
}
handlers/teachers.go
func GetTeachersHandler(w http.ResponseWriter, r *http.Request) {
db, err := sqlconnect.ConnectDb()
if err != nil {
http.Error(w, "Error connecting to database", http.StatusInternalServerError)
return
}
defer db.Close()
query := "SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE 1=1"
var args []interface{}
query, args = addFilters(r, query, args)
query = addSorting(r, query)
rows, err := db.Query(query, args...)
if err != nil {
fmt.Println("err")
http.Error(w, "Database Query Error", http.StatusInternalServerError)
return
}
defer rows.Close()
teacherList := make([]models.Teacher, 0)
for rows.Next() {
teacher := models.Teacher{}
err = rows.Scan(&teacher.ID, &teacher.FirstName, &teacher.LastName, &teacher.Email, &teacher.Class, &teacher.Subject)
if err != nil {
http.Error(w, "Error Scanning the database results", http.StatusInternalServerError)
return
}
teacherList = append(teacherList, teacher)
}
response := struct {
Status string `json:"status"`
Count int `json:"count"`
Data []models.Teacher `json:"data"`
}{
Status: "success",
Count: len(teacherList),
Data: teacherList,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func GetTeacherHandler(w http.ResponseWriter, r *http.Request) {
db, err := sqlconnect.ConnectDb()
if err != nil {
http.Error(w, "Error connecting to database", http.StatusInternalServerError)
return
}
defer db.Close()
idStr := r.PathValue("id")
// Handle Path parameter
id, err := strconv.Atoi(idStr)
if err != nil {
fmt.Println(err)
return
}
var teacher models.Teacher
err = db.QueryRow("SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE id = ?", id).Scan(&teacher.ID, &teacher.Class, &teacher.FirstName, &teacher.LastName, &teacher.Subject, &teacher.Email)
if err == sql.ErrNoRows {
http.Error(w, "Teacher not found", http.StatusNotFound)
return
} else if err != nil {
fmt.Println(err)
http.Error(w, "Database Query Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(teacher)
}
Modifying Multiple Entries - PATCH
A transaction means that we are going to run multiple SQL statementd one after another.
db.Begin() starts a transaction and the default isolation is dependent on the driver. But to simplify it starts a transaction and that's why it returns a transaction sql.Tx and an error.
In Go, when you need to execute a series of SQL statements that should eiter all succeed or all fail. In that case we can use transactions. The database sql package provides a way to handle transactions using db.Begin and then from the resulting transaction we can use tx.Exec to execute statements, tx.Rollback to rollback the execution and tx.Commit to finally commit the transaction.
In the context of databases and SQl, a transaction is a sequence of one or more SQL operations that are executed as a single unit of work. The key characteristics of transaction is that, it ensures ACID properties. ACID stands for Atomicity, Consistency, Isolation and Durability for the operations within it.
-
Atomicity means that all operations within the transaction must succeed, or none should happen at all. If any operation within the transaction failsm the entire transaction is rolled back and the database is left in it's original state.
-
Consistency means that a transaction, brings the database from one valid state to another. If a transaction is successful, the database will be in a consistent state according to all defined rules such as constraints, triggers, etc.
-
Isolation means that the transactions are isolated from each other, meaning that the operation in one transaction are not visible to other transactions until the transaction is committed. This ensures that the transactions do not interfere with each other.
-
Duratbility means, once a transaction is committed, it's changes are permanennt, even in the event of a system failure, the data will persist and it will not be lost.
tx.Exec() executes an SQL statement within the context of a transaction. It is similar to db.Exec() as it also returns a result and an error but it ensures that the operation is part of the transaction. db.Exec() is for individual statement, tx.Exec is part of a transaction and we can use this for insert, update or delete statements within a transaction.
tx.Rollback just simply rolls back the transaction, undoing all the changes made during the transaction. This should be called if an error occurs, and that you want to discard any changes made in the transaction.
tx.Commit() commits the transaction applying all the changes made during the transaction and this should be called when all operations within the transaction succeeded and that you want to make the changes permanent.
Since we implemented transactions, transaction either finishes completely means either it is executed successfully completely or it fails completely. There's no partial success or partial failure when it comes to transactions.
// PATCH /teachers/
func PatchTeachersHandler(w http.ResponseWriter, r *http.Request) {
db, err := sqlconnect.ConnectDb()
if err != nil {
log.Println(err)
http.Error(w, "Unable to connect to databse", http.StatusInternalServerError)
return
}
defer db.Close()
var updates []map[string]interface{}
err = json.NewDecoder(r.Body).Decode(&updates)
if err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
tx, err := db.Begin()
if err != nil {
log.Println(err)
http.Error(w, "Error starting transaction", http.StatusInternalServerError)
return
}
for _, update := range updates {
id, ok := update["id"].(string)
if !ok {
tx.Rollback()
http.Error(w, "Invalid teacher ID in update", http.StatusBadRequest)
return
}
var teacherFromDb models.Teacher
err := db.QueryRow("SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE id = ?", id).Scan(&teacherFromDb.ID, &teacherFromDb.FirstName, &teacherFromDb.LastName, &teacherFromDb.Email, &teacherFromDb.Class, &teacherFromDb.Subject)
if err != nil {
tx.Rollback()
if err == sql.ErrNoRows {
http.Error(w, "Teacher not found", http.StatusNotFound)
return
}
http.Error(w, "Error retrieving teacher", http.StatusInternalServerError)
return
}
// Applu updates using reflection
teacherVal := reflect.ValueOf(&teacherFromDb).Elem()
teacherType := teacherVal.Type()
for k, v := range update {
if k == "id" {
continue // skip updating the fields
}
for i := 0; i < teacherVal.NumField(); i++ {
field := teacherType.Field(i)
if field.Tag.Get("json") == k+",omitempty" {
fieldVal := teacherVal.Field(i)
if fieldVal.CanSet() {
val := reflect.ValueOf(v)
if val.Type().ConvertibleTo(fieldVal.Type()) {
fieldVal.Set(val.Convert(fieldVal.Type()))
} else {
tx.Rollback()
log.Printf("cannot convert %v to %v", val.Type(), fieldVal.Type())
return
}
}
break
}
}
}
_, err = tx.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", teacherFromDb.FirstName, teacherFromDb.LastName, teacherFromDb.Email, teacherFromDb.Class, teacherFromDb.Subject, teacherFromDb.ID)
if err != nil {
tx.Rollback()
http.Error(w, "Error updating teacher", http.StatusInternalServerError)
return
}
}
// Commit the transaction
err = tx.Commit()
if err != nil {
http.Error(w, "Error comitting transaction", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
Deleting Multiple Entries : Delete
func DeleteTeachersHandler(w http.ResponseWriter, r *http.Request){
db, err := sqlconnect.ConnectDb()
if err != nil {
log.Println(err)
http.Error(w, "Unable to connect to database", http.StatusInternalServerError)
return
}
defer db.Close()
var ids []int
err = json.NewDecoder(r.Body).Decode(&ids)
if err != nil {
log.Println(err)
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
tx, err := db.Begin()
if err != nil {
log.Println(err)
http.Error(w, "Error starting transaction", http.StatusInternalServerError)
return
}
stmt, err := tx.Prepare("DELETE FROM teachers WHERE id = ?")
if err != nil {
log.Println(err)
tx.Rollback()
http.Error(w, "Error preparing delete statement", http.StatusInternalServerError)
return
}
defer stmt.Close()
deleteIds := []int{}
for _, id := range ids {
result, err := stmt.Exec(id)
if err != nil {
tx.Rollback()
log.Println(err)
http.Error(w, "Error deleting teacher", http.StatusInternalServerError)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
tx.Rollback()
http.Error(w, "Error retrieving deleted result", http.StatusInternalServerError)
return
}
// If teacher was deleted then add ID to the deletedIDs slice
if rowsAffected > 0{
deleteIds = append(deleteIds, id)
}
if rowsAffected < 1 {
tx.Rollback()
http.Error(w, fmt.Sprintf("ID %d does not exist",id), http.StatusInternalServerError)
return
}
}
// Commit
err = tx.Commit()
if err != nil {
log.Println(err)
http.Error(w, "Error Commiting transaction", http.StatusInternalServerError)
return
}
if len(deleteIds) < 1 {
http.Error(w, "IDs do not exist", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
response := struct {
Status string `json:"status"`
DeletedIDs []int `json:"deleted_ids"`
}{
Status: "Teachers successfully deleted",
DeletedIDs: deleteIds,
}
json.NewEncoder(w).Encode(response)
}
Modelling Data
In API development, a model represents the structure of a data that your application handles. Models define the shape of the data, including the types of each field,relationships between different data types and any constraints or validations. Models provide a clear and structured way to represent the data in our application. This helps in maintaining consistency and understanding the data flow. By defining models, we can enfore validation rulesm ensuring that the data entering your system is in the expected format. Models act as an abstraction layer between the database and the application logic. This makes the application more modular and easier to maintain.
Proper use of models can prevent common security issues like SQL injection by ensuring that data is correctly sanitized and validated. When handling APi request, we can use models to ensure data is correctly parsed and validated.
Models are used for
- Data Representation
- Data Validation
- Abstraction
- Documentation
- Security
Industry Standards for Models:
- Naming Conventions
- Field Tags
- Validation
- Modularity
Importance
- Data integrity
- Maintainability
- Scalability
- Security
What happens if we don't use Models ?
- Inconsistent Data
- Code Duplication
- Difficult Maintenance
- Security Risks
Best Practices
- Keep Models Simple
- Use Field Tags
- Document Models
- Versioning
Common Pitfalls
- Tight Coupling
- Overcomplicating Models
- Ignoring Validation
- Lack of Documentation
The concept of Data Modelling is simple. Data Modelling takes unstructured data and then transforms it into a structured data. In Go, we are using struct to structure that unstructured data.
models/student.go
package models
type Student struct {
ID int `json:"id,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Email string `json:"email,omitempty"`
Class string `json:"class,omitempty"`
}
Refactoring Database Operations
So, until now, our teacher's handlers are too big and we have included the database operations inside the teachers handlers as well. So now, we are going to extract the database operations into a separate function. And that refactoring is pretty simple. So all you need to do first is bring the statements together which are related to database.
All the error responses are going to go from the handler functions, not from the database handlers.
internal/api/handlers/teachers.go
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"school_management_api/internal/models"
"school_management_api/internal/repository/sqlconnect"
"strconv"
)
func GetTeachersHandler(w http.ResponseWriter, r *http.Request) {
var teachers []models.Teacher
teachers, err := sqlconnect.GetTeachersDbHandler(teachers, r)
if err != nil {
return
}
response := struct {
Status string `json:"status"`
Count int `json:"count"`
Data []models.Teacher `json:"data"`
}{
Status: "success",
Count: len(teachers),
Data: teachers,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func GetOneTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
fmt.Println(err)
return
}
teacher, err := sqlconnect.GetTeacherByID(id)
if err != nil {
fmt.Println(err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(teacher)
}
func AddTeacherHandler(w http.ResponseWriter, r *http.Request) {
var newTeachers []models.Teacher
err := json.NewDecoder(r.Body).Decode(&newTeachers)
if err != nil {
http.Error(w, "Invalid Request Body", http.StatusBadRequest)
return
}
addedTeachers, err := sqlconnect.AddTeachersDBHandler(newTeachers)
if err != nil {
fmt.Println(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
response := struct {
Status string `json:"status"`
Count int `json:"count"`
Data []models.Teacher `json:"data"`
}{
Status: "success",
Count: len(addedTeachers),
Data: addedTeachers,
}
json.NewEncoder(w).Encode(response)
}
func UpdateTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
var updatedTeacher models.Teacher
err = json.NewDecoder(r.Body).Decode(&updatedTeacher)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Request Payload", http.StatusBadRequest)
return
}
updatedTeacherFromDb, err := sqlconnect.UpdateTeacher(id, updatedTeacher)
if err != nil {
log.Println(err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedTeacherFromDb)
func PatchTeachersHandler(w http.ResponseWriter, r *http.Request) {
var updates []map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&updates)
if err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
err = sqlconnect.PatchTeachers(updates)
if err != nil {
log.Println(err)
return
}
w.WriteHeader(http.StatusNoContent)
func PatchOneTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
var updates map[string]interface{}
err = json.NewDecoder(r.Body).Decode(&updates)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Request Payload", http.StatusBadRequest)
return
}
updatedTeacher, err := sqlconnect.PatchOneTeacher(id, updates)
if err != nil {
log.Println(err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedTeacher)
}
func DeleteOneTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
err = sqlconnect.DeleteOneTeacher(id)
if err != nil {
log.Println(err)
return
}
w.Header().Set("Content-Type", "application/json")
response := struct {
Status string `json:"status"`
ID int `json:"id"`
}{
Status: "Teacher deleted successfully",
ID: id,
}
json.NewEncoder(w).Encode(response)
w.WriteHeader(http.StatusNoContent)
func DeleteTeachersHandler(w http.ResponseWriter, r *http.Request) {
var ids []int
err := json.NewDecoder(r.Body).Decode(&ids)
if err != nil {
log.Println(err)
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
deleteIds, err := sqlconnect.DeleteTeachers(ids)
if err != nil {
return
}
w.Header().Set("Content-Type", "application/json")
response := struct {
Status string `json:"status"`
DeletedIDs []int `json:"deleted_ids"`
}{
Status: "Teachers successfully deleted",
DeletedIDs: deleteIds,
}
json.NewEncoder(w).Encode(response)
}
repository/sqlconnect/teachers_crud.go
package sqlconnect
import (
"database/sql"
"fmt"
"log"
"net/http"
"reflect"
"school_management_api/internal/models"
"strings"
)
func isValidOrder(order string) bool {
return order == "asc" || order == "desc"
}
func isValidField(field string) bool {
validFields := map[string]bool{
"first_name": true,
"last_name": true,
"class": true,
"email": true,
"subject": true,
}
return validFields[field]
}
func addSorting(r *http.Request, query string) string {
sortParams := r.URL.Query()["sortby"]
if len(sortParams) > 0 {
query += " ORDER BY"
for i, param := range sortParams {
parts := strings.Split(param, ":")
if len(parts) != 2 {
continue
}
field, order := parts[0], parts[1]
if !isValidField(field) || !isValidOrder(order) {
continue
}
if i > 0 {
query += ","
}
query += " " + field + " " + order
}
}
return query
}
func addFilters(r *http.Request, query string, args []interface{}) (string, []interface{}) {
params := map[string]string{
"first_name": "first_name",
"last_name": "last_name",
"email": "email",
"class": "class",
"subject": "subject",
}
for param, dbField := range params {
value := r.URL.Query().Get(param)
if value != "" {
query += " AND " + dbField + " = ?"
args = append(args, value)
}
}
return query, args
}
func GetTeachersDbHandler(teachers []models.Teacher, r *http.Request) ([]models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
return nil, err
}
defer db.Close()
query := "SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE 1=1"
var args []interface{}
query, args = addFilters(r, query, args)
query = addSorting(r, query)
rows, err := db.Query(query, args...)
if err != nil {
fmt.Println("err")
return nil, err
}
defer rows.Close()
for rows.Next() {
teacher := models.Teacher{}
err = rows.Scan(&teacher.ID, &teacher.FirstName, &teacher.LastName, &teacher.Email, &teacher.Class, &teacher.Subject)
if err != nil {
return nil, err
}
teachers = append(teachers, teacher)
}
return teachers, nil
}
func GetTeacherByID(id int) (models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
return models.Teacher{}, err
}
defer db.Close()
var teacher models.Teacher
err = db.QueryRow("SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE id = ?", id).Scan(&teacher.ID, &teacher.Class, &teacher.FirstName, &teacher.LastName, &teacher.Subject, &teacher.Email)
if err == sql.ErrNoRows {
return models.Teacher{}, err
} else if err != nil {
fmt.Println(err)
return models.Teacher{}, err
}
return teacher, nil
}
func AddTeachersDBHandler(newTeachers []models.Teacher) ([]models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
return nil, err
}
defer db.Close()
stmt, err := db.Prepare("INSERT INTO teachers (first_name, last_name, email, class, subject) VALUES (?,?,?,?,?)")
if err != nil {
return nil, err
}
defer stmt.Close()
addedTeachers := make([]models.Teacher, len(newTeachers))
for i, newTeacher := range newTeachers {
res, err := stmt.Exec(newTeacher.FirstName, newTeacher.LastName, newTeacher.Email, newTeacher.Class, newTeacher.Subject)
if err != nil {
return nil, err
}
lastID, err := res.LastInsertId()
if err != nil {
return nil, err
}
newTeacher.ID = int(lastID)
addedTeachers[i] = newTeacher
}
return addedTeachers, nil
}
func UpdateTeacher(id int, updatedTeacher models.Teacher) (models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
log.Println(err)
return models.Teacher{}, err
}
defer db.Close()
var existingTeacher models.Teacher
err = db.QueryRow("SELECT id, class, email, first_name, last_name, subject FROM teachers WHERE id = ?", id).Scan(&existingTeacher.ID, &existingTeacher.Class, &existingTeacher.Email, &existingTeacher.FirstName, &existingTeacher.LastName, &existingTeacher.Subject)
if err != nil {
if err != sql.ErrNoRows {
return models.Teacher{}, err
}
return models.Teacher{}, err
}
updatedTeacher.ID = existingTeacher.ID
_, err = db.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", updatedTeacher.FirstName, updatedTeacher.LastName, updatedTeacher.Email, updatedTeacher.Class, updatedTeacher.Subject, updatedTeacher.ID)
if err != nil {
return models.Teacher{}, err
}
return updatedTeacher, nil
}
func PatchTeachers(updates []map[string]interface{}) error {
db, err := ConnectDb()
if err != nil {
log.Println(err)
return err
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
log.Println(err)
return err
}
for _, update := range updates {
id, ok := update["id"].(string)
if !ok {
tx.Rollback()
return err
}
var teacherFromDb models.Teacher
err := db.QueryRow("SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE id = ?", id).Scan(&teacherFromDb.ID, &teacherFromDb.FirstName, &teacherFromDb.LastName, &teacherFromDb.Email, &teacherFromDb.Class, &teacherFromDb.Subject)
if err != nil {
tx.Rollback()
if err == sql.ErrNoRows {
return err
}
return err
}
teacherVal := reflect.ValueOf(&teacherFromDb).Elem()
teacherType := teacherVal.Type()
for k, v := range update {
if k == "id" {
contin
}
for i := 0; i < teacherVal.NumField(); i++ {
field := teacherType.Field(i)
if field.Tag.Get("json") == k+",omitempty" {
fieldVal := teacherVal.Field(i)
if fieldVal.CanSet() {
val := reflect.ValueOf(v)
if val.Type().ConvertibleTo(fieldVal.Type()) {
fieldVal.Set(val.Convert(fieldVal.Type()))
} else {
tx.Rollback()
log.Printf("cannot convert %v to %v", val.Type(), fieldVal.Type())
return err
}
}
break
}
}
}
_, err = tx.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", teacherFromDb.FirstName, teacherFromDb.LastName, teacherFromDb.Email, teacherFromDb.Class, teacherFromDb.Subject, teacherFromDb.ID)
if err != nil {
tx.Rollback()
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func PatchOneTeacher(id int, updates map[string]interface{}) (models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
log.Println(err)
return models.Teacher{}, err
}
defer db.Close()
var existingTeacher models.Teacher
err = db.QueryRow("SELECT id, class, email, first_name, last_name, subject FROM teachers WHERE id = ?", id).Scan(&existingTeacher.ID, &existingTeacher.Class, &existingTeacher.Email, &existingTeacher.FirstName, &existingTeacher.LastName, &existingTeacher.Subject)
if err != nil {
if err != sql.ErrNoRows {
return models.Teacher{}, err
}
return models.
Teacher{}, err
}
teacherVal := reflect.ValueOf(&existingTeacher).Elem()
teacherType := teacherVal.Type(
for k, v := range updates {
for i := 0; i < teacherVal.NumField(); i++ {
field := teacherType.Field(i)
if field.Tag.Get("json") == k+",omitempty" {
if teacherVal.Field(i).CanSet() {
teacherVal.Field(i).Set(reflect.ValueOf(v).Convert(teacherVal.Field(i).Type()))
}
}
}
}
_, err = db.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", existingTeacher.FirstName, existingTeacher.LastName, existingTeacher.Email, existingTeacher.Class, existingTeacher.Subject, existingTeacher.ID)
if err != nil {
return models.Teacher{}, err
}
return existingTeacher, nil
}
func DeleteOneTeacher(id int) error {
db, err := ConnectDb()
if err != nil {
log.Println(err)
return err
}
defer db.Close()
result, err := db.Exec("DELETE FROM teachers WHERE id = ?", id)
if err != nil {
return err
}
fmt.Println(result.RowsAffected())
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return err
}
return err
}
func DeleteTeachers(ids []int) ([]int, error) {
db, err := ConnectDb()
if err != nil {
log.Println(err)
return nil, err
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
log.Println(err)
return nil, err
}
stmt, err := tx.Prepare("DELETE FROM teachers WHERE id = ?")
if err != nil {
log.Println(err)
tx.Rollback()
return nil, err
}
defer stmt.Close()
deleteIds := []int{}
for _, id := range ids {
result, err := stmt.Exec(id)
if err != nil {
tx.Rollback()
log.Println(err)
return nil, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
tx.Rollback()
return nil, err
}
if rowsAffected > 0 {
deleteIds = append(deleteIds, id)
}
if rowsAffected < 1 {
tx.Rollback()
return nil, err
}
}
err = tx.Commit()
if err != nil {
log.Println(err)
return nil, err
}
if len(deleteIds) < 1 {
return nil, err
}
return deleteIds, nil
}
Error Handling
pkg/utils/error_handler.go
package utils
import (
"fmt"
"log"
"os"
)
func ErrorHandler(err error, message string) error {
errorLogger := log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
errorLogger.Println(message, err)
return fmt.Errorf("%s", message)
}
The reason, we are not sending the error as a return value from this function is because we want to keep the error, the actual error as private. Private means that it will be available to us. We can read the error and we will get the error onto out logger, but the client should not get the actual error. The client should get the message string, the error string that we send to the client. We want to make a customized generic error string, an error message that we want to send to the client. We don't want actual technical errors sent to a user, because those errors are of no use to the user and sometimes we actually don't want the user to know the exact error. Sometimes less is more and that's why we want to communicate less information about the exact error. But we want to send a generic message to the end user.
/repository/sqlconnect/teachers_crud.go
package sqlconnect
import (
"database/sql"
"fmt"
"log"
"net/http"
"reflect"
"school_management_api/internal/models"
"school_management_api/pkg/utils"
"strconv"
"strings"
)
func isValidOrder(order string) bool {
return order == "asc" || order == "desc"
}
func isValidField(field string) bool {
validFields := map[string]bool{
"first_name": true,
"last_name": true,
"class": true,
"email": true,
"subject": true,
}
return validFields[field]
}
func addSorting(r *http.Request, query string) string {
sortParams := r.URL.Query()["sortby"]
if len(sortParams) > 0 {
query += " ORDER BY"
for i, param := range sortParams {
parts := strings.Split(param, ":")
if len(parts) != 2 {
continue
}
field, order := parts[0], parts[1]
if !isValidField(field) || !isValidOrder(order) {
continue
}
if i > 0 {
query += ","
}
query += " " + field + " " + order
}
}
return query
}
func addFilters(r *http.Request, query string, args []interface{}) (string, []interface{}) {
params := map[string]string{
"first_name": "first_name",
"last_name": "last_name",
"email": "email",
"class": "class",
"subject": "subject",
}
for param, dbField := range params {
value := r.URL.Query().Get(param)
if value != "" {
query += " AND " + dbField + " = ?"
args = append(args, value)
}
}
return query, args
}
func GetTeachersDbHandler(teachers []models.Teacher, r *http.Request) ([]models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
return nil, utils.ErrorHandler(err, "error retrieving data")
}
defer db.Close()
query := "SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE 1=1"
var args []interface{}
query, args = addFilters(r, query, args)
query = addSorting(r, query)
rows, err := db.Query(query, args...)
if err != nil {
fmt.Println("err")
return nil, utils.ErrorHandler(err, "error retrieving data")
}
defer rows.Close()
for rows.Next() {
teacher := models.Teacher{}
err = rows.Scan(&teacher.ID, &teacher.FirstName, &teacher.LastName, &teacher.Email, &teacher.Class, &teacher.Subject)
if err != nil {
return nil, utils.ErrorHandler(err, "error retrieving data")
}
teachers = append(teachers, teacher)
}
return teachers, nil
}
func GetTeacherByID(id int) (models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
return models.Teacher{}, utils.ErrorHandler(err, "error retrieving data ")
}
defer db.Close()
var teacher models.Teacher
err = db.QueryRow("SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE id = ?", id).Scan(&teacher.ID, &teacher.Class, &teacher.FirstName, &teacher.LastName, &teacher.Subject, &teacher.Email)
if err == sql.ErrNoRows {
return models.Teacher{}, utils.ErrorHandler(err, "error retrieving data ")
} else if err != nil {
fmt.Println(err)
return models.Teacher{}, utils.ErrorHandler(err, "error retrieving data ")
}
return teacher, nil
}
func AddTeachersDBHandler(newTeachers []models.Teacher) ([]models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
return nil, utils.ErrorHandler(err, "error adding data")
}
defer db.Close()
stmt, err := db.Prepare("INSERT INTO teachers (first_name, last_name, email, class, subject) VALUES (?,?,?,?,?)")
if err != nil {
return nil, utils.ErrorHandler(err, "error adding data")
}
defer stmt.Close()
addedTeachers := make([]models.Teacher, len(newTeachers))
for i, newTeacher := range newTeachers {
res, err := stmt.Exec(newTeacher.FirstName, newTeacher.LastName, newTeacher.Email, newTeacher.Class, newTeacher.Subject)
if err != nil {
return nil, utils.ErrorHandler(err, "error adding data")
}
lastID, err := res.LastInsertId()
if err != nil {
return nil, utils.ErrorHandler(err, "error adding data")
}
newTeacher.ID = int(lastID)
addedTeachers[i] = newTeacher
}
return addedTeachers, nil
}
func UpdateTeacher(id int, updatedTeacher models.Teacher) (models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
return models.Teacher{}, utils.ErrorHandler(err, "error updating data")
}
defer db.Close()
var existingTeacher models.Teacher
err = db.QueryRow("SELECT id, class, email, first_name, last_name, subject FROM teachers WHERE id = ?", id).Scan(&existingTeacher.ID, &existingTeacher.Class, &existingTeacher.Email, &existingTeacher.FirstName, &existingTeacher.LastName, &existingTeacher.Subject)
if err != nil {
if err != sql.ErrNoRows {
return models.Teacher{}, utils.ErrorHandler(err, "error updating data")
}
return models.Teacher{}, utils.ErrorHandler(err, "error updating data")
}
updatedTeacher.ID = existingTeacher.ID
_, err = db.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", updatedTeacher.FirstName, updatedTeacher.LastName, updatedTeacher.Email, updatedTeacher.Class, updatedTeacher.Subject, updatedTeacher.ID)
if err != nil {
return models.Teacher{}, utils.ErrorHandler(err, "error updating data")
}
return updatedTeacher, nil
}
func PatchTeachers(updates []map[string]interface{}) error {
db, err := ConnectDb()
if err != nil {
return utils.ErrorHandler(err, "error updating data")
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
return utils.ErrorHandler(err, "error updating data")
}
for _, update := range updates {
idStr, ok := update["id"].(string)
if !ok {
tx.Rollback()
return utils.ErrorHandler(err, "Invalid id")
}
id, err := strconv.Atoi(idStr)
if err != nil {
tx.Rollback()
return utils.ErrorHandler(err, "invalid id")
}
var teacherFromDb models.Teacher
err = db.QueryRow("SELECT id, first_name, last_name, email, class, subject FROM teachers WHERE id = ?", id).Scan(&teacherFromDb.ID, &teacherFromDb.FirstName, &teacherFromDb.LastName, &teacherFromDb.Email, &teacherFromDb.Class, &teacherFromDb.Subject)
if err != nil {
tx.Rollback()
if err == sql.ErrNoRows {
return utils.ErrorHandler(err, "Teacher Not Found")
}
return utils.ErrorHandler(err, "error updating data")
}
teacherVal := reflect.ValueOf(&teacherFromDb).Elem()
teacherType := teacherVal.Type()
for k, v := range update {
if k == "id" {
contin
}
for i := 0; i < teacherVal.NumField(); i++ {
field := teacherType.Field(i)
if field.Tag.Get("json") == k+",omitempty" {
fieldVal := teacherVal.Field(i)
if fieldVal.CanSet() {
val := reflect.ValueOf(v)
if val.Type().ConvertibleTo(fieldVal.Type()) {
fieldVal.Set(val.Convert(fieldVal.Type()))
} else {
tx.Rollback()
log.Printf("cannot convert %v to %v", val.Type(), fieldVal.Type())
return utils.ErrorHandler(err, "error updating data")
}
}
break
}
}
}
_, err = tx.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", teacherFromDb.FirstName, teacherFromDb.LastName, teacherFromDb.Email, teacherFromDb.Class, teacherFromDb.Subject, teacherFromDb.ID)
if err != nil {
tx.Rollback()
return utils.ErrorHandler(err, "error updating data")
}
}
err = tx.Commit()
if err != nil {
return utils.ErrorHandler(err, "error updating data")
}
return nil
}
func PatchOneTeacher(id int, updates map[string]interface{}) (models.Teacher, error) {
db, err := ConnectDb()
if err != nil {
log.Println(err)
return models.Teacher{}, utils.ErrorHandler(err, "error updating data")
}
defer db.Close()
var existingTeacher models.Teacher
err = db.QueryRow("SELECT id, class, email, first_name, last_name, subject FROM teachers WHERE id = ?", id).Scan(&existingTeacher.ID, &existingTeacher.Class, &existingTeacher.Email, &existingTeacher.FirstName, &existingTeacher.LastName, &existingTeacher.Subject)
if err != nil {
if err != sql.ErrNoRows {
return models.Teacher{}, utils.ErrorHandler(err, "Teacher not Found")
}
return models.Teacher{}, utils.ErrorHandler(err, "error updating data")
teacherVal := reflect.ValueOf(&existingTeacher).Elem()
teacherType := teacherVal.Type(
for k, v := range updates {
for i := 0; i < teacherVal.NumField(); i++ {
field := teacherType.Field(i)
if field.Tag.Get("json") == k+",omitempty" {
if teacherVal.Field(i).CanSet() {
teacherVal.Field(i).Set(reflect.ValueOf(v).Convert(teacherVal.Field(i).Type()))
}
}
}
}
_, err = db.Exec("UPDATE teachers SET first_name = ?, last_name = ?, email = ?, class = ?, subject = ? WHERE id = ?", existingTeacher.FirstName, existingTeacher.LastName, existingTeacher.Email, existingTeacher.Class, existingTeacher.Subject, existingTeacher.ID)
if err != nil {
return models.Teacher{}, utils.ErrorHandler(err, "error updating data")
}
return existingTeacher, nil
}
func DeleteOneTeacher(id int) error {
db, err := ConnectDb()
if err != nil {
log.Println(err)
return utils.ErrorHandler(err, "error deleting data")
}
defer db.Close()
result, err := db.Exec("DELETE FROM teachers WHERE id = ?", id)
if err != nil {
return utils.ErrorHandler(err, "error deleting data")
}
fmt.Println(result.RowsAffected())
rowsAffected, err := result.RowsAffected()
if err != nil {
return utils.ErrorHandler(err, "error deleting data")
}
if rowsAffected == 0 {
return utils.ErrorHandler(err, "teacher not found")
}
return nil
}
func DeleteTeachers(ids []int) ([]int, error) {
db, err := ConnectDb()
if err != nil {
return nil, utils.ErrorHandler(err, "error deleting data")
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
return nil, utils.ErrorHandler(err, "error deleting data")
}
stmt, err := tx.Prepare("DELETE FROM teachers WHERE id = ?")
if err != nil {
tx.Rollback()
return nil, utils.ErrorHandler(err, "error deleting data")
}
defer stmt.Close()
deleteIds := []int{}
for _, id := range ids {
result, err := stmt.Exec(id)
if err != nil {
tx.Rollback()
return nil, utils.ErrorHandler(err, "error deleting data")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
tx.Rollback()
return nil, utils.ErrorHandler(err, "error deleting data")
}
if rowsAffected > 0 {
deleteIds = append(deleteIds, id)
}
if rowsAffected < 1 {
tx.Rollback()
return nil, utils.ErrorHandler(err, fmt.Sprintf("ID %d does not exist", id))
}
}
err = tx.Commit()
if err != nil {
log.Println(err)
return nil, utils.ErrorHandler(err, "error deleting data")
}
if len(deleteIds) < 1 {
return nil, utils.ErrorHandler(err, "IDs do not exist")
}
return deleteIds, nil
}
/internal/api/handler/teachers.go
package handlers
import (
"encoding/json"
"log"
"net/http"
"school_management_api/internal/models"
"school_management_api/internal/repository/sqlconnect"
"strconv"
// "sync"
)
func GetTeachersHandler(w http.ResponseWriter, r *http.Request) {
var teachers []models.Teacher
teachers, err := sqlconnect.GetTeachersDbHandler(teachers, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
response := struct {
Status string `json:"status"`
Count int `json:"count"`
Data []models.Teacher `json:"data"`
}{
Status: "success",
Count: len(teachers),
Data: teachers,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func GetOneTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
// Handle Path parameter
id, err := strconv.Atoi(idStr)
if err != nil {
// fmt.Println(err)
http.Error(w, "invalid ID", http.StatusBadRequest)
return
}
teacher, err := sqlconnect.GetTeacherByID(id)
if err != nil {
// fmt.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(teacher)
}
func AddTeacherHandler(w http.ResponseWriter, r *http.Request) {
var newTeachers []models.Teacher
err := json.NewDecoder(r.Body).Decode(&newTeachers)
if err != nil {
http.Error(w, "Invalid Request Body", http.StatusBadRequest)
return
}
addedTeachers, err := sqlconnect.AddTeachersDBHandler(newTeachers)
if err != nil {
// fmt.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
response := struct {
Status string `json:"status"`
Count int `json:"count"`
Data []models.Teacher `json:"data"`
}{
Status: "success",
Count: len(addedTeachers),
Data: addedTeachers,
}
json.NewEncoder(w).Encode(response)
}
func UpdateTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
var updatedTeacher models.Teacher
err = json.NewDecoder(r.Body).Decode(&updatedTeacher)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Request Payload", http.StatusBadRequest)
return
}
updatedTeacherFromDb, err := sqlconnect.UpdateTeacher(id, updatedTeacher)
if err != nil {
// log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedTeacherFromDb)
}
// PATCH /teachers/
func PatchTeachersHandler(w http.ResponseWriter, r *http.Request) {
var updates []map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&updates)
if err != nil {
log.Println(err)
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
err = sqlconnect.PatchTeachers(updates)
if err != nil {
// log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
// PATCH /teachers/{id}
func PatchOneTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
var updates map[string]interface{}
err = json.NewDecoder(r.Body).Decode(&updates)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Request Payload", http.StatusBadRequest)
return
}
updatedTeacher, err := sqlconnect.PatchOneTeacher(id, updates)
if err != nil {
// log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedTeacher)
}
func DeleteOneTeacherHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
log.Println(err)
http.Error(w, "Invalid Teacher ID", http.StatusBadRequest)
return
}
err = sqlconnect.DeleteOneTeacher(id)
if err != nil {
// log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Response Body -> Optional
w.Header().Set("Content-Type", "application/json")
response := struct {
Status string `json:"status"`
ID int `json:"id"`
}{
Status: "Teacher deleted successfully",
ID: id,
}
json.NewEncoder(w).Encode(response)
w.WriteHeader(http.StatusNoContent)
}
func DeleteTeachersHandler(w http.ResponseWriter, r *http.Request) {
var ids []int
err := json.NewDecoder(r.Body).Decode(&ids)
if err != nil {
log.Println(err)
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
deleteIds, err := sqlconnect.DeleteTeachers(ids)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
response := struct {
Status string `json:"status"`
DeletedIDs []int `json:"deleted_ids"`
}{
Status: "Teachers successfully deleted",
DeletedIDs: deleteIds,
}
json.NewEncoder(w).Encode(response)
}
Struct Tags
In our functions, we are preparing SQL statements manually. So DB tags are not directly utilized for database operations but for better readability and maintainability. Using DB tags can still be beneficial if you decide to use an ORM (Object Relational Mapping library) using the sqlx or gorm libraries.
func generateInsertQuery(model interface{}) string {
modelType := reflect.TypeOf(model)
var columns, placeholders string
for i := 0; i < modelType.NumField(); i++ {
dbTag := modelType.Field(i).Tag.Get("db")
dbTag = strings.TrimSuffix(dbTag, ",omitempty")
fmt.Println("dbTag:", dbTag)
if dbTag != "" && dbTag != "id" { // skip the ID field if it's auto increment
if columns != "" {
columns += ", "
placeholders += ", "
}
columns += dbTag
placeholders += "?"
}
}
fmt.Printf("INSERT INTO teachers (%s) VALUES (%s)\n", columns, placeholders)
return fmt.Sprintf("INSERT INTO teachers (%s) VALUE (%s)", columns, placeholders)
}
func getStructValues(model interface{}) []interface{} {
modelValue := reflect.ValueOf(model)
modelType := modelValue.Type()
values := []interface{}{}
for i := 0; i < modelType.NumField(); i++ {
dbTag := modelType.Field(i).Tag.Get("db")
if dbTag != "" && dbTag != "id,omitempty" {
values = append(values, modelValue.Field(i).Interface())
}
}
log.Println("Values:", values)
return values
}
Data Validation
Data validation is the process of ensuring that incoming data is accurate, complete and meets specific requirements before it is processed or stored. In the context of APIs, data validation is crucial for maintaining the integrity of your application and ensuring a good user experience.
Importance :
- Security
- Data Integrity
- User Experience
Validating data helps protect against malicious inputs that could lead to vulnerabilities like SQL injection or cross-site scripting. Data validation also ensures that the data stored in your database is accurate and reliable. By providing meaningful error messages you guide users to submit the correct data enhancing their experience.
Types of Data Validation
- Format Validation
- Presence Validation
- Type Validation
- Value Validation
- Length Validation
Best Practices
- Validate Early
- Provide Clear Error Messages
- Use libraries and frameworks
- Implement server-side validation
- Be consistent
Common Pitfalls :
- Overly Restrictive Validation
- Neglecting Security
- Ignoring Data Types
- Inadequate Testing
When we are making servers, we are making a protection layer, a firewall, a line of defense for our database in a way. Moreover database operations are expensive as in time and resource consuming, so our server should prevent such cases from reaching the API. It needs to be the strongest line of defense against any illegal operation, against our database whether retrieval of data or posting of data. But other than our servers, there are other lines of defenses like middleware proxies, our front end and many more. Our main asset is the database and server is the strong titanium gate that needs to protect it.
In Conclusion, data validatino is a critical aspect of building robust APIs. It not only enhances security and data integrity, but also improves user experience by guiding users towards the correct input format. By following best practices and avoiding pitfalls, you can create a solid foundation for your API project.
internal/api/handler/teachers.go
func AddTeachersHandler(w http.ResponseWriter, r *http.Request) {
var newTeachers []models.Teacher
var rawTeachers []map[string]interface{}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request Body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
err = json.Unmarshal(body, &newTeachers)
if err != nil {
http.Error(w, "Invalid Request Body", http.StatusBadRequest)
return
}
fmt.Println(rawTeachers)
fields := GetFieldNames(models.Teacher{})
allowedFields := make(map[string]struct{})
for _, field := range fields {
allowedFields[field] = struct{}{}
}
for _, teacher := range rawTeachers {
for key := range teacher {
_, ok := allowedFields[key]
if !ok {
http.Error(w, "Unacceptable field found in request. Only use allowed fields.", http.StatusBadRequest)
}
}
}
err = json.Unmarshal(body, &rawTeachers)
if err != nil {
http.Error(w, "Invalid Request Body", http.StatusBadRequest)
fmt.Println("New Teachers:", newTeachers)
return
}
for _, teacher := range newTeachers {
err = CheckBlankFields(teacher)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
addedTeachers, err := sqlconnect.AddTeachersDBHandler(newTeachers)
if err != nil {
// fmt.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
response := struct {
Status string `json:"status"`
Count int `json:"count"`
Data []models.Teacher `json:"data"`
}{
Status: "success",
Count: len(addedTeachers),
Data: addedTeachers,
}
json.NewEncoder(w).Encode(response)
}
internal/api/handlers/helpers.go
package handlers
import (
"errors"
"reflect"
"school_management_api/pkg/utils"
"strings"
)
func CheckBlankFields(value interface{}) error {
val := reflect.ValueOf(value)
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if field.Kind() == reflect.String && field.String() == "" {
// http.Error(w, "All fields are required", http.StatusBadRequest)
return utils.ErrorHandler(errors.New("all fields are required"), "All fields are required")
}
}
return nil
}
func GetFieldNames(model interface{}) []string {
val := reflect.TypeOf(model)
fields := []string{}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldToAdd := strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
fields = append(fields, fieldToAdd)
}
return fields
}