+
+## How to Use Mokapi for API Validation with Request Forwarding?
+
+Mokapi cannot only be used for mocking APIs, but it can also sit between any
+consumer and a backend service to validate real traffic. Using a small
+JavaScript script, Mokapi can forward requests to your backend and
+validates both requests and responses.
+
+
+ Consumer (Frontend, Playwright, Microservice) → Mokapi → Backend API
+
+```typescript
+import { on } from 'mokapi';
+import { fetch } from 'mokapi/http';
+
+/**
+ * This script demonstrates how to forward incoming HTTP requests
+ * to a real backend while letting Mokapi validate responses according
+ * to your OpenAPI spec.
+ *
+ * The script listens to all HTTP requests and forwards them based
+ * on the `request.api` field. Responses from the backend are
+ * validated when possible, and any errors are reported back to
+ * the client.
+ */
+export default async function () {
+
+ /**
+ * Register a global HTTP event handler.
+ * This function is called for every incoming request.
+ */
+ on('http', async (request, response) => {
+
+ // Determine the backend URL to forward this request to
+ const url = getForwardUrl(request)
+
+ // If no URL could be determined, return an error immediately
+ if (!url) {
+ response.statusCode = 500;
+ response.body = 'Failed to forward request: unknown backend';
+ return;
+ }
+
+ try {
+ // Forward the request to the backend
+ const res = await fetch(url, {
+ method: request.method,
+ body: request.body,
+ headers: request.header,
+ timeout: '30s'
+ });
+
+ // Copy status code and headers from the backend response
+ response.statusCode = res.statusCode;
+ response.headers = res.headers
+
+ // Check the content type to decide whether to validate the response
+ const contentType = res.headers['Content-Type']?.[0] || '';
+
+ if (contentType.includes('application/json')) {
+ // Mokapi can validate JSON responses automatically
+ response.data = res.json();
+ } else {
+ // For other content types, skip validation
+ response.body = res.body;
+ }
+
+ } catch (e) {
+ // Handle any errors that occur while forwarding
+ response.statusCode = 500;
+ response.body = e.toString();
+ }
+ });
+
+ /**
+ * Maps the incoming request to a backend URL based on the API name
+ * defined in the OpenAPI specification (`info.title`).
+ * @see https://mokapi.io/docs/javascript-api/mokapi/eventhandler/httprequest
+ *
+ * @param request - the incoming Mokapi HTTP request
+ * @returns the full URL to forward the request to, or undefined
+ */
+ function getForwardUrl(request: HttpRequest): string | undefined {
+ switch (request.api) {
+ case 'backend-1': {
+ return `https://backend1.example.com${request.url.path}?${request.url.query}`;
+ }
+ case 'backend-2': {
+ return `https://backend1.example.com${request.url.path}?${request.url.query}`;
+ }
+ default:
+ return undefined;
+ }
+ }
+}
+```
+
+For each interaction, Mokapi performs four important steps:
+
+### 1. Validates incoming requests
+
+Mokapi checks every incoming request against your OpenAPI specification:
+
+- HTTP method
+- URL & parameters
+- headers
+- request body
+
+If the client sends anything invalid, Mokapi blocks it and returns a clear
+validation error.
+
+### 2. Forwards valid requests to your backend
+
+If the request is valid, Mokapi forwards it unchanged to the backend using JavaScript.
+
+- No changes are required in your backend.
+- No additional infrastructure is necessary.
+
+### 3. Validates backend responses
+
+Once the backend responds, Mokapi validates the response against the OpenAPI specification:
+
+- status codes
+- headers
+- response body
+
+If something doesn't match the contract, Mokapi blocks it and sends a validation error back to the client.
+
+### 4. Return the validated response to the client
+
+Only responses that pass validation reach the client, guaranteeing contract fidelity end-to-end.
+
+## Where You Can Use Mokapi for Request Forwarding and Validation
+
+Mokapi’s forwarding and validation capabilities make it useful far beyond local development or Playwright scripting.
+
+### Between Frontend and Backend
+
+Placing Mokapi between your frontend and backend ensures:
+- automatic request and response validation
+- immediate detection of breaking changes
+- backend and API specification evolve together
+- fewer “why is the frontend broken?” debugging loops
+
+Frontend developers can experiment with confidence, knowing the backend
+cannot silently diverge from the published contract.
+
+### Between Backend Services (Service-to-Service)
+
+In microservice architectures, API drift between services is a frequent cause of instability.
+Routing service-to-service traffic through Mokapi gives you:
+- strict contract enforcement between services
+- early detection of incompatible changes
+- stable integrations even as teams evolve independently
+- clear validation errors during development and CI
+
+Mokapi becomes a lightweight, spec-driven contract guardian across your backend ecosystem.
+
+### In Automated Testing (e.g., Playwright)
+
+This is one of the most powerful setups.
+
+ Playwright → Mokapi → Backend
+
+Benefits:
+- CI fails immediately when the backend breaks the API contract
+- tests interact with the real backend, not mocks
+- validation errors are clear and actionable
+- tests remain simpler — no need to validate everything in Playwright
+
+Your tests are guaranteed to hit a backend that actually matches the API contract.
+
+### In Kubernetes Test Environments
+
+Mokapi can also be used in temporary or preview environments to ensure contract validation across the entire cluster.
+
+In Kubernetes, Mokapi can be deployed as:
+- a sidecar container
+- a standalone validation layer in front of backend services
+- a temporary component inside preview environments
+
+This brings:
+- consistent contract validation for all cluster traffic
+- early detection of breaking API changes before staging
+- contract enforcement without modifying backend services
+- transparent operation — apps talk to Mokapi, Mokapi talks to the backend
+
+You can integrate Mokapi into Helm charts, GitOps workflows, or test namespaces.
+
+## Why Teams Benefit from Using Mokapi Between Client and Backend
+
+### Automatic Contract Enforcement
+
+Every interaction is validated against your OpenAPI specification. Your backend can no longer quietly drift from the contract.
+
+### Immediate Detection of Breaking Changes
+
+Issues are caught early, not just in staging or production, such as:
+ - renamed or missing fields
+ - wrong or inconsistent formats
+ - unexpected status codes
+ - mismatched data types
+
+### More Reliable Frontend Development
+
+Frontend teams get:
+- consistent, validated API responses
+- fewer sudden breaking changes
+- a smoother development workflow
+
+This reduces context-switching and debugging time.
+
+### Better Collaboration Between Teams
+
+With Mokapi validating both sides:
+- backend developers instantly see when they violate the contract
+- frontend engineers get stable, predictable APIs
+- QA gets reliable test environments
+- platform engineers reduce risk during deployments
+
+Mokapi becomes a shared API contract watchdog across the organization.
+
+### Smooth Transition from Mocks to Real Systems
+
+Teams often start with mocked endpoints in early development. Later, they can simply begin forwarding requests to the
+real backend—while keeping validation in place.
+
+## Conclusion
+
+Using Mokapi between frontend and backend, between backend services, or inside Kubernetes environments provides:
+- strong contract enforcement
+- automatic validation for every interaction
+- early detection of breaking changes
+- stable multi-team integration
+- more reliable CI pipelines
+- a smooth path from mocking to real backend validation
+
+Mokapi ensures your API stays aligned with its specification, no matter how quickly your system evolves.
\ No newline at end of file
diff --git a/engine/js_test.go b/engine/js_test.go
index cd4b2b3b9..b95f6e04e 100644
--- a/engine/js_test.go
+++ b/engine/js_test.go
@@ -13,7 +13,6 @@ import (
"net/url"
"strings"
"testing"
- "time"
r "github.com/stretchr/testify/require"
)
@@ -23,31 +22,30 @@ func TestJsScriptEngine(t *testing.T) {
t.Run("valid", func(t *testing.T) {
t.Parallel()
e := enginetest.NewEngine()
- err := e.AddScript(newScript("test.js", "export default function(){}"))
+ err := e.AddScript(newScript("valid.js", "export default function(){}"))
r.NoError(t, err)
r.Equal(t, 0, e.Scripts(), "no events and jobs, script should be closed")
})
t.Run("blank", func(t *testing.T) {
t.Parallel()
e := enginetest.NewEngine()
- err := e.AddScript(newScript("test.js", ""))
+ err := e.AddScript(newScript("blank.js", ""))
r.NoError(t, err)
r.Equal(t, 0, e.Scripts(), "no events and jobs, script should be closed")
})
t.Run("typescript", func(t *testing.T) {
t.Parallel()
e := enginetest.NewEngine()
- err := e.AddScript(newScript("test.ts", "const msg: string = 'Hello World';"))
+ err := e.AddScript(newScript("typescript.ts", "const msg: string = 'Hello World';"))
r.NoError(t, err)
r.Equal(t, 0, e.Scripts(), "no events and jobs, script should be closed")
})
- t.Run("typescript async default function", func(t *testing.T) {
+ t.Run("async default function", func(t *testing.T) {
t.Parallel()
e := enginetest.NewEngine()
- err := e.AddScript(newScript("test.js", "import { every } from 'mokapi'; export default async function(){ setTimeout(() => { every('1m', function() {}) }, 500)}"))
+ err := e.AddScript(newScript("async.js", "import { every } from 'mokapi'; export default async function(){ setTimeout(() => { every('1m', function() {}) }, 500)}"))
r.NoError(t, err)
r.Equal(t, 1, e.Scripts())
- time.Sleep(2 * time.Second)
e.Close()
})
t.Run("script from GIT provider", func(t *testing.T) {
@@ -60,6 +58,63 @@ func TestJsScriptEngine(t *testing.T) {
r.Equal(t, 1, e.Scripts())
e.Close()
})
+ t.Run("example from Mokapi as proxy article", func(t *testing.T) {
+ t.Parallel()
+ e := enginetest.NewEngine()
+ err := e.AddScript(newScript("proxy.ts", `
+import { on } from 'mokapi';
+import { fetch } from 'mokapi/http';
+
+export default async function () {
+ on('http', async (request, response) => {
+ const url = getForwardUrl(request)
+ if (!url) {
+ response.statusCode = 500;
+ response.body = 'failed to forward request';
+ } else {
+ try {
+ const res = await fetch(url, {
+ method: request.method,
+ body: request.body,
+ headers: request.header,
+ timeout: '30s'
+ });
+ response.statusCode = res.statusCode;
+ response.headers = res.headers
+ switch (res.headers['Content-Type'][0]) {
+ case 'application/json':
+ // mokapi validates the data
+ response.data = res.json();
+ default:
+ // mokapi skips validation
+ response.body = res.body;
+ }
+ } catch (e) {
+ response.statusCode = 500;
+ response.body = e.toString();
+ }
+ }
+ });
+
+ function getForwardUrl(request: HttpRequest): string | undefined {
+ switch (request.api) {
+ case 'backend-1': {
+ const url = {host: 'https://backend1.example.com', ...request.url}
+ return url.toString();
+ }
+ case 'backend-2': {
+ const url = {host: 'https://backend1.example.com', ...request.url}
+ return url.toString();
+ }
+ default:
+ return undefined;
+ }
+ }
+}
+`))
+ r.NoError(t, err)
+ e.Close()
+ })
}
func TestJsEvery(t *testing.T) {
diff --git a/go.mod b/go.mod
index 89ec96c95..13ac3ea1c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,12 +4,12 @@ go 1.25.1
require (
github.com/Masterminds/sprig v2.22.0+incompatible
- github.com/blevesearch/bleve/v2 v2.5.5
+ github.com/blevesearch/bleve/v2 v2.5.6
github.com/blevesearch/bleve_index_api v1.2.11
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/brianvoe/gofakeit/v6 v6.28.0
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c
- github.com/evanw/esbuild v0.27.0
+ github.com/evanw/esbuild v0.27.1
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.4
diff --git a/go.sum b/go.sum
index 62f3bc215..3b9e17c71 100644
--- a/go.sum
+++ b/go.sum
@@ -22,8 +22,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
-github.com/blevesearch/bleve/v2 v2.5.5 h1:lzC89QUCco+y1qBnJxGqm4AbtsdsnlUvq0kXok8n3C8=
-github.com/blevesearch/bleve/v2 v2.5.5/go.mod h1:t5WoESS5TDteTdnjhhvpA1BpLYErOBX2IQViTMLK7wo=
+github.com/blevesearch/bleve/v2 v2.5.6 h1:YdixQmOUuZHojQRe8Te7BY2cRirbzpbcpybAFs0m2DI=
+github.com/blevesearch/bleve/v2 v2.5.6/go.mod h1:t5WoESS5TDteTdnjhhvpA1BpLYErOBX2IQViTMLK7wo=
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
@@ -81,8 +81,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
-github.com/evanw/esbuild v0.27.0 h1:1fbrgepqU1rZeu4VPcQRZJpvIfQpbrYqRr1wJdeMkfM=
-github.com/evanw/esbuild v0.27.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
+github.com/evanw/esbuild v0.27.1 h1:SkYgb1wrwJkJYwBp5hjmQGm3riUHbCdfViyEvjAA/KA=
+github.com/evanw/esbuild v0.27.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
diff --git a/js/mokapi/on.go b/js/mokapi/on.go
index 5b32e2c40..5e234a806 100644
--- a/js/mokapi/on.go
+++ b/js/mokapi/on.go
@@ -42,16 +42,6 @@ func (m *Module) On(event string, do goja.Value, vArgs goja.Value) {
if err != nil {
return nil, err
}
- /*for i, param := range params {
- e := param.Export()
- if p, ok := e.(*Proxy); ok {
- rv := reflect.ValueOf(ctx.Args[i])
- if rv.Kind() != reflect.Ptr {
- panic(m.vm.ToValue(fmt.Errorf("parameter %v is not a pointer", i)))
- }
- rv.Elem().Set(reflect.ValueOf(p.Export()))
- }
- }*/
return v, nil
}, &eventloop.JobContext{EventLogger: ctx.EventLogger})
diff --git a/js/mokapi/proxy.go b/js/mokapi/proxy.go
index f893d546a..32d3678b8 100644
--- a/js/mokapi/proxy.go
+++ b/js/mokapi/proxy.go
@@ -192,10 +192,16 @@ func (p *Proxy) Keys() []string {
t := target.Type()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
- if f.PkgPath == "" {
+ if f.PkgPath != "" {
continue
}
- result = append(result, f.Name)
+ v := f.Tag.Get("json")
+ if v != "" {
+ tagValues := strings.Split(v, ",")
+ result = append(result, tagValues[0])
+ } else {
+ result = append(result, f.Name)
+ }
}
}
diff --git a/js/mokapi/shared.go b/js/mokapi/shared.go
index 6b91ee439..0b018cc63 100644
--- a/js/mokapi/shared.go
+++ b/js/mokapi/shared.go
@@ -50,8 +50,10 @@ func (m *SharedMemory) Clear() {
func (m *SharedMemory) Update(key string, fn goja.Value) any {
p := m.store.Update(key, func(v any) any {
var arg goja.Value
+ var sv *SharedValue
if v != nil {
- arg = v.(*SharedValue).Use(m.vm).ToValue()
+ sv = v.(*SharedValue)
+ arg = sv.ToValue()
}
call, ok := goja.AssertFunction(fn)
if !ok {
@@ -61,6 +63,9 @@ func (m *SharedMemory) Update(key string, fn goja.Value) any {
if err != nil {
panic(m.vm.ToValue(err))
}
+ if sv != nil && r == arg {
+ return sv.Use(m.vm)
+ }
return NewSharedValue(r, m.vm)
})
@@ -110,9 +115,6 @@ func Export(v any) any {
// SharedValue represents a Go-managed value that can be shared across
// multiple Goja runtimes, while maintaining reference identity.
type SharedValue struct {
- KeyNormalizer func(string) string
- ToJSValue func(vm *goja.Runtime, k string, v goja.Value) goja.Value
-
vm *goja.Runtime
source goja.Value
}
@@ -125,23 +127,16 @@ func NewSharedValue(v goja.Value, vm *goja.Runtime) *SharedValue {
}
func (p *SharedValue) Use(vm *goja.Runtime) *SharedValue {
- return &SharedValue{source: p.source, vm: vm}
+ return NewSharedValue(p.source, vm)
}
func (p *SharedValue) Get(key string) goja.Value {
- if p.KeyNormalizer != nil {
- key = p.KeyNormalizer(key)
- }
-
switch v := p.source.(type) {
case *goja.Object:
f := v.Get(key)
if _, ok := goja.AssertFunction(f); ok {
return f
} else if _, isObject := f.(*goja.Object); isObject {
- if p.ToJSValue != nil {
- return p.ToJSValue(p.vm, key, f)
- }
return p.vm.NewDynamicObject(NewSharedValue(f, p.vm))
}
return f
@@ -151,10 +146,6 @@ func (p *SharedValue) Get(key string) goja.Value {
}
func (p *SharedValue) Has(key string) bool {
- if p.KeyNormalizer != nil {
- key = p.KeyNormalizer(key)
- }
-
switch v := p.source.(type) {
case *goja.Object:
return slices.Contains(v.Keys(), key)
@@ -164,10 +155,6 @@ func (p *SharedValue) Has(key string) bool {
}
func (p *SharedValue) Set(key string, value goja.Value) bool {
- if p.KeyNormalizer != nil {
- key = p.KeyNormalizer(key)
- }
-
switch v := p.source.(type) {
case *goja.Object:
err := v.Set(key, value)
@@ -180,10 +167,6 @@ func (p *SharedValue) Set(key string, value goja.Value) bool {
}
func (p *SharedValue) Delete(key string) bool {
- if p.KeyNormalizer != nil {
- key = p.KeyNormalizer(key)
- }
-
switch v := p.source.(type) {
case *goja.Object:
err := v.Delete(key)
@@ -209,6 +192,7 @@ func (p *SharedValue) ToValue() goja.Value {
if p.source == nil {
return goja.Undefined()
}
+
switch p.source.ExportType().Kind() {
case reflect.Map, reflect.Slice:
return p.vm.NewDynamicObject(p)
diff --git a/js/mokapi/shared_test.go b/js/mokapi/shared_test.go
index f881fc808..385cee25d 100644
--- a/js/mokapi/shared_test.go
+++ b/js/mokapi/shared_test.go
@@ -9,6 +9,7 @@ import (
"mokapi/js/eventloop"
"mokapi/js/mokapi"
"mokapi/js/require"
+ "mokapi/try"
"testing"
"github.com/dop251/goja"
@@ -307,6 +308,37 @@ func TestModule_Shared(t *testing.T) {
r.Equal(t, []any{int64(1), int64(2), int64(3)}, mokapi.Export(v))
},
},
+ {
+ name: "set array using index operator",
+ test: func(t *testing.T, newVm func() *goja.Runtime) {
+ vm1 := newVm()
+
+ v, err := vm1.RunString(`
+ const m = require('mokapi');
+ m.shared.set('foo', [1,2])
+ const shared = m.shared.get('foo');
+ shared[1] = 10
+ m.shared.get('foo')
+ `)
+ r.NoError(t, err)
+ r.Equal(t, []any{int64(1), int64(10)}, mokapi.Export(v))
+ },
+ },
+ {
+ name: "get index from array",
+ test: func(t *testing.T, newVm func() *goja.Runtime) {
+ vm1 := newVm()
+
+ v, err := vm1.RunString(`
+ const m = require('mokapi');
+ m.shared.set('foo', [1,2])
+ const shared = m.shared.get('foo');
+ shared[1];
+ `)
+ r.NoError(t, err)
+ r.Equal(t, int64(2), mokapi.Export(v))
+ },
+ },
{
name: "splice array",
test: func(t *testing.T, newVm func() *goja.Runtime) {
@@ -375,3 +407,51 @@ func TestModule_Shared(t *testing.T) {
})
}
}
+
+func TestSharedFromModule(t *testing.T) {
+ // import shared multiple times should not result to illegal runtime error
+
+ host := &enginetest.Host{}
+ host.OpenFunc = func(file, hint string) (*dynamic.Config, error) {
+ return &dynamic.Config{
+ Info: dynamictest.NewConfigInfo(),
+ Raw: []byte(`import { shared } from "mokapi"
+export const store = shared.update('stored', (v) => v ?? [{ foo: 'bar' }]);
+`),
+ }, nil
+ }
+
+ s1, err := js.New(&dynamic.Config{
+ Info: dynamictest.NewConfigInfo(),
+ Raw: []byte(`import { store } from "store.js"
+
+`),
+ }, host)
+ r.NoError(t, err)
+ defer s1.Close()
+
+ err = s1.Run()
+ r.NoError(t, err)
+
+ s2, err := js.New(&dynamic.Config{
+ Info: dynamic.ConfigInfo{Url: try.MustUrl("script1.js")},
+ Raw: []byte(`import { store } from "store.js"
+`),
+ }, host)
+ r.NoError(t, err)
+ defer s2.Close()
+
+ err = s2.Run()
+ r.NoError(t, err)
+
+ s3, err := js.New(&dynamic.Config{
+ Info: dynamic.ConfigInfo{Url: try.MustUrl("script1.js")},
+ Raw: []byte(`import { store } from "store.js"
+`),
+ }, host)
+ r.NoError(t, err)
+ defer s3.Close()
+
+ err = s3.Run()
+ r.NoError(t, err)
+}
diff --git a/npm/types/http.d.ts b/npm/types/http.d.ts
index 314b9c31a..5bbc019a3 100644
--- a/npm/types/http.d.ts
+++ b/npm/types/http.d.ts
@@ -86,11 +86,83 @@ export function del(url: string, body?: JSONValue, args?: Args): Response;
*/
export function options(url: string, body?: JSONValue, args?: Args): Response;
-export function fetch(url: string, args: FetchOptions): PromiseDuration
{{ duration(eventData.duration) }}
-Client IP
{{ eventData.clientIP }}
Build better software by mocking external APIs and testing without dependencies.
Free, open-source, and under your control — your data is yours.
Test without external dependencies and build more reliable software.
Free, open-source, and fully under your control.