diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 99bd77458..3e0c96813 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,6 +17,7 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: 1.25.5
+ - run: npm install -g npm@latest
- uses: ./.github/actions/build-release-notes
- uses: actions/setup-node@v4
with:
diff --git a/Taskfile.yml b/Taskfile.yml
index be5e6ea05..c27c59567 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -47,7 +47,7 @@ tasks:
dir: npm
cmds:
- npm version {{.VERSION}}
- - npm publish
+ - npm publish --provenance
npm-build-windows:
cmds:
- go build -o ./npm/dist/mokapi-windows-amd64/mokapi.exe -ldflags="-X mokapi/version.BuildVersion={{.VERSION}}" ./cmd/mokapi
diff --git a/acceptance/cmd_test.go b/acceptance/cmd_test.go
index 851d20369..429acc517 100644
--- a/acceptance/cmd_test.go
+++ b/acceptance/cmd_test.go
@@ -9,6 +9,7 @@ import (
"mokapi/config/static"
"mokapi/engine"
"mokapi/feature"
+ "mokapi/health"
"mokapi/providers/asyncapi3"
"mokapi/providers/directory"
mail2 "mokapi/providers/mail"
@@ -63,14 +64,29 @@ func Start(cfg *static.Config) (*Cmd, error) {
app.UpdateConfig(e)
})
+ apiHandler := api.New(app, cfg.Api)
if u, err := api.BuildUrl(cfg.Api); err == nil {
- err = http.AddInternalService("api", u, api.New(app, cfg.Api))
+ err = http.AddInternalService("api", u, apiHandler)
if err != nil {
return nil, err
}
} else {
return nil, err
}
+ if cfg.Health.Enabled {
+ if u, err := health.BuildUrl(cfg.Health); err == nil {
+ if cfg.Api.Port == cfg.Health.Port {
+ apiHandler.RegisterHealthHandler(u.Path, health.New(cfg.Health))
+ } else {
+ err = http.AddInternalService("health", u, health.New(cfg.Health))
+ if err != nil {
+ return nil, err
+ }
+ }
+ } else {
+ return nil, err
+ }
+ }
pool := safe.NewPool(context.Background())
s := server.NewServer(pool, app, watcher, kafka, http, mailManager, ldap, scriptEngine)
diff --git a/acceptance/petstore_test.go b/acceptance/petstore_test.go
index 36eacb298..36a1211e5 100644
--- a/acceptance/petstore_test.go
+++ b/acceptance/petstore_test.go
@@ -24,6 +24,7 @@ type PetStoreSuite struct{ BaseSuite }
func (suite *PetStoreSuite) SetupSuite() {
cfg := static.NewConfig()
cfg.Api.Port = try.GetFreePort()
+ cfg.Health.Port = cfg.Api.Port
cfg.Providers.File.Directories = []static.FileConfig{{Path: "./petstore"}}
cfg.Api.Search.Enabled = true
suite.initCmd(cfg)
@@ -392,3 +393,10 @@ func (suite *PetStoreSuite) TestSearch_Paging() {
}),
)
}
+
+func (suite *PetStoreSuite) TestHealth() {
+ try.GetRequest(suite.T(), fmt.Sprintf("http://127.0.0.1:%v/health", suite.cfg.Api.Port), nil,
+ try.HasStatusCode(http.StatusOK),
+ try.HasBody(`{"status":"healthy"}`),
+ )
+}
diff --git a/api/handler.go b/api/handler.go
index 37fc68db6..4af96236a 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -19,13 +19,20 @@ import (
log "github.com/sirupsen/logrus"
)
+type Handler interface {
+ http.Handler
+ RegisterHealthHandler(path string, h http.Handler)
+}
+
type handler struct {
- config static.Api
- path string
- base string
- app *runtime.App
- fileServer http.Handler
- index string
+ config static.Api
+ path string
+ base string
+ app *runtime.App
+ fileServer http.Handler
+ index string
+ healthPath string
+ healthHandler http.Handler
}
type info struct {
@@ -67,7 +74,7 @@ type apiError struct {
Message string `json:"message"`
}
-func New(app *runtime.App, config static.Api) http.Handler {
+func New(app *runtime.App, config static.Api) Handler {
h := &handler{
config: config,
path: config.Path,
@@ -99,6 +106,11 @@ func BuildUrl(cfg static.Api) (*url.URL, error) {
return url.Parse(s)
}
+func (h *handler) RegisterHealthHandler(path string, handler http.Handler) {
+ h.healthPath = path
+ h.healthHandler = handler
+}
+
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "POST" {
http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed)
@@ -140,6 +152,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleFakerTree(w, r)
case strings.HasPrefix(p, "/api/search"):
h.getSearchResults(w, r)
+ case strings.HasPrefix(p, h.healthPath) && h.healthHandler != nil:
+ h.healthHandler.ServeHTTP(w, r)
case h.fileServer != nil:
if r.Method != "GET" {
http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed)
diff --git a/api/handler_test.go b/api/handler_test.go
index 9d0d4716a..52040c65d 100644
--- a/api/handler_test.go
+++ b/api/handler_test.go
@@ -3,6 +3,7 @@ package api_test
import (
"mokapi/api"
"mokapi/config/static"
+ "mokapi/health"
"mokapi/providers/openapi"
"mokapi/runtime"
"mokapi/runtime/runtimetest"
@@ -207,3 +208,56 @@ func TestHandler_SearchEnabled(t *testing.T) {
try.HasHeader("Content-Type", "application/json"),
try.HasBody(`{"version":"0.0.0","buildTime":"","search":{"enabled":true}}`))
}
+
+func TestHandler_Health(t *testing.T) {
+ testcases := []struct {
+ name string
+ cfg *static.Config
+ test func(t *testing.T, h http.Handler)
+ }{
+ {
+ name: "POST is not allowed",
+ cfg: &static.Config{},
+ test: func(t *testing.T, h http.Handler) {
+ r := httptest.NewRequest(http.MethodPatch, "http://foo.api/health", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusMethodNotAllowed, rr.Code)
+ },
+ },
+ {
+ name: "GET /health",
+ cfg: &static.Config{},
+ test: func(t *testing.T, h http.Handler) {
+ r := httptest.NewRequest(http.MethodGet, "http://foo.api/health", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Equal(t, `{"status":"healthy"}`, rr.Body.String())
+ },
+ },
+ {
+ name: "use path but request does not adapt",
+ cfg: &static.Config{Api: static.Api{Path: "/foo"}},
+ test: func(t *testing.T, h http.Handler) {
+ r := httptest.NewRequest(http.MethodGet, "http://foo.api/health", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Equal(t, `{"status":"healthy"}`, rr.Body.String())
+ },
+ },
+ }
+
+ t.Parallel()
+ for _, tc := range testcases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ h := api.New(runtime.New(tc.cfg), tc.cfg.Api)
+ h.RegisterHealthHandler("/health", health.New(static.Health{}))
+ tc.test(t, h)
+ })
+ }
+}
diff --git a/cmd/mokapi/main_test.go b/cmd/mokapi/main_test.go
index 81e1868fb..4ce178986 100644
--- a/cmd/mokapi/main_test.go
+++ b/cmd/mokapi/main_test.go
@@ -84,6 +84,11 @@ api:
enabled: true
indexPath: ""
inMemory: false
+health:
+ enabled: true
+ path: /health
+ port: 8080
+ log: false
rootCaCert: ""
rootCaKey: ""
configs: []
diff --git a/config/dynamic/provider/git/git.go b/config/dynamic/provider/git/git.go
index 31724ec7a..48fa360d1 100644
--- a/config/dynamic/provider/git/git.go
+++ b/config/dynamic/provider/git/git.go
@@ -15,7 +15,6 @@ import (
"time"
"github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/client"
"github.com/go-git/go-git/v5/plumbing/transport/http"
@@ -271,9 +270,9 @@ func pull(r *repository) {
if r.repo == nil {
return
}
- err := r.repo.Fetch(&git.FetchOptions{RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}})
+ err := r.repo.Fetch(&git.FetchOptions{})
if errors.Is(err, git.ErrForceNeeded) {
- err = r.repo.Fetch(&git.FetchOptions{RefSpecs: []config.RefSpec{"+refs/*:refs/*", "HEAD:refs/heads/HEAD"}})
+ err = r.repo.Fetch(&git.FetchOptions{})
}
if err != nil {
if !errors.Is(err, git.NoErrAlreadyUpToDate) {
diff --git a/config/static/static_config.go b/config/static/static_config.go
index 85b05813e..1960ffc72 100644
--- a/config/static/static_config.go
+++ b/config/static/static_config.go
@@ -18,6 +18,7 @@ type Config struct {
ConfigFile string `json:"-" yaml:"-" flag:"config-file"`
Providers Providers `json:"providers" yaml:"providers"`
Api Api `json:"api" yaml:"api"`
+ Health Health `json:"health" yaml:"health"`
RootCaCert tls.FileOrContent `json:"rootCaCert" yaml:"rootCaCert" name:"root-ca-cert"`
RootCaKey tls.FileOrContent `json:"rootCaKey" yaml:"rootCaKey" name:"root-ca-cert"`
Configs Configs `json:"configs" yaml:"configs" explode:"config"`
@@ -39,6 +40,10 @@ func NewConfig() *Config {
cfg.Api.Dashboard = true
cfg.Api.Search.Enabled = true
+ cfg.Health.Enabled = true
+ cfg.Health.Port = 8080
+ cfg.Health.Path = "/health"
+
cfg.Providers.File.SkipPrefix = []string{"_"}
cfg.Event.Store = map[string]Store{"default": {Size: 100}}
cfg.DataGen.OptionalProperties = "0.85"
@@ -286,3 +291,10 @@ func (fc *FileConfig) Set(v any) error {
}
return fmt.Errorf("expected string, got %T", v)
}
+
+type Health struct {
+ Enabled bool `yaml:"enabled" json:"enabled"`
+ Path string `yaml:"path" json:"path"`
+ Port int `yaml:"port" json:"port"`
+ Log bool `yaml:"log" json:"log"`
+}
diff --git a/docs/config.json b/docs/config.json
index 75f4cbb4c..7130a90ff 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -182,6 +182,11 @@
"path": "/docs/configuration/reference"
}
]
+ },
+ {
+ "label": "Health Check",
+ "source": "configuration/healthcheck.md",
+ "path": "/docs/configuration/health-check"
}
]
},
diff --git a/docs/configuration/healthcheck.md b/docs/configuration/healthcheck.md
new file mode 100644
index 000000000..aac1882c3
--- /dev/null
+++ b/docs/configuration/healthcheck.md
@@ -0,0 +1,221 @@
+---
+title: Health Check Configuration
+description: Configure Mokapi's health endpoint for uptime monitoring, orchestration systems, and Kubernetes probes.
+subtitle: Configure Mokapi's health endpoint for uptime monitoring, orchestration systems, and Kubernetes probes.
+---
+
+# Health Check Configuration
+
+## Overview
+
+Mokapi provides a simple health endpoint to monitor service availability. This endpoint can be used for:
+
+- **Uptime monitoring:** Check if Mokapi is responding to requests
+- **Load balancer health checks:** Ensure traffic is only routed to healthy instances
+- **Kubernetes probes:** Liveness and readiness probes for pod management
+- **CI/CD validation:** Verify Mokapi started successfully before running tests
+
+By default, the health check listens on `http://localhost:8080/health` but can be fully customized.
+
+``` box=info title"Simple Health Check"
+The health endpoint is minimal and does not perform checks of external dependencies. It simply indicates that Mokapi is up and listening. A 200 OK response means Mokapi is ready to accept requests.
+```
+
+## Configuration
+
+Configure the health endpoint using YAML configuration or CLI flags. The health endpoint is controlled by the health section:
+
+```yaml
+health:
+ enabled: true
+ path: /health
+ port: 8080
+ log: false
+```
+
+### Configuration Fields
+
+
+
+
+
+ | Field |
+ Type |
+ Default |
+ Description |
+
+
+
+
+ | enabled |
+ bool |
+ true |
+ Enables or disables the health check endpoint. When `false`, Mokapi does not expose any health endpoint. |
+
+
+ | path |
+ string |
+ /health |
+ The HTTP path for the health endpoint. Must start with /. |
+
+
+ | port |
+ int |
+ 8080 |
+ The port on which the health endpoint is exposed. If it matches the API/dashboard port, the health endpoint is served by the same HTTP server. |
+
+
+ | log |
+ bool |
+ false |
+ If `true`, Mokapi logs all requests to the health endpoint using structured JSON logs. Useful for debugging but can generate high log volume. |
+
+
+
+
+
+### Example Configuration
+
+```yaml
+health:
+ enabled: true
+ path: /healthz # Custom path
+ port: 8081 # Dedicated port
+ log: true # Enable logging for debugging
+```
+
+### CLI Flags
+
+The health endpoint can also be configured using command-line flags:
+
+
+
+
+
+ | Flag |
+ Description |
+ Example |
+
+
+
+
+ | --health-enabled |
+ Enable or disable the health endpoint |
+ mokapi --health-enabled=true |
+
+
+ | --health-path |
+ Path for the health endpoint |
+ mokapi --health-path=/healthz |
+
+
+ | --health-port |
+ Port for the health endpoint |
+ mokapi --health-port=8081 |
+
+
+ | --health-log |
+ Log all health requests |
+ mokapi --health-log |
+
+
+
+
+
+### Example Command
+
+```
+mokapi /api/spec.yaml \
+ --health-path=/healthz \
+ --health-port=8081 \
+ --health-log
+```
+
+## Health Endpoint Response
+
+When Mokapi is healthy and responding, the endpoint returns:
+
+```http
+GET /health HTTP/1.1
+Host: localhost:8080
+
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+{"status":"healthy"}
+```
+
+- **Status Code:** 200 OK indicates the service is healthy and ready to accept requests
+- **Content Type:** application/json
+- **Body:** JSON object with status: "healthy"
+
+``` box=warning title="Limited Health Check"
+The health endpoint only validates that Mokapi's HTTP server is running and can process requests.
+```
+
+## Kubernetes Integration
+
+Use Mokapi's health endpoint with Kubernetes liveness and readiness probes to ensure proper orchestration:
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mokapi
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mokapi
+ template:
+ metadata:
+ labels:
+ app: mokapi
+ spec:
+ containers:
+ - name: mokapi
+ image: mokapi:latest
+ ports:
+ - containerPort: 8080
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 2
+ failureThreshold: 3
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 3
+ periodSeconds: 5
+ timeoutSeconds: 2
+ successThreshold: 1
+```
+
+### Understanding the Probes
+
+**Liveness Probe:** Kubernetes uses this to determine if the container is alive. If the liveness probe fails, Kubernetes restarts the container.
+
+**Readiness Probe:** Kubernetes uses this to determine if the container is ready to accept traffic. If the readiness probe fails, Kubernetes removes the pod from service endpoints.
+
+``` box=info title="Single Endpoint for Both Probes"
+Mokapi provides only one health endpoint (/health). Both livenessProbe and readinessProbe can point to
+the same endpoint. The probes differ in their configuration (timing, thresholds), not the endpoint they check.
+```
+
+### Kubernetes Configuration Notes
+
+- **Port conflicts:** Ensure the health port does not conflict with mocked APIs or dashboard ports in the same container
+- **Timing:** Adjust `initialDelaySeconds` based on Mokapi's startup time with your configuration
+- **Logging:** If `log`: true is enabled, health requests will appear in logs
+- **Failure thresholds:** Set appropriate `failureThreshold` values to avoid premature restarts
+
+## Best Practices
+
+- **Use a dedicated port (optional):**
+ If your mocked APIs or dashboard run on port 8080, consider setting `health.port` to a different port (e.g., 8081) to avoid conflicts and simplify network routing.
+- **Enable logging only when debugging:**
+ High-frequency probes can generate significant log volume. Use `log: true` only when troubleshooting probe failures or validating probe configuration.
\ No newline at end of file
diff --git a/docs/get-started/dashboard.md b/docs/get-started/dashboard.md
index 574c80ac1..06a2af874 100644
--- a/docs/get-started/dashboard.md
+++ b/docs/get-started/dashboard.md
@@ -106,12 +106,12 @@ You can find the OpenAPI specification of these endpoints
| Path | Description |
|-----------------------------------|----------------------------------------|
| /api/info | get information about mokapi's runtime |
- | /api/services | list of all services |
- | /api/services/http/{name} | Information about the http service |
- | /api/services/kafka/{name} | Information about the kafka cluster |
- | /api/services/mail/{name} | Information about the smtp server |
- | /api/services/kafka/{name}/groups | list of kafka groups |
- | /api/events | list of events |
- | /api/events/{id} | get event by id |
- | /api/metrics | get list of metrics |
- | /api/schema/example | returns example for given schema |
\ No newline at end of file
+| /api/services | list of all services |
+| /api/services/http/{name} | Information about the http service |
+| /api/services/kafka/{name} | Information about the kafka cluster |
+| /api/services/mail/{name} | Information about the smtp server |
+| /api/services/kafka/{name}/groups | list of kafka groups |
+| /api/events | list of events |
+| /api/events/{id} | get event by id |
+| /api/metrics | get list of metrics |
+| /api/schema/example | returns example for given schema |
\ No newline at end of file
diff --git a/go.mod b/go.mod
index d085e918a..3ebbbb4fa 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
github.com/evanw/esbuild v0.27.3
github.com/fsnotify/fsnotify v1.9.0
github.com/go-co-op/gocron v1.37.0
- github.com/go-git/go-git/v5 v5.16.5
+ github.com/go-git/go-git/v5 v5.17.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/jinzhu/inflection v1.0.0
@@ -20,7 +20,7 @@ require (
github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1
github.com/yuin/gopher-lua v1.1.1
- golang.org/x/net v0.50.0
+ golang.org/x/net v0.51.0
golang.org/x/text v0.34.0
gopkg.in/go-asn1-ber/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d
gopkg.in/yaml.v3 v3.0.1
@@ -57,7 +57,7 @@ require (
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
- github.com/go-git/go-billy/v5 v5.6.2 // indirect
+ github.com/go-git/go-billy/v5 v5.8.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v0.0.4 // indirect
diff --git a/go.sum b/go.sum
index 9af7b61d2..36ec8e37e 100644
--- a/go.sum
+++ b/go.sum
@@ -91,12 +91,12 @@ github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
-github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
-github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
+github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
+github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
-github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
+github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
+github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
@@ -195,8 +195,8 @@ golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVo
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
+golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
+golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/health/http_handler.go b/health/http_handler.go
new file mode 100644
index 000000000..df1093f69
--- /dev/null
+++ b/health/http_handler.go
@@ -0,0 +1,59 @@
+package health
+
+import (
+ "fmt"
+ "mokapi/config/static"
+ "mokapi/lib"
+ "net/http"
+ "net/url"
+
+ log "github.com/sirupsen/logrus"
+)
+
+type handler struct {
+ path string
+ log bool
+}
+
+func New(cfg static.Health) http.Handler {
+ return &handler{path: healthPath(cfg), log: cfg.Log}
+}
+
+func BuildUrl(cfg static.Health) (*url.URL, error) {
+ s := fmt.Sprintf("http://:%v%v", cfg.Port, healthPath(cfg))
+ return url.Parse(s)
+}
+
+func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ w.Header().Set("Allow", http.MethodGet)
+ http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed)
+ if h.log {
+ log.Warnf("healthcheck: method not allowed: %v %v", r.Method, lib.GetUrl(r))
+ }
+ return
+ }
+
+ switch r.URL.Path {
+ case h.path:
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"status":"healthy"}`))
+ if h.log {
+ log.Infof("healthcheck: %v %v: healthy", r.Method, lib.GetUrl(r))
+ }
+ default:
+ http.NotFound(w, r)
+ if h.log {
+ log.Debugf("healthcheck: not found: %v %v", r.Method, lib.GetUrl(r))
+ }
+ }
+}
+
+func healthPath(cfg static.Health) string {
+ path := "/health"
+ if cfg.Path != "" {
+ path = cfg.Path
+ }
+ return path
+}
diff --git a/health/http_handler_test.go b/health/http_handler_test.go
new file mode 100644
index 000000000..945aa3765
--- /dev/null
+++ b/health/http_handler_test.go
@@ -0,0 +1,112 @@
+package health_test
+
+import (
+ "mokapi/config/static"
+ "mokapi/health"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/sirupsen/logrus"
+ "github.com/sirupsen/logrus/hooks/test"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHealth(t *testing.T) {
+ testcases := []struct {
+ name string
+ cfg static.Health
+ test func(t *testing.T, h http.Handler, hook *test.Hook)
+ }{
+ {
+ name: "Health OK",
+ cfg: static.Health{},
+ test: func(t *testing.T, h http.Handler, hook *test.Hook) {
+ r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/health", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Equal(t, `{"status":"healthy"}`, rr.Body.String())
+ },
+ },
+ {
+ name: "empty path invalid request URL",
+ cfg: static.Health{},
+ test: func(t *testing.T, h http.Handler, hook *test.Hook) {
+ r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusNotFound, rr.Code)
+ },
+ },
+ {
+ name: "set path",
+ cfg: static.Health{Path: "/health/live"},
+ test: func(t *testing.T, h http.Handler, hook *test.Hook) {
+ r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/health/live", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Equal(t, `{"status":"healthy"}`, rr.Body.String())
+ },
+ },
+ {
+ name: "404",
+ cfg: static.Health{},
+ test: func(t *testing.T, h http.Handler, hook *test.Hook) {
+ r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/foo", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusNotFound, rr.Code)
+ },
+ },
+ {
+ name: "404 with logging",
+ cfg: static.Health{Log: true},
+ test: func(t *testing.T, h http.Handler, hook *test.Hook) {
+ r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/foo", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Len(t, hook.Entries, 1)
+ require.Equal(t, logrus.DebugLevel, hook.LastEntry().Level)
+ require.Equal(t, "healthcheck: not found: GET http://127.0.0.1:8080/foo", hook.LastEntry().Message)
+ },
+ },
+ {
+ name: "method not allowed with logging",
+ cfg: static.Health{Log: true},
+ test: func(t *testing.T, h http.Handler, hook *test.Hook) {
+ r := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:8080/foo", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Equal(t, http.StatusMethodNotAllowed, rr.Code)
+ require.Equal(t, http.MethodGet, rr.Header().Get("Allow"))
+ require.Len(t, hook.Entries, 1)
+ require.Equal(t, logrus.WarnLevel, hook.LastEntry().Level)
+ require.Equal(t, "healthcheck: method not allowed: POST http://127.0.0.1:8080/foo", hook.LastEntry().Message)
+ },
+ },
+ {
+ name: "healthy with logging",
+ cfg: static.Health{Log: true},
+ test: func(t *testing.T, h http.Handler, hook *test.Hook) {
+ r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/health", nil)
+ rr := httptest.NewRecorder()
+ h.ServeHTTP(rr, r)
+ require.Len(t, hook.Entries, 1)
+ require.Equal(t, logrus.InfoLevel, hook.LastEntry().Level)
+ require.Equal(t, "healthcheck: GET http://127.0.0.1:8080/health: healthy", hook.LastEntry().Message)
+ },
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ logrus.SetLevel(logrus.DebugLevel)
+ hook := test.NewGlobal()
+
+ h := health.New(tc.cfg)
+ tc.test(t, h, hook)
+ })
+ }
+}
diff --git a/npm/package.json b/npm/package.json
index 13f1e1e9a..f8c9bf0ad 100644
--- a/npm/package.json
+++ b/npm/package.json
@@ -5,7 +5,7 @@
"homepage": "https://mokapi.io",
"repository": {
"type": "git",
- "url": "https://github.com/marle3003/mokapi.git"
+ "url": "git+https://github.com/marle3003/mokapi.git"
},
"bin": {
"mokapi": "bin/mokapi.js"
diff --git a/npm/types/index.d.ts b/npm/types/index.d.ts
index a0dc7912d..328e5d640 100644
--- a/npm/types/index.d.ts
+++ b/npm/types/index.d.ts
@@ -16,7 +16,7 @@ import "./mustache";
import "./yaml";
import "./encoding";
import "./mail";
-import "./file"
+import "./file";
/**
* Attaches an event handler for the given event.
@@ -794,4 +794,4 @@ export interface SharedMemory {
* mokapi.log(`Current counter: ${count}`)
* ```
*/
-export const shared: SharedMemory;
\ No newline at end of file
+export const shared: SharedMemory;
diff --git a/pkg/cmd/mokapi/flags/health.go b/pkg/cmd/mokapi/flags/health.go
new file mode 100644
index 000000000..c5eef08b6
--- /dev/null
+++ b/pkg/cmd/mokapi/flags/health.go
@@ -0,0 +1,75 @@
+package flags
+
+import "mokapi/pkg/cli"
+
+func RegisterHealthFlags(cmd *cli.Command) {
+ cmd.Flags().Bool("health-enabled", true, healthEnabled)
+ cmd.Flags().Int("health-port", 8080, healthPort)
+ cmd.Flags().String("health-path", "/health", healthPath)
+ cmd.Flags().Bool("health-log", false, healthLog)
+}
+
+var healthEnabled = cli.FlagDoc{
+ Short: "Enables or disables the health check endpoint entirely.",
+ Long: `Enables or disables the health check endpoint entirely.
+When set to false, Mokapi will not expose any health endpoint.`,
+ Examples: []cli.Example{
+ {
+ Codes: []cli.Code{
+ {Title: "CLI", Source: "--health-enabled false"},
+ {Title: "Env", Source: "MOKAPI_HEALTH_ENABLED=false"},
+ {Title: "File", Source: "health:\n enabled: false"},
+ },
+ },
+ },
+}
+
+var healthPort = cli.FlagDoc{
+ Short: "The port on which the health endpoint is exposed.",
+ Long: `The port on which the health endpoint is exposed.
+- If the value matches the dashboard/API port, the health endpoint is served by the same HTTP server.
+- If a different port is specified, Mokapi starts a separate HTTP listener dedicated to health checks.`,
+ Examples: []cli.Example{
+ {
+ Codes: []cli.Code{
+ {Title: "CLI", Source: "--health-port 8081"},
+ {Title: "Env", Source: "MOKAPI_HEALTH_PORT=8081"},
+ {Title: "File", Source: "health:\n port: 8081"},
+ },
+ },
+ },
+}
+
+var healthPath = cli.FlagDoc{
+ Short: "The HTTP path for the health endpoint (default: /health).",
+ Long: `The HTTP path for the health endpoint.
+- If empty, the default path /health is used.
+- The value must be an absolute path (starting with /).`,
+ Examples: []cli.Example{
+ {
+ Codes: []cli.Code{
+ {Title: "CLI", Source: "--health-path /health/live"},
+ {Title: "Env", Source: "MOKAPI_HEALTH_PATH=/health/live"},
+ {Title: "File", Source: "health:\n path: /health/live"},
+ },
+ },
+ },
+}
+
+var healthLog = cli.FlagDoc{
+ Short: "Controls whether HTTP requests to the health endpoint are logged.",
+ Long: `Controls whether HTTP requests to the health endpoint are logged.
+By default, health check requests are not logged to avoid excessive log noise
+from load balancers, uptime monitors, and orchestration systems.
+
+Enable this option when debugging health check behavior.`,
+ Examples: []cli.Example{
+ {
+ Codes: []cli.Code{
+ {Title: "CLI", Source: "--health-log"},
+ {Title: "Env", Source: "MOKAPI_HEALTH_LOG=true"},
+ {Title: "File", Source: "health:\n log: true"},
+ },
+ },
+ },
+}
diff --git a/pkg/cmd/mokapi/flags/health_test.go b/pkg/cmd/mokapi/flags/health_test.go
new file mode 100644
index 000000000..33d598d28
--- /dev/null
+++ b/pkg/cmd/mokapi/flags/health_test.go
@@ -0,0 +1,71 @@
+package flags_test
+
+import (
+ "mokapi/config/static"
+ "mokapi/pkg/cli"
+ "mokapi/pkg/cmd/mokapi"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRoot_Health(t *testing.T) {
+ testcases := []struct {
+ name string
+ cmd func(t *testing.T) *cli.Command
+ test func(t *testing.T, cfg *static.Config, flags *cli.FlagSet)
+ }{
+ {
+ name: "port",
+ cmd: func(t *testing.T) *cli.Command {
+ cmd := mokapi.NewCmdMokapi()
+ cmd.SetArgs([]string{"--health-port", "8081"})
+ return cmd
+ },
+ test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) {
+ require.Equal(t, 8081, cfg.Health.Port)
+ },
+ },
+ {
+ name: "path",
+ cmd: func(t *testing.T) *cli.Command {
+ cmd := mokapi.NewCmdMokapi()
+ cmd.SetArgs([]string{"--health-path", "/health/live"})
+ return cmd
+ },
+ test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) {
+ require.Equal(t, "/health/live", cfg.Health.Path)
+ },
+ },
+ {
+ name: "disabled",
+ cmd: func(t *testing.T) *cli.Command {
+ cmd := mokapi.NewCmdMokapi()
+ cmd.SetArgs([]string{"--health-enabled", "false"})
+ return cmd
+ },
+ test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) {
+ require.Equal(t, false, cfg.Health.Enabled)
+ },
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ defer func() {
+ cli.SetFileReader(&cli.FileReader{})
+ }()
+
+ cmd := tc.cmd(t)
+ var cfg *static.Config
+ cmd.Run = func(cmd *cli.Command, args []string) error {
+ cfg = cmd.Config.(*static.Config)
+ return nil
+ }
+ err := cmd.Execute()
+ require.NoError(t, err)
+
+ tc.test(t, cfg, cmd.Flags())
+ })
+ }
+}
diff --git a/pkg/cmd/mokapi/mokapi.go b/pkg/cmd/mokapi/mokapi.go
index e14a37bbc..65e24ae91 100644
--- a/pkg/cmd/mokapi/mokapi.go
+++ b/pkg/cmd/mokapi/mokapi.go
@@ -11,6 +11,7 @@ import (
"mokapi/config/static"
"mokapi/engine"
"mokapi/feature"
+ "mokapi/health"
"mokapi/pkg/cli"
"mokapi/pkg/cmd/mokapi/flags"
"mokapi/providers/asyncapi3"
@@ -62,6 +63,7 @@ func NewCmdMokapi() *cli.Command {
flags.RegisterNpmProvider(cmd)
flags.RegisterApiFlags(cmd)
+ flags.RegisterHealthFlags(cmd)
flags.RegisterTlsFlags(cmd)
flags.RegisterEventStoreFlags(cmd)
flags.RegisterDataGeneratorFlags(cmd)
@@ -136,14 +138,30 @@ func createServer(cfg *static.Config) (*server.Server, error) {
return nil, err
}
http := server.NewHttpManager(scriptEngine, certStore, app)
+
+ apiHandler := api.New(app, cfg.Api)
if u, err := api.BuildUrl(cfg.Api); err == nil {
- err = http.AddInternalService("api", u, api.New(app, cfg.Api))
+ err = http.AddInternalService("api", u, apiHandler)
if err != nil {
return nil, err
}
} else {
return nil, err
}
+ if cfg.Health.Enabled {
+ if u, err := health.BuildUrl(cfg.Health); err == nil {
+ if cfg.Api.Port == cfg.Health.Port {
+ apiHandler.RegisterHealthHandler(u.Path, health.New(cfg.Health))
+ } else {
+ err = http.AddInternalService("health", u, health.New(cfg.Health))
+ if err != nil {
+ return nil, err
+ }
+ }
+ } else {
+ return nil, err
+ }
+ }
kafka := server.NewKafkaManager(scriptEngine, app)
mqtt := server.NewMqttManager(scriptEngine, app)
diff --git a/runtime/index.go b/runtime/index.go
index 290934629..d7188ed00 100644
--- a/runtime/index.go
+++ b/runtime/index.go
@@ -133,9 +133,11 @@ initialization:
case <-ctx.Done():
close(s.queue)
- indexPath := getSearchIndexPath(s.cfg)
- if indexPath != "" {
- _ = os.RemoveAll(indexPath)
+ if !s.cfg.InMemory {
+ indexPath := getSearchIndexPath(s.cfg)
+ if indexPath != "" {
+ _ = os.RemoveAll(indexPath)
+ }
}
return
@@ -351,13 +353,9 @@ func getTypeFacet(term *bleveSearch.TermFacet) search.FacetValue {
}
func getSearchIndexPath(cfg static.Search) string {
- if cfg.InMemory {
- return ""
- }
-
indexPath := cfg.IndexPath
if indexPath == "" {
indexPath = os.TempDir()
}
- return filepath.Join(cfg.IndexPath, "mokapi-bleve-index")
+ return filepath.Join(indexPath, "mokapi-bleve-index")
}
diff --git a/server/server_http_test.go b/server/server_http_test.go
index 593d97d2a..4809b4f79 100644
--- a/server/server_http_test.go
+++ b/server/server_http_test.go
@@ -2,11 +2,14 @@ package server_test
import (
"fmt"
+ "io"
"mokapi/config/dynamic"
"mokapi/config/dynamic/dynamictest"
"mokapi/config/static"
"mokapi/engine/enginetest"
+ "mokapi/health"
"mokapi/providers/openapi/openapitest"
+ "mokapi/providers/openapi/schema/schematest"
"mokapi/runtime"
"mokapi/server"
"mokapi/server/cert"
@@ -217,6 +220,47 @@ func TestHttp(t *testing.T) {
require.Error(t, err)
},
},
+ {
+ name: "new service and healthcheck on same port",
+ test: func(t *testing.T, m *server.HttpManager) {
+ healthCfg := static.Health{Port: server.DefaultHttpPort}
+ u, err := health.BuildUrl(healthCfg)
+ require.NoError(t, err)
+ err = m.AddInternalService("health", u, health.New(healthCfg))
+ require.NoError(t, err)
+
+ m.Update(dynamic.ConfigEvent{
+ Config: &dynamic.Config{
+ Info: dynamictest.NewConfigInfo(),
+ Data: openapitest.NewConfig("3.1.0",
+ openapitest.WithPath("/", openapitest.NewPath(
+ openapitest.WithOperation("GET", openapitest.NewOperation(
+ openapitest.WithResponse(200, openapitest.WithContent(
+ "application/json",
+ openapitest.NewContent(openapitest.WithSchema(schematest.New("string", schematest.WithConst("foo")))),
+ )))),
+ )),
+ ),
+ },
+ })
+ waitStartup()
+
+ c := http.Client{}
+ r, err := c.Get(fmt.Sprintf("http://127.0.0.1:%v", server.DefaultHttpPort))
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, r.StatusCode)
+ body, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.Equal(t, `"foo"`, string(body))
+
+ r, err = c.Get(fmt.Sprintf("http://127.0.0.1:%v/health", server.DefaultHttpPort))
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, r.StatusCode)
+ body, err = io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.Equal(t, `{"status":"healthy"}`, string(body))
+ },
+ },
}
for _, tc := range testcases {
diff --git a/webui/e2e/home.spec.ts b/webui/e2e/home.spec.ts
index 00051f622..310117416 100644
--- a/webui/e2e/home.spec.ts
+++ b/webui/e2e/home.spec.ts
@@ -4,5 +4,5 @@ test('home overview', async ({ home }) => {
await home.open()
await expect(home.heroTitle).toHaveText('Mock APIs. Test Faster. Ship Better.')
- await expect(home.heroDescription).toHaveText(`Mokapi is your always-on API contract guardian — lightweight, transparent, and spec-driven. Free, open-source, and fully under your control.`)
+ await expect(home.heroDescription).toHaveText(`Develop faster without waiting for backends. Test reliably without external dependencies. Deploy confidently with contract validation. Free, open-source, and fully under your control.`)
})
\ No newline at end of file
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 5b8e5c151..6624c3c5e 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -13,7 +13,7 @@
"@types/bootstrap": "^5.2.10",
"@types/mokapi": "^0.34.0",
"@types/nodemailer": "^7.0.10",
- "@types/whatwg-mimetype": "^3.0.2",
+ "@types/whatwg-mimetype": "^5.0.0",
"ace-builds": "^1.43.5",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
@@ -31,7 +31,7 @@
"ncp": "^2.0.0",
"nodemailer": "^8.0.1",
"vue": "^3.5.28",
- "vue-router": "^5.0.2",
+ "vue-router": "^5.0.3",
"vue3-ace-editor": "^2.2.4",
"vue3-highlightjs": "^1.0.5",
"vue3-markdown-it": "^1.0.10",
@@ -43,13 +43,13 @@
"@rushstack/eslint-patch": "^1.16.1",
"@types/js-yaml": "^4.0.9",
"@types/markdown-it-container": "^4.0.0",
- "@types/node": "^25.2.3",
+ "@types/node": "^25.3.2",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.2",
- "eslint-plugin-vue": "^10.6.2",
+ "eslint-plugin-vue": "^10.8.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.8.1",
"typescript": "~5.9.3",
@@ -1374,12 +1374,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.2.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
- "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
+ "version": "25.3.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz",
+ "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==",
"license": "MIT",
"dependencies": {
- "undici-types": "~7.16.0"
+ "undici-types": "~7.18.0"
}
},
"node_modules/@types/nodemailer": {
@@ -1392,9 +1392,9 @@
}
},
"node_modules/@types/whatwg-mimetype": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
- "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-YYiBDCoqBgDIF2ByYn4qDb4RaXZ46cOQ6j2We1Ni3bikFNI7YFeL8jxSiYowWsriZrb1mw09CLZ+b+ZkIhsLVw==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
@@ -1764,21 +1764,21 @@
}
},
"node_modules/@vue/devtools-api": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz",
- "integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==",
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz",
+ "integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==",
"license": "MIT",
"dependencies": {
- "@vue/devtools-kit": "^8.0.5"
+ "@vue/devtools-kit": "^8.0.6"
}
},
"node_modules/@vue/devtools-kit": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz",
- "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==",
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz",
+ "integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==",
"license": "MIT",
"dependencies": {
- "@vue/devtools-shared": "^8.0.5",
+ "@vue/devtools-shared": "^8.0.6",
"birpc": "^2.6.1",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
@@ -1788,9 +1788,9 @@
}
},
"node_modules/@vue/devtools-shared": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz",
- "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==",
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz",
+ "integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
@@ -3420,9 +3420,9 @@
}
},
"node_modules/eslint-plugin-vue": {
- "version": "10.7.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.7.0.tgz",
- "integrity": "sha512-r2XFCK4qlo1sxEoAMIoTTX0PZAdla0JJDt1fmYiworZUX67WeEGqm+JbyAg3M+pGiJ5U6Mp5WQbontXWtIW7TA==",
+ "version": "10.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz",
+ "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3439,7 +3439,7 @@
"peerDependencies": {
"@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
"@typescript-eslint/parser": "^7.0.0 || ^8.0.0",
- "eslint": "^8.57.0 || ^9.0.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"vue-eslint-parser": "^10.0.0"
},
"peerDependenciesMeta": {
@@ -7123,9 +7123,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unicorn-magic": {
@@ -7427,14 +7427,14 @@
}
},
"node_modules/vue-router": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz",
- "integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==",
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz",
+ "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.28.6",
"@vue-macros/common": "^3.1.1",
- "@vue/devtools-api": "^8.0.0",
+ "@vue/devtools-api": "^8.0.6",
"ast-walker-scope": "^0.8.3",
"chokidar": "^5.0.0",
"json5": "^2.2.3",
diff --git a/webui/package.json b/webui/package.json
index 661e7e17a..723f76c82 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -22,7 +22,7 @@
"@types/bootstrap": "^5.2.10",
"@types/mokapi": "^0.34.0",
"@types/nodemailer": "^7.0.10",
- "@types/whatwg-mimetype": "^3.0.2",
+ "@types/whatwg-mimetype": "^5.0.0",
"ace-builds": "^1.43.5",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
@@ -40,7 +40,7 @@
"ncp": "^2.0.0",
"nodemailer": "^8.0.1",
"vue": "^3.5.28",
- "vue-router": "^5.0.2",
+ "vue-router": "^5.0.3",
"vue3-ace-editor": "^2.2.4",
"vue3-highlightjs": "^1.0.5",
"vue3-markdown-it": "^1.0.10",
@@ -52,13 +52,13 @@
"@rushstack/eslint-patch": "^1.16.1",
"@types/js-yaml": "^4.0.9",
"@types/markdown-it-container": "^4.0.0",
- "@types/node": "^25.2.3",
+ "@types/node": "^25.3.2",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.2",
- "eslint-plugin-vue": "^10.6.2",
+ "eslint-plugin-vue": "^10.8.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.8.1",
"typescript": "~5.9.3",
diff --git a/webui/src/views/DocsView.vue b/webui/src/views/DocsView.vue
index 56d33c19e..cc39c78ae 100644
--- a/webui/src/views/DocsView.vue
+++ b/webui/src/views/DocsView.vue
@@ -314,6 +314,9 @@ table {
margin-bottom: 20px;
font-size: 0.9rem;
}
+.content table tbody td {
+ padding: 4px 0 3px 12px;
+}
table.selectable td {
cursor: pointer;
}
diff --git a/webui/src/views/Home.vue b/webui/src/views/Home.vue
index 0f3c5cb87..d939bbc6b 100644
--- a/webui/src/views/Home.vue
+++ b/webui/src/views/Home.vue
@@ -48,16 +48,24 @@ function showImage(evt: MouseEvent) {
Mock APIs. Test Faster. Ship Better.
-
+
+
+ HTTP
+
+
+ Kafka
+
+
+ LDAP
+
+
+ Email
+
+
- Mokapi is your always-on API contract guardian —
- lightweight, transparent, and spec-driven.
-
+ Develop faster without waiting for backends. Test reliably without
+ external dependencies. Deploy confidently with contract validation.
+
Free, open-source, and fully under your control.
@@ -65,13 +73,19 @@ function showImage(evt: MouseEvent) {
Get Started
-
- Tutorials
-
+
+ See Demo
+
-

+
@@ -509,7 +523,7 @@ function showImage(evt: MouseEvent) {
-