Init
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
bin/
|
25
Makefile
Normal file
25
Makefile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.PHONY: run build test clean help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
help:
|
||||||
|
@echo "Available commands:"
|
||||||
|
@echo " run - Run the API server"
|
||||||
|
@echo " build - Build the API binary"
|
||||||
|
@echo " test - Run tests"
|
||||||
|
@echo " clean - Clean build artifacts"
|
||||||
|
|
||||||
|
# Run the API server
|
||||||
|
run:
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
# Build the API binary
|
||||||
|
build:
|
||||||
|
go build -o bin/api main.go
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
rm -rf bin/
|
36
main.go
Normal file
36
main.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"goapi-template/src/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Get port from environment or use default
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
host := os.Getenv("HOST")
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup router
|
||||||
|
mux := router.SetupRoutes()
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
fmt.Printf("Todo API running at http://%s:%s/\n", host, port)
|
||||||
|
fmt.Println("Available endpoints:")
|
||||||
|
fmt.Println(" GET /todos - Get all todos")
|
||||||
|
fmt.Println(" POST /todos - Create todo")
|
||||||
|
fmt.Println(" PUT /todos/:id - Update todo")
|
||||||
|
fmt.Println(" DELETE /todos/:id - Delete todo")
|
||||||
|
|
||||||
|
log.Fatal(http.ListenAndServe(":"+port, mux))
|
||||||
|
}
|
123
src/handlers/todo_handler.go
Normal file
123
src/handlers/todo_handler.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"goapi-template/src/models"
|
||||||
|
"goapi-template/src/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TodoHandler handles todo-related HTTP requests
|
||||||
|
type TodoHandler struct {
|
||||||
|
storage *storage.TodoStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTodoHandler creates a new TodoHandler
|
||||||
|
func NewTodoHandler(storage *storage.TodoStorage) *TodoHandler {
|
||||||
|
return &TodoHandler{storage: storage}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTodos handles GET /todos
|
||||||
|
func (h *TodoHandler) GetTodos(w http.ResponseWriter, r *http.Request) {
|
||||||
|
todos := h.storage.GetAll()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(todos); err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTodo handles POST /todos
|
||||||
|
func (h *TodoHandler) CreateTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req models.CreateTodoRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Title == "" {
|
||||||
|
http.Error(w, "Title is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo := h.storage.Create(req)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
if err := json.NewEncoder(w).Encode(todo); err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTodo handles PUT /todos/:id
|
||||||
|
func (h *TodoHandler) UpdateTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := h.extractIDFromPath(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid todo ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req models.UpdateTodoRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
todo, err := h.storage.Update(id, req)
|
||||||
|
if err != nil {
|
||||||
|
if err == storage.ErrTodoNotFound {
|
||||||
|
http.Error(w, "Todo not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Failed to update todo", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(todo); err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTodo handles DELETE /todos/:id
|
||||||
|
func (h *TodoHandler) DeleteTodo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := h.extractIDFromPath(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid todo ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.storage.Delete(id)
|
||||||
|
if err != nil {
|
||||||
|
if err == storage.ErrTodoNotFound {
|
||||||
|
http.Error(w, "Todo not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Failed to delete todo", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractIDFromPath extracts the ID from a URL path like /todos/123
|
||||||
|
func (h *TodoHandler) extractIDFromPath(path string) (int, error) {
|
||||||
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
|
if len(parts) != 2 || parts[0] != "todos" {
|
||||||
|
return 0, fmt.Errorf("invalid path format")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
26
src/models/todo.go
Normal file
26
src/models/todo.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Todo represents a todo item
|
||||||
|
type Todo struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Completed bool `json:"completed"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTodoRequest represents the request body for creating a todo
|
||||||
|
type CreateTodoRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTodoRequest represents the request body for updating a todo
|
||||||
|
type UpdateTodoRequest struct {
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Completed *bool `json:"completed,omitempty"`
|
||||||
|
}
|
91
src/router/router.go
Normal file
91
src/router/router.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"goapi-template/src/handlers"
|
||||||
|
"goapi-template/src/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupRoutes configures and returns the HTTP router
|
||||||
|
func SetupRoutes() *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Initialize storage and handlers
|
||||||
|
todoStorage := storage.NewTodoStorage()
|
||||||
|
todoHandler := handlers.NewTodoHandler(todoStorage)
|
||||||
|
|
||||||
|
// Setup todo routes
|
||||||
|
mux.HandleFunc("/todos", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Enable CORS
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
todoHandler.GetTodos(w, r)
|
||||||
|
case "POST":
|
||||||
|
todoHandler.CreateTodo(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/todos/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Enable CORS
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a specific todo request (has ID)
|
||||||
|
if strings.Count(strings.Trim(r.URL.Path, "/"), "/") != 1 {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "PUT":
|
||||||
|
todoHandler.UpdateTodo(w, r)
|
||||||
|
case "DELETE":
|
||||||
|
todoHandler.DeleteTodo(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Root endpoint showing available endpoints
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"message": "Todo API",
|
||||||
|
"endpoints": map[string]string{
|
||||||
|
"GET /todos": "Get all todos",
|
||||||
|
"POST /todos": "Create todo",
|
||||||
|
"PUT /todos/:id": "Update todo",
|
||||||
|
"DELETE /todos/:id": "Delete todo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
})
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
110
src/storage/todo_storage.go
Normal file
110
src/storage/todo_storage.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"goapi-template/src/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTodoNotFound = errors.New("todo not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// TodoStorage provides in-memory storage for todos
|
||||||
|
type TodoStorage struct {
|
||||||
|
todos map[int]*models.Todo
|
||||||
|
nextID int
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTodoStorage creates a new TodoStorage instance
|
||||||
|
func NewTodoStorage() *TodoStorage {
|
||||||
|
return &TodoStorage{
|
||||||
|
todos: make(map[int]*models.Todo),
|
||||||
|
nextID: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll returns all todos
|
||||||
|
func (ts *TodoStorage) GetAll() []*models.Todo {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
|
||||||
|
todos := make([]*models.Todo, 0, len(ts.todos))
|
||||||
|
for _, todo := range ts.todos {
|
||||||
|
todos = append(todos, todo)
|
||||||
|
}
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID returns a todo by ID
|
||||||
|
func (ts *TodoStorage) GetByID(id int) (*models.Todo, error) {
|
||||||
|
ts.mutex.RLock()
|
||||||
|
defer ts.mutex.RUnlock()
|
||||||
|
|
||||||
|
todo, exists := ts.todos[id]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrTodoNotFound
|
||||||
|
}
|
||||||
|
return todo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new todo
|
||||||
|
func (ts *TodoStorage) Create(req models.CreateTodoRequest) *models.Todo {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
todo := &models.Todo{
|
||||||
|
ID: ts.nextID,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
Completed: false,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.todos[ts.nextID] = todo
|
||||||
|
ts.nextID++
|
||||||
|
|
||||||
|
return todo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing todo
|
||||||
|
func (ts *TodoStorage) Update(id int, req models.UpdateTodoRequest) (*models.Todo, error) {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
|
||||||
|
todo, exists := ts.todos[id]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrTodoNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Title != nil {
|
||||||
|
todo.Title = *req.Title
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
todo.Description = *req.Description
|
||||||
|
}
|
||||||
|
if req.Completed != nil {
|
||||||
|
todo.Completed = *req.Completed
|
||||||
|
}
|
||||||
|
todo.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
return todo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a todo by ID
|
||||||
|
func (ts *TodoStorage) Delete(id int) error {
|
||||||
|
ts.mutex.Lock()
|
||||||
|
defer ts.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, exists := ts.todos[id]; !exists {
|
||||||
|
return ErrTodoNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(ts.todos, id)
|
||||||
|
return nil
|
||||||
|
}
|
Reference in New Issue
Block a user