7.1 KiB
Library Usage — pkg/contactform
pkg/contactform is the public, reusable Go package that the nuntius HTTP server uses for validation. It has no third-party dependencies and no awareness of HTTP, SMTP, or the filesystem — drop it into any Go project that needs to validate a contact-style form payload.
Installation
go get sourcedock.dev/petrbalvin/nuntius/pkg/contactform
Because nuntius ships with no go.sum entries from third-party modules, this import pulls in only stdlib transitively.
Types
Request
The JSON body sent to the contact endpoint.
type Request struct {
Name string `json:"name"`
Email string `json:"email"`
Service string `json:"service,omitempty"`
Message string `json:"message"`
Honeypot string `json:"-"`
}
| Field | JSON tag | Notes |
|---|---|---|
Name |
name |
Required for contact, feedback, generic |
Email |
email |
Required for all four types |
Service |
service,omitempty |
Optional. Validated against an allow-list for contact only. |
Message |
message |
Required for contact, feedback, generic |
Honeypot |
- |
Never marshalled to / from JSON. Read it from a separate field in your own request struct, or wire it through your favourite JSON tag. |
The handler-side mapping (used inside the nuntius server) reads the honeypot value into Request.Honeypot based on the form's honeypot_field config. If you embed contactform.Request directly in your own API, you are responsible for reading the honeypot field separately.
Response
type Response struct {
OK bool `json:"ok"`
}
A successful submission returns {"ok": true}.
FieldError
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
}
One entry in the details[] array of a validation error.
ErrorResponse
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message,omitempty"`
Details []FieldError `json:"details,omitempty"`
}
Returned for any non-2xx response. The Error field is a short, machine-readable code (validation, invalid_json, origin_not_allowed, rate_limited, send_failed, storage_failed). Message is a human-readable summary; Details carries the per-field validation failures.
Functions
Validate
func Validate(r *Request, formType string) []FieldError
Dispatches to the right per-type validator. Returns a slice of field errors; an empty (or nil) slice means the request is valid.
formType |
What is checked |
|---|---|
"contact" |
name (2–100 runes), email (RFC 5322), service (allow-list), message (10–5,000 runes) |
"feedback" |
name, email, message (no service) |
"newsletter" |
email only |
"generic" |
name, email, message (no service) |
"" (empty) |
Falls back to contact |
| anything else | Falls back to contact — the strict allow-list is enforced at the config layer, not here |
Whitespace is trimmed from name, email, service, and message before validation. The trimmed values are what the validator sees and what the email body sees.
Constants
| Name | Value | Used by |
|---|---|---|
MinNameRunes |
2 |
name minimum length |
MaxNameRunes |
100 |
name maximum length |
MinMessageRunes |
10 |
message minimum length |
MaxMessageRunes |
5000 |
message maximum length |
Standalone Example
The examples/minimal/ directory contains a runnable demo. From the repo root:
go run ./examples/minimal
Source (examples/minimal/main.go):
// Minimal example showing how to use the contactform package
// standalone (without the HTTP server).
package main
import (
"fmt"
"os"
"sourcedock.dev/petrbalvin/nuntius/pkg/contactform"
)
func main() {
req := &contactform.Request{
Name: "Jane Doe",
Email: "jane@example.com",
Service: "architecture",
Message: "Hi, I'd like to discuss a possible engagement.",
}
if errs := contactform.Validate(req, "contact"); len(errs) > 0 {
fmt.Fprintln(os.Stderr, "validation failed:")
for _, e := range errs {
fmt.Fprintf(os.Stderr, " - %s: %s\n", e.Field, e.Message)
}
os.Exit(1)
}
fmt.Println("Request is valid:")
fmt.Printf(" name: %s\n", req.Name)
fmt.Printf(" email: %s\n", req.Email)
fmt.Printf(" service: %s\n", req.Service)
fmt.Printf(" message: %s\n", req.Message)
}
Custom Validators
If you need to extend the allow-list of service values, edit validServices in pkg/contactform/validate.go:
var validServices = map[string]bool{
"": true,
"architecture": true,
"ai": true,
"infrastructure": true,
"software": true,
"unix": true,
"other": true,
// add yours here, e.g.:
// "security": true,
}
If you need different length limits, edit the four Min* / Max* constants in the same file. Or fork the package — it is intentionally small (~150 LOC) so a copy-into-your-project is also reasonable.
Using Validate From Your Own HTTP Handler
package main
import (
"encoding/json"
"log"
"net/http"
"sourcedock.dev/petrbalvin/nuntius/pkg/contactform"
)
func contactHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
defer r.Body.Close()
var req contactform.Request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, contactform.ErrorResponse{
Error: "invalid_json",
Message: "Could not parse JSON body.",
})
return
}
if errs := contactform.Validate(&req, "contact"); len(errs) > 0 {
writeJSON(w, http.StatusBadRequest, contactform.ErrorResponse{
Error: "validation",
Details: errs,
})
return
}
// ... your SMTP / persistence / webhook code here ...
writeJSON(w, http.StatusOK, contactform.Response{OK: true})
}
func writeJSON(w http.ResponseWriter, code int, body any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(body)
}
func main() {
http.HandleFunc("/api/contact", contactHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
The example deliberately omits CORS, rate limiting, and the honeypot — copy those from internal/handler/contact.go if you need them.
Why a Separate Package?
pkg/contactform is the one piece of nuntius that is not tied to HTTP, SMTP, or the filesystem. Keeping it in a separate, dependency-free package:
- Makes it trivially embeddable in any Go service.
- Makes it cheap to unit-test (see
pkg/contactform/validate_test.go). - Makes the public API obvious — there is one entry point (
Validate) and four types, all in one file pair. - Keeps
internal/handlerfocused on HTTP,internal/emailon SMTP, andinternal/storageon disk.