feat: add support for creating users and chirps

This commit is contained in:
Stevan Freeborn
2026-08-06 19:31:58 -05:00
parent d3811aa1b9
commit 5c5a126d88
14 changed files with 389 additions and 23 deletions
+159 -23
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
@@ -14,22 +15,17 @@ import (
"sync/atomic"
"syscall"
"time"
"github.com/StevanFreeborn/chirpy/internal/database"
"github.com/google/uuid"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
)
type server struct {
fileServerHits atomic.Int32
}
type chirpRequest struct {
Body string `json:"body"`
}
type validationError struct {
Err string `json:"error"`
}
type chirpResponse struct {
CleanedBody string `json:"cleaned_body"`
database *database.Queries
platform string
}
func writeJsonResponse(w http.ResponseWriter, response any) {
@@ -56,8 +52,15 @@ func (s *server) HandleHealthChecks(w http.ResponseWriter, r *http.Request) {
}
func (s *server) HandleReset(w http.ResponseWriter, r *http.Request) {
if s.platform != "dev" {
w.WriteHeader(http.StatusForbidden)
return
}
s.fileServerHits.Store(0)
s.database.DeleteAllUsers(r.Context())
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
@@ -81,38 +84,75 @@ func (s *server) HandleMetrics(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, template, hits)
}
func (s *server) HandleChirpValidation(w http.ResponseWriter, r *http.Request) {
type apiError struct {
Err string `json:"error"`
}
type createChirpRequest struct {
Body string `json:"body"`
UserId string `json:"user_id"`
}
type createChirpResponse struct {
Id string `json:"id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Body string `json:"body"`
UserId string `json:"user_id"`
}
func (s *server) HandleCreateChirp(w http.ResponseWriter, r *http.Request) {
blacklist := []string{
"kerfuffle",
"sharbert",
"fornax",
}
var chirpRequest chirpRequest
var createChirpRequest createChirpRequest
defer r.Body.Close()
w.Header().Add("Content-Type", "application/json")
err := json.NewDecoder(r.Body).Decode(&chirpRequest)
err := json.NewDecoder(r.Body).Decode(&createChirpRequest)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, validationError{
writeJsonResponse(w, apiError{
Err: "Unable to deserialize JSON",
})
return
}
if len(chirpRequest.Body) > 140 {
if len(createChirpRequest.Body) > 140 {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, validationError{
writeJsonResponse(w, apiError{
Err: "Chirp is too long",
})
return
}
words := strings.Split(chirpRequest.Body, " ")
requestUserId, err := uuid.Parse(createChirpRequest.UserId)
invalidUserIdError := apiError{
Err: "Invalid user_id. user_id must be valid UUID",
}
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, invalidUserIdError)
return
}
existingUser, err := s.database.GetUserById(r.Context(), requestUserId)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, invalidUserIdError)
return
}
words := strings.Split(createChirpRequest.Body, " ")
sanitized := []string{}
for _, word := range words {
@@ -126,12 +166,105 @@ func (s *server) HandleChirpValidation(w http.ResponseWriter, r *http.Request) {
cleanedBody := strings.Join(sanitized, " ")
w.WriteHeader(http.StatusOK)
writeJsonResponse(w, chirpResponse{CleanedBody: cleanedBody})
createChirpParams := database.CreateChirpParams{
Body: cleanedBody,
UserID: existingUser.ID,
}
createdChirp, err := s.database.CreateChirp(r.Context(), createChirpParams)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeJsonResponse(w, apiError{
Err: "Failed to create chirp. 🤷🏻‍♂️",
})
}
w.WriteHeader(http.StatusCreated)
writeJsonResponse(w, createChirpResponse{
Id: createdChirp.ID.String(),
CreatedAt: createdChirp.CreatedAt.Format(time.RFC3339),
UpdatedAt: createdChirp.UpdatedAt.Format(time.RFC3339),
Body: createdChirp.Body,
UserId: createdChirp.UserID.String(),
})
}
type createUserRequest struct {
Email string `json:"email"`
}
type createUserResponse struct {
Id string `json:"id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Email string `json:"email"`
}
func (s *server) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
var createUserRequest createUserRequest
defer r.Body.Close()
err := json.NewDecoder(r.Body).Decode(&createUserRequest)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, apiError{
Err: "Unable to deserialize JSON",
})
}
trimmedEmail := strings.TrimSpace(createUserRequest.Email)
if strings.TrimSpace(trimmedEmail) == "" {
w.WriteHeader(http.StatusBadRequest)
writeJsonResponse(w, apiError{
Err: "email is required. must be valid email address.",
})
}
createdUser, err := s.database.CreateUser(r.Context(), trimmedEmail)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeJsonResponse(w, apiError{
Err: "Uh oh we were unable to create a new user",
})
}
w.WriteHeader(http.StatusCreated)
writeJsonResponse(w, createUserResponse{
Id: createdUser.ID.String(),
CreatedAt: createdUser.CreatedAt.Format(time.RFC3339),
UpdatedAt: createdUser.UpdatedAt.Format(time.RFC3339),
Email: createdUser.Email,
})
}
func main() {
server := &server{}
err := godotenv.Load()
if err != nil {
log.Fatalf("Failed to load environment variables")
os.Exit(1)
}
dbURL := os.Getenv("DB_URL")
platform := os.Getenv("PLATFORM")
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
os.Exit(1)
}
server := &server{
database: database.New(db),
platform: platform,
}
mux := http.NewServeMux()
appRoot := "/app/"
@@ -140,9 +273,12 @@ func main() {
mux.HandleFunc("GET /admin/metrics", server.HandleMetrics)
mux.HandleFunc("POST /admin/reset", server.HandleReset)
mux.HandleFunc("POST /api/validate_chirp", server.HandleChirpValidation)
mux.HandleFunc("GET /api/healthz", server.HandleHealthChecks)
mux.HandleFunc("POST /api/users", server.HandleCreateUser)
mux.HandleFunc("POST /api/chirps", server.HandleCreateChirp)
port := ":8080"
httpServer := http.Server{