Skip to content

Commit 53d4cd7

Browse files
Initial commit
0 parents  commit 53d4cd7

File tree

7 files changed

+909
-0
lines changed

7 files changed

+909
-0
lines changed

.github/workflows/test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v5
18+
with:
19+
go-version-file: 'go.mod'
20+
21+
- name: Run tests
22+
run: go test ./... -v -coverprofile=coverage.out
23+
24+
- name: Generate coverage report
25+
if: always()
26+
run: go tool cover -func=coverage.out
27+

README.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# httpserver
2+
3+
A simple and opinionated HTTP server package for Go that provides easy server creation, graceful shutdown, and JSON response helpers.
4+
5+
## Features
6+
7+
- **Simple Server Creation**: Create HTTP servers with route patterns and handlers
8+
- **TLS Support**: Built-in TLS/HTTPS support
9+
- **Graceful Shutdown**: Handles context cancellation and OS signals (SIGINT, SIGTERM)
10+
- **JSON Helpers**: Convenient function for writing JSON responses
11+
- **Error Handling**: Proper error handling and logging
12+
13+
## Installation
14+
15+
```bash
16+
go get github.com/ducksify/httpserver
17+
```
18+
19+
## Usage
20+
21+
### Basic Example
22+
23+
```go
24+
package main
25+
26+
import (
27+
"context"
28+
"net/http"
29+
"github.com/ducksify/httpserver"
30+
)
31+
32+
func main() {
33+
// Define handlers
34+
helloHandler := func(w http.ResponseWriter, r *http.Request) {
35+
httpserver.WriteJSON(w, http.StatusOK, map[string]string{
36+
"message": "Hello, World!",
37+
})
38+
}
39+
40+
healthHandler := func(w http.ResponseWriter, r *http.Request) {
41+
httpserver.WriteJSON(w, http.StatusOK, map[string]string{
42+
"status": "ok",
43+
})
44+
}
45+
46+
// Create server with routes
47+
patterns := []string{"/hello", "/health"}
48+
handlers := []http.HandlerFunc{helloHandler, healthHandler}
49+
50+
server, err := httpserver.NewServer(":8080", patterns, handlers...)
51+
if err != nil {
52+
panic(err)
53+
}
54+
55+
// Run server with graceful shutdown
56+
ctx := context.Background()
57+
if err := httpserver.RunServer(ctx, server, "config/tls/tls.crt", "config/tls/tls.key"); err != nil {
58+
panic(err)
59+
}
60+
}
61+
```
62+
63+
### API Reference
64+
65+
#### `NewServer(port string, patterns []string, handlers ...http.HandlerFunc) (*http.Server, error)`
66+
67+
Creates a new HTTP server with the specified routes and handlers.
68+
69+
**Parameters:**
70+
- `port`: The address to listen on (e.g., `:8080`)
71+
- `patterns`: Slice of URL patterns (routes)
72+
- `handlers`: Variadic slice of handler functions corresponding to each pattern
73+
74+
**Returns:**
75+
- `*http.Server`: The configured HTTP server
76+
- `error`: Error if patterns and handlers lengths don't match
77+
78+
**Example:**
79+
```go
80+
patterns := []string{"/api/users", "/api/posts"}
81+
handlers := []http.HandlerFunc{usersHandler, postsHandler}
82+
server, err := httpserver.NewServer(":8080", patterns, handlers...)
83+
```
84+
85+
#### `RunServer(ctx context.Context, server *http.Server, certFile, keyFile string) error`
86+
87+
Runs the server with TLS support and handles graceful shutdown. The server will:
88+
- Start listening with TLS using the provided certificate and key files
89+
- Handle context cancellation
90+
- Listen for OS signals (SIGINT, SIGTERM) for graceful shutdown
91+
- Return errors from server startup failures
92+
93+
**Parameters:**
94+
- `ctx`: Context for cancellation
95+
- `server`: The HTTP server instance to run
96+
- `certFile`: Path to the TLS certificate file
97+
- `keyFile`: Path to the TLS private key file
98+
99+
**Returns:**
100+
- `error`: Error from server startup or shutdown
101+
102+
**Example:**
103+
```go
104+
ctx, cancel := context.WithCancel(context.Background())
105+
defer cancel()
106+
107+
if err := httpserver.RunServer(ctx, server, "config/tls/tls.crt", "config/tls/tls.key"); err != nil {
108+
log.Fatal(err)
109+
}
110+
```
111+
112+
#### `WriteJSON(w http.ResponseWriter, status int, payload interface{})`
113+
114+
Writes a JSON response to the HTTP response writer.
115+
116+
**Parameters:**
117+
- `w`: The HTTP response writer
118+
- `status`: HTTP status code (e.g., `http.StatusOK`)
119+
- `payload`: The data to encode as JSON (any JSON-serializable type)
120+
121+
**Example:**
122+
```go
123+
httpserver.WriteJSON(w, http.StatusOK, map[string]interface{}{
124+
"id": 1,
125+
"name": "John Doe",
126+
"email": "john@example.com",
127+
})
128+
```
129+
130+
## TLS Configuration
131+
132+
The `RunServer` function requires TLS certificate and key file paths as parameters. Make sure these files exist before running your server.
133+
134+
Common locations:
135+
- Certificate: `config/tls/tls.crt`
136+
- Private Key: `config/tls/tls.key`
137+
138+
## Graceful Shutdown
139+
140+
The server supports graceful shutdown through:
141+
1. **Context Cancellation**: Cancel the context passed to `RunServer`
142+
2. **OS Signals**: Send SIGINT or SIGTERM to the process
143+
144+
The shutdown process allows up to 10 seconds for in-flight requests to complete.
145+
146+
## Testing
147+
148+
Run tests with:
149+
150+
```bash
151+
go test ./...
152+
```
153+
154+
Run tests with coverage:
155+
156+
```bash
157+
go test ./... -coverprofile=coverage.out
158+
go tool cover -html=coverage.out
159+
```
160+
161+
Current test coverage: **78.8%**
162+
163+
## License
164+
165+
[Add your license here]
166+

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/ducksify/httpserver
2+
3+
go 1.25.5

httpserver.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package httpserver
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"log/slog"
8+
"net/http"
9+
"os"
10+
"os/signal"
11+
"syscall"
12+
"time"
13+
)
14+
15+
func NewServer(port string, patterns []string, handlers ...http.HandlerFunc) (*http.Server, error) {
16+
if len(patterns) != len(handlers) {
17+
return nil, errors.New("patterns and handlers must have the same length")
18+
}
19+
mux := http.NewServeMux()
20+
for i, pattern := range patterns {
21+
mux.HandleFunc(pattern, handlers[i])
22+
}
23+
24+
return &http.Server{
25+
Addr: port,
26+
Handler: mux,
27+
}, nil
28+
}
29+
30+
func RunServer(ctx context.Context, server *http.Server, certFile, keyFile string) error {
31+
serverError := make(chan error, 1)
32+
go func() {
33+
if err := server.ListenAndServeTLS(certFile, keyFile); err != nil && err != http.ErrServerClosed {
34+
serverError <- err
35+
}
36+
close(serverError)
37+
}()
38+
39+
stop := make(chan os.Signal, 1)
40+
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
41+
defer signal.Stop(stop)
42+
43+
select {
44+
case <-ctx.Done():
45+
if err := shutdownServer(context.Background(), server); err != nil {
46+
return err
47+
}
48+
case err := <-serverError:
49+
if err != nil {
50+
slog.Error("Server error", "error", err)
51+
return err
52+
}
53+
case <-stop:
54+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
55+
defer cancel()
56+
if err := shutdownServer(shutdownCtx, server); err != nil {
57+
return err
58+
}
59+
}
60+
return nil
61+
}
62+
63+
func shutdownServer(ctx context.Context, server *http.Server) error {
64+
if err := server.Shutdown(ctx); err != nil {
65+
slog.Error("Failed to shutdown server", "error", err)
66+
return err
67+
}
68+
return nil
69+
}
70+
71+
func WriteJSON(w http.ResponseWriter, status int, payload interface{}) {
72+
w.Header().Set("Content-Type", "application/json")
73+
w.WriteHeader(status)
74+
if err := json.NewEncoder(w).Encode(payload); err != nil {
75+
slog.Error("Failed to encode JSON response", "error", err)
76+
}
77+
}

0 commit comments

Comments
 (0)