Overview
JavaScript plugins let you run custom logic directly inside Tyk Gateway, without an external process or a separate runtime. There are three components that can be scripted with JavaScript:- Custom JavaScript plugins: middleware that executes either pre or post request processing. A pre plugin runs before any session or token validation; a post plugin runs after the request has passed all checks and is ready to be proxied upstream.
- Dynamic event handlers: components that fire on API events. They run asynchronously and are documented in Gateway events. JavaScript event handlers currently run on the legacy otto (ES5.1) engine regardless of the API’s plugin driver; the goja engine applies to custom plugins and virtual endpoints only.
- Virtual endpoints: programmable middleware invoked towards the end of the request chain. Unlike custom plugins, a virtual endpoint terminates the request and returns its own response.
Each plugin runs in its own scopeOn the goja engine, every custom-middleware plugin is compiled in an isolated scope and its handler is exposed under a unique internal alias. Plugins on the same API can therefore reuse the same handler name (such as
var handler) without clobbering each other — across files, bundles and inline code alike. The legacy otto engine does not isolate plugins: there, all functions share one global namespace and the last one loaded wins, so distinct names are required.The goja engine
Tyk Gateway runs JavaScript on goja, a pure-Go ECMAScript engine embedded in the gateway binary. goja’s guaranteed language level is ECMAScript 5.1, which it implements in full. It also runs much of ES6 and later (let/const, arrow functions, template literals, destructuring, classes, Promise, Map/Set/Symbol, optional chaining ?., nullish coalescing ??, BigInt), but that surface is a subset that grows with the gateway’s bundled goja version, so treat those features as available rather than guaranteed. For maximum portability, keep your build target at ES5.1 and let your bundler down-level modern syntax to it. (Earlier gateway releases ran JavaScript on otto, which is limited to ES5.1 and remains selectable for backward compatibility; see Selecting the engine.)
AvailabilityThe goja engine was released in Tyk Gateway v5.14. On earlier versions, JavaScript runs on the otto engine only.
- One virtual machine per API. Each managed API gets its own isolated VM, so plugins on different APIs cannot share globals or call each other’s functions. Cross-API data sharing is only possible through the session object when a key is shared across APIs.
- A fresh runtime per request. Each request executes on a new runtime, so global state does not leak between concurrent requests. Treat the VM as stateless between invocations; persist anything durable through
session.meta_dataor the key session API.
require/import, no async/await, no setTimeout, and no Node standard library. For the full list of runtime constraints, available globals and the libraries that bundle cleanly, see AGENTS.md in the starter repo.
Enabling the JSVM
The JavaScript Virtual Machine (JSVM) is disabled by default. Enable it by settingenable_jsvm to true in tyk.conf. This single flag enables JavaScript for custom middleware, event handlers and virtual endpoints alike.
Selecting the engine
The engine is chosen per API by the plugin driver:javascriptselects the goja engine. In OAS this isx-tyk-api-gateway.middleware.global.pluginConfig.driver; in a Tyk Classic API definition it iscustom_middleware.driver.ottoselects the legacy engine, retained for backward compatibility.
javascript. With no driver set, virtual endpoints default to the legacy otto engine, so set javascript explicitly to run them on goja.
Migrating from otto
goja is API-definition compatible with otto — the same hooks, the samerequest/session/config objects and the same TykJS middleware contract — so most plugins run unchanged on the javascript driver. One difference catches plugins that relied on it:
The underscore library is no longer a global. The otto engine auto-loaded underscore.js and exposed it as _. goja does not bundle it, so a plugin that calls _.map, _.template and similar will fail. Most underscore helpers have native equivalents in goja’s fuller ES5.1+/ES6 runtime (Array.prototype.map/filter/reduce, Object.keys/values/entries); where you need more, bundle a utility library such as lodash into your plugin.js with the starter rather than relying on a gateway-provided global.
Hook types
A JavaScript plugin attaches to one point in the request lifecycle. The hook you choose determines when it runs and what it can do:
Each plugin entry attaches to exactly one hook. For a fuller treatment of the request chain and what each stage can do, see Plugin types. Virtual endpoints are documented separately under Virtual endpoints.
Writing a plugin
The tyk-plugin-starter repository is the supported way to author JavaScript plugins. It is designed for both humans and AI agents and provides:- A TypeScript project with the published
@tyk-technologies/tyk-plugin-typestypings, so the request/session/config objects and the Tyk globals autocomplete. - A local test harness that mocks the goja runtime, so you can unit-test a plugin in plain Node without running a gateway.
- A webpack build that emits a single
plugin.js, and a script that packages it into a deployable bundle. - Six runnable, tested examples covering every hook type (pre, auth_check, post_key_auth, post, response).
plugin.js, deploy it using one of the methods in Deploying JavaScript plugins.
The plugin contract
Creating a middleware component
Tyk injects aTykJS namespace into the VM. You create a middleware object with the NewMiddleware({}) constructor and register your function with NewProcessRequest(). This closure receives the three Tyk data objects — request, session and config:
Two rules to remember
- For custom plugins and event handlers loaded from a file, the source filename must match the function name. Virtual endpoints have no such restriction.
- If you build with a bundler (such as the webpack setup in the starter), the bundler scopes your
var handlerlocally, so the gateway cannot see it. AssignglobalThis.handler = handlerto expose it. The starter’s test harness asserts this for you.
Returning from a plugin
How you return depends on the plugin type. Custom plugins modify therequest and session.meta_data objects, but those changes are applied by the gateway after the function returns — so you must pass them back. Omitting the return causes the plugin to fail:
request.ReturnOverrides — see Using ReturnOverrides.
Virtual endpoints always terminate the request, so they return differently. Build a response object and pass it, with the session metadata, to TykJsResponse:
responseObject has the structure:
Code: integer HTTP status codeHeaders: object of header key/value pairsBody: response body string (plain text, JSON or XML)
The request, session and config objects
Your function receives three Tyk data objects. This section covers their behaviour and the fields you’ll use most; for the full struct shapes, see the data structures in Rich Plugins.The request object
The request object exposes the API request as a set of fields you can read and modify. Changes take effect as the request continues through the middleware chain. (Virtual endpoints receive a different request shape.)
Headers: current request headers, as string arrays. Read-only; modify headers throughSetHeaders/DeleteHeaders.SetHeaders: headers to set when the function returns. Existing headers are overwritten; new ones are added.DeleteHeaders: header names to delete from the outgoing request.DeleteHeadersis applied beforeSetHeaders.Body: the request body. Assigning to it overwrites the body.URL: the path portion of the outbound URL. Modify it to route to a different upstream path.AddParams: query parameters to add to the request.DeleteParams: query parameters to remove.DeleteParamsis applied beforeAddParams.ReturnOverrides: set these to terminate the request and return a response (see below).IgnoreBody: iftrue, the original request body is used; iffalse(default) theBodyfield is used.Method: the HTTP method (GET,POST, …).RequestURI: the request URI including query string, e.g./path?key=value.Scheme: the URL scheme, e.g.httporhttps.
Using ReturnOverrides
Set values in request.ReturnOverrides to terminate the request and respond to the client directly — the request is not proxied upstream. In this example, when a condition is met the gateway returns HTTP 403 with a custom header:
The virtual endpoint request object
Virtual endpoint functions receive this request shape instead:
Body: HTTP request body.Headers: request headers, e.g."Accept": ["*/*"].Params: decoded query and form parameters, e.g.{ "userId": ["123"] }.Scheme: URL scheme (httporhttps).URL: full request URL, e.g./vendpoint/anything?user_id=123&confirm=true.
NoteEach query and form parameter is stored as an array. Repeated assignments append to the array — a request to
/vendpoint/anything?user_id[]=123&user_id[]=234 produces Params: { "user_id[]": ["123", "234"] }.The session object
Tyk uses an internal session object to track quota, rate limits, access rights and auth data for a key. A plugin can read the session, but — other than meta_data — it cannot be modified directly, because the gateway relies on it. Deserialising the session into the VM is relatively expensive, so it can be disabled where it isn’t needed.
Availability
- Pre-stage plugins have no session object — authentication hasn’t run yet.
- Virtual endpoints receive the session only if it is enabled in the middleware configuration.
meta_data key/value field is written back to the session store, so later middleware can read what an earlier plugin wrote. This data is persistent and is also accessible through the /tyk/keys/ REST API. Because each API has its own VM, sharing data across APIs is only possible through the session object when a key is shared between them.
The config object
The read-only config object carries data from the API definition:
APIID: the API’s unique identifierOrgID: the organization identifierconfig_data: custom attributes from the API definition
x-tyk-api-gateway.middleware.global.pluginConfig.data:
config_data at the root of the API definition:
The JavaScript API
The gateway adds a set of global functions to the VM, giving your script the ability to log, make outbound HTTP requests and read or write key session data:log(string): logs the string to the gateway’s logger as an INFO line prefixed withJSVM Log:. Useful for debugging.rawlog(string): logs the string with no prefix or timestamp, for custom log formats.b64enc(string)/b64dec(string): standard base64 encode/decode.rawb64enc(string)/rawb64dec(string): raw (unpadded) base64 encode/decode.TykMakeHttpRequest(JSON.stringify(requestObject)): make an outbound HTTP request (see below).TykBatchRequest(...): likeTykMakeHttpRequestbut uses Tyk’s batch request feature.TykGetKeyData/TykSetKeyData: read and write key session data (see Working with the key session object).
Making HTTP requests
TykMakeHttpRequest encodes the request as JSON, which the gateway deserialises in the main binary and translates into a system HTTP call. The request object has the structure:
NoteTo include query-string values, append them to the
Domain property.{ Code: int, Body: string, Headers: map[string][]string }. The call is synchronous — execution blocks until the response is received.
Working with the key session object
Two functions read and write the session object for a key:-
TykGetKeyData(key, apiID): retrieve a session object. -
TykSetKeyData(key, value, ...): write data back into the session store.
Storage Operations
JavaScript plugins can persist small key-value data in the storage (Redis) used by Tyk Gateway using theTykStorage functions. This bounded key-value store lets plugins share state across requests and across gateway nodes that use the same storage, supporting patterns such as idempotency guards, distributed locks, replay protection and simple counters.
The storage functions are available from Tyk Gateway v5.15.0 and only in the goja JSVM engine. They are not available in the legacy otto engine.
TykStorageGet(key): retrieves the value stored atkey, returned as a string; returnsnullif the key does not existTykStorageSet(key, value, ttlSeconds): storesvalueatkeywith an expiry ofttlSeconds; pass0for no expiryTykStorageSetNX(key, value, ttlSeconds): atomically storesvalueatkeyonly if the key does not already exist (RedisSET NX EX); returnstrueif the key was claimed,falseif it already existed; this is the primitive to use for idempotency guards, locks and replay protectionTykStorageDel(key): deleteskeyfrom storageTykStorageTTL(key): returns the remaining time to live ofkeyin seconds; returns-1if the key has no expiry and-2if the key does not existTykStorageIncr(key, ttlSeconds): atomically increments the integer stored atkeyand returns the new value as a string (a string is returned because JavaScript numbers lose precision above 2^53);ttlSecondsis applied only when the increment creates the key
TykStorageSetNX is used to implement an idempotency guard: the first request carrying a given idempotency key claims it and is passed through to the upstream, while any repeat request within 24 hours is rejected with HTTP 409 Conflict:
All storage functions throw a JavaScript exception on failure, for example if the storage is unreachable, the operation exceeds the 2 second timeout, the key exceeds 256 bytes or the value exceeds 64 KB. They never fail silently by returning
false or undefined, so wrap calls in try/catch and decide explicitly how your plugin should behave when storage is unavailable. Keys are automatically placed under a dedicated namespace (jsvm-store:) that is enforced by the gateway, so plugins cannot read or modify keys used internally by Tyk.Deploying JavaScript plugins
A builtplugin.js reaches the gateway one of three ways. They differ in how the plugin artifact is distributed to your gateway nodes — not in capability or environment. All three are production-grade, and all require the JSVM to be enabled (enable_jsvm: true).
Inline
code and file path can be combined on a single API — they’re loaded by independent passes, so some hooks can be inline while others are file-mounted. A bundle is the exception: it replaces an API’s entire middleware section, so it can’t be mixed with code or path entries (see Bundles).
Inline code
Inline code embeds the plugin source directly in the API definition, so a single document carries both the API configuration and its custom logic — no bundle server and no separate artifact. The source is supplied base64-encoded in thecode field of a plugin entry, and the plugin driver is set to javascript.
The following minimal Tyk OAS document attaches a pre-plugin that renames a header:
code value above decodes to the plugin source, which renames the X-Foo header to X-Bar:
prePlugins / postAuthenticationPlugins / postPlugins / responsePlugins accepts:
The
code field must be base64; the gateway decodes it unconditionally, so raw JavaScript in this field will fail to load. Produce the value from your built plugin with:
PUT the document and reload. The gateway re-decodes and re-compiles the code field on reload; there is no on-disk cache to invalidate:
The source is visibleInline
code is plain text in the API definition to anyone with read access. Don’t embed secrets, and remember to rebuild before re-encoding — the gateway will happily run stale code.Bundles
A bundle packagesplugin.js with a manifest.json (declaring the driver and hook entries) into a zip that the gateway downloads from a bundle server. Bundles are the right choice when one plugin is shared across many APIs or when you want to version plugin artifacts independently of API definitions. The starter repo’s npm run build:bundle produces a valid bundle without a gateway binary.
An API references its bundle through custom_middleware_bundle (Classic) and requires the bundle downloader to be enabled on the gateway. That single field also accepts a comma-separated list of bundle names — for example "auth.zip,logging.zip" — which fetches each and merges their manifests in declaration order.
Bundles replace inline and file-mounted middlewareWhen an API references a bundle, the bundle’s manifest replaces the API’s
custom_middleware section entirely — any inline code or file path entries on the definition are ignored. Ship all of an API’s middleware in the bundle, or define it on the API with code/path and leave the bundle field unset.File mount
Reference a.js file from the API definition with the path field. This is a first-class alternative to inline code — the only difference is that you distribute the file to your gateway nodes yourself, the same way you would a Go plugin: bake it into the gateway image, mount it as a volume, or ship it with your configuration management. Every node that serves the API must have the file at the referenced path.
By path in the API definition. Register the plugin in the custom_middleware block, pointing path at the file. The path is resolved relative to the gateway’s working directory (or given as an absolute path):
pre/post: lists of plugins run in order, before authentication (pre) or after all checks and before proxying upstream (post).name: the handler name. Case-sensitive; must match the name your script registers withNewMiddleware.path: the JS file, loaded into the VM when the API initialises. Reloading the API hot-swaps the plugin with no interruption.require_session: irrelevant forpre(no session yet); forpost, settrueto receive thesessionobject.
path reference in the API definition:
pre and post plugins from the respective directories. The filename must match the registered object name exactly. Append _with_session to a post plugin’s filename to inject the session object.
Virtual endpoints and the driver
Virtual endpoints honour the samedriver flag as custom middleware. A virtual endpoint runs on goja when its API sets the driver to javascript; with no driver set it runs on the legacy otto engine. Set the driver explicitly to move virtual endpoints onto goja. See Virtual endpoints for configuration.
See also
- tyk-plugin-starter — author, test and build JavaScript plugins, with runnable examples
- Plugin types — hook stages and capabilities
- Rich plugins — plugin data structures and bundles
- Virtual endpoints — programmable terminating middleware
- Go plugins — native plugins for higher performance