In the ever-evolving landscape of web development, template engines have become indispensable tools for generating dynamic content. From simple variable interpolation to complex conditional rendering and iteration, template engines bridge the gap between raw data and polished HTML output. However, one persistent challenge has plagued developers across the industry: the fragmentation of template solutions across different programming languages and frameworks.
Enter Neutral TS, a revolutionary template engine that dares to ask a fundamental question: What if you could write a template once and use it everywhere, regardless of the programming language your backend employs?
Neutral TS is a template engine for the Web, designed to work with any programming language (language-agnostic) via IPC/Package and natively as library/crate in Rust. This groundbreaking approach addresses a long-standing pain point in software development, where teams often find themselves maintaining multiple template implementations across different services written in various languages.
This comprehensive guide will explore every facet of Neutral TS, from its foundational principles to advanced usage patterns, providing developers with the knowledge needed to leverage this powerful tool in their projects.
Neutral TS is a safe, modular, language-agnostic template engine built in Rust. It works as a native Rust library or via IPC for other languages like Python and PHP. With Neutral TS you can reuse the same template across multiple languages with consistent results.
At its core, Neutral TS represents a paradigm shift in how we think about template engines. Traditional template engines are tightly coupled to their host programming language—Jinja2 is for Python, Blade is for PHP, ERB is for Ruby, and so on. While this tight integration offers certain conveniences, it also creates silos that can complicate multi-language architectures and make template reuse virtually impossible across language boundaries.
Similarly, Neutral TS has an IPC server, and each programming language has a client. No matter where you run the template, the result will always be the same. Thanks to this, and to its modular and parameterizable design, it is possible to create utilities or plugins that will work everywhere.
The name “Neutral” perfectly encapsulates the engine’s philosophy. It doesn’t favor any particular programming language or platform. It maintains neutrality by providing consistent output regardless of the calling environment. This neutrality extends to:
Other key features include: Safe Language-agnostic Modular Parameterizable How It Works Neutral TS integrates with other programming languages in two main ways: In Rust: You can use Neutral TS as a library by downloading the crate.
The engine distinguishes itself through several key characteristics:
One of the most illuminating ways to understand Neutral TS’s architecture is through the database analogy provided by its creators:
Imagine a database. It has a server, and different programming languages access the data through a client. This means that if you run a “SELECT …” query from any programming language, the result will always be the same.
Just as a database server provides consistent data access regardless of whether you’re querying from Python, Java, or Ruby, Neutral TS provides consistent template rendering regardless of your backend language. Just like an SQL query returns the same data from any language, a Neutral TS template returns the same HTML from Python, PHP, Rust… with added security isolation.
Neutral TS employs several design principles that make templates predictable and secure:
By design the templates do not have access to arbitrary application data, the data has to be passed to the template in a JSON, then the data that you have not included in the JSON cannot be read by the template.
This explicit data passing mechanism serves multiple purposes:
Thanks to this, and to its modular and parameterizable design, it is possible to create utilities or plugins that will work everywhere. For example, you can develop tools to create forms or form fields and create your own libraries of “snippets” for repetitive tasks.
Modularity permeates every aspect of Neutral TS:
Neutral TS operates in two distinct modes, each suited to different use cases:
In Rust: You can use Neutral TS template engine as a library by downloading the crate.
When used directly in Rust applications, Neutral TS operates as a native library without any IPC overhead. This mode offers:
In other programming languages: Inter-Process Communication (IPC) is necessary, similar to how databases like MariaDB work.
For non-Rust languages, Neutral TS uses a client-server architecture:
IPC Server: Universal standalone application (written in Rust) for all languages - download from: IPC Server · IPC Clients: Language-specific libraries to include in your project - available at: IPC Clients
Neutral TS IPC Server for template rendering, a TCP/IP interface for Neutral TS template engine.
The IPC server is a standalone Rust application that:
Examples for Rust, Python, PHP, Node.js and Go here: download.
Each supported language has its own client library that:
For Rust developers, installation is straightforward through Cargo:
[dependencies]
neutralts = "1.3.0"
For non-Rust languages, you’ll need to download and run the IPC server:
IPC Server: Universal standalone application (written in Rust) for all languages - download from: IPC Server
The IPC server can be downloaded from the official releases page and run as a standalone application or as a system service.
The Python client is available on PyPI as neutraltemplate. Installation:
pip install neutraltemplate
Client libraries for these languages are available through their respective package managers or as direct downloads from the IPC Clients repository.
Configuration in Neutral TS is handled through a JSON schema. Here’s a minimal configuration:
{
"config": {
"comments": "remove",
"cache_disable": false
},
"data": {
"title": "My Page"
}
}
The main element of Neutral TS is the BIF (Build-in function), would be the equivalent of functions and would display an output, the output is always a string or nothing (empty string).
BIFs (Built-in Functions) are the fundamental building blocks of Neutral TS templates. They encapsulate all template logic, from simple variable display to complex conditional rendering and iteration.
.-- open bif
| .-- bif name
| | .-- name separator
| | | .-- params
| | | | .-- params/code separator
| | | | | .-- code
| | | | | | .-- close bif
v v v v v v v
-- ----- - -------- -- ----- --
{:snippet; snipname >> ... :}
------------------------------
^ -------------------
| ^
| |
| `-- source
`-- Built-in function
The general syntax for a BIF is:
{:bifname; params >> code :}
Where:
{: opens the BIFbifname is the function name; separates the name from parametersparams are the function parameters>> separates parameters from the code blockcode is the template content:} closes the BIFNeutral TS template engine is based on BIFs with block structure, we call the set of nested BIFs of the same level a block
By design all Bifs can be nested and there can be a Bif anywhere in another Bif except in the name.
This nesting capability is crucial for building complex templates:
{:coalesce;
{:code;
{:filled; user >>
Welcome, {:;user->name:}!
:}
:}
{:code;
Welcome, Guest!
:}
:}
Short circuit at block level, if varname is not defined, the following ‘»’ is not evaluated:
{:defined; varname >>
{:code; {:code; ... :} :}
:}
This short-circuit behavior is crucial for performance and safety—expensive operations inside a BIF’s code block aren’t evaluated if the condition fails.
The simplest BIF displays variable values:
{:;variablename:}
{:;user->profile->name:}
The arrow syntax (->) navigates nested JSON structures.
filled: Outputs code if a variable has a non-empty value
{:filled; varname >> Hello! :}
defined: Checks if a variable exists
{:defined; varname >> Variable exists :}
array: Checks if a variable is an array/object {:array; variable » code :} {:array; varname » this shown if varname is array :} Note that there is no distinction between objects and arrays.
each: Iterates over arrays {:^each; {:param; array-name :} key value » {:array; value » {:;:} {:;key:}: {:snippet; iterate-array-next-level :} :}{:else; {:;:} {:;key:}={:;value:} :} :}
else: Provides fallback content {:else; code :} {:;varname:}{:else; shown if varname is empty :}
coalesce: Returns the first non-empty block
{:coalesce;
{:code; {:;primary:} :}
{:code; {:;secondary:} :}
{:code; Default :}
:}
Snippets are reusable template fragments:
{:snippet; header >>
<header>
<h1>{:;site->name:}</h1>
</header>
:}
{:snippet; header :}
“Calling a snippet that does not exist is not an error, it will result in a empty string that we can evaluate with else. {:snippet; option-{:;varname:} :} {:else; {:snippet; option-default :} :}”
BIFs can be modified with prefixes: {:!array; … :} {:+array; … :} {:^array; …
! - Negation (NOT)+ - Additional modifier^ - Special behavior modifierNeutral TS supports comments that can be configured to be removed from output:
{:* This is a comment *:}
The behavior is controlled by the config:
{
"config": {
"comments": "remove"
}
}
| “Any delimiter can be used, but a delimiter is always required, even if only one parameter is used. {:fetch; "url" » … :} {:fetch; ~url~event~ » … :} {:fetch; #url#event# » … :} {:fetch; | url | event | » … :} {:fetch; ‘url’event’ » … :}” |
“The reason you can use different delimiters is to use one that does not appear in the parameter, using / would cause problems with the url so we use " or any other”
This flexibility prevents escaping nightmares when parameters contain the delimiter character.
“Neutral TS template engine provides a basic JavaScript to perform simple fetch requests”
| {:fetch; | url | event | wrapperId | class | id | name | » code :} Code is the html that will be displayed before performing the fetch, it can be a message, a button or the fields of a form. |
Example:
{:fetch; "/api/user-data" >>
<div>Loading user data...</div>
:}
“If you use the bif "exit" or "redirect" it is necessary to manage the status codes in the application… let status_code = template.get_status_code(); // e.g.: 500… let status_text = template.get_status_text(); // e.g.: Internal Server Error… let status_param = template.get_status_param(); // e.g.: template error x”
Templates can control HTTP responses:
let template = Template::from_file_value("file.ntpl", schema).unwrap();
let content = template.render();
let status_code = template.get_status_code();
let status_text = template.get_status_text();
let status_param = template.get_status_param();
The schema is a JSON where we define the data that will represent the templates, as well as the configuration and translations, although you can render a template without schema since Neutral TS provides one by default, it will be of little use since it has no data.
A complete schema has several sections:
{
"config": { ... },
"inherit": { ... },
"locale": { ... },
"data": { ... }
}
“You need two things, a template file and a json schema: { "config": { "comments": "remove", "cache_prefix": "neutral-cache", "cache_dir": "", "cache_on_post": false, "cache_on_get": true, "cache_on_cookies": true, "cache_disable": false, "filter_all": false, "disable_js": false }”
Configuration options include:
| Option | Description | Default |
|---|---|---|
comments |
How to handle comments (“remove”, “keep”) | “remove” |
cache_prefix |
Prefix for cache files | “neutral-cache” |
cache_dir |
Directory for cache storage | ”” |
cache_on_post |
Cache POST requests | false |
cache_on_get |
Cache GET requests | true |
cache_on_cookies |
Cache when cookies present | true |
cache_disable |
Disable caching entirely | false |
filter_all |
Apply filters to all output | false |
disable_js |
Disable JavaScript generation | false |
The schema includes a special CONTEXT object for web request data:
{
"data": {
"CONTEXT": {
"ROUTE": "/users/profile",
"HOST": "example.com",
"GET": { "id": "123" },
"POST": {},
"HEADERS": {},
"FILES": {},
"COOKIES": {},
"SESSION": {},
"ENV": {}
}
}
}
This standardized structure ensures consistent access to request data across all languages.
The data section contains all variables available to templates:
{
"data": {
"site_name": "MySite",
"site": {
"name": "MySite",
"url": "https://mysite.com"
},
"user": {
"name": "John",
"email": "john@example.com"
}
}
}
It works as a native Rust library or via IPC for other languages like Python and PHP.
Inter-Process Communication enables Neutral TS to serve as a universal template engine while maintaining its Rust-based core for performance and safety.
“The IPC architecture provides important security benefits: Sandboxed execution: Templates run in isolated processes · Reduced attack surface: Main application protected from template engine vulnerabilities… Crash containment: Template engine failures don’t affect the main application · Zero-downtime updates: IPC server can be updated independently without restarting client applications”
The IPC approach offers significant advantages:
The IPC approach introduces performance overhead due to inter-process communication. The impact varies depending on: … For most web applications, the security and interoperability benefits compensate for the performance overhead.
Factors affecting IPC performance:
For performance-critical applications, consider:
“Easy updates: No application recompilation needed - simply update the IPC server for engine improvements”
This decoupled architecture means template engine updates don’t require redeploying your entire application stack.
By design the templates do not have access to arbitrary application data, the data has to be passed to the template in a JSON, then the data that you have not included in the JSON cannot be read by the template.
This is perhaps the most fundamental security feature. Unlike some template engines that have access to the entire application context, Neutral TS requires explicit data passing. This means:
“The IPC architecture provides important security benefits: Sandboxed execution: Templates run in isolated processes · Reduced attack surface: Main application protected from template engine vulnerabilities · Resource control: Memory and CPU limits can be enforced at server level · Crash containment: Template engine failures don’t affect the main application”
When using IPC mode, templates run in a completely separate process:
Neutral TS is built in Rust, which provides:
Declare supports wildcards, see bif “declare” for details. … {:allow; templates » file.txt :}{:else; fails :}
The allow BIF enables whitelisting:
{:allow; templates >> file.ntpl :}{:else; File not allowed :}
{:allow; languages >> es-ES :}{:else; Language not supported :}
“Neutral TS template engine provides powerful and easy-to-use translation utilities… define the translation in a JSON”
Neutral TS has built-in support for multilingual content. Translations are defined in the schema:
{
"locale": {
"current": "en",
"trans": {
"en": {
"Hello": "Hello",
"Welcome": "Welcome"
},
"es": {
"Hello": "Hola",
"Welcome": "Bienvenido"
},
"de": {
"Hello": "Hallo",
"Welcome": "Willkommen"
},
"fr": {
"Hello": "Bonjour",
"Welcome": "Bienvenue"
}
}
}
}
The trans BIF handles translations:
{:trans; Hello :}
This outputs:
For longer translation keys, you can use references:
{
"trans": {
"en": {
"ref:greeting-nts": "Hello and welcome to our site!"
},
"es": {
"ref:greeting-nts": "¡Hola y bienvenido a nuestro sitio!"
}
}
}
{:trans; ref:greeting-nts :}
If a translation key doesn’t exist for the current locale, the original text is returned, making it safe to use trans everywhere even if not all text is translated yet.
“The cache is modular, allowing only parts of the template to be included in the cache”
Neutral TS provides fine-grained control over caching at multiple levels.
Cache an entire template:
{:cache; /120/ >>
<!DOCTYPE html>
<html>
<head>
<title>Cached Page</title>
</head>
<body>
<!-- Content cached for 120 seconds -->
</body>
</html>
:}
Cache specific sections:
<!DOCTYPE html>
<html>
<body>
{:cache; /3600/ >>
<nav><!-- Expensive navigation, cached for 1 hour --></nav>
:}
<main><!-- Dynamic content --></main>
{:cache; /600/ >>
<aside><!-- Sidebar, cached for 10 minutes --></aside>
:}
</body>
</html>
“Or exclude parts of the cache, the previous example would be much better like this: {:cache; /120/ » <!DOCTYPE html> <html>…”
Use !cache to exclude sections from parent caches:
{:cache; /120/ >>
<!DOCTYPE html>
<html>
<body>
<div>Cached content</div>
{:!cache; {:date; %H:%M:%S :} :}
<div>More cached content</div>
</body>
</html>
:}
The timestamp remains dynamic while surrounding content is cached.
Control caching behavior through configuration:
{
"config": {
"cache_prefix": "myapp-neutral-cache",
"cache_dir": "/tmp/neutralts",
"cache_on_post": false,
"cache_on_get": true,
"cache_on_cookies": true,
"cache_disable": false
}
}
“The data is defined in a JSON: "data": { "true": true, "false": false, "hello": "hello", "zero": "0", "one": "1", "spaces": " ", "empty": "", "null": null, "emptyarr": []…”
Neutral TS handles all standard JSON types:
Use arrow syntax for nested structures:
{:;user->profile->address->city:}
{:;settings->theme->colors->primary:}
The each BIF iterates over arrays and objects:
{:each; users key user >>
<div class="user">
<span>{:;key:}</span>: {:;user->name:}
</div>
:}
Check if data is an array/object:
{:array; items >>
<!-- items is an array/object -->
{:each; items key item >> ... :}
:}{:else;
<!-- items is not an array -->
{:;items:}
:}
Get the first non-empty value:
{:coalesce;
{:code; {:;user->nickname:} :}
{:code; {:;user->name:} :}
{:code; Anonymous :}
:}
{:eval; {:code; {:code; … :} :} » {:code; {:code; … :} :} :}
The eval BIF enables dynamic template generation and execution, allowing templates to build and evaluate other template code.
“This feature is experimental. Executes a external script (currently only Python) and processes its output. The script receives parameters and can access the template schema. … {:obj; { "engine": "Python", "file": "script.py", "params": {}, "callback": "main", "template": "template.ntpl" } :}”
The obj BIF allows executing external scripts:
{:obj; {
"engine": "Python",
"file": "script.py",
"params": {"id": 123},
"callback": "main",
"template": "result.ntpl"
} :}
This powerful feature enables:
Build snippet names dynamically:
{:snippet; option-foo >> Option A :}
{:snippet; option-bar >> Option B :}
{:snippet; option-baz >> Option C :}
<!-- Call dynamically based on variable -->
{:snippet; option-{:;selectedOption:} :}
“This is clearer and less computationally expensive: {:snippet; option-foo » … :} {:snippet; option-bar » … :} {:snippet; option-{:;varname:} :}”
The date BIF formats dates:
{:date; %Y-%m-%d :} <!-- 2026-02-13 -->
{:date; %H:%M:%S :} <!-- 14:30:45 -->
{:date; %A, %B %d, %Y :} <!-- Thursday, February 13, 2026 -->
“In Rust it is enough with two methods, create the template with a file and a schema and then render: // Data let schema = json!({ "data": { "hello": "Hello, World!", "site": { "name": "My Site" } } }); // Create template // In file.ntpl use {:;hello:} and {:;site->name:} for show data. let template = Template::from_file_value("file.ntpl", schema).unwrap(); // Render template let content = template.render();”
use neutralts::Template;
use serde_json::json;
// Create schema
let schema = json!({
"data": {
"hello": "Hello, World!",
"site": {
"name": "My Site"
}
}
});
// Create and render template
let template = Template::from_file_value("file.ntpl", schema).unwrap();
let content = template.render();
Using the neutraltemplate package:
from neutraltemplate import Template
schema = {
"data": {
"hello": "Hello, World!",
"site": {
"name": "My Site"
}
}
}
template = Template.from_file_value("file.ntpl", schema)
content = template.render()
<?php
use NeutralTS\Template;
$schema = [
"data" => [
"hello" => "Hello, World!",
"site" => [
"name" => "My Site"
]
]
];
$template = Template::fromFileValue("file.ntpl", $schema);
$content = $template->render();
const { Template } = require('neutralts');
const schema = {
data: {
hello: "Hello, World!",
site: {
name: "My Site"
}
}
};
const template = Template.fromFileValue("file.ntpl", schema);
const content = template.render();
package main
import (
"github.com/neutralts/client-go"
)
func main() {
schema := map[string]interface{}{
"data": map[string]interface{}{
"hello": "Hello, World!",
"site": map[string]interface{}{
"name": "My Site",
},
},
}
template := neutralts.FromFileValue("file.ntpl", schema)
content := template.Render()
}
All PWA examples use the same template: Neutral templates.
The official documentation includes Progressive Web App examples demonstrating how the same templates work across all supported languages.
Examples for Rust, Python, PHP, Node.js and Go here: download. All PWA examples use the same template: Neutral templates.
{:snippet; button >> <button class="{:;class:}">{:;text:}</button> :}
templates/
├── layouts/
│ ├── base.ntpl
│ └── admin.ntpl
├── components/
│ ├── header.ntpl
│ ├── footer.ntpl
│ └── nav.ntpl
└── pages/
├── home.ntpl
└── about.ntpl
{
"data": {
"user_profile": { ... },
"site_settings": { ... },
"page_meta": { ... }
}
}
{
"data": {
"navigation": {
"main_menu": [...],
"footer_links": [...]
}
}
}
Minimize Nested Iterations Deeply nested loops can impact performance.
// DON'T
{ "data": { "user": entireUserRecord } }
// DO
{ "data": { "user": { "name": user.name, "avatar": user.avatar } } }
Validate Input Data Sanitize user-provided data before including in schemas.
| Aspect | Native (Rust) | IPC |
|---|---|---|
| Latency | Minimal | Additional network hop |
| Memory | Shared with application | Separate process |
| Scaling | Application scaling | Independent scaling |
| Updates | Requires rebuild | Hot-swappable |
Connection Pooling Reuse IPC connections instead of creating new ones per request.
Template Precompilation If supported, precompile templates at startup.
Measure performance in your specific environment:
use std::time::Instant;
let start = Instant::now();
for _ in 0..1000 {
let template = Template::from_file_value("test.ntpl", schema.clone()).unwrap();
let _ = template.render();
}
let duration = start.elapsed();
println!("1000 renders: {:?}", duration);
| Feature | Neutral TS | Jinja2 |
|---|---|---|
| Language Support | Multiple | Python only |
| Syntax | BIF-based | Django-like |
| Security | Process isolation | Sandboxed |
| Learning Curve | Moderate | Low |
| Ecosystem | Growing | Mature |
| Feature | Neutral TS | Blade |
|---|---|---|
| Language Support | Multiple | PHP only |
| Framework Dependency | None | Laravel |
| Compilation | Runtime | Compiled |
| Extensibility | BIFs | Directives |
| Feature | Neutral TS | Handlebars |
|---|---|---|
| Language Support | Multiple | JS (with ports) |
| Logic-less | No | Yes (mostly) |
| Helpers | BIFs | Custom helpers |
| Performance | Rust-speed | V8 speed |
In a microservices environment with services written in different languages, Neutral TS allows sharing templates across all services:
┌─────────────────────────────────────────────────────────────┐
│ Shared Templates │
│ (templates/*.ntpl) │
└─────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Service │ │ Product Svc │ │ Order Service│ │ Admin Panel │
│ (Python) │ │ (Go) │ │ (Rust) │ │ (PHP) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │ │ │
└──────────────┴──────────────┴──────────────┘
│
▼
┌──────────────┐
│ Neutral TS │
│ IPC Server │
└──────────────┘
When migrating from one technology stack to another, Neutral TS enables gradual migration:
Build once, deploy everywhere:
Thanks to this, and to its modular and parameterizable design, it is possible to create utilities or plugins that will work everywhere. For example, you can develop tools to create forms or form fields and create your own libraries of “snippets” for repetitive tasks.
Create shareable template libraries:
SaaS products with customizable themes benefit from:
The Neutral TS community continues to develop IPC clients for additional languages. The TCP/IP-based protocol makes it possible to add support for virtually any language capable of network communication.
Ongoing optimization efforts include:
Future versions may include:
Improvements to tooling:
NeutralTS, Template system for the Web backend, language-agnostic via IPC/Package and natively as library/crate in Rust.
The project continues to grow with:
Neutral TS represents a significant evolution in template engine technology. We’re developing a new web template engine with a standout feature: it’s language-agnostic. This means the same web template will work on any system and with any programming language.
By addressing the fundamental challenge of template portability across programming languages, Neutral TS opens new possibilities for software architecture:
The engine’s architecture—built in Rust for performance and safety, accessible via IPC for universal compatibility—strikes an elegant balance between power and practicality. For most web applications, the security and interoperability benefits compensate for the performance overhead.
Whether you’re building a monolithic application in Rust or orchestrating a polyglot microservices architecture, Neutral TS provides a solid foundation for template management. Its BIF-based syntax, while requiring some initial learning, offers flexibility and power that simpler template engines cannot match.
As web development continues to embrace diverse technology stacks and architectural patterns, tools like Neutral TS that bridge language boundaries will become increasingly valuable. The ability to share not just data and APIs, but also presentation logic, represents a meaningful step toward truly language-agnostic software development.
For developers and organizations looking to streamline their template management, reduce duplication, and embrace polyglot architectures without sacrificing consistency, Neutral TS offers a compelling solution backed by modern engineering principles and an active community.
Neutral TS is released under the Apache License 2.0, allowing both personal and commercial use with proper attribution.
This comprehensive guide provides a foundation for understanding and using Neutral TS. As the project evolves, consult the official documentation for the latest features and best practices.