Init
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ go.work.sum
|
||||
# env file
|
||||
.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