nexus

package module
v1.4.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 13 Imported by: 0

README

nexus

Nexus is a Go library designed to simplify the creation of HTTP servers by leveraging Go's standard library. It provides developers with a streamlined interface to set up robust and efficient web servers without relying on external frameworks. By utilizing Go's native capabilities, Nexus ensures optimal performance and seamless integration within the Go ecosystem.

Install

go get github.com/codecraftkit/nexus

Quick Start

package main

import (
	"net/http"
	"os"

	"github.com/codecraftkit/nexus"
)

func main() {
	server := &nexus.Server{
		ServerName: "MyApp",
		Port:       os.Getenv("PORT"),
		Debug:      true,
		Endpoints: [][]nexus.Endpoint{
			HomeEndpoints,
		},
	}

	server.Run()
}

var HomeEndpoints = []nexus.Endpoint{
	{Path: "GET /", HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello World"))
	}},
}

Real-World Example

A common pattern is to organize handlers in separate packages with dependency injection. Each handler package registers its own routes via server.Group():

main.go
package main

import "myapp/server"

func main() {
	server.InitServer().Run()
}
server/server.go
package server

import (
	"net/http"
	"os"

	"myapp/handlers/me"
	"myapp/handlers/users"
	"myapp/handlers/conversations"

	"github.com/codecraftkit/nexus"
	"github.com/rs/cors"
)

func InitServer() *nexus.Server {
	server := &nexus.Server{
		ServerName: "API",
		Port:       os.Getenv("PORT"),
		CorsOptions: cors.Options{
			AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
			AllowedHeaders:   []string{"Origin", "Content-Type", "Accept", "Authorization"},
			AllowCredentials: true,
		},
		Settings: &nexus.Settings{
			PathPrefix: "/api/v1",
		},
	}

	// Global middlewares
	server.Use(LogMiddleware)
	server.Use(AuthMiddleware)

	// Register handler groups — each package registers its own routes
	userApp := &users.UserApplication{/* ... */}
	me.NewMeHandler(server, userApp)
	users.NewUsersHandler(server, userApp)
	conversations.NewConversationsHandler(server)

	return server
}

func LogMiddleware(next http.Handler, server *nexus.Server) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("%s %s\n", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
	})
}

func AuthMiddleware(next http.Handler, server *nexus.Server) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := r.Header.Get("Authorization")
		if token == "" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}
handlers/users/handler.go
package users

import "github.com/codecraftkit/nexus"

type UsersHandler struct {
	userApp UserApplicationPort
}

func NewUsersHandler(server *nexus.Server, userApp UserApplicationPort) {
	handler := &UsersHandler{userApp: userApp}

	server.Group("/users", []nexus.Endpoint{
		{Path: "GET /", HandlerFunc: handler.GetAll},
		{Path: "GET /{user_id}", HandlerFunc: handler.GetByID},
		{Path: "PUT /{user_id}/email", HandlerFunc: handler.UpdateEmail},
		{Path: "POST /ban", HandlerFunc: handler.ToBan},
		{Path: "DELETE /ban", HandlerFunc: handler.UnBan},
	})
}
handlers/users/get_all.go
package users

import (
	"net/http"

	"github.com/codecraftkit/nexus"
)

func (h *UsersHandler) GetAll(w http.ResponseWriter, r *http.Request) {
	users, err := h.userApp.GetAll(r.Context())
	if err != nil {
		nexus.ResponseWithError(w, http.StatusInternalServerError, err.Error())
		return
	}

	nexus.ResponseWithJSON(w, http.StatusOK, users)
}
handlers/me/handler.go
package me

import "github.com/codecraftkit/nexus"

type MeHandler struct {
	userApp UserApplicationPort
}

func NewMeHandler(server *nexus.Server, userApp UserApplicationPort) {
	handler := &MeHandler{userApp: userApp}

	server.Group("/me", []nexus.Endpoint{
		{Path: "GET /", HandlerFunc: handler.GetMe},
		{Path: "PUT /update-email", HandlerFunc: handler.UpdateEmail},
	})

	server.Group("/me/organizations", []nexus.Endpoint{
		{Path: "GET /", HandlerFunc: handler.GetOrganizations},
		{Path: "GET /owned", HandlerFunc: handler.GetOrganizationsOwned},
	})
}
handlers/conversations/handler.go
package conversations

import "github.com/codecraftkit/nexus"

type ConversationsHandler struct {
	// ...
}

func NewConversationsHandler(server *nexus.Server) {
	handler := &ConversationsHandler{}

	server.Group("/conversations", []nexus.Endpoint{
		{Path: "GET /", HandlerFunc: handler.GetAll},
		{Path: "DELETE /", HandlerFunc: handler.DeleteMany},
	})

	server.Group("/conversations/{conversation_id}", []nexus.Endpoint{
		{Path: "GET /", HandlerFunc: handler.GetByID},
		{Path: "DELETE /", HandlerFunc: handler.Delete},
	})

	server.Group("/conversations/{conversation_id}/security", []nexus.Endpoint{
		{Path: "POST /pin/validate", HandlerFunc: handler.ValidatePin},
	})
}

Multiple Servers

Use nexus.Serve() to run multiple servers concurrently:

func main() {
	apiServer := &nexus.Server{
		ServerName: "API",
		Port:       "8081",
		Settings:   &nexus.Settings{PathPrefix: "/api/v1"},
		Endpoints:  [][]nexus.Endpoint{ApiEndpoints},
	}

	adminServer := &nexus.Server{
		ServerName: "Admin",
		Port:       "8082",
		Settings:   &nexus.Settings{PathPrefix: "/admin"},
		Endpoints:  [][]nexus.Endpoint{AdminEndpoints},
	}

	nexus.Serve([]*nexus.Server{apiServer, adminServer})
}

Server Configuration

Field Type Description
Port string Port to listen on (defaults to "8080")
Debug bool Enables request logging for all requests (except /_health)
Secret string Secret value for the ValidateSecret built-in middleware
ServerName string Name shown in logs (defaults to "Server 0")
RunningServerMessage string Custom startup message
Middlewares []func(next http.Handler, server *Server) http.Handler Global middleware chain
Endpoints [][]Endpoint Grouped endpoint slices
CorsOptions cors.Options CORS configuration (via github.com/rs/cors)
Settings *Settings Additional settings (see below)
Settings
server := &nexus.Server{
	Settings: &nexus.Settings{
		PathPrefix:   "/api/v1", // Prepended to all endpoint paths
		IgnoreSecret: true,      // Disable secret validation

		// Server timeouts. Zero keeps the defaults below, so leaving them out
		// behaves exactly as before they were configurable.
		WriteTimeout: 30 * time.Second, // default: 15s
		ReadTimeout:  30 * time.Second, // default: 15s
	},
}
Streaming: do not disable the timeout server-wide

Zero means "keep the default", not "no timeout" — on purpose. Turning the write timeout off for the whole server so that one endpoint can stream lets a single stuck client hold a connection forever, and removes the protection from every other route to fix one.

For SSE, chunked downloads or any long-lived response, clear the deadline on that connection instead. It is in the standard library:

func streamHandler(w http.ResponseWriter, r *http.Request) {
	// Clears the write deadline for THIS connection only. The server default
	// keeps protecting every other route.
	if err := http.NewResponseController(w).SetWriteDeadline(time.Time{}); err != nil {
		http.Error(w, "streaming unsupported", http.StatusInternalServerError)
		return
	}
	// ... write events, Flush after each one
}

The Settings fields are for when the whole service needs a different number — a gateway proxying slow upstreams, a service whose every route uploads.

Endpoints

Endpoints use Go 1.22+ routing syntax: "METHOD /path".

var Endpoints = []nexus.Endpoint{
	{Path: "GET /users", HandlerFunc: GetUsers},
	{Path: "POST /users", HandlerFunc: CreateUser},
	{Path: "GET /users/{id}", HandlerFunc: GetUser},
	{Path: "PUT /users/{id}", HandlerFunc: UpdateUser},
	{Path: "DELETE /users/{id}", HandlerFunc: DeleteUser},
}
Handler Types

Each endpoint supports three mutually exclusive handler types:

// Standard http.HandlerFunc
{Path: "GET /home", HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("home"))
}}

// http.Handler (used internally by GroupWithOptions)
{Path: "GET /home", Handler: myHandler}

// Handler with access to the Server instance
{Path: "GET /status", HandlerServerFunc: func(server *nexus.Server) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		nexus.ResponseWithJSON(w, http.StatusOK, map[string]string{
			"server": server.ServerName,
		})
	}
}}

Setting both HandlerFunc and Handler on the same endpoint will panic.

Endpoint Options
{
	Path:        "GET /public",
	HandlerFunc: PublicHandler,
	Options: nexus.EndpointOptions{
		IsPublic:                 true, // Skips secret validation
		NoRequiresAuthentication: true, // Marks as no-auth required
		IgnorePrefix:             true, // Ignores Settings.PathPrefix
	},
}

Fluent API

Configure the server using methods instead of struct fields:

server := &nexus.Server{Port: "8080"}

// Add middlewares
server.Use(AuthMiddleware, LogMiddleware)

// Add a single endpoint
server.Endpoint("GET /ping", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("pong"))
})

server.Run()

Endpoint Groups

Group

Group() prefixes all endpoint paths with the group string:

server.Group("/users", []nexus.Endpoint{
	{Path: "GET /", HandlerFunc: handler.GetAll},             // → GET /users
	{Path: "GET /{user_id}", HandlerFunc: handler.GetByID},   // → GET /users/{user_id}
	{Path: "PUT /{user_id}/email", HandlerFunc: handler.UpdateEmail}, // → PUT /users/{user_id}/email
	{Path: "POST /ban", HandlerFunc: handler.ToBan},          // → POST /users/ban
})

You can use multiple groups with nested paths:

server.Group("/conversations", []nexus.Endpoint{
	{Path: "GET /", HandlerFunc: handler.GetAll},
})

server.Group("/conversations/{conversation_id}", []nexus.Endpoint{
	{Path: "GET /", HandlerFunc: handler.GetByID},
	{Path: "DELETE /", HandlerFunc: handler.Delete},
})

server.Group("/conversations/{conversation_id}/security", []nexus.Endpoint{
	{Path: "POST /pin/validate", HandlerFunc: handler.ValidatePin},
})
GroupWithOptions

GroupWithOptions() works like Group() but additionally wraps each endpoint with group-level middlewares:

server.GroupWithOptions("/admin", AdminEndpoints, &nexus.GroupOptions{
	Middlewares: []func(next http.Handler) http.Handler{
		RequireAdmin,
	},
})

Note: Group-level middlewares use the standard func(next http.Handler) http.Handler signature (without the server parameter).

Middlewares

Nexus middlewares receive both the next handler and the server instance:

func AuthMiddleware(next http.Handler, server *nexus.Server) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := r.Header.Get("Authorization")
		if token == "" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

Middlewares are applied in reverse order (last added wraps outermost). Register them via struct field or Use():

// Via struct
server := &nexus.Server{
	Middlewares: []func(next http.Handler, server *nexus.Server) http.Handler{
		AuthMiddleware,
	},
}

// Via Use() — can chain multiple at once
server.Use(ValidateSession)
server.Use(SetLanguage, GenerateCacheID, CachingMiddleware)
Built-in Middlewares
  • LogRequest — Automatically enabled when Debug: true. Logs [ServerName] METHOD /path for every request (except /_health).
  • ValidateSecret — When Secret is set, checks the x-secret header on non-public endpoints.

Response Helpers

// JSON response
nexus.ResponseWithJSON(w, http.StatusOK, data)

// Error response (returns structured ErrorResponse)
nexus.ResponseWithError(w, http.StatusBadRequest, "invalid input")

// Error response with custom ErrorResponse
nexus.ResponseJsonWithError(w, http.StatusBadRequest, &nexus.ErrorResponse{
	Code:     400,
	Message:  "Validation failed",
	CodeName: "validation_error",
	Errors:   map[string]string{"name": "required"},
})

// Pagination helper — parses ?page=&limit= query params (defaults: page=1, limit=20)
skip, limit, page := nexus.GetOptions(r.URL.Query())

// Paginated response
nexus.ResponseWithPagination(w, http.StatusOK, users)

Built-in Endpoints

Every server automatically includes these endpoints (they ignore PathPrefix and are always public):

Endpoint Description
GET /_health Returns "{ServerName} is running"
GET /_routes Returns a JSON map of all registered routes
GET /_routes/raw Returns the full list of endpoints as JSON

Documentation

Index

Constants

View Source
const (
	DefaultWriteTimeout = 15 * time.Second
	DefaultReadTimeout  = 15 * time.Second
)

Server timeout defaults, applied when Settings leaves them at zero. They are the values Run() hardcoded before they were configurable, so upgrading changes nothing on its own.

Variables

View Source
var ServerEndpoints = []Endpoint{
	{Path: "GET /_health", HandlerServerFunc: Health, Options: EndpointOptions{IsPublic: true, NoRequiresAuthentication: true, IgnorePrefix: true}},
	{Path: "GET /_routes", HandlerServerFunc: RoutesList, Options: EndpointOptions{IsPublic: true, NoRequiresAuthentication: true, IgnorePrefix: true}},
	{Path: "GET /_routes/raw", HandlerServerFunc: RawRoutesList, Options: EndpointOptions{IsPublic: true, NoRequiresAuthentication: true, IgnorePrefix: true}},
}

ServerEndpoints is the list of endpoints for the server

Functions

func GetOptions added in v1.3.2

func GetOptions(opt url.Values) (skip, limit, page int64)

func Health

func Health(server *Server) http.HandlerFunc

Health check if the server is running

func RawRoutesList added in v1.3.0

func RawRoutesList(server *Server) http.HandlerFunc

func RequestScheme added in v1.3.3

func RequestScheme(r *http.Request) string

func ResponseJsonWithError added in v1.3.2

func ResponseJsonWithError(w http.ResponseWriter, code int, errorResponse *ErrorResponse) error

func ResponseWithError

func ResponseWithError(w http.ResponseWriter, code int, msg string) error

ResponseWithError return a json response with an error

func ResponseWithJSON

func ResponseWithJSON(w http.ResponseWriter, code int, payload interface{}) error

ResponseWithJSON return a json response

func ResponseWithPagination added in v1.3.0

func ResponseWithPagination(w http.ResponseWriter, code int, payload interface{}) error

func ResponseWithPaginationTest added in v1.3.2

func ResponseWithPaginationTest(w http.ResponseWriter, code int, options *PaginationOptions) error

func RoutesList

func RoutesList(server *Server) http.HandlerFunc

RoutesList return all routes

func Serve added in v1.1.0

func Serve(servers []*Server)

Serve set and run several Severs

Types

type Endpoint added in v1.2.0

type Endpoint struct {
	Path              string
	HandlerFunc       http.HandlerFunc
	Handler           http.Handler                          // Handler is a http.Handler and is used to create a new http.Handler with the server's middlewares and endpoints
	HandlerServerFunc func(server *Server) http.HandlerFunc // HandlerServerFunc is a function that returns a http.HandlerFunc and is used to create a new http.HandlerFunc with the server's middlewares and endpoints
	Options           EndpointOptions
	RegexPattern      *regexp.Regexp
}

Endpoint is a struct that contains the endpoint's configuration and handlers

type EndpointOptions added in v1.2.0

type EndpointOptions struct {
	IsPublic                 bool
	NoRequiresAuthentication bool
	IgnorePrefix             bool
}

EndpointOptions is a struct that contains the endpoint's options

type ErrorResponse added in v1.3.2

type ErrorResponse struct {
	Code     int               `json:"code"`
	Message  string            `json:"message"`
	CodeName string            `json:"code_name"`
	Errors   map[string]string `json:"errors"`
}

type GroupOptions added in v1.3.14

type GroupOptions struct {
	Middlewares []func(next http.Handler) http.Handler
}

type PaginationOptions added in v1.3.2

type PaginationOptions struct {
	Page    int64
	Limit   int64
	Skip    int64
	Payload interface{}
	Path    string
	Total   int64
}

type ResponsePagination added in v1.3.0

type ResponsePagination struct {
	TotalPages   int64       `json:"total_pages"`
	Total        int64       `json:"total"`
	CurrentPage  int64       `json:"current_page"`
	From         int64       `json:"from"`
	To           int64       `json:"to"`
	Offset       int64       `json:"offset"`
	Limit        int64       `json:"limit"`
	PerPage      int         `json:"per_page"`
	Path         string      `json:"path"`
	FirstPageURL string      `json:"first_page_url"`
	NextPageURL  string      `json:"next_page_url"`
	LastPageURL  string      `json:"last_page_url"`
	PrevPageURL  string      `json:"prev_page_url"`
	Data         interface{} `json:"data"`
}

func (*ResponsePagination) Set added in v1.3.2

func (p *ResponsePagination) Set()

type Server

type Server struct {
	ServerName           string
	ServerNumber         string
	RunningServerMessage string
	Secret               string
	Debug                bool
	Port                 string
	Middlewares          []func(next http.Handler, server *Server) http.Handler
	Endpoints            [][]Endpoint
	EndpointsPaths       map[string]*Endpoint

	CorsOptions cors.Options
	Settings    *Settings
	// contains filtered or unexported fields
}

Server is a struct that contains the server's configuration and endpoints

func (*Server) ApplyMiddlewares added in v1.2.0

func (server *Server) ApplyMiddlewares(mux http.Handler) http.Handler

ApplyMiddlewares apply all middlewares to the mux; if the server is in debug mode, the server will be register the LogRequest middleware that will log the request on the console if the server has a secret, the server will be register the ValidateSecret middleware that will check if the request has a secret

func (*Server) Endpoint added in v1.3.3

func (server *Server) Endpoint(path string, handler http.HandlerFunc)

func (*Server) EndpointIsPublic added in v1.2.0

func (server *Server) EndpointIsPublic(r *http.Request) bool

EndpointIsPublic evalue if the endpoint is public

func (*Server) GetEndpoint added in v1.2.0

func (server *Server) GetEndpoint(r *http.Request) (*Endpoint, bool)

GetEndpoint evaluate if a path exists in the endpoints and return the endpoint and a bool if exists

func (*Server) GetEndpoints added in v1.2.0

func (server *Server) GetEndpoints() map[string]*Endpoint

GetEndpoints return all endpoints

func (*Server) Group added in v1.3.3

func (server *Server) Group(group string, apiEndpoints []Endpoint)

func (*Server) GroupWithOptions added in v1.3.14

func (server *Server) GroupWithOptions(group string, apiEndpoints []Endpoint, groupOptions *GroupOptions)

func (*Server) LogRequest added in v1.2.0

func (server *Server) LogRequest(next http.Handler) http.Handler

LogRequest log the request on the console

func (*Server) NoRequiresAuthentication added in v1.3.13

func (server *Server) NoRequiresAuthentication(r *http.Request) bool

func (*Server) PrepareEndpoints added in v1.4.3

func (server *Server) PrepareEndpoints() *http.ServeMux

PrepareEndpoints applies the path prefix, registers every endpoint on a mux and fills the route index that EndpointIsPublic and NoRequiresAuthentication read. It returns that mux. Run calls it and then serves the result.

It is exported so a caller can build the SAME wiring without listening. Until now the index was filled only inside Run, and that had a consequence worth spelling out: a test that assembled its own mux saw an EMPTY index, so matchRoute returned nil and every endpoint looked non-public. An assertion like "this group requires the API secret" then passed for a reason that had nothing to do with the group — it would have passed just the same with the group marked public. That is a green result that does not mean what it looks like, and the only way to avoid it without duplicating this route matching somewhere else is to let callers run this exact code.

It is idempotent: calling it twice neither prefixes the paths twice nor adds the library endpoints twice.

func (*Server) Run added in v1.2.1

func (server *Server) Run()

Run a new Server

func (*Server) Use added in v1.3.3

func (server *Server) Use(middlewares ...func(next http.Handler, server *Server) http.Handler)

func (*Server) ValidateSecret added in v1.2.0

func (server *Server) ValidateSecret(next http.Handler) http.Handler

ValidateSecret check if the request has a secret

type Settings added in v1.3.3

type Settings struct {
	IgnoreSecret bool
	PathPrefix   string

	// WriteTimeout and ReadTimeout override the server defaults
	// (DefaultWriteTimeout / DefaultReadTimeout). Zero means "not set" and keeps
	// the default, so existing servers behave exactly as before.
	//
	// They were hardcoded at 15s until now. That is a sane default for a REST
	// API and impossible for anything long-lived: an SSE stream, a chunked
	// download, or an upload over a slow connection dies mid-flight, and the
	// client sees a truncated body rather than an error.
	//
	// # Do NOT reach for a zero timeout to stream
	//
	// Zero here means "keep the default", not "no timeout" — deliberately.
	// Disabling the write timeout for the WHOLE server so that one endpoint can
	// stream lets a single stuck client hold a connection forever, and takes the
	// protection away from every other route to fix one.
	//
	// The right tool is per-connection and lives in the standard library:
	//
	//	// inside a streaming handler
	//	rc := http.NewResponseController(w)
	//	if err := rc.SetWriteDeadline(time.Time{}); err != nil {
	//		// the server does not support deadline control; fail loudly
	//	}
	//
	// That clears the deadline for THAT connection and leaves the server's
	// default protecting everything else. These fields are for when the whole
	// service needs a different number — a gateway that proxies slow upstreams,
	// a service whose every route uploads.
	WriteTimeout time.Duration
	ReadTimeout  time.Duration
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址