DEV Community

Cover image for How I Wrote Express-Go in 19 Hours
Bruno Ciccarino λ
Bruno Ciccarino λ

Posted on • Edited on

How I Wrote Express-Go in 19 Hours

silicon valley

If you've ever worked with web frameworks like Express.js, you know how convenient and easy to use they can be. Now, imagine this ease in Go, with its performance and robustness. Well, that motivated me to create express-go, a micro-framework inspired by Express.js, and best of all: I built it in 19 hours! The journey was intense, but worth every second. Let me tell you how it all happened. Official repository link

Errata:

For those who downloaded, now I had forgotten to add the repository as a module in go mod, I uploaded the repository as it was when I finished the unit tests and examples and I was sleepy and hadn't thought about changing go mod, but now I already corrected it and can now install normally.

To install

Type in your terminal this command

go get github.com/BrunoCiccarino/GopherLight/router
go get github.com/BrunoCiccarino/GopherLight/req

the idea

It all started when I thought: "It would be cool to have something simple like Express.js, but with the performance of Go!". Go is already known for being minimalist and performant, but when it came to writing web servers, I felt something easier to use like Express.js was still missing.

So instead of complaining, I decided to get my hands dirty and make something happen. I was determined to create a micro-framework that would allow me to configure routes, handle HTTP requests and responses quickly and easily.

The Beginning of the Journey

I started with the basic structure: a Go application that could listen to HTTP requests and, depending on the route, perform different functions.

First Stop: Routes

The first thing I needed to do was set up the routing. I wish it was possible to define routes in a similar way to Express.js, where you specify a URL and a function to handle that route.

Here's the magic of routes:

type App struct {
    routes map[string]func(req *req.Request, res *req.Response)
}

func NewApp() *App {
    return &App{
        routes: make(map[string]func(req *req.Request, res *req.Response)),
    }
}

func (a *App) Route(path string, handler func(req *req.Request, res *req.Response)) {
    a.routes[path] = handler
}

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if handler, exists := a.routes[r.URL.Path]; exists {
        request := req.NewRequest(r)
        response := req.NewResponse(w)
        handler(request, response)
    } else {
        http.NotFound(w, r)
    }
}

func (a *App) Listen(addr string) error {
    return http.ListenAndServe(addr, a)
}
Enter fullscreen mode Exit fullscreen mode

The idea here was simple: I wanted a route map (map[string]func) where the key was the URL and the value was the function that would handle the request.

The Magic of the Handler

One of the things I liked most about Express.js was how easy to use route handlers are. So, I adopted the idea that each route would be just a function that would receive two parameters: the request and the response. In Go, this is a bit more work, as the standard library requires a lot of manual work, so I wrote some abstractions to make it easier.

Handling Requests
HTTP requests in Go involve a lot of structures and methods, so I encapsulated all of this in a struct called Request, with some convenient methods for getting query parameters, headers, and the request body.

type Request struct {
    Req  *http.Request
    Body string
}

func NewRequest(req *http.Request) *Request {

    bodyBytes, _ := io.ReadAll(req.Body)
    bodyString := string(bodyBytes)

    return &Request{
        Req:  req,
        Body: bodyString,
    }
}

func (r *Request) QueryParam(key string) string {
    params := r.Req.URL.Query()
    return params.Get(key)
}

func (r *Request) Header(key string) string {
    return r.Req.Header.Get(key)
}

func (r *Request) BodyAsString() string {
    return r.Body
}
Enter fullscreen mode Exit fullscreen mode

Now, instead of dealing with the http.Request directly, I can do something like:

app.Get("/greet", func(r *req.Request, w *req.Response) {
    name := r.QueryParam("name")
    if name == "" {
        name = "Guest"
    }
    w.Send("Hello, " + name + "!")
})
Enter fullscreen mode Exit fullscreen mode

This makes things much cleaner and more readable!

Responding Easy

After the requests, it was time to make it easier to send responses. Response also needed a touch of simplicity so I could send text or JSONs quickly.

type Response struct {
    http.ResponseWriter
}

func NewResponse(w http.ResponseWriter) *Response {
    return &Response{w}
}

func (res *Response) Send(data string) {
    res.Write([]byte(data))
}

func (res *Response) Status(statusCode int) *Response {
    res.WriteHeader(statusCode)
    return res
}

func (res *Response) JSON(data interface{}) {
    res.Header().Set("Content-Type", "application/json")
    jsonData, err := json.Marshal(data)
    if err != nil {
        res.Status(http.StatusInternalServerError).Send("Error encoding JSON")
        return
    }
    res.Write(jsonData)
}
Enter fullscreen mode Exit fullscreen mode

The Result

At the end of these 19 hours of work, I managed to create express-go: a fast and easy-to-use micro-framework, where configuring routes and sending responses is as simple as Express.js, but with all the power and performance of Go.

Usage Example:

Here's a complete example of how it all fits together:

package main

import (
    "github.com/BrunoCiccarino/GopherLight/router"
    "github.com/BrunoCiccarino/GopherLight/req"
    "fmt"
)

func main() {
    app := router.NewApp()

    app.Get("/hello", func(r *req.Request, w *req.Response) {
        w.Send("Hello, World!")
    })

   app.Get("/json", func(r *req.Request, w *req.Response) {
    w.JSON(map[string]string{"message": "Hello, JSON"})
   })


    fmt.Println("Server listening on port 3333")
    app.Listen(":3333")
}
Enter fullscreen mode Exit fullscreen mode

Simple, clean and to the point. I'm proud to say that I was able to build this in less than a day, and the cool thing is that it offers enough flexibility for small projects, without all the complexity of larger frameworks.

Final Reflection

Creating the express-go in 19 hours was a fun and challenging journey. I focused on solving real problems I've faced with Go servers and tried to make everything as intuitive as possible. Of course, there's more work to be done, but there's plenty to play with!

If you're curious, take a look at the code and feel free to contribute. After all, building tools like this is much cooler when we can share the process!

Now, if you'll excuse me, I'm going to get a coffee... after 19 hours, I deserve it, right?

Top comments (22)

Collapse
 
tempmaildetector profile image
TMD

Nicely done! I once wrote an RPC web server in Go before generics were a thing using the reflect package quite intensively. Writing your own router is such an interesting and sometimes frustrating process. I love the flexibility I have, but you keep discovering corner cases such as OPTIONS header, the obvious CORS, returning errors and subsequently their appropriate error codes etc... Then maybe you want to return a file rather than json and that becomes a thing.

Similar to your code, I never really liked the syntax of adding an additional return line rather than doing it on one line. But maybe that's just a me thing. I also have it in mine.

res.Status(http.StatusInternalServerError).Send("Error encoding JSON")
        return
Enter fullscreen mode Exit fullscreen mode

In any case, this is great stuff. I'm sure those 19h were intense but full of interest and curiosity. From my learnings, I now use what I'd probably say is the standard Mux router. It's not exactly how I'd like to do things, but it's shielded me from a lot of corner cases and has made building my current project (temp mail detector) a lot simpler and quicker.

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ

Congratulations on the project, I'm sure it was rewarding to write it, writing a router is very interesting, even more so when you focus on simplicity. One thing I noticed is, writing generic code is more difficult than it seems, there were a few hours when my brain went wrong and I got stuck, that's why it took me so long. and I'm going to take a look at this mux project, it looks very interesting.

Collapse
 
tempmaildetector profile image
TMD

Absolutely. More often than not you write something generic and then realise later on there's a better way to do it. All part of the fun.

Collapse
 
evandroad profile image
Evandro Abreu Domingues

Your work is very good. What would be the next step? Create the manipulation of the methods (get, post, pu, delete ...)

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ • Edited

I spent yesterday afternoon thinking and made a to-do list, I have some goals for this micro framework, if anyone wants to help, all help is welcome.

  • 1) Implement this manipulation of methods as you said (get, post put and delete)

  • 2) Implement middleware against CSRF, auth middleware, and implement a time out system in the format of a middleware

  • 3) create a website for documentation, with more examples and maintaining colloquial language to make it easier to understand...

  • 4) Improvements in error management and customized responses.

Collapse
 
k__ profile image
Kev

What do you think about enhancing this with next function and w.locals?

Thread Thread
 
brunociccarino profile image
Bruno Ciccarino λ

The next function I will have to implement after implementing some middlewares and with w.locals did you mean app.locals, or res.locals?

Thread Thread
 
k__ profile image
Kev

yep. I mentioned w.locals since you used w instead of res as the variable name. Maybe I will raise a pr. What do you think about starting a slack/discord?

Thread Thread
 
brunociccarino profile image
Bruno Ciccarino λ

I just created a discord channel, if you want to join, this is the link: discord

Collapse
 
igeorge17 profile image
iGEORGE17

You built your own framework 😯. Respect man 🙌

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ

Thanks bro, it was really fun to do, now I want to improve it more and more.

Collapse
 
kekelidev profile image
Aaron Kofi Gayi

I love stuffs like this but what happened to GoFiber?

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ

Gofiber is cool, but I thought it would also be cool for me to make my own to learn lol Gofiber is much more complex than mine...

Collapse
 
adi73 profile image
Aditya

Great stuff, we built a set of tools and HTTP router is one such module in our open source library - golly
Do check it out Repository Link
HTTP Router Link
A simple and quick implementation!

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ

I left a ⭐, congratulations on the project, really cool.

Collapse
 
adesoji1 profile image
Adesoji1

Beautiful, thank you for making me contribute to the project, I will do more

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ

I thank you for contributing, your help is very important.

Collapse
 
skr3am profile image
SKR3AM

Thanks for doing what Rob Pike never did for us.

Collapse
 
adesoji1 profile image
Adesoji1

He could too, prolly he isn't interested in express js

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ

😂😂😂😂

Collapse
 
honzakadlec profile image
honzakadlec

Have you checked github.com/gin-gonic/gin ?

Collapse
 
brunociccarino profile image
Bruno Ciccarino λ

I had never seen it before, but now I opened the repository, it's pretty cool.