1+ // Package main contains the core application logic for the Notes API.
12package main
23
34import (
@@ -18,129 +19,180 @@ import (
1819 "github.com/go-chi/chi/v5/middleware"
1920)
2021
21- // App represents the main application
22+ // App represents the main application that coordinates all components:
23+ // - Storage backend (in-memory, CouchDB, or MongoDB)
24+ // - REST API server
25+ // - gRPC API server
26+ // It handles initialization, running, and graceful shutdown of these components.
2227type App struct {
23- storage storage.NoteStorage
24- restServer * http.Server
25- grpcServer * grpc.Server
26- config * Config
28+ storage storage.NoteStorage // Interface for storing and retrieving notes
29+ restServer * http.Server // HTTP server for REST API
30+ grpcServer * grpc.Server // gRPC server for gRPC API
31+ config * Config // Application configuration
2732}
2833
29- // NewApp creates a new App instance
34+ // NewApp creates a new App instance with the provided configuration.
35+ // It only initializes the App struct with the configuration; actual component
36+ // initialization happens in the Initialize method.
3037func NewApp (config * Config ) * App {
3138 return & App {
3239 config : config ,
3340 }
3441}
3542
36- // Initialize sets up the application components
43+ // Initialize sets up the application components in the following order:
44+ // 1. Initializes the appropriate storage backend based on configuration
45+ // 2. Sets up the REST server with routes
46+ // 3. Sets up the gRPC server
47+ // This method must be called before Run.
3748func (a * App ) Initialize (ctx context.Context ) error {
38- // Initialize storage
49+ // Initialize storage backend (in-memory, CouchDB, or MongoDB)
50+ // based on the configuration
3951 storage , err := a .initializeStorage (ctx )
4052 if err != nil {
4153 return fmt .Errorf ("failed to initialize storage: %w" , err )
4254 }
4355 a .storage = storage
4456
45- // Setup servers
57+ // Setup the REST and gRPC servers with the initialized storage
4658 a .restServer = a .setupRESTServer ()
4759 a .grpcServer = a .setupGRPCServer ()
4860
4961 return nil
5062}
5163
52- // Run starts the application servers
64+ // Run starts the application servers and performs the following steps:
65+ // 1. Starts the REST and gRPC servers in separate goroutines
66+ // 2. Creates sample notes in the storage
67+ // 3. Waits for a shutdown signal (e.g., Ctrl+C)
68+ // This method blocks until the application is shut down.
5369func (a * App ) Run (ctx context.Context ) error {
54- // Start servers
70+ // Start the REST and gRPC servers in separate goroutines
5571 if err := a .startServers (ctx ); err != nil {
5672 return fmt .Errorf ("failed to start servers: %w" , err )
5773 }
5874
59- // Create sample notes
75+ // Create sample notes in the storage for demonstration purposes
6076 if err := a .createSampleNotes (ctx ); err != nil {
6177 return fmt .Errorf ("failed to create sample notes: %w" , err )
6278 }
6379
64- // Wait for shutdown signal
80+ // Wait for shutdown signal (context cancellation)
81+ // This blocks until the context is canceled (e.g., by Ctrl+C)
6582 return a .waitForShutdown (ctx )
6683}
6784
68- // initializeStorage initializes the storage based on configuration
85+ // initializeStorage initializes the storage backend based on the configuration.
86+ // It supports three types of storage:
87+ // - "couchdb": Uses CouchDB as the storage backend
88+ // - "mongodb": Uses MongoDB as the storage backend
89+ // - Any other value (default): Uses in-memory storage
90+ //
91+ // If connecting to CouchDB or MongoDB fails, it falls back to in-memory storage
92+ // to ensure the application can still run.
6993func (a * App ) initializeStorage (ctx context.Context ) (storage.NoteStorage , error ) {
7094 var noteStorage storage.NoteStorage
7195 var err error
7296
97+ // Choose the storage backend based on the configuration
7398 switch a .config .StorageType {
7499 case "couchdb" :
100+ // Try to connect to CouchDB
75101 log .Printf ("Connecting to CouchDB at %s, database: %s" , a .config .CouchDBURL , a .config .CouchDBName )
76102 noteStorage , err = storage .NewCouchDBStorage (a .config .CouchDBURL , a .config .CouchDBName )
77103 if err != nil {
104+ // If connection fails, log the error and fall back to in-memory storage
78105 log .Printf ("Failed to connect to CouchDB: %v, falling back to in-memory storage" , err )
79106 noteStorage = storage .NewInMemoryStorage ()
80107 } else {
81108 log .Println ("Successfully connected to CouchDB" )
82109 }
83110 case "mongodb" :
111+ // Try to connect to MongoDB
84112 log .Printf ("Connecting to MongoDB at %s, database: %s, collection: %s" ,
85113 a .config .MongoDBURI , a .config .MongoDBName , a .config .MongoDBCollection )
86114 noteStorage , err = storage .NewMongoDBStorage (a .config .MongoDBURI , a .config .MongoDBName , a .config .MongoDBCollection )
87115 if err != nil {
116+ // If connection fails, log the error and fall back to in-memory storage
88117 log .Printf ("Failed to connect to MongoDB: %v, falling back to in-memory storage" , err )
89118 noteStorage = storage .NewInMemoryStorage ()
90119 } else {
91120 log .Println ("Successfully connected to MongoDB" )
92121 }
93122 default :
123+ // Use in-memory storage by default
94124 log .Println ("Using in-memory storage" )
95125 noteStorage = storage .NewInMemoryStorage ()
96126 }
97127
98128 return noteStorage , nil
99129}
100130
101- // setupRESTServer creates and configures the REST server
131+ // setupRESTServer creates and configures the REST API server.
132+ // It sets up:
133+ // 1. A new REST handler with the storage backend
134+ // 2. A Chi router with middleware for logging and panic recovery
135+ // 3. Routes for the REST API endpoints
136+ // 4. An HTTP server with the configured port
102137func (a * App ) setupRESTServer () * http.Server {
138+ // Create a new REST handler with the storage backend
103139 restHandler := rest .NewHandler (a .storage )
140+
141+ // Create a new Chi router
142+ // Chi is a lightweight, idiomatic and composable router for Go HTTP services
104143 r := chi .NewRouter ()
105144
106- // Middleware
107- r .Use (middleware .Logger )
108- r .Use (middleware .Recoverer )
145+ // Add middleware to the router
146+ r .Use (middleware .Logger ) // Log all HTTP requests
147+ r .Use (middleware .Recoverer ) // Recover from panics without crashing the server
109148
149+ // Register the API routes with the router
150+ // This sets up endpoints like GET /api/notes, POST /api/notes, etc.
110151 restHandler .RegisterRoutes (r )
111152
153+ // Create and return an HTTP server with the configured port and router
112154 return & http.Server {
113- Addr : a .config .RESTPort ,
114- Handler : r ,
155+ Addr : a .config .RESTPort , // Port to listen on (e.g., ":8080")
156+ Handler : r , // The router that handles requests
115157 }
116158}
117159
118- // setupGRPCServer creates and configures the gRPC server
160+ // setupGRPCServer creates and configures the gRPC server.
161+ // It extracts the port number from the configuration and creates a new gRPC server
162+ // with the storage backend and port.
119163func (a * App ) setupGRPCServer () * grpc.Server {
120- // Remove the colon prefix and convert to int
121- port := 8081 // Default port
164+ // Extract the port number from the configuration
165+ // The port might be in the format ":8081", so we need to remove the colon prefix
166+ port := 8081 // Default port if parsing fails
122167 if a .config .GRPCPort != "" {
123168 portStr := strings .TrimPrefix (a .config .GRPCPort , ":" )
124169 if p , err := strconv .Atoi (portStr ); err == nil {
125170 port = p
126171 }
127172 }
173+
174+ // Create and return a new gRPC server with the storage backend and port
128175 return grpc .NewServer (a .storage , port )
129176}
130177
131- // startServers starts the REST and gRPC servers in separate goroutines
178+ // startServers starts the REST and gRPC servers in separate goroutines.
179+ // This method doesn't block; it returns immediately after starting the servers.
180+ // Each server runs in its own goroutine (a lightweight thread) to allow them to run concurrently.
132181func (a * App ) startServers (ctx context.Context ) error {
133- // Start REST server
182+ // Start REST server in a separate goroutine
134183 go func () {
135184 log .Printf ("Starting REST server on %s" , a .config .RESTPort )
185+ // ListenAndServe blocks until the server is stopped or encounters an error
136186 if err := a .restServer .ListenAndServe (); err != nil && err != http .ErrServerClosed {
187+ // Log any error that isn't just the server being closed normally
137188 log .Printf ("REST server failed: %v" , err )
138189 }
139190 }()
140191
141- // Start gRPC server
192+ // Start gRPC server in a separate goroutine
142193 go func () {
143194 log .Printf ("Starting gRPC server on %s" , a .config .GRPCPort )
195+ // Start blocks until the server is stopped or encounters an error
144196 if err := a .grpcServer .Start (); err != nil {
145197 log .Printf ("gRPC server failed: %v" , err )
146198 }
@@ -149,31 +201,40 @@ func (a *App) startServers(ctx context.Context) error {
149201 return nil
150202}
151203
152- // waitForShutdown waits for interrupt signal and gracefully shuts down the servers
204+ // waitForShutdown waits for the context to be canceled (e.g., by an interrupt signal)
205+ // and then gracefully shuts down the servers.
206+ // This method blocks until the context is canceled and the servers are shut down.
153207func (a * App ) waitForShutdown (ctx context.Context ) error {
208+ // Block until the context is canceled (e.g., by Ctrl+C)
154209 <- ctx .Done ()
155210 log .Println ("Shutting down servers..." )
156211
157- // Create a new context with timeout for shutdown
212+ // Create a new context with a 5-second timeout for the shutdown process
213+ // This ensures that shutdown doesn't hang indefinitely
158214 shutdownCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
159- defer cancel ()
215+ defer cancel () // Ensure the context is canceled when the function returns
160216
161- // Shutdown the REST server
217+ // Gracefully shut down the REST server
218+ // This allows in-flight requests to complete before shutting down
162219 if err := a .restServer .Shutdown (shutdownCtx ); err != nil {
163220 log .Printf ("REST server shutdown failed: %v" , err )
164221 }
165222
166- // Close the storage
223+ // Close the storage connection
224+ // This ensures any database connections are properly closed
167225 if err := a .storage .Close (shutdownCtx ); err != nil {
168226 log .Printf ("Storage shutdown failed: %v" , err )
169227 }
170228
171229 log .Println ("Servers stopped" )
230+ // Return the original context's error (typically context.Canceled)
172231 return ctx .Err ()
173232}
174233
175- // createSampleNotes creates some sample notes in the storage
234+ // createSampleNotes creates some sample notes in the storage for demonstration purposes.
235+ // This provides initial data for users to see when they first access the API.
176236func (a * App ) createSampleNotes (ctx context.Context ) error {
237+ // Define a list of sample notes to create
177238 notes := []struct {
178239 title string
179240 content string
@@ -192,41 +253,60 @@ func (a *App) createSampleNotes(ctx context.Context) error {
192253 },
193254 }
194255
256+ // Create each sample note in the storage
195257 for _ , note := range notes {
258+ // Create a new Note object with the title and content
196259 n := model .NewNote (note .title , note .content )
260+
261+ // Try to save the note to the storage
197262 err := a .storage .Create (ctx , n )
198263 if err != nil {
199- // Ignore duplicate key errors (MongoDB error code 11000)
264+ // If the note already exists (duplicate key error), skip it and continue
200265 if isDuplicateKeyError (err ) {
201266 continue
202267 }
268+ // For any other error, return it
203269 return fmt .Errorf ("failed to create sample note: %w" , err )
204270 }
205- // Add a small delay to ensure unique IDs when using timestamp-based ID generation
271+
272+ // Add a small delay between creating notes
273+ // This ensures unique IDs when using timestamp-based ID generation
274+ // (since our ID generation uses the current timestamp)
206275 time .Sleep (1 * time .Millisecond )
207276 }
208277
209278 return nil
210279}
211280
212- // isDuplicateKeyError checks if the error is a duplicate key error
281+ // isDuplicateKeyError checks if the error is a duplicate key error from any of the
282+ // supported storage backends (MongoDB, CouchDB, or in-memory).
283+ //
284+ // Different databases return different error messages for duplicate key errors,
285+ // so this function normalizes them to a single boolean result.
213286func isDuplicateKeyError (err error ) bool {
214287 if err == nil {
215288 return false
216289 }
217290 errStr := err .Error ()
218291
219- // MongoDB duplicate key error
292+ // Check for MongoDB duplicate key error
293+ // MongoDB returns error code E11000 for duplicate key errors
220294 if strings .Contains (errStr , "E11000 duplicate key error" ) {
221295 return true
222296 }
223- // CouchDB duplicate key error
297+
298+ // Check for CouchDB duplicate key error
299+ // CouchDB returns "conflict" or "Document update conflict" for duplicate key errors
224300 if strings .Contains (errStr , "conflict" ) || strings .Contains (errStr , "Document update conflict" ) {
225301 return true
226302 }
227- // In-memory storage duplicate key error
303+
304+ // Check for in-memory storage duplicate key error
305+ // Our in-memory implementation returns "note already exists" for duplicate key errors
228306 if strings .Contains (errStr , "note already exists" ) {
229307 return true
230308 }
309+
310+ // If none of the above patterns match, it's not a duplicate key error
231311 return false
232312}
0 commit comments