Skip to main content

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:
  1. 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.
  2. 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.
  3. 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.
This page is the reference for the JavaScript runtime, the plugin contract, the objects and API available to your code, and how to deploy a plugin. To author and test a plugin end to end, use the tyk-plugin-starter repository (see Writing a plugin).
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.
Two properties of the runtime are worth knowing up front:
  • 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_data or the key session API.
goja is not Node.js. There is no 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 setting enable_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:
  • javascript selects the goja engine. In OAS this is x-tyk-api-gateway.middleware.global.pluginConfig.driver; in a Tyk Classic API definition it is custom_middleware.driver.
  • otto selects the legacy engine, retained for backward compatibility.
The same driver flag governs virtual endpoints: a virtual endpoint runs on goja only when its API sets the driver to 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 same request/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-types typings, 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).
The typical loop is:
Once you have a built plugin.js, deploy it using one of the methods in Deploying JavaScript plugins.

The plugin contract

Creating a middleware component

Tyk injects a TykJS 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 handler locally, so the gateway cannot see it. Assign globalThis.handler = handler to 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 the request 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:
A custom plugin normally passes the request on to the next middleware. It can instead terminate the request and return a custom response to the client by setting 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:
The responseObject has the structure:
  • Code: integer HTTP status code
  • Headers: object of header key/value pairs
  • Body: response body string (plain text, JSON or XML)
See the virtual endpoint examples for complete handlers.

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 through SetHeaders/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. DeleteHeaders is applied before SetHeaders.
  • 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. DeleteParams is applied before AddParams.
  • ReturnOverrides: set these to terminate the request and return a response (see below).
  • IgnoreBody: if true, the original request body is used; if false (default) the Body field 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. http or https.

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 (http or https).
  • 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.
Sharing data between plugins The 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 identifier
  • OrgID: the organization identifier
  • config_data: custom attributes from the API definition
Adding custom attributes. For Tyk OAS APIs, add them under x-tyk-api-gateway.middleware.global.pluginConfig.data:
For Tyk Classic APIs, add them under 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 with JSVM 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(...): like TykMakeHttpRequest but 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.
Tyk returns a simplified, JSON-encoded response that you decode before use:
The response shape is { 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 the TykStorage 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.
The following functions are provided:
  • TykStorageGet(key): retrieves the value stored at key, returned as a string; returns null if the key does not exist
  • TykStorageSet(key, value, ttlSeconds): stores value at key with an expiry of ttlSeconds; pass 0 for no expiry
  • TykStorageSetNX(key, value, ttlSeconds): atomically stores value at key only if the key does not already exist (Redis SET NX EX); returns true if the key was claimed, false if it already existed; this is the primitive to use for idempotency guards, locks and replay protection
  • TykStorageDel(key): deletes key from storage
  • TykStorageTTL(key): returns the remaining time to live of key in seconds; returns -1 if the key has no expiry and -2 if the key does not exist
  • TykStorageIncr(key, ttlSeconds): atomically increments the integer stored at key and returns the new value as a string (a string is returned because JavaScript numbers lose precision above 2^53); ttlSeconds is applied only when the increment creates the key
In this example, 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.
Atomicity guarantees, such as those provided by TykStorageSetNX and TykStorageIncr, apply per Redis instance. In deployments where gateway nodes use separate Redis instances, which is common in Hybrid deployments, guarantees such as idempotency and replay protection hold per node rather than globally. For a global guarantee, all gateways must share a single Redis deployment. This is the same consistency model as Tyk’s rate limiting.
For worked examples of these and the other globals, see the examples in the starter repo.

Deploying JavaScript plugins

A built plugin.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 the code 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:
The code value above decodes to the plugin source, which renames the X-Foo header to X-Bar:
Each entry under 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:
Because inline plugins live in the API definition, updating one is an ordinary API update — 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 packages plugin.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 with NewMiddleware.
  • 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 for pre (no session yet); for post, set true to receive the session object.
By directory structure (auto-loader). As a convenience — useful when you can’t edit the API definition directly, for example a Dashboard-managed Self-Managed or Hybrid deployment — Tyk can auto-load plugins from a directory named after the API ID, with no path reference in the API definition:
Tyk matches a folder to the API ID being loaded and loads the 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 same driver 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