As a language that emerged over 14 years in the past, Go has many use circumstances. From net improvement, APIs, and CLIs to Wasm, cloud applied sciences, and even AI-powered instruments, its purposes are broad.
The identical goes for demo tasks, which have numerous variations and functions!
Now, wouldn’t it’s nice if there was a full-stack, API-oriented, and well-maintained demo challenge for Go? If you happen to’ve ever skilled a demo challenge fail throughout a dwell presentation, you then’d actually admire a extra secure challenge!
On this weblog publish, I’ll stroll you thru the method of making such a challenge – a full-stack app utilizing Go and React.
Take a look at the supply code right here to comply with alongside.
Introducing Go Eats
Go Eats is an open-source challenge just like the meals supply apps you’ve most likely seen and used earlier than. The challenge simulates the operations concerned when inserting an order in a real-world meals supply app.
To construct Go Eats, I used Go because the programming language, Postgres because the database, React for the frontend, and NATS for messaging. Whereas Go is an effective selection for these advanced full-stack purposes, discovering demos will be difficult.
I used GoLand for this challenge as a result of it takes care of establishing the Go SDK, putting in packages, and way more straight out of the field.
Let’s take a better take a look at the entry level of the supply code: major.go.
This code initializes a number of companies and handlers, establishes a database connection, hundreds atmosphere variables, and units up middleware, NATS, and the WebSocket.
func major() { // load .env file err := godotenv.Load() if err != nil { log.Deadly("Error loading .env file") } env := os.Getenv("APP_ENV") db := database.New() // Create Tables if err := db.Migrate(); err != nil { log.Fatalf("Error migrating database: %s", err) } // Join NATS natServer, err := nats.NewNATS("nats://127.0.0.1:4222") // WebSocket Purchasers wsClients := make(map[string]*websocket.Conn) s := handler.NewServer(db, true) // Initialize Validator validate := validator.New() // Middlewares Record middlewares := []gin.HandlerFunc{middleware.AuthMiddleware()} // Consumer userService := usr.NewUserService(db, env) consumer.NewUserHandler(s, "/consumer", userService, validate) // Restaurant restaurantService := restro.NewRestaurantService(db, env) restaurant.NewRestaurantHandler(s, "/restaurant", restaurantService) //.... For the sake of brevity, the complete code snippet is omitted right here. log.Deadly(s.Run()) }
The journey
I began by engaged on backend duties, comparable to creating APIs, earlier than shifting to the frontend. I selected Gin because the framework to construct these APIs.
Whereas I may have gone with Gorilla, Echo, Fiber, or some other framework, I felt Gin can be a better option for this challenge. It’s extra mature and secure, it’s broadly used, and it’s additionally considerably of a private choice.
Earlier than choosing any framework, you must fastidiously think about the issue you are attempting to unravel or the precise enterprise purpose you are attempting to attain. Then weigh up the potential options and go for the most effective one on your particular use case.
The backend
Whereas making a demo app, it’s necessary to make sure that the appliance is versatile sufficient to accommodate future modifications and refactoring.
For Go Eats, I carried out a service layer sample, together with dependency injection. Moreover, I utilized handlers for request and response communication with the service layer, which manages enterprise logic and interacts with the database.
// Consumer userService := usr.NewUserService(db, env) consumer.NewUserHandler(s, "/consumer", userService, validate) // Restaurant restaurantService := restro.NewRestaurantService(db, env) restaurant.NewRestaurantHandler(s, "/restaurant", restaurantService) // Evaluations reviewService := evaluate.NewReviewService(db, env) revw.NewReviewProtectedHandler(s, "/evaluate", reviewService, middlewares, validate) // Cart cartService := cart_order.NewCartService(db, env, natServer) crt.NewCartHandler(s, "/cart", cartService, middlewares, validate)
The challenge construction proven above ensures that the challenge is modular and versatile. Do not forget that creating too many nested directories can add pointless complexity. Particularly when you’re simply beginning your programming journey, it’s greatest to concentrate on simplicity relatively than going via a number of layers. Understand that your code will inevitably must be refactored sooner or later.
The well-known quote by Donald Knuth, “Untimely optimization is the basis of all evil,” feels notably apt right here.
I did one thing related for the database. I used the light-weight Bun SQL shopper.
Nevertheless, I made certain that if I would like to switch the ORM sooner or later, my code will be capable of adapt to the brand new modifications based mostly on the interface I’ve outlined beneath.
kind Database interface { Insert(ctx context.Context, mannequin any) (sql.Outcome, error) Delete(ctx context.Context, tableName string, filter Filter) (sql.Outcome, error) Choose(ctx context.Context, mannequin any, columnName string, parameter any) error SelectAll(ctx context.Context, tableName string, mannequin any) error SelectWithRelation(ctx context.Context, mannequin any, relations []string, Situation Filter) error SelectWithMultipleFilter(ctx context.Context, mannequin any, Situation Filter) error Uncooked(ctx context.Context, mannequin any, question string, args ...interface{}) error Replace(ctx context.Context, tableName string, Set Filter, Situation Filter) (sql.Outcome, error) Rely(ctx context.Context, tableName string, ColumnExpression string, columnName string, parameter any) (int64, error) Migrate() error HealthCheck() bool Shut() error }
This interface offers a blueprint for CRUD (create, learn, replace, and delete) operations and different frequent database interactions, guaranteeing that any struct implementing this interface can be utilized to carry out these operations.
GoLand retains observe of class implementation, making navigation a lot smoother.
To navigate to the implementation, press ⌘⌥B / Ctrl+Alt+B.
Many of the operations contain CRUD actions. I used to be additionally wanting to experiment with server-sent occasions (SSE) and WebSockets, which I’ve not explored a lot up to now. This challenge was a great use case for these applied sciences.
Let’s start with SSE, a expertise that permits the server to push notifications, messages, and occasions to shoppers over an HTTP connection.
The code snippet beneath reads knowledge from a JSON file and pushes the data so the shopper renders and shows the related knowledge.
func (s *AnnouncementHandler) flashNews(c *gin.Context) { _, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() occasions, err := s.service.FlashEvents() if err != nil { c.AbortWithStatusJSON(500, gin.H{"error": err.Error()}) return } // Set headers for SSE c.Header("Content material-Kind", "textual content/event-stream") c.Header("Cache-Management", "no-cache") c.Header("Connection", "keep-alive") ticker := time.NewTicker(6 * time.Second) defer ticker.Cease() eventIndex := 0 for { choose { case <-ticker.C: // Ship the present occasion occasion := (*occasions)[eventIndex] c.SSEvent("message", occasion.Message) c.Author.Flush() // Transfer to the subsequent occasion eventIndex = (eventIndex + 1) % len(*occasions) case <-c.Request.Context().Carried out(): ticker.Cease() return } } }
WebSocket and NATS
One thing else I’ve not labored with earlier than however have all the time wished to strive is utilizing WebSockets with the NATS messaging system. I evaluated these applied sciences as potential options for the challenge and in the end determined to make use of them within the demo app.
Right here’s how the messaging course of within the Go Eats app features:
Each time a brand new order is created, a message is revealed to NATS informing the subscriber in regards to the newly positioned order.
When the supply individual updates the order – whether or not it’s on the way in which, has failed, or has been delivered – the client will obtain this info in actual time, a course of that shall be mirrored within the UI.
The code beneath defines the 2 matters:
- orders.new.*: Sends a notification to the client when a brand new order is positioned.
- orders.standing.*: Sends a notification to the client when the supply standing is up to date.
For every acquired message, the appliance logs the message, extracts the consumer ID and message knowledge, and makes an attempt to ship the info to the corresponding WebSocket shopper. If sending fails, the app logs the error, closes the WebSocket connection, and removes the shopper from the energetic shopper’s map.
Word: Utilizing map[string]*websocket.Conn to handle WebSocket connections with keys as shopper identifiers, comparable to userID, won’t deal with a number of connections from the identical shopper, because the map keys should be distinctive. That is one thing I’m planning to increase on in a future weblog publish.
Why NATS over Kafka?
Kafka is able to dealing with massive quantities of information, providing database-level sturdiness and being enterprise-ready.
Nevertheless, I’ve by no means used it earlier than, and I wanted one thing that may work effectively for my particular use case. In that context, I got here throughout NATS, which is written in Go and is good for light-weight, low-latency messaging, notably in microservices and cloud-native architectures.
A key level I want to spotlight is its simplicity. NATS was straightforward to make use of even when simply beginning to work with it.
The code beneath initializes a NATS connection and defines strategies to publish and subscribe to messages. It connects to a NATS server, publishes messages to a subject, and forwards acquired messages to WebSocket shoppers based mostly on consumer IDs.
kind NATS struct { Conn *nats.Conn } func NewNATS(url string) (*NATS, error) { nc, err := nats.Join(url, nats.Title("food-delivery-nats")) if err != nil { log.Fatalf("Error connecting to NATS:: %s", err) } return &NATS{Conn: nc}, err } func (n *NATS) Pub(subject string, message []byte) error { err := n.Conn.Publish(subject, message) if err != nil { return err } return nil } func (n *NATS) Sub(subject string, shoppers map[string]*websocket.Conn) error { _, err := n.Conn.Subscribe(subject, func(msg *nats.Msg) { message := string(msg.Information) slog.Data("MESSAGE_REPLY_FROM_NATS", "RECEIVED_MESSAGE", message) userId, messageData := n.formatMessage(message) if conn, okay := shoppers[userId]; okay { err := conn.WriteMessage(websocket.TextMessage, []byte(messageData)) if err != nil { log.Println("Error sending message to shopper:", err) conn.Shut() delete(shoppers, userId) } } }) if err != nil { return err } return nil } //.... For the sake of brevity, the complete code snippet is omitted right here.
The frontend
To construct the Go Eats UI, I used React. I don’t come from a frontend background, and it was fairly difficult to be taught and apply unfamiliar ideas. Alongside the way in which, I took Stephen Grider’s Trendy React with Redux course.
Whereas I’m actually no knowledgeable in React, the course helped quite a bit. That stated, it was the usage of GoLand, the JetBrains IDE for Go, which proved to be most precious all through this course of.
GoLand has wonderful help for full-stack improvement with much less distraction and context switching. The IDE offers distinctive coding help for JavaScript, TypeScript, Dart, React, and lots of different languages.
Utilizing GoLand, I can develop frontend and backend purposes with the identical IDE.
Lastly, right here is the ultimate output of how the UI will get rendered via React.
I haven’t gone into a lot element in regards to the means of growing the Go Eats frontend. Nevertheless, when you’re , you possibly can take a look at the supply code right here.
Abstract
As I mirror on my journey growing this demo app, I understand there’s a lot to be taught and lots of enhancements that may very well be made.
Listed below are some key classes from this expertise:
- Give attention to performance first!
If you happen to’re somebody who’s simply beginning to work with Go, don’t fear about making your app scalable or extra modular by specializing in dependency injection instantly. These ideas will ultimately be required, and you’ll observe the patterns and restructure accordingly. No matter you construct, it received’t be excellent at first, and that’s okay! The journey is considered one of incremental and fixed enhancements. - Prioritize testing!
Don’t overlook the significance of standard testing. Ensuring you check your challenge ceaselessly offers confidence that your options work as anticipated. Whereas I’ve labored on assessments for Go Eats, I nonetheless want to enhance them additional. Moreover, I’ve tried totally different testing libraries, comparable to Testcontainers, which assist me check options in an actual database as an alternative of utilizing mock ones. - Database design issues!
Spending time on the database schema is extraordinarily invaluable. Moreover, there are database migrations, which I haven’t used, however they’re fairly necessary, particularly once you’re in search of rollbacks, versioning modifications, and guaranteeing that modifications are utilized within the right order.
Whereas I’m proud of what I’ve constructed to date, it’s just the start for Go Eats. Keep tuned for extra updates quickly!
I’d additionally love to listen to your suggestions. If you happen to’ve had related experiences or when you’ve got ideas on what you favored or disliked, or when you’ve got options for enchancment, please share them within the feedback.