Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
dev/
main.go
GoWeb
Makefile
Makefile
public/
static/
79 changes: 70 additions & 9 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ GoWeb is a lightweight Go web framework that mimics the structure, design, and f

## 📚 Table of Contents

- [✨ Features](#-features)
- [🚀 Usage](#-usage)
1. [Define Request/Response DTOs](#1-define-requestresponse-dtos)
2. [Create a Controller](#2-create-a-controller)
Expand All @@ -27,6 +26,10 @@ GoWeb is a lightweight Go web framework that mimics the structure, design, and f
- [💡 Why These Matter](#-why-these-matter)
- [🧪 Response Builder](#-response-builder)
- [📌 Example JSON Response](#-example-json-response)
- [⚙️ Configuration](#-configuration)
- [Example Configs](#example-configs)
- [How It Works](#how-it-works)
- [Loading and Using Configs](#loading-and-using-configs)
- [🌐 CORS Middleware](#-cors-middleware)
- [✅ Registering the CORS Middleware](#-registering-the-cors-middleware)
- [⚙️ Behavior](#-behavior)
Expand Down Expand Up @@ -86,12 +89,6 @@ func main() {
&controllers.UsersController{}, // You can create a controller through OOP
controllers.New(), // You can create a controller using the builder
)

// Register pre-middleware
app.Use(middlewares.LoggingPre)

// Register post-middleware
app.UseAfter(middlewares.LoggingPost)

// Launch the server
logger.Info("Server listening on http://localhost:8080")
Expand All @@ -108,7 +105,7 @@ func main() {
GoWeb is built on a clean and extendable foundation inspired by Spring Boot, but optimized for Go. Below are the key architectural components of the framework:

| Concept | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`Controller`** | A struct implementing `BasePath()` and `Routes()` to define a route group. It may embed `ControllerBase` to enable optional controller-specific middleware. |
| **`Route`** | Defines a single HTTP endpoint via `Method`, `Path`, and a string-based `Handler` name that maps to a function in the controller. |
| **DTOs** | Plain structs representing request or response data (e.g., `UserRequest`, `UserResponse`). GoWeb automatically binds path/query/body values to arguments. |
Expand All @@ -119,6 +116,7 @@ GoWeb is built on a clean and extendable foundation inspired by Spring Boot, but
| **Middleware** | Middleware objects implement the `Middleware` interface. They're registered globally or per controller using `app.Use(...)` or `controller.Use(...)`. |
| **Middleware Builder** | Use `NewMiddlewareBuilder(...)` to create strongly-typed, reusable middleware with config (`.Config`), init logic (`.WithInit()`), and error hooks (`.OnError()`). |
| **Request Context Helpers** | Access path params, query strings, and headers using `types.PathVar(ctx, "id")`, `QueryParam(ctx, "q")`, and `Header(ctx, "X-Token")`. Injected automatically by the router. |
| **Configuration** | A very straight-forward and easy way to manage configurations that your entire project can easily access via the `app/config` util |

### 💡 Why These Matter

Expand Down Expand Up @@ -166,6 +164,70 @@ X-Custom: example
"Email": "Test@example.com"
}
```
---

## ⚙️ Configuration

This project supports simple, extensible configuration using a single JSON file, typically located at `./application.json`. All key server settings—such as port and static resource mappings—are defined here.

### Example Configs

```json
{
"server": {
"port": 8080
},
"static": [
{ "path": "/public", "directory": "./public" },
{ "path": "/static", "directory": "./static" }
]
}
```

### How It Works
- **Server Port**
- The server listens on the port defined at server.port
- you can use any valid port number (e.g., `8080`, `3000`, etc.).
- **Static Resources**
- Each entry in the `static` array maps a URL prefix (`path`) to a directory on disk (`directory`).
- For example, requests to `/public/example.png` will serve the file located at `./public/example.png`.

### Adding or Changing Static Resources
To add more static resources, just simply add new entries in the `static` array:
```json
"static": [
{ "path": "/assets", "directory": "./assets" },
{ "path": "/media", "directory": "./media" }
]
```
Each mapping supports any valid URL prefix and directory path (relative to your project root)
### Loading and Using Configs
On startup, the server loads `application.json` and applies the configuration automatically. Config values are available anywhere in your Go code via the config package (works as a singleton).
```go
import (
"github.com/isaacwallace123/GoWeb/app/config"
"github.com/isaacwallace123/GoUtils/logger"
"github.com/isaacwallace123/GoWeb/app"
)

func main() {
// This is load your configs into a singleton struct.
if err := config.LoadConfig("./dev/application.json"); err != nil {
logger.Fatal("Failed to load config: %v", err)
}

router := app.NewRouter()

// UseStatic is a method that will automatically create a static resource URI to the designated directory
for _, s := range config.Static {
router.UseStatic(s.Path, s.Directory)
}

// Get the configured port (as string)
port := config.PortString() // e.g., ":8080"
}
```

---
## 🌐 CORS Middleware

Expand Down Expand Up @@ -211,7 +273,6 @@ Access-Control-Allow-Origin: https://example.com
```

---

### ❤️ Inspired By

- Java Spring Boot
Expand Down
41 changes: 41 additions & 0 deletions app/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package config

import (
"github.com/isaacwallace123/GoUtils/jsonutil"
"github.com/isaacwallace123/GoWeb/app/types"
"os"
"strconv"
)

type ServerConfig struct {
Port int `json:"port"`
}

var (
Server ServerConfig
Static []types.StaticConfig
)

type appConfig struct {
Server ServerConfig `json:"server"`
Static []types.StaticConfig `json:"static"`
}

func LoadConfig(path string) error {
file, err := os.ReadFile(path)
if err != nil {
return err
}
var loaded appConfig
if err := jsonutil.FromBytes(file, &loaded); err != nil {
return err
}
Server = loaded.Server
Static = loaded.Static
return nil
}

// PortString returns ":8080" (or whatever garbage port you configured)
func PortString() string {
return ":" + strconv.Itoa(Server.Port)
}
7 changes: 2 additions & 5 deletions app/internal/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,8 @@ func RegisterControllersImpl(controllers ...types.Controller) []CompiledRoute {
return compiled
}

func ListenImpl(routes []CompiledRoute, addr string) error {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
Dispatch(routes, w, req)
})
return http.ListenAndServe(addr, nil)
func ListenImpl(router http.Handler, addr string) error {
return http.ListenAndServe(addr, router)
}

func Dispatch(routes []CompiledRoute, w http.ResponseWriter, req *http.Request) {
Expand Down
48 changes: 43 additions & 5 deletions app/router.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package app

import (
"github.com/isaacwallace123/GoUtils/logger"
"github.com/isaacwallace123/GoWeb/app/internal"
"github.com/isaacwallace123/GoWeb/app/types"
"net/http"
"strings"
)

type Router struct {
routes []internal.CompiledRoute
routes []internal.CompiledRoute
resources []func(http.ResponseWriter, *http.Request) bool
}

// NewRouter creates a new Router.
Expand All @@ -21,11 +24,46 @@ func (r *Router) RegisterControllers(controllers ...types.Controller) {
}

// Listen starts the HTTP server.
func (r *Router) Listen(addr string) error {
return internal.ListenImpl(r.routes, addr)
}
func (r *Router) Listen(addr string) error { return internal.ListenImpl(r, addr) }

// ServeHTTP allows Router to implement http.Handler.
// ServeHTTP first tries static handlers, then dispatches dynamic routes.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for _, handler := range r.resources {
if handler(w, req) {
return
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand this is to return the static resource and break the flow so that it doesn't try dynamic route, it doesn't break the ServeHTTP flow right? (I assumed this didn't do anything and you were missing something lol)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this would break the typical dispatch to then serve the static resource instead. However, I'm thinking of maybe incorporating static resources more in dispatch rather than the servlet. What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatch would make more sense, however it depends how dispatch is handled. If dispatch does any other extra processing, then maybe it would be best to keep it in the servlet as it doesn't exhaust more resources than it needs to since it's just serving a static resource.

}
}

internal.Dispatch(r.routes, w, req)
}

// UseStatic registers a static file handler for the given URL prefix and directory.
func (r *Router) UseStatic(prefix, dir string) {
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
if len(prefix) > 1 && strings.HasSuffix(prefix, "/") {
prefix = strings.TrimSuffix(prefix, "/")
}

fs := http.FileServer(http.Dir(dir))
handler := func(w http.ResponseWriter, req *http.Request) bool {
path := req.URL.Path

if path == prefix {
http.Redirect(w, req, prefix+"/", http.StatusMovedPermanently)
logger.Info("[Static] Redirected: %s → %s/", path, prefix)
return true
}

if strings.HasPrefix(path, prefix+"/") {
logger.Info("[Static] %s → %s (%s)", prefix, dir, path)
http.StripPrefix(prefix, fs).ServeHTTP(w, req)
return true
}
return false
}

r.resources = append(r.resources, handler)
logger.Info("[Static] Registered: %-12s → %s", prefix, dir)
}
6 changes: 6 additions & 0 deletions app/types/static.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package types

type StaticConfig struct {
Path string `json:"path"`
Directory string `json:"directory"`
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/isaacwallace123/GoWeb

go 1.24.2

require github.com/isaacwallace123/GoUtils v1.0.1
require github.com/isaacwallace123/GoUtils v1.0.2
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/isaacwallace123/GoUtils v1.0.1 h1:XGG8zKeG+K/mWf6NQw1KIOppiz9WyxHuGVvFuc4MWC0=
github.com/isaacwallace123/GoUtils v1.0.1/go.mod h1:7SX/JIf8Zdml2dh6Zn6vNSrEsYlXwlkLzU8o85UG3Rk=
github.com/isaacwallace123/GoUtils v1.0.2 h1:CrWqtcuuWU6DxNWSbf5bkOGiZsILQvnNasvADYfgle4=
github.com/isaacwallace123/GoUtils v1.0.2/go.mod h1:7SX/JIf8Zdml2dh6Zn6vNSrEsYlXwlkLzU8o85UG3Rk=
8 changes: 7 additions & 1 deletion pkg/middlewares/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ var LoggingPost = types.NewMiddlewareBuilder("logging_post", &LoggingConfig{
}, func(ctx *types.MiddlewareContext, cfg *LoggingConfig) error {
if cfg.Enabled && ctx.ResponseEntity != nil {
methodColored := color.HTTPMethodToColor[ctx.Request.Method] + ctx.Request.Method + color.Reset
logger.Info("%s %s %d", methodColored, ctx.Request.URL.Path, ctx.ResponseEntity.StatusCode)
statusColor := color.HTTPStatusToColor(ctx.ResponseEntity.StatusCode)

logger.Info("%s %s %s%d%s",
methodColored,
ctx.Request.URL.Path,
statusColor, ctx.ResponseEntity.StatusCode, color.Reset,
)
}
return ctx.Next()
})