Source: https://primate.run/docs/application # Application Primate exposes an **application facade** through `config/app.ts`. It provides app-level utilities that are useful from server code, especially in route handlers. Use it when you need access to: * configuration values * environment variables * the project root * server-side views ## Importing the application facade The application facade is the default export of `config/app.ts`. You can import it directly: ```ts import app from "../config/app.ts"; ``` or, if you have configured a path alias in `tsconfig.json`, via the alias: ```ts import app from "@/config/app"; ``` A typical route might look like this: ```ts import app from "@/config/app"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { const title = app.config("name"); return response.page({ title }); }, }); ``` !!! `@/config/app` is a path alias, not a built-in language feature. It only works if you configure it in `tsconfig.json`, for example by mapping `@/config/app` to `config/app.ts`. !!! ## `app.config(path)` Read values from your app configuration using a dot-separated path: ```ts const name = app.config("name"); const secure = app.config("server.secure"); ``` This is useful when route handlers or other server-side code need to react to application settings without importing configuration files directly. ## `app.env(key)` Read environment variables through the application facade: ```ts const token = app.env("API_TOKEN"); ``` This gives you a single app-level API for environment access instead of calling runtime-specific APIs directly. ### Typed environment variables You can make `app.env()` type-safe by declaring an environment schema in `config/app.ts`: ```ts // config/app.ts import config from "primate/config"; import p from "pema"; export default config({ env: { schema: p({ API_TOKEN: p.string, PORT: p.u16, }), }, }); ``` With a schema in place, Primate validates environment variables when the app starts serving, and `app.env()` becomes typed: ```ts const token = app.env("API_TOKEN"); // string const port = app.env("PORT"); // number ``` If a required key is missing, or a value does not satisfy the schema, Primate fails early instead of letting misconfiguration surface later at runtime. If no `env.schema` is configured, `app.env()` reads from the environment directly. !!! `app.env()` is server-only. Do not use it in frontend code. !!! ## `app.view(name)` Load a server-side view by name: ```ts const page = app.view("docs/home/index.md"); ``` This is mainly useful in advanced cases where you need the rendered or parsed view result before passing it somewhere else. For ordinary page rendering, prefer returning `response.page(...)` directly. Use `app.view(...)` only when you need to load a view manually and work with its result before returning a response. ```ts import app from "@/config/app"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { const { html } = app.view("docs/home/index.md"); return response.page({ content: html }); }, }); ``` ## `app.root` `app.root` is a `FileRef` pointing at your project root. Use it to access app files relative to the root directory: ```ts const guides = await app.root.join("guides.json").json(); ``` This is useful for reading local assets, generated files, or project metadata without hard-coding absolute paths. ## Summary | API | Description | | ------------------ | -------------------------------------- | | `app.config(path)` | Read app configuration values | | `app.env(key)` | Read environment variables | | `app.view(name)` | Load a server-side view | | `app.root` | Access the project root as a `FileRef` | In most apps, you will use the application facade from route handlers when you need information or utilities that belong to the application as a whole rather than to the current request. Source: https://primate.run/docs/backend/go # Go Primate runs [Go][Documentation] with WebAssembly compilation, strongly-typed validation, sessions, and server-side routing. ## Setup ### Install Go First, install Go 1.25 or later from [go.dev](https://go.dev/dl/). ### Install Module ```bash npm install @primate/go ``` ### Configure ```ts import config from "primate/config"; import go from "@primate/go"; export default config({ modules: [go()], }); ``` ### Initialize Go Create a `go.mod` file in your project root: ```bash go mod init your-project-name ``` Add the Primate Go dependency: ```bash go get github.com/primate-run/go@latest ``` This creates `go.mod` and `go.sum` files that manage your Go dependencies. ## Routes Create Go route handlers in `routes` using `.go` files. Routes are compiled to WebAssembly and run in the JavaScript runtime. ```go // routes/hello.go package main import "github.com/primate-run/go/route" var _ = route.Get(func(request route.Request) any { return "Hello, world!" }) ``` ### HTTP Methods All standard HTTP methods are supported: ```go package main import "github.com/primate-run/go/route" var _ = route.Get(func(request route.Request) any { return "GET request" }) var _ = route.Post(func(request route.Request) any { return "POST request" }) var _ = route.Put(func(request route.Request) any { return "PUT request" }) var _ = route.Delete(func(request route.Request) any { return "DELETE request" }) ``` ## Request Handling ### Query Parameters Access query parameters through the `Query` request bag: ```go // routes/query.go package main import "github.com/primate-run/go/route" var _ = route.Get(func(request route.Request) any { if request.Query.Has("foo") { foo, _ := request.Query.Get("foo") return foo } return "foo missing" }) ``` ### Request Body Handle different body types based on content: #### JSON Body ```go // routes/json.go package main import ( "github.com/primate-run/go/route" "github.com/primate-run/go/types" ) var _ = route.Post(func(request route.Request) any { json, err := request.Body.JSON() if err != nil { return types.Dict{"error": err.Error()} } return json }) ``` #### Form Fields ```go // routes/form.go package main import ( "github.com/primate-run/go/route" "github.com/primate-run/go/types" ) var _ = route.Post(func(request route.Request) any { form, err := request.Body.Form() if err != nil { return types.Dict{"error": err.Error()} } return form }) ``` #### Text Body ```go // routes/text.go package main import "github.com/primate-run/go/route" var _ = route.Post(func(request route.Request) any { text, _ := request.Body.Text() return text }) ``` #### Binary Data ```go // routes/blob.go package main import ( "github.com/primate-run/go/route" "github.com/primate-run/go/types" ) var _ = route.Post(func(request route.Request) any { blob, err := request.Body.Blob() if err != nil { return types.Dict{"error": err.Error()} } head := []int{} for i := 0; i < len(blob.Data) && i < 4; i++ { head = append(head, int(blob.Data[i])) } return map[string]any{ "type": blob.Type, "size": len(blob.Data), "head": head, } }) ``` ### File Uploads Handle multipart file uploads: ```go // routes/upload.go package main import ( "github.com/primate-run/go/route" "github.com/primate-run/go/types" ) var _ = route.Post(func(request route.Request) any { // get form fields form, err := request.Body.Form() if err != nil { return types.Dict{"error": err.Error()} } // get uploaded files files, err := request.Body.Files() if err != nil { return types.Dict{"error": err.Error()} } // process files for _, file := range files { // file.Field - form field name // file.Name - original filename // file.Type - MIME type // file.Size - file size in bytes // file.Bytes - file content } return types.Dict{ "form": form, "files": len(files), } }) ``` ## Validation Use Primate's strongly-typed validation system with the `pema` package: ```go // routes/validate.go package main import ( "github.com/primate-run/go/route" "github.com/primate-run/go/pema" "github.com/primate-run/go/types" ) var schema = pema.Schema(types.Dict{ "baz": pema.Int(), "foo": pema.String(), }) var _ = route.Get(func(request route.Request) any { parsed, err := request.Query.Parse(schema, true) if err != nil { return err.Error() } return parsed }) ``` ### Field Types The validation system supports multiple strongly-typed field types: - `pema.String()` - validates string values - `pema.Int()` - validates integer values - `pema.Int64()` - validates 64-bit integer values - `pema.Float()` - validates float64 values - `pema.Boolean()` - validates boolean values ### Coercion Enable automatic type coercion by passing `true` as the second parameter: ```go parsed, err := r.Query.Parse(schema, true) // enables coercion ``` With coercion enabled: - Strings are converted to numbers when possible - Empty strings become `false` for booleans, `0` for numbers - Numbers are converted between types as needed ## Responses ### Plain Data Return any Go type that can be JSON serialized: ```go var _ = route.Get(func(request route.Request) any { return types.Dict{"name": "Donald"} }) var _ = route.Get(func(request route.Request) any { return "Hello, world!" }) var _ = route.Get(func(request route.Request) any { return types.Array[types.Dict]{ {"name": "Donald"}, {"name": "Ryan"}, } }) ``` ### Views Render views with props: ```go // routes/view.go package main import ( "github.com/primate-run/go/response" "github.com/primate-run/go/route" "github.com/primate-run/go/types" ) var _ = route.Get(func(request route.Request) any { return response.View("index.html", types.Dict{"hello": "world"}) }) ``` With options: ```go var _ = route.Get(func(request route.Request) any { return response.View("index.html", types.Dict{"hello": "world"}, types.Dict{"partial": true}, ) }) ``` ### Redirects Redirect to another route: ```go // routes/redirect.go package main import ( "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return response.Redirect("/redirected") }) ``` With custom status code: ```go var _ = route.Get(func(request route.Request) any { return response.Redirect("/redirected", 301) // moved permanently }) ``` ### Error Responses Return error responses: ```go // routes/error.go package main import ( "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return response.Error() }) ``` With custom error options: ```go var _ = route.Get(func(request route.Request) any { return response.Error(types.Dict{"body": "Custom error message"}) }) ``` ## Sessions Manage user sessions with the session package: ```go // routes/session.go package main import ( "github.com/primate-run/go/route" "github.com/primate-run/go/session" "github.com/primate-run/go/types" ) var _ = route.Get(func(request route.Request) any { // create a session session.Create(types.Dict{"foo": "bar"}) // get session data return session.Get() }) ``` ### Session Methods - `session.Create(data)` - creates a new session with data - `session.Get()` - gets session data (throws if no session) - `session.Try()` - gets session data (returns empty if no session) - `session.Set(data)` - updates session data - `session.Destroy()` - destroys the session - `session.Exists()` - checks if session exists - `session.Id()` - gets the session ID ## Types Primate Go provides convenient type aliases in the `core` package for common data structures: ```go import "github.com/primate-run/go/core" // dictionary (map[string]any) data := core.Dict{"key": "value", "count": 42} // generic object with typed values user := core.Object[string]{"name": "John", "role": "admin"} // generic array with typed elements names := core.Array[string]{"John", "Bob", "Charlie"} // array of dictionaries users := core.Array[core.Dict]{ {"name": "Donald"}, {"name": "Ryan"}, } ``` ## Configuration | Option | Type | Default | Description | | --------- | -------- | ------- | ------------------------- | | extension | `string` | `".go"` | Associated file extension | ### Example ```ts import go from "@primate/go"; import config from "primate/config"; export default config({ modules: [ go({ // use `.golang` as associated file extension extension: ".golang", }), ], }); ``` ## Resources - [Documentation] - [Go Language Specification](https://go.dev/ref/spec) - [WebAssembly Support](https://github.com/golang/go/wiki/WebAssembly) [Documentation]: https://go.dev Source: https://primate.run/docs/backend/python # Python Primate runs [Python][Documentation] with WebAssembly compilation, strongly-typed validation, sessions, and server-side routing. ## Setup ### Install Python First, install Python 3.8 or later from the [official website][Python]. ### Install Module ```bash npm install @primate/python ``` ### Configure ```ts import config from "primate/config"; import python from "@primate/python"; export default config({ modules: [python()], }); ``` ### Initialize Python Create a `requirements.txt` file in your project root to specify Python dependencies: ```txt numpy ``` Install the Primate Python dependency: ```bash pip install primate-run ``` Primate will automatically install packages listed in `requirements.txt` when compiling routes to WebAssembly. ## Routes Create Python route handlers in `routes` using `.py` files. Routes are compiled to WebAssembly and run in the JavaScript runtime. ```python # routes/hello.py from primate import Route @Route.get def get(request): return "Hello, world!" ``` ### HTTP Methods All standard HTTP methods are supported: ```python from primate import Route @Route.get def get(request): return "GET request" @Route.post def post(request): return "POST request" @Route.put def put(request): return "PUT request" @Route.delete def delete(request): return "DELETE request" ``` ## Request Handling ### Query Parameters Access query parameters through the `query` request bag: ```python # routes/query.py from primate import Route @Route.get def get(request): if request.query.has("foo"): return request.query.get("foo") else: return "foo missing" ``` ### Request Body Handle different body types based on content: #### JSON Body ```python # routes/json.py from primate import Route @Route.post def post(request): return request.body.json() ``` #### Form Fields ```python # routes/form.py from primate import Route @Route.post def post(request): return request.body.form() ``` #### Text Body ```python # routes/text.py from primate import Route @Route.post def post(request): return request.body.text() ``` #### Binary Data ```python # routes/blob.py from primate import Route @Route.post def post(request): blob = request.body.blob() return { "type": blob.type, "size": blob.size, "head": blob.head(4), } ``` ### File Uploads Handle multipart file uploads: ```python # routes/upload.py from primate import Route @Route.post def post(request): # get form fields form = request.body.form() # get uploaded files files = request.body.files() # process files file_info = [] for file in files: file_info.append({ "field": file.field, # form field name "name": file.filename, # original filename "type": file.content_type, # MIME type "size": file.size, # file size in bytes "content": file.io.read().decode("utf-8") # file content }) return { "form": form, "files": file_info } ``` ## Validation Use Primate's strongly-typed validation system with the `pema` module: ```python # routes/validate.py from primate import Route, pema schema = pema.schema({ "baz": pema.int(), "foo": pema.string() }) @Route.get def get(request): try: parsed = request.query.parse(schema, True) return parsed except pema.ValidationError as e: return str(e) ``` ### Field Types The validation system supports multiple strongly-typed field types: - `pema.string()` - validates string values - `pema.int()` - validates integer values - `pema.float()` - validates float values - `pema.boolean()` - validates boolean values ### Coercion Enable automatic type coercion by passing `True` as the second parameter: ```python parsed = request.query.parse(schema, True) # enables coercion ``` With coercion enabled: - Strings are converted to numbers when possible - Empty strings become `False` for booleans, `0` for numbers - Numbers are converted between types as needed ## Responses ### Plain Data Return any Python object that can be JSON serialized: ```python @Route.get def get(request): return {"name": "Donald"} @Route.get def get(request): return "Hello, world!" @Route.get def get(request): return [ {"name": "Donald"}, {"name": "Ryan"} ] ``` ### Views Render views with props: ```python # routes/view.py from primate import Route, Response @Route.get def get(request): return Response.view("index.html", {"hello": "world"}) ``` With options: ```python from primate import Route, Response @Route.get def get(request): return Response.view("index.html", {"hello": "world"}, {"partial": True} ) ``` ### Redirects Redirect to another route: ```python # routes/redirect.py from primate import Route, Response @Route.get def get(request): return Response.redirect("/redirected") ``` With custom status code: ```python @Route.get def get(request): return Response.redirect("/redirected", status=301) # moved permanently ``` ### Error Responses Return error responses: ```python # routes/error.py from primate import Route, Response @Route.get def get(request): return Response.error() ``` With custom error options: ```python @Route.get def get(request): return Response.error({"body": "Custom error message"}) ``` ## Sessions Manage user sessions with the Session module: ```python # routes/session.py from primate import Route, Session @Route.get def get(request): # create a session Session.create({"foo": "bar"}) # get session data return Session.get() ``` ### Session Methods - `Session.create(data)` - creates a new session with data - `Session.get()` - gets session data (raises if no session) - `Session.try_get()` - gets session data (returns empty dict if no session) - `Session.set(data)` - updates session data - `Session.destroy()` - destroys the session - `Session.exists()` - checks if session exists - `Session.id()` - gets the session ID ## Python Conventions Primate Python follows standard Python conventions: ### Function Names - Use `snake_case` for function and variable names - Use descriptive names for route handlers ### Dictionary Access Use standard Python dictionary syntax: ```python # accessing form data form = request.body.form() name = form.get("name", "") # building responses return { "status": "success", "data": processed_data } ``` ### Exception Handling Use try-except blocks for error handling: ```python @Route.post def post(request): try: data = request.body.json() return process_data(data) except Exception as e: return {"error": str(e)} ``` ## External Packages Install Python packages by adding them to `requirements.txt`: ```txt numpy pandas ``` Use them in your routes: ```python # routes/data.py from primate import Route import numpy as np import pandas as pd @Route.get def get(request): # Create sample data data = np.array([1, 2, 3, 4, 5]) df = pd.DataFrame({"values": data}) return { "mean": float(np.mean(data)), "sum": int(np.sum(data)), "dataframe_info": df.describe().to_dict() } ``` ## Configuration | Option | Type | Default | Description | | --------- | -------- | ------- | ------------------------- | | extension | `string` | `".py"` | Associated file extension | ### Example ```ts import python from "@primate/python"; import config from "primate/config"; export default config({ modules: [ python({ // use `.python` as associated file extension extension: ".python", }), ], }); ``` ## Resources - [Documentation] - [Python Language Reference](https://docs.python.org/3/) - [Pyodide Documentation](https://pyodide.org/) - [WebAssembly with Python](https://pyodide.org/en/stable/usage/wasm-constraints.html) [Documentation]: https://docs.python.org/3/ [Python]: https://www.python.org Source: https://primate.run/docs/backend/ruby # Ruby Primate runs [Ruby][Documentation] with WebAssembly compilation, strongly-typed validation, sessions, and server-side routing. ## Setup ### Install Ruby First, install Ruby 3.0 or later from the [official website][Ruby]. ### Install Module ```bash npm install @primate/ruby ``` ### Configure ```ts import config from "primate/config"; import ruby from "@primate/ruby"; export default config({ modules: [ruby()], }); ``` ### Initialize Ruby Create a `Gemfile` in your project root: ```bash bundle init ``` Add the Primate Ruby dependency: ```ruby # Gemfile source "https://rubygems.org" gem "primate-run", "~> 0.1.0" ``` Install dependencies to vendor/bundle (required for WASM): ```bash bundle config set --local path 'vendor/bundle' bundle install ``` This creates `Gemfile` and `Gemfile.lock` files that manage your Ruby dependencies. ## Routes Create Ruby route handlers in `routes` using `.rb` files. Routes are compiled to WebAssembly and run in the JavaScript runtime. ```ruby # routes/hello.rb require 'primate/route' Route.get do |request| 'Hello, world!' end ``` ### HTTP Methods All standard HTTP methods are supported: ```ruby require 'primate/route' Route.get do |request| 'GET request' end Route.post do |request| 'POST request' end Route.put do |request| 'PUT request' end Route.delete do |request| 'DELETE request' end ``` ## Request Handling ### Query Parameters Access query parameters through the `query` request bag: ```ruby # routes/query.rb require 'primate/route' Route.get do |request| if request.query.has?('foo') request.query.get('foo') else 'foo missing' end end ``` ### Request Body Handle different body types based on content: #### JSON Body ```ruby # routes/json.rb require 'primate/route' Route.post do |request| begin request.body.json rescue => e { error: e.message } end end ``` #### Form Fields ```ruby # routes/form.rb require 'primate/route' Route.post do |request| begin request.body.form rescue => e { error: e.message } end end ``` #### Text Body ```ruby # routes/text.rb require 'primate/route' Route.post do |request| request.body.text end ``` #### Binary Data ```ruby # routes/blob.rb require 'primate/route' Route.post(content_type: "application/octet-stream") do |request| blob = request.body.blob { "type" => blob.type, "size" => blob.size, "head" => blob.head(4), } end ``` ### File Uploads Handle multipart file uploads: ```ruby # routes/upload.rb require 'primate/route' Route.post(content_type: "multipart/form-data") do |request| multipart = request.body.multipart begin # Get form form form = multipart.form # Get uploaded files files = multipart.files # Process files file_info = files.map do |file| { field: file.field, # form field name name: file.filename, # original filename type: file.content_type, # MIME type size: file.size # file size in bytes } end { form: form, files: file_info } rescue => e { error: e.message } end end ``` ## Validation Use Primate's strongly-typed validation system with the `pema` module: ```ruby # routes/validate.rb require 'primate/route' require 'primate/pema' schema = Pema.schema({ 'baz' => Pema.int, 'foo' => Pema.string }) Route.get do |request| begin request.query.parse(schema, true) rescue Pema::ValidationError => e e.message end end ``` ### Field Types The validation system supports multiple strongly-typed field types: - `Pema.string` - validates string values - `Pema.int` - validates integer values - `Pema.float` - validates float values - `Pema.boolean` - validates boolean values ### Coercion Enable automatic type coercion by passing `true` as the second parameter: ```ruby request.query.parse(schema, true) # enables coercion ``` With coercion enabled: - Strings are converted to numbers when possible - Empty strings become `false` for booleans, `0` for numbers - Numbers are converted between types as needed ## Responses ### Plain Data Return any Ruby object that can be JSON serialized: ```ruby Route.get do |request| { name: 'Donald' } end Route.get do |request| 'Hello, world!' end Route.get do |request| [ { name: 'Donald' }, { name: 'Ryan' } ] end ``` ### Views Render views with props: ```ruby # routes/view.rb require 'primate/route' require 'primate/response' Route.get do |request| Response.view('index.html', hello: 'world') end ``` With options: ```ruby require 'primate/route' require 'primate/response' Route.get do |request| Response.view('index.html', { hello: 'world' }, { partial: true } ) end ``` ### Redirects Redirect to another route: ```ruby # routes/redirect.rb require 'primate/route' require 'primate/response' Route.get do |request| Response.redirect('/redirected') end ``` With custom status code: ```ruby Route.get do |request| Response.redirect('/redirected', status: 301) # moved permanently end ``` ### Error Responses Return error responses: ```ruby # routes/error.rb require 'primate/route' require 'primate/response' Route.get do |request| Response.error end ``` With custom error options: ```ruby Route.get do |request| Response.error(body: 'Custom error message') end ``` ## Sessions Manage user sessions with the session module: ```ruby # routes/session.rb require 'primate/route' require 'primate/session' Route.get do |request| # Create a session Session.create(foo: 'bar') # Get session data Session.get end ``` ### Session Methods - `Session.create(data)` - creates a new session with data - `Session.get` - gets session data (raises if no session) - `Session.try` - gets session data (returns nil if no session) - `Session.set(data)` - updates session data - `Session.destroy` - destroys the session - `Session.exists?` - checks if session exists - `Session.id` - gets the session ID ## Ruby Conventions Primate Ruby follows standard Ruby conventions: ### Method Names - Use `snake_case` for method and variable names - Use `?` suffix for boolean methods: `has?`, `exists?` - Use `!` suffix for destructive methods (when applicable) ### Hash Syntax Use modern Ruby hash syntax with symbols: ```ruby # Preferred { name: 'John', age: 30 } # Also valid { :name => 'John', :age => 30 } ``` ### String Literals Use single quotes for plain strings, double quotes for interpolation: ```ruby # Plain strings message = 'Hello, world!' # String interpolation greeting = "Hello, #{name}!" ``` ## Configuration | Option | Type | Default | Description | | --------- | -------- | ------- | ------------------------- | | extension | `string` | `".rb"` | Associated file extension | ### Example ```ts import ruby from "@primate/ruby"; import config from "primate/config"; export default config({ modules: [ ruby({ // use `.ruby` as associated file extension extension: ".ruby", }), ], }); ``` ## Resources - [Documentation] - [Ruby Language Reference](https://ruby-doc.org/) - [WebAssembly with Ruby](https://github.com/ruby/ruby.wasm) [Documentation]: https://ruby-doc.org [Ruby]: https://www.ruby-lang.org Source: https://primate.run/docs/backend # Backends Primate supports multiple backend languages compiled to WebAssembly. Use one or mix them in the same app for different routes. ## Overview | Backend | Routes | Body | Query | Sessions | Stores | WebSocket | Validation | | ------- | ------ | ---- | ----- | -------- | ------ | --------- | ---------- | | [Go] | ✓ | ✓ | ✓ | ✓ | | | ✓ | | [Python]| ✓ | ✓ | ✓ | ✓ | | | ✓ | | [Ruby] | ✓ | ✓ | ✓ | ✓ | | | ✓ | [Go]: /docs/backend/go [Python]: /docs/backend/python [Ruby]: /docs/backend/ruby ## Features * **Routes** — HTTP method handlers (GET, POST, PUT, DELETE, etc.) * **Body** — handle JSON, form data, text, binary, and file uploads * **Query** — access URL query parameters and path variables * **Sessions** — server-side session management with cookies * **Stores** — built-in database operations (CRUD, queries, validation) * **WebSocket** — real-time bidirectional communication * **Validation** — strongly-typed request/response validation schemas ## WebAssembly Backends run in the JS runtime via WebAssembly or a Wasm-based runtime (e.g. Pyodide for Python). ## Language Categories ### Pre-Compiled Languages *Go* Compiled languages with strong type systems, memory safety, and excellent WebAssembly support. Well-suited for high-performance APIs and complex business logic. ### Dynamic Languages *Python*, *Ruby* Interpreted languages with flexible syntax and extensive ecosystems. Good for rapid prototyping, data processing, and scripting tasks. ## Quickstart Install a backend module: ```bash npm install @primate/go ``` Configure it: ```ts // config/app.ts import config from "primate/config"; import go from "@primate/go"; export default config({ modules: [go()], }); ``` Create a route handler: ```go // routes/hello.go package main import "github.com/primate-run/go/route" var _ = route.Get(func(request route.Request) any { return "Hello, World!" }) ``` ## Route Structure Each backend follows a consistent pattern for route handlers: ### HTTP Methods All backends support standard HTTP methods through dedicated functions: ```go // Go var _ = route.Get(func(request route.Request) any { ... }) var _ = route.Post(func(request route.Request) any { ... }) ``` ```python # Python @Route.get def get(request): ... @Route.post def post(request): ... ``` ```ruby # Ruby Route.get do |request| ... end Route.post do |request| ... end ``` ### Request Handling Access request data through consistent APIs: ```go // Go - Query parameters if request.Query.Has("name") { name, _ := request.Query.Get("name") return name } // Go - JSON body json, err := request.Body.JSON() ``` ## Response Types ### Data Responses Return structured data as JSON: ```go return map[string]any{"message": "Hello"} ``` ### Views Render views: ```go return response.View("view.html", data) ``` ### Redirects Redirect to other routes: ```go return response.Redirect("/other-route") ``` ## Database Operations Built-in store operations: ## Session Management Server-side sessions with automatic cookie handling: ```go // Go session.Create(map[string]any{"user": "john"}) data := session.Get() ``` ## Mixing Backends Multiple backends can coexist; each handles its own file extensions: ```ts import config from "primate/config"; import go from "@primate/go"; import python from "@primate/python"; export default config({ modules: [go(), python()], }); ``` Routes can be implemented in different languages: ``` routes/ ├── auth.go # Go authentication ├── api.py # Python data processing └── admin.rb # Ruby admin interface ``` ## Ecosystem Integration ### Package Management Each backend uses its native package manager: - **Go** — `go.mod` and `go get` - **Python** — `requirements.txt` and `pip` - **Ruby** — `Gemfile` and `bundle` ### External Libraries Import and use libraries native to each language, compiled to WebAssembly: ```go // Go import "encoding/json" import "crypto/sha256" ``` ```python # Python import numpy as np import pandas as pd ``` ```ruby # Ruby require 'json' require 'digest' ``` Source: https://primate.run/docs/configuration # Configuration Primate works out of the box with zero configuration. In some cases, you may wish to change the defaults. The most common use case is activating additional modules. Configuration files are located in `config`. Anything you configure is merged into the defaults. ## `app.ts` %%% TypeScript:config/app.ts, JavaScript:config/app.js ```ts import config from "primate/config"; export default config({ /* options */ }); ``` ```js import config from "primate/config"; export default config({ /* options */ }); ``` %%% `config/app.ts` exports the application facade for your app. For using that value at runtime — including `app.config()`, `app.env()`, `app.view()`, and `app.root` — see the [Application] page. ### App options |Option|Default|Description| |-|-|-| |[build.external](#build-external)|`[]`|packages excluded from the server bundle| |[client.partitions](#client-partitions)|`undefined`|isolated client bundle partitions| |[client.routes](#client-partitions)|`undefined`|route-prefix to bundle-partition mapping| |[db.migrations](@/config/db-migrations)|`undefined`|database migration configuration| |[entrypoints](#entrypoints)|`{}`|named client entrypoints| |[http.csp](#http-csp)|`{}`|content security policy| |[http.headers](#http-headers)|`{}`|default HTTP response headers| |[http.host](#http-host)|`"localhost"`|server host| |[http.port](#http-port)|`6161`|server port| |[http.timeout](#http-timeout)|`undefined`|in-flight HTTP idle timeout| |[http.ssl.cert](#http-ssl-cert)|`undefined`|path to SSL certificate| |[http.ssl.key](#http-ssl-key)|`undefined`|path to SSL private key| |[http.static.root](#http-static-root)|`"/"`|web path of static assets| |[loaders](#loaders)|`{}`|esbuild file loaders by extension| |[env.schema](#env-schema)|`undefined`|schema for typed environment variables| |[modules](#modules)|`[]`|extension modules| |[i18n.defaultLocale](#i18n-defaultlocale)|`undefined`|default locale| |[i18n.locales](#i18n-locales)|`[]`|list of locales| |[i18n.currency](#i18n-currency)|`"USD"`|active currency| |[i18n.persist](#i18n-persist)|`"cookie"`|locale persistence mode| ### `build.external` Packages and import paths to exclude from the production server bundle. At runtime they are loaded from `node_modules` instead. Use this for dependencies that load adjacent WebAssembly modules, native addons, or data files that would otherwise be separated from the package by bundling. ```ts import config from "primate/config"; export default config({ build: { external: ["satori", "@resvg/resvg-js"], }, }); ``` External dependencies must be installed wherever the production server runs. Development already keeps packages external by default. ### `db.migrations` Configuration for Primate's opt-in migration system. ```ts import config from "primate/config"; import db from "@/config/db"; export default config({ db: { migrations: { table: "migration", db, }, }, }); ``` Apply migrations explicitly with `primate migrate:apply` during development or `build/migrate.js apply` before starting a production server. Automatic startup application is not supported. See the [Stores] page for the complete migration workflow and 0.41 upgrade guidance. ### `client.partitions` Client partitions isolate browser code and assets by URL route prefix. Each partition is an independent build branch, compiled into its own output file tree. Shared dependencies are duplicated rather than emitted as chunks shared across an access boundary. ```ts import config from "primate/config"; export default config({ client: { partitions: { public: "/", admin: "/admin/app/", }, routes: { "/": "public", "/admin": "admin", }, }, }); ``` `client.partitions` maps partition names to asset URL prefixes. `client.routes` maps URL path prefixes to those names; values that are not keys in `client.partitions` are rejected. The longest matching route prefix wins, and prefixes match whole path segments: `/admin` includes `/admin/users` but not `/administrator`. A `/` assignment is required as the default and is checked during startup. Mappings to unknown partitions, duplicate normalized mappings, and duplicate asset prefixes are also rejected during startup. In 0.42, `client.partitions` replaces the 0.41 `client.groups` key. Rename the key when upgrading; the old key is rejected with a migration diagnostic rather than treated as a virtual route group. `client.routes` is unchanged. Each partition receives its own bootstrap, route-page registry, CSS, dynamic imports, and file-loader assets beneath its configured asset prefix. Top-level files in `views` are shared deliberately and are duplicated into every partition; put partition-private pages and layouts beside their handlers in `routes`. Imported fonts, images, and other file-loader assets are likewise emitted under each partition's prefix, even when their contents are identical. Distinct URLs are part of the isolation guarantee. Assets that are deliberately public and shared across every authorization boundary can instead live in `static`. Navigation within a partition keeps normal client routing. Navigation across partitions stays in the same document: Primate first loads the destination assets and inactive styles and waits for its frontend to become ready. It then disposes the active frontend instance, replaces its partition styles, and mounts the destination. Links, form redirects, and browser history all use this transition. If a destination asset cannot be loaded or is rejected by the proxy, the active app, URL, and styles remain in place. Static module code may remain in the browser's module cache after disposal. This allows a reverse proxy to apply the same authentication rule to both the HTML route and its complete client file tree. For example, protect `/admin` and `/admin/app/`, with the more specific asset rule evaluated before a catch-all public rule. Production filenames are content-hashed and can be cached as immutable assets; HTML should not be cached with the same policy. Client partitions provide asset isolation, not application authorization. Continue to authorize every protected request in route hooks or handlers. Source maps are not emitted by Primate's client production build. ### `entrypoints` Named client entrypoints bundled from the `client` directory and injected into `templates/app.html` as placeholders. ```ts import config from "primate/config"; export default config({ entrypoints: { css: "master.css", colorscheme: "colorscheme.ts", }, }); ``` Each key becomes a placeholder in your HTML template. For a key `css`, add `%css%` wherever you want the asset injected: ```html %css% %head% %body% ``` Entrypoint keys follow the placeholder naming rules: they start with a letter or underscore and may contain letters, numbers, underscores, and hyphens. Any file type supported by your active frontend modules works as an entrypoint — if you have `@primate/svelte` configured, you can point an entrypoint at a `.svelte` file and it will be bundled and injected like any other asset. The names `app`, `head`, and `body` are reserved and cannot be used as entrypoint keys. ### `http.csp` The Content Security Policy (CSP) to use. Example of a restrictive policy. ```js { // all content must come from own origin, excluding subdomains "default-src": ["'self'"], // styles must come from own origin, excluding subdomains "style-src": ["'self'"], // disallow , and elements "object-src": ["'none'"], // disallow embedding "frame-ancestors": ["'none'"], // all form submissions must be to own origin "form-action": ["'self'"], // allow only own origin in "base-uri": ["'self'"], } ``` ### `http.headers` HTTP headers to use when generating requests using the `view` handler. ### `http.host` The HTTP host to use. This value is directly passed to the runtime. ### `http.port` The HTTP port to use. This value is directly passed to the runtime. ### `http.timeout` The maximum time, in whole milliseconds, that an in-flight request or response may remain inactive in production. Accepted values range from `0` through `255000`; `0` disables the timeout. If omitted, the HTTP runtime's default is preserved. This is an inactivity timeout, not an overall request deadline or a completed connection's keep-alive timeout. Development always disables this timeout so debugging and live reload are not interrupted, regardless of the configured value. Bun rounds positive values up to whole seconds. Deno does not expose an HTTP idle-timeout setting, so the option is validated but Deno's native default is preserved. For server-sent events and other long-lived responses, either disable the timeout or send heartbeats more frequently than it. Reverse proxies and load balancers have independent idle timeouts; configure heartbeat cadence below the shortest timeout in the complete request path. ### `http.ssl.cert` Path to SSL certificate. If this property and `http.ssl.key` are set and point to a valid key/certificate pair, Primate uses https instead of http. ### `http.ssl.key` Path to SSL key. If `http.ssl.cert` and this property are set and point to a valid key/certificate pair, Primate uses https instead of http. ### `http.static.root` The path at which to serve static assets (those located in the `static` directory). Static assets take precedence over routes. This option allows you to have all static assets served at a subpath, like `/public`. ### `loaders` A map of file extensions to esbuild loaders. Currently only `"file"` is supported, which copies the asset into the build output and replaces the import with its URL — useful for fonts and other binary assets referenced from CSS. ```ts import config from "primate/config"; export default config({ loaders: { ".woff2": "file", }, }); ``` ### `env.schema` A Pema object schema used to validate and type environment variables exposed through the application facade. ```ts import config from "primate/config"; import p from "pema"; export default config({ env: { schema: p({ API_TOKEN: p.string, PORT: p.u16, }), }, }); ``` With `env.schema` configured, `app.env(key)` validates values when the app starts serving and becomes type-aware. See the [Application] page for usage. ### `modules` Additional modules to load at runtime. ### `i18n.defaultLocale` The default locale to use, must be one from the `locales` list. ### `i18n.locales` List of locales to use, must have at least one locale. ### `i18n.currency` Currency to use in localization. ### `i18n.persist` Locale persistence mode. ### Reference ```ts import type { Module } from "@primate/core"; import type { FileRef } from "@rcompat/fs"; interface Config { http?: { csp?: Record; headers?: Record; host?: string; // "localhost" port?: number; // 6161 ssl?: { cert?: FileRef | string; key?: FileRef | string; }; static?: { root?: string; // "/" }; }; modules?: Module[]; } ``` ## `session.ts` %%% TypeScript:config/session.ts, JavaScript:config/session.js ```ts import session from "primate/session"; export default session/**/({ /* options */ }); ``` ```js import session from "primate/session"; export default session({ /* options */ }); ``` %%% ### Session options |Option|Default|Description| |-|-|-| |[cookie.httpOnly](#cookie-httponly)|`true`|mark cookie as `HttpOnly`| |[cookie.maxAge](#cookie-maxage)|none|persist cookie for this many seconds| |[cookie.name](#cookie-name)|`"session_id"`|name of the session cookie| |[cookie.path](#cookie-path)|`"/"`|path for which the cookie is valid| |[cookie.sameSite](#cookie-samesite)|`"Lax"`|`SameSite` cookie policy| |[cookie.secure](#cookie-secure)|app TLS setting|mark cookie as `Secure`| |[store](#store)|default in-memory store|store managing sessions| ### `cookie.httpOnly` Whether the session cookie should be marked `HttpOnly` and hidden from client-side JavaScript. ### `cookie.maxAge` The number of seconds for which the browser should retain the session cookie. When omitted, it is removed when the browser session ends. ### `cookie.name` The name of the session cookie. ### `cookie.path` The session cookie path (paths on which the cookie is loaded). ### `cookie.sameSite` The level of security to use in sending the session cookies when browsing between websites. ### `cookie.secure` Whether the session cookie should be marked `Secure`. By default, this follows the app's TLS setting. Set it explicitly when TLS terminates at a trusted reverse proxy. ### `store` The store used to validate and persist sessions. By default, Primate uses an in-memory store that accepts JSON-compatible object data and resets when the app restarts. A custom store validates data against its declared fields. ## `db/*.ts` %%% TypeScript:config/db.ts, JavaScript:config/db.js ```ts import sqlite from "@primate/sqlite"; export default sqlite(); ``` ```js import sqlite from "@primate/sqlite"; export default sqlite(); ``` %%% [Application]: /docs/application [Stores]: /docs/stores Source: https://primate.run/docs/database/jsondb # JSON Database JSON Database is a file-backed document database that persists data as JSON files on disk. It is suited to lightweight local development and small deployments that do not require a full DBMS. ## Setup ### Install ```bash npm install @primate/jsondb ``` ### Configure ```ts // config/db.ts import jsondb from "@primate/jsondb"; export default jsondb({ // directory: "data", }); ``` ## Options | option | type | default | description | | --------- | -------- | -------- | ------------------------------------- | | directory | `string` | `"data"` | Directory where JSON files are stored | Each table is stored as a separate JSON file in the configured directory, named after the table (e.g. `data/user.json`). ## Notes JSON Database supports the full Primate store API including relations, field projection, sorting, and limiting. It handles all Primate field types including `bigint`, `blob`, `datetime`, and `url`, which are serialized to JSON using type-tagged objects and revived on load. JSON Database is not suited to high-concurrency workloads — it holds all table data in memory and flushes to disk on every write. For production use, prefer a dedicated DBMS such as PostgreSQL or MySQL. ## MemoryDB vs JSON Database | Feature | MemoryDB | JSON Database | | ----------- | --------------- | --------------------- | | Persistence | None | File-backed | | Setup | None | Directory on disk | | Use case | Tests, REPL | Local dev, small apps | Source: https://primate.run/docs/database/mongodb # MongoDB MongoDB is a document database with flexible schemas and rich querying. ## Setup ### Install ```bash npm install @primate/mongodb ``` You need a running MongoDB server. ### Configure ```ts // config/db.ts import mongodb from "@primate/mongodb"; export default mongodb({ database: "app", // host: "localhost", // port: 27017, // username: "mongo", // password: "secret", // pool: { max: 100, connectTimeout: 30_000, idleTimeout: 0 }, }); ``` ## Options | option | type | default | description | | -------- | -------- | ------------- | ------------- | | database | `string` | — | Database name | | host | `string` | `"localhost"` | MongoDB host | | port | `number` | `27017` | MongoDB port | | username | `string` | — | Username | | password | `string` | — | Password | | pool | `object` | driver defaults | Connection pool controls | Pool controls use the shared `max`, `connectTimeout`, and `idleTimeout` options documented in [Connection pools](/docs/database#connection-pools). Timeouts are milliseconds. `max` is an integer from 1 to 65,535. The timeout maximum is 2,147,483,647; `connectTimeout` starts at 1 and `idleTimeout` starts at 0 (`0` disables MongoDB idle expiry). Omitted values preserve MongoDB driver defaults. Use `db.fork({ pool: { max: 2 } })` to create another `MongoClient` and pool that privately inherit the connection configuration. Close the fork when finished; its `close()` does not affect the parent pool. Source: https://primate.run/docs/database/mysql # MySQL MySQL is a relational DBMS. Use it for durable, multi-connection workloads. ## Setup ### Install ```bash npm install @primate/mysql ``` You need a running MySQL server. Create a database and user with access. ### Configure ```ts // config/db.ts import mysql from "@primate/mysql"; export default mysql({ database: "app", // host: "localhost", // port: 3306, // username: "root", // password: "secret", // pool: { max: 10, connectTimeout: 10_000, idleTimeout: 60_000 }, }); ``` ## Options | option | type | default | description | | -------- | -------- | ------------- | ------------------------------- | | database | `string` | — | Database name to use | | host | `string` | `"localhost"` | MySQL host | | port | `number` | `3306` | MySQL port | | username | `string` | — | Username (omit for socket auth) | | password | `string` | — | Password (omit if not required) | | pool | `object` | driver defaults | Connection pool controls | Pool controls use the shared `max`, `connectTimeout`, and `idleTimeout` options documented in [Connection pools](/docs/database#connection-pools). Timeouts are milliseconds. `max` is an integer from 1 to 65,535. The timeout maximum is 2,147,483,647; `connectTimeout` starts at 1 and `idleTimeout` starts at 0. With mysql2, `idleTimeout: 0` makes an idle connection eligible for the next cleanup pass. Omitted values preserve mysql2 defaults. Use `db.fork({ pool: { max: 2 } })` to create another pool that privately inherits the connection configuration. Close the fork when finished; its `close()` does not affect the parent pool. Source: https://primate.run/docs/database/oracle # Oracle Oracle is a relational DBMS suited to transactional and enterprise workloads. ## Setup ### Install ```bash npm install @primate/oracledb ``` Primate’s Oracle driver uses the npm `oracledb` driver. By default, node-oracledb runs in Thin mode, so Oracle Client libraries are not required unless you explicitly enable Thick mode. ### Requirements This driver uses Oracle’s native `JSON` and SQL `BOOLEAN` column types. Native `JSON` is available in Oracle 21c and later, while SQL `BOOLEAN` is available in Oracle 23c / 23ai. In practice, use Oracle 23c / 23ai or newer for full compatibility. The configured database user must be able to connect to the database. If you use migrations, it also needs permission to create, alter, and drop tables in its schema. ### Configure ```ts // config/db.ts import oracledb from "@primate/oracledb"; export default oracledb({ database: "FREEPDB1", // host: "localhost", // port: 1521, // username: "app", // password: "secret", }); ``` ## Options | option | type | default | description | | -------- | -------- | ------------- | -------------------- | | database | `string` | — | Oracle service name | | host | `string` | `"localhost"` | Oracle host | | port | `number` | `1521` | Oracle listener port | | username | `string` | — | Username | | password | `string` | — | Password | ## Notes Primate connects using Oracle Easy Connect syntax in the form `host:port/service`, so the `database` option should be the service name exposed by your Oracle instance. Source: https://primate.run/docs/database/postgresql # PostgreSQL PostgreSQL is a relational DBMS focused on correctness and extensibility. ## Setup ### Install ```bash npm install @primate/postgresql ``` You need a running PostgreSQL server and a database/user with access. ### Configure ```ts // config/db.ts import postgresql from "@primate/postgresql"; export default postgresql({ database: "app", // host: "localhost", // port: 5432, // username: "postgres", // password: "secret", // pool: { max: 10, connectTimeout: 30_000, idleTimeout: 0 }, }); ``` ## Options | option | type | default | description | | -------- | -------- | ------------- | --------------- | | database | `string` | — | Database name | | host | `string` | `"localhost"` | PostgreSQL host | | port | `number` | `5432` | PostgreSQL port | | username | `string` | — | Username | | password | `string` | — | Password | | pool | `object` | driver defaults | Connection pool controls | ### Pool options | option | type | postgres.js default | description | | ---------------- | -------- | ------------------- | ----------- | | max | `number` | `10` | Maximum connections; integer from 1 to 65,535 | | connectTimeout | `number` | `30000` | Milliseconds allowed to establish and authenticate a new connection; integer from 1 to 2,147,483,647 | | idleTimeout | `number` | `0` | Milliseconds before an idle connection closes; integer from 0 to 2,147,483,647, with `0` disabling idle closure | These controls do not impose a timeout on requests waiting for pool capacity. Omitting an option preserves the postgres.js default. ## Independent pools Create a separately owned PostgreSQL instance without repeating credentials: ```ts const locks = db.fork({ pool: { max: 4, connectTimeout: 5_000, idleTimeout: 1_000 }, }); try { await locks.client.begin(async sql => { await sql`SELECT pg_advisory_xact_lock(10, 20)`; // transaction-scoped work }); } finally { await locks.close(); } ``` The fork inherits the parent's endpoint, credentials, and unspecified pool settings, but owns another pool. Its `client`, typed `sql()` queries, Store operations, transactions, and `close()` are independent from the parent. If a transaction connection is lost, `client.begin()` rejects, including after an advisory lock was acquired. Rejection reports the loss but cannot cancel application work already running in the callback. Use application-level cancellation or fencing when work must stop after lock loss. Source: https://primate.run/docs/database/sqlite # SQLite SQLite is an embedded relational database. It stores data in a single file, or fully in memory for ephemeral use. ## Setup ### Install ```bash npm install @primate/sqlite ``` ### Configure ```ts // config/db.ts import sqlite from "@primate/sqlite"; export default sqlite({ // database: ":memory:", // in-memory (default) // database: "/var/data.db", // file path (persists) }); ``` ## Options | option | type | default | description | | -------- | -------- | ------------ | ---------------------------------------- | | database | `string` | `":memory:"` | Path to a database file, or `":memory:"` | `":memory:"` uses an in-memory database. It does **not** persist across runs. Source: https://primate.run/docs/database# Databases Databases back stores. Define one in your project, map it to a path alias in `tsconfig.json`, and import it wherever you need it. ## Available databases | Database | Storage | Type | | --------------------------------------- | -------------- | ----------- | | [MemoryDB](#memorydb) | in-memory | relational | | [SQLite](/docs/database/sqlite) | in-memory/file | relational | | [MySQL](/docs/database/mysql) | DBMS | relational | | [PostgreSQL](/docs/database/postgresql) | DBMS | relational | | [OracleDB](/docs/database/oracle) | DBMS | relational | | [JSONDB](/docs/database/jsondb) | JSON files | document | | [MongoDB](/docs/database/mongodb) | DBMS | document | ## MemoryDB For development, tests, or ephemeral use, Primate ships a built-in in-memory database: ```ts import memorydb from "primate/memorydb"; const db = memorydb(); ``` MemoryDB requires no configuration and holds no state between instances. It is particularly useful in test suites where you want a clean database per test. ## Setting up a database Create a file for your database configuration and map it to a path alias in `tsconfig.json`: ```ts // config/db.ts import postgresql from "@primate/postgresql"; export default postgresql({ host: "localhost", port: 5432, database: "myapp", username: "myapp", password: "myapp", }); ``` ```json // tsconfig.json { "compilerOptions": { "paths": { "@/config/db": ["config/db"] } } } ``` The `@/config/db` alias is conventional but not required — `@db`, `@/db`, or any other alias you prefer works equally well. If you plan to use your database or stores outside of Primate, make sure to add a `package.json` import as well. ```json // package.json { "imports": { "@/config/db": "./config/db.ts" } } ``` ## Using a database in a store Import your database and pass it explicitly to the store: ```ts import db from "@/config/db"; import store from "primate/store"; import p from "pema"; export default store({ table: "post", db, schema: { id: store.key.primary(p.u32), title: p.string, }, }); ``` See the [stores](/docs/stores) page for the full store API. ## Connection pools PostgreSQL, MySQL, and MongoDB accept the same optional pool controls: ```ts const db = postgresql({ database: "myapp", pool: { max: 10, connectTimeout: 5_000, idleTimeout: 60_000, }, }); ``` Timeouts are in milliseconds. `connectTimeout` limits establishing a new network connection; it does not limit how long a query can wait for pool capacity. Omitted values preserve each driver's defaults. See the driver page for exact defaults and `idleTimeout: 0` behavior. Use `fork()` when work needs capacity and lifecycle independent from the Store pool, such as a long transaction: ```ts const dedicated = db.fork({ pool: { max: 2 } }); try { // dedicated.client, dedicated.sql(), and Store operations use another pool } finally { await dedicated.close(); } ``` The fork privately inherits the endpoint, credentials, and omitted pool controls. Closing it does not close the parent, and closing the parent does not close the fork. Call `close()` on every fork you create. ## Typed bespoke SQL For queries that don't fit the store API — complex joins, aggregates, database-specific functions, or raw DDL — all four SQL drivers (SQLite, MySQL, PostgreSQL, and OracleDB) expose `db.sql`: ```ts import db from "@/config/db"; import p from "pema"; const findByAge = db.sql({ input: p({ age: p.u8 }), query: "SELECT name FROM users WHERE age > :age", output: p.array(p({ name: p.string })), }); const results = await findByAge({ age: 18 }); ``` `db.sql` returns a function. Named placeholders (`:age`) map to input schema keys. TypeScript enforces at compile time that every placeholder has a matching input key and vice versa. Input is validated against the input schema before the query runs. Output is validated against the output schema after. Both `input` and `output` are optional. A write-only query needs no output: ```ts const insert = db.sql({ input: p({ name: p.string, age: p.u8 }), query: "INSERT INTO users (name, age) VALUES (:name, :age)", }); await insert({ name: "John", age: 30 }); ``` A query with no parameters needs no input: ```ts const findAll = db.sql({ query: "SELECT name FROM users", output: p.array(p({ name: p.string })), }); const results = await findAll(); ``` DDL statements work too: ```ts const createIndex = db.sql({ query: "CREATE INDEX idx_users_name ON users (name)", }); await createIndex(); ``` ### Store schema interoperability `db.sql` accepts a store's schema directly as input, letting you reuse your existing type definitions without duplication: ```ts import db from "@/config/db"; import User from "@/stores/User"; const findByAge = db.sql({ input: User.schema, query: "SELECT name FROM users WHERE age > :age", output: p.array(p({ name: p.string })), }); ``` TypeScript enforces that every required field in the store schema has a matching placeholder in the query. Optional fields are exempt. If a required field is missing from the query, the error tells you exactly which placeholders are missing. Placeholder translation is handled automatically per driver — SQLite uses `$name`, MySQL uses `:name` natively, PostgreSQL and OracleDB translate to positional parameters. You always write `:name` and the driver does the rest. ## Typical project layout ``` config/ db.ts # database configuration stores/ Post.ts # store using @/config/db ``` Source: https://primate.run/docs/events # Events Use `primate/events` for small in-memory event channels inside one running app process. ```ts // services/ItemEvents.ts import events from "primate/events"; type ItemEvent = | { type: "comment.created"; comment: { id: number; body: string } } | { type: "item.status.changed"; status: string }; export default events.channel(); ``` Emit events with a key. ```ts import ItemEvents from "@/services/ItemEvents"; ItemEvents.emit(item.id, { type: "comment.created", comment: { id: comment.id, body: comment.body }, }); ``` Subscribe to events for a key. `subscribe` returns an unsubscribe function. ```ts const unsubscribe = ItemEvents.subscribe(item.id, event => { console.log(event.type); }); unsubscribe(); ``` ## With SSE Channels pair naturally with `response.sse`. ```ts import ItemEvents from "@/services/ItemEvents"; import response from "primate/response"; import route from "primate/route"; export default route({ get(request) { const item_id = Number(request.query.get("item")); return response.sse(source => { return ItemEvents.subscribe(item_id, event => { source.send(event.type, event); }); }); }, }); ``` When the browser disconnects, Primate calls the function returned from `response.sse`, which unsubscribes from the channel. ## Scope `primate/events` is local and in-memory. It does not persist events and does not broadcast across multiple server processes or machines. If your app runs more than one instance, events only reach clients connected to the same instance that emitted them. Source: https://primate.run/docs/frontend/angular # Angular Primate runs [Angular][Documentation] with server-side rendering, hydration, client navigation, layouts, validation and i18n. Unlike most frontends that Primate runs, Angular is a full-stack framework. To achieve parity with other frontends, Primate uses only the client portion and provides the server implementation itself. ## Setup ### Install ```bash npm install @primate/angular @angular/core @angular/common @angular/compiler ``` ### Configure ```ts import config from "primate/config"; import angular from "@primate/angular"; export default config({ modules: [ angular(), ], }); ``` ## Components Create Angular components in `views`. ```ts // views/PostIndex.component.ts import { Component, input } from "@angular/core"; import { CommonModule } from "@angular/common"; @Component({ imports: [CommonModule], template: `

{{ title() }}

{{ post.title }}

@if (post.excerpt) {

{{ post.excerpt }}

}
`, }) export default class PostIndex { title = input("Blog"); posts = input<{ title: string; excerpt?: string }[]>([]); } ``` !!! Components only require a `selector` when referenced by tag from another component's template. Pages rendered from routes and layouts are instantiated programmatically and do not need a selector. When referencing child components by tag (e.g., ``), the child must declare a selector and the parent must include the child in `imports: [Child]`. Retain `CommonModule` when using `*ngFor`. !!! Serve the component from a route. ```ts // routes/posts.ts import PostIndex from "@/views/PostIndex"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { title: "First Post", excerpt: "Introduction to Primate with Angular" }, { title: "Second Post", excerpt: "Building reactive applications" }, ]; return response.view(PostIndex, { title: "Blog", posts }); }, }); ``` ## Props Props passed to `response.view` are mapped to `input()` signals inside Angular components. Pass props from a route: ```ts import UserView from "@/views/User"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view(UserView, { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` These props become `input()` properties in the component: ```ts import { Component, input } from "@angular/core"; import { CommonModule } from "@angular/common"; @Component({ imports: [CommonModule], template: `

{{ user().name }}

Role: {{ user().role }}

  • {{ permission }}
`, }) export default class User { user = input(); permissions = input([]); } ``` ## Request Import the `request` signal from `app:angular` to access the current request inside any component. The signal updates automatically on client-side navigation. ```ts import { Component } from "@angular/core"; import { request } from "app:angular"; @Component({ template: `

Current path: {{ request().url.pathname }}

`, }) export default class Page { request = request; } ``` The `request` signal exposes a `RequestPublic` object. | Property | Type | Description | | --------- | -------------- | ----------------------- | | `url` | `URL` | current request URL | | `query` | `Dict` | query string parameters | ## Reactivity with signals Angular's signals provide fine-grained reactivity for state management and computed values. ```ts import { Component, signal, computed } from "@angular/core"; @Component({ template: `
{{ count() }}

Doubled: {{ doubled() }}

`, }) export default class Counter { count = signal(0); doubled = computed(() => this.count() * 2); increment() { this.count.update(n => n + 1); } decrement() { this.count.update(n => n - 1); } } ``` ## Validation Use Primate's validated state wrapper to synchronize with backend routes. ```ts import { Component, computed, input } from "@angular/core"; import client from "@primate/angular/client"; @Component({ template: `

Counter

{{ c().value() }} @if (c().error()) {

{{ c().error().message }}

} `, }) export default class Counter { id = input(""); counter = input(0); c = computed(() => client.field(this.counter()).post(`/counter?id=${this.id()}`)); increment() { this.c().update(n => n + 1); } decrement() { this.c().update(n => n - 1); } } ``` Add corresponding backend validation in the route: ```ts // routes/counter.ts import CounterView from "@/views/Counter"; import Counter from "@/stores/Counter"; import route from "primate/route"; import response from "primate/response"; import p from "pema"; await Counter.create(); export default route({ async get() { const [existing] = await Counter.find({}); const counter = existing ?? await Counter.insert({ value: 10 }); return response.view(CounterView, { id: counter.id, counter: counter.value, }); }, async post(request) { const id = p.string.parse(request.query.get("id")); const body = p.loose({ value: p.number }).parse(await request.body.form()); await Counter.update(id, { set: { value: body.value } }); return null; }, }); ``` The wrapper automatically tracks loading states, captures validation errors, and posts updates on state changes. ## Forms Use `client.form` from `@primate/angular/client` to wire forms to backend routes with automatic field-level validation and error display. See [Client forms](/docs/frontend/forms) for route actions, dynamic paths, and guidance on choosing between forms and `fetch`. ```ts // views/LoginForm.component.ts import { Component, OnInit } from "@angular/core"; import client from "@primate/angular/client"; import route from "@/routes/login"; @Component({ template: ` @if (form) {
@if (form.field('email').error()) {

{{ form.field('email').error() }}

} @if (form.field('password').error()) {

{{ form.field('password').error() }}

} @if (form.errors().length) {

{{ form.errors()[0] }}

}
} `, }) export default class LoginForm implements OnInit { form!: ReturnType; ngOnInit() { this.form = client.form(route.post, { initial: { email: "", password: "" }, }); } } ``` Add the corresponding route: ```ts // routes/login.ts import LoginForm from "@/views/LoginForm"; import p from "pema"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view(LoginForm); }, post: route.with({ contentType: "application/json", body: p({ email: p.string.email(), password: p.string.min(8) }), }, async request => { const { email, password } = await request.body.json(); // implement authentication logic return null; }), }); ``` Validation errors from the server are automatically surfaced per-field via `form.field(name).error()`. The `form.submitting()` signal disables the submit button while the request is in flight. ### Form API | Property | Type | Description | | ------------------ | --------------------- | ----------------------------------- | | `form.id` | `string` | Unique form ID for the `id` attr | | `form.submit` | `(event?) => Promise` | Submit handler for `(submit)` | | `form.submitting` | `() => boolean` | True while the request is in flight | | `form.errors` | `() => string[]` | Form-level errors | | `form.field(name)` | `Field` | Access a named field | ### Field API | Property | Type | Description | | -------------- | -------------------- | ------------------------------- | | `field.name` | `string` | Field name for the `name` attr | | `field.value` | `T` | Initial field value | | `field.error` | `() => string\|null` | First validation error or null | | `field.errors` | `() => string[]` | All validation errors for field | ## Layouts For SSR with hydration, layouts accept a `slot: TemplateRef` and render it using `*ngTemplateOutlet`. Note that `slot` must remain as `@Input()` since it is a template reference passed internally by Angular, not a regular prop. Create a layout view: ```ts // views/Layout.component.ts import { Component, Input, TemplateRef } from "@angular/core"; import { CommonModule } from "@angular/common"; @Component({ imports: [CommonModule], template: `
© 1996 My App
`, }) export default class Layout { @Input({ required: true }) slot!: TemplateRef; } ``` Next, register the layout using a `+layout.ts` file: ```ts // routes/+layout.ts import Layout from "@/views/Layout"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view(Layout); }, }); ``` ### Passing Props to Layouts ```ts // views/Layout.component.ts import { Component, Input, TemplateRef, input } from "@angular/core"; import { CommonModule } from "@angular/common"; @Component({ imports: [CommonModule], template: `

{{ brand() }}

`, }) export default class Layout { @Input({ required: true }) slot!: TemplateRef; brand = input("My App"); } ``` ```ts // routes/+layout.ts import Layout from "@/views/Layout"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view(Layout, { brand: "Primate Angular Demo" }); }, }); ``` ## Internationalization Create an i18n bridge file that adapts Primate's headless translator to Angular's reactivity model: ```ts // lib/i18n.ts import app from "@/config/app"; import i18n from "@primate/angular/i18n"; export default i18n(app.i18n); ``` Import and use the bridged translator directly in views: ```ts import { Component } from "@angular/core"; import t from "@/lib/i18n"; @Component({ template: `

{{ t("welcome") }}

{{ t("current_locale") }}: {{ currentLocale() }}

`, }) export default class Welcome { t = (key: string) => t(key); setLocale(locale: string) { t.locale.set(locale); } currentLocale() { return t.locale.get(); } } ``` Primate's integration automatically subscribes to locale changes and triggers rerenders when switching languages. ## Head Tags Use Angular's `Title` and `Meta` to dynamically set page titles and meta tags. ```ts import type { OnInit } from "@angular/core"; import { Component, inject } from "@angular/core"; import { Meta, Title } from "@angular/platform-browser"; @Component({ template: "

{{ pageTitle }}

", }) export default class Page implements OnInit { pageTitle = "About Us"; title = inject(Title); meta = inject(Meta); ngOnInit() { this.title.setTitle(this.pageTitle); this.meta.addTag({ name: "description", content: "Learn more about us" }); this.meta.addTag({ property: "og:title", content: this.pageTitle }); } } ``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ------------------- | ---------------------------- | | extensions | `string[]` | `[".component.ts"]` | Associated file extensions | | ssr | `boolean` | *production* | Enable server-side rendering | | csr | `boolean` | `true` | Enable client-side rendering | ### Example ```ts import config from "primate/config"; import angular from "@primate/angular"; export default config({ modules: [ angular({ extensions: [".component.ts", ".ng.ts"], }), ], }); ``` ## Resources - [Documentation] - [Components guide](https://angular.dev/guide/components) - [Reactive forms](https://angular.dev/guide/forms/reactive-forms) - [Signals](https://angular.dev/guide/signals) [Documentation]: https://angular.dev Source: https://primate.run/docs/frontend/eta # Eta Primate runs [Eta][Documentation] templates with server-side rendering and props mapped to template variables. ## Setup ### Install ```bash npm install @primate/eta ``` ### Configure ```ts import config from "primate/config"; import eta from "@primate/eta"; export default config({ modules: [ eta(), ], }); ``` ## Templates Create Eta templates in `views`. ```html

All posts

<% it.posts.forEach(function(post){ %>

<%= post.title %>

<% }) %>
``` Serve the template from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" }, ]; return response.view("post-index.eta", { posts }); }, }); ``` ## Props Props passed to `response.view` are available in templates as `it`. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("user.eta", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the template: ```html

<%= it.user.name %>

Role: <%= it.user.role %>

    <% it.permissions.forEach(function(permission){ %>
  • <%= permission %>
  • <% }) %>
``` ## Configuration | Option | Type | Default | Description | | -----------| ---------- | ---------- | -------------------------- | | extensions | `string[]` | `[".eta"]` | Associated file extensions | ### Example ```ts import eta from "@primate/eta"; import config from "primate/config"; export default config({ modules: [ eta({ // add `.eta.html` to associated file extensions extensions: [".eta", ".eta.html"], }), ], }); ``` ## Resources - [Documentation] [Documentation]: https://eta.js.org Source: https://primate.run/docs/frontend/forms # Client forms Use `client.form` for browser forms that mutate application state. It connects an imported route method to the form, serializes its fields, preserves Primate's validation response format, and exposes reactive submission and error state. ## Choose a submission pattern | Situation | Preferred pattern | |-|-| | One browser mutation | `client.form(route.post)` | | Dynamic route | `client.form(route.post, { path })` | | Structured JSON body | `client.form(route.post, { serialize })` | | Field validation | `throw p.error(field, message)` and read `form.field(field).error` | | API or programmatic endpoint | Explicit `fetch` and HTTP responses | | Multiple actions | Separate forms or an explicit hidden intent field | Use the frontend package that renders the form, for example `@primate/svelte/client`, `@primate/react/client`, or `@primate/angular/client`. ## Bind a route action Import the route and pass its mutation method to `client.form`: ```svelte
{#if $form.field("displayName").error}

{$form.field("displayName").error}

{/if}
``` The route method supplies the request content type, body type, and result type. The adapter supplies framework-specific reactive values for `submitting`, `submitted`, `result`, form errors, and field errors. ## Dynamic route paths A route imported from a dynamic filename still needs concrete path values. For `routes/project/[project]/settings/roles.ts`, pass `project` separately from the form body: ```svelte ``` TypeScript requires the options argument for a dynamic route and checks the `path` names and values. Static route forms can continue to omit it. Path values identify the route; inputs inside `
` supply its body. Do not add a dynamic path value as a hidden field in place of the `path` option. ## Structured JSON bodies By default, a JSON route receives a flat object created from the form's successful controls. Use `serialize` when the route expects nested objects, arrays, booleans, or another structure that HTML field names cannot represent unambiguously. ```svelte ``` The serializer receives the submitted `FormData`, including repeated controls and the initiating submit button. Its return type is checked against the imported route's declared body schema. It may also read component state instead of the `FormData` when that state is the authoritative value. The returned value enters the normal form lifecycle: submitting state, route interpolation, redirects, results, and Pema issue handling remain active. This option does not change URL-encoded or multipart forms. Pema JSON Pointer issue paths map to bracketed form field names. For example, `/notifications/preferences/desktop` maps to `notifications[preferences][desktop]`. Use matching `name` attributes when a nested input needs field-level error display, and read it with `form.field("notifications[preferences][desktop]").error` (or the adapter's reactive equivalent). Bracketed names are checked against the route body type. Object segments must name declared properties and array segments must be numeric indices. A `Record` deliberately permits any key at that record segment while continuing to check paths inside `T`. The field's `value` follows the same path through `initial`, so `field("notifications[preferences][desktop]").value` returns the nested initial boolean rather than the top-level object. ## Field validation Throw a Pema field error when a mutation fails validation that depends on application state: ```ts import p from "pema"; if (await role_exists(role)) { throw p.error("role", "This role already exists"); } ``` `client.form` maps that issue to the matching field. Read it through the adapter's field view, for example `$form.field("role").error` in Svelte or `form.field("role").error` in React and Solid. Schema validation errors are mapped the same way. Use the form-level `errors` value for failures without a specific field. Do not replace a field error with an unrelated raw `400` or `409` response. That discards the structured issue the form needs to render beside the input. ## Multiple actions Prefer one form for each mutation. If several controls must share a form, put the selected operation in an explicit hidden field and validate it on the server: ```html ``` Update that value deliberately before submission. Do not make correctness depend only on which submit button a browser reports as the submitter; keyboard submission and programmatic submission may not select the button you expect. ## When to use `fetch` Use explicit `fetch` for endpoints designed as programmatic APIs, background requests without a form, streaming, or protocols that need custom status and header handling. Handle the response as an HTTP response in those cases. Do not start with `fetch` for an ordinary browser mutation. It duplicates body serialization, submitting state, route interpolation, and Pema issue mapping that `client.form` already provides. ## Troubleshooting ### A dynamic path is `undefined` A message such as `/project: expected string, got undefined` means the route method did not receive its path parameters. Pass them in the second argument: ```ts client.form(route.post, { path: { project: project.name } }); ``` ### Submitting navigates to a raw 400 or 409 response Check that the form's submit event is bound to `form.submit` and that its `id` uses `form.id`. Then return or throw structured Pema validation issues for form-facing errors. A native form submission navigates to the response because the client handler did not intercept it. ### The endpoint is not a browser form Use `fetch` when the caller needs to inspect raw statuses, headers, or an API payload. Keep that endpoint's HTTP contract separate from form-oriented field validation. Source: https://primate.run/docs/frontend/handlebars # Handlebars Primate runs [Handlebars][Documentation] templates with server-side rendering and props mapped to template variables. ## Setup ### Install ```bash npm install @primate/handlebars ``` ### Configure ```ts import config from "primate/config"; import handlebars from "@primate/handlebars"; export default config({ modules: [ handlebars(), ], }); ``` ## Templates Create Handlebars templates in `views`. ```html

All posts

{{#each posts}}

{{this.title}}

{{/each}}
``` Serve the template from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" }, ]; return response.view("post-index.hbs", { posts }); }, }); ``` ## Props Props passed to `response.view` are available directly in templates as variables. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("user.hbs", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the template: ```html

{{user.name}}

Role: {{user.role}}

    {{#each permissions}}
  • {{this}}
  • {{/each}}
``` ## Configuration | Option | Type | Default | Description | | -----------| ---------- | ---------- | -------------------------- | | extensions | `string[]` | `[".hbs"]` | Associated file extensions | ### Example ```ts import handlebars from "@primate/handlebars"; import config from "primate/config"; export default config({ modules: [ handlebars({ // add `.handlebars` to associated extensions extensions: [".hbs", ".handlebars"], }), ], }); ``` ## Resources - [Documentation] [Documentation]: https://handlebarsjs.com Source: https://primate.run/docs/frontend/html # HTML Primate runs HTML templates with server-side rendering and props mapped to template literal variables. ## Setup ### Install ```bash npm install @primate/html ``` ### Configure ```ts import config from "primate/config"; import html from "@primate/html"; export default config({ modules: [ html(), ], }); ``` ## Templates Create HTML templates in `views` using template literal syntax. ```html

All posts

${posts.map(post => `

${post.title}

`).join("")} ``` Serve the template from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" }, ]; return response.view("post-index.html", { posts }); }, }); ``` ## Props Props passed to `response.view` are available directly in templates as variables. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("user.html", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the template: ```html

${user.name}

Role: ${user.role}

    ${permissions.map(permission => `
  • ${permission}
  • `).join("")}
``` ## Component-Scoped Assets HTML templates support `
${count}
``` While the CSS and JavaScript appear local to the component, they are extracted during build time and included in the global bundle. ## Escaping All props are automatically HTML-escaped for security. To output raw HTML, you'll need to handle it carefully in your template logic. Use `\${...}` to output literal `${...}` text in templates: ```html

Learn about \${variable} syntax

``` ## Configuration | Option | Type | Default | Description | | ----------| ---------- | ----------- | -------------------------- | | extension | `string[]` | `[".html"]` | Associated file extensions | ### Example ```ts import html from "@primate/html"; import config from "primate/config"; export default config({ modules: [ html({ // add `.htm` to associated file extensions extensions: [".html", ".htm"], }), ], }); ``` Source: https://primate.run/docs/frontend/htmx # HTMX Primate runs [HTMX][Documentation] templates with server-side rendering and props mapped to template literal variables. ## Setup ### Install ```bash npm install @primate/htmx ``` ### Configure ```ts import config from "primate/config"; import htmx from "@primate/htmx"; export default config({ modules: [ htmx(), ], }); ``` ## Templates Create HTMX templates in `views` using template literal syntax with HTMX attributes. ```html

All posts

${posts.map(post => `

${post.title}

`).join("")} ``` Serve the template from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" }, ]; return response.view("post-index.htmx", { posts }); }, }); ``` !!! When you use HTMX to fetch content, it sends its request with the `hx-request` header set. This header is used to return the component HTML in [partial mode](/docs/responses#partial). The example above thus works with or without JavaScript. !!! ## Props Props passed to `response.view` are available directly in templates as variables. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("user.htmx", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the template: ```html

${user.name}

Role: ${user.role}

    ${permissions.map(permission => `
  • ${permission}
  • `).join("")}
``` ## Component-Scoped Assets HTMX templates support `
``` While the CSS and JavaScript appear local to the component, they are extracted during build time and included in the global bundle. ## Escaping All props are automatically HTML-escaped for security. To output raw HTML, you'll need to handle it carefully in your template logic. Use `\${...}` to output literal `${...}` text in templates: ```html

Learn about \${variable} syntax

``` ## Configuration | Option | Type | Default | Description | | -------------- | ---------- | ----------- | -------------------------- | | extensions | `string[]` | `[".htmx"]` | Associated file extensions | | htmxExtensions | `string[]` | `[]` | HTMX extensions to load | | templates | `string[]` | `[]` | Client template engines | ### Example ```ts import htmx from "@primate/htmx"; import config from "primate/config"; export default config({ modules: [ htmx({ // use the `client-side-templates` extension htmxExtensions: ["client-side-templates"], // with the `handlebars` template templates: ["handlebars"], }), ], }); ``` ## Resources - [Documentation] - [Extensions](https://htmx.org/extensions) [Documentation]: https://htmx.org Source: https://primate.run/docs/frontend/markdown # Markdown Primate runs [Markdown][Documentation] templates with server-side rendering, frontmatter support, and table of contents generation. Primate uses `marked` to compile Markdown to HTML. ## Setup ### Install ```bash npm install @primate/markdown ``` ### Configure ```ts import config from "primate/config"; import markdown from "@primate/markdown"; export default config({ modules: [ markdown(), ], }); ``` ## Templates Create Markdown templates in `views`. ```md # All Posts Here are all the posts: ## First Post Introduction to Primate ## Second Post Building applications ``` Serve the template from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("post-index.md"); }, }); ``` ## Frontmatter Use YAML frontmatter to define metadata within your Markdown files. ```md --- title: "My Blog Post" author: "John Adams" date: "2024-01-01" published: true --- # My Blog Post By John Adams on 2024-01-01 This is the content of the post. ``` Serve the Markdown file: ```ts // routes/blog.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("blog-post.md"); }, }); ``` ## Table of Contents Markdown automatically generates a table of contents from headings within the document. ```md # Article Title ## Introduction Some intro text. ## Main Content More content. ## Conclusion Final thoughts. ``` Serve the article: ```ts // routes/article.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("article.md"); }, }); ``` The rendered view includes `toc` data with heading information that can be used to generate navigation. ### Using the Table of Contents Access the `toc` data and frontmatter `meta` in your route to build navigation: ```ts // routes/docs/[page].ts import type Component from "@primate/markdown/Component"; import respone from "primate/response"; import route from "primate/route"; import app from "@/config/app"; export default route({ get(request) { const page = request.path.get("page"); const { html, toc, meta } = app.view(`docs/${page}.md`); return response.view("DocPage.html", { content: html, toc, meta, title: meta.title || page, }); }, }); ``` Create an HTML view that uses the table of contents and metadata: ```html

${meta.title || title}

${meta.author ? `

By ${meta.author}

` : ""} ${meta.date ? `

${meta.date}

` : ""}
${content}
``` !!! You must have `@primate/html` installed and configured in your `config/app.ts`, or alternatively any other frontend for this. !!! ## Configuration | Option | Type | Default | Description | | ------------ | ----------------- | ------------ | -------------------------- | | extensions | `string[]` | `[".md"]` | Associated file extensions | | options | `MarkedExtension` | `{}` | `marked` options | | pretransform | `Pretransform` | `() => void` | Pretransform function | ### Example ```ts import markdown from "@primate/markdown"; import config from "primate/config"; export default config({ modules: [ markdown({ // add `.markdown` to associated file extensions extensions: [".md", ".markdown"], }), ], }); ``` ## Resources - [Documentation] - [Marked documentation](https://marked.js.org) [Documentation]: https://daringfireball.net/projects/markdown Source: https://primate.run/docs/frontend/marko # Marko Primate runs [Marko][Documentation] with server-side rendering, hydration, client navigation, layouts, validation and i18n. ## Setup ### Install ```bash npm install @primate/marko marko ``` ### Configure ```ts import config from "primate/config"; import marko from "@primate/marko"; export default config({ modules: [ marko(), ], }); ``` ## Components Create Marko components in `views`. ```marko // views/PostIndex.marko export interface Input { title: string; posts: { title: string; excerpt?: string }[]; }

${input.title}

${post.title}

${post.excerpt}

``` Serve the component from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { title: "First Post", excerpt: "Introduction to Primate with Marko" }, { title: "Second Post", excerpt: "Building reactive applications" }, ]; return response.view("PostIndex.marko", { title: "Blog", posts }); }, }); ``` ## Props Props passed to `response.view` are available as `input` in Marko views. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("User.marko", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the view: ```marko // views/User.marko export interface Input { user: { name: string; role: string }; permissions: string[]; }

${input.user.name}

Role: ${input.user.role}

  • ${permission}
``` ## Request Import the `request` object from `app:marko` to access the current request inside any component. It updates automatically on client-side navigation. ```marko import { request } from "app:marko";

Current path: ${request.url.pathname}

``` The `request` object exposes a `RequestPublic` object. | Property | Type | Description | | --------- | -------------- | ----------------------- | | `url` | `URL` | current request URL | | `query` | `Dict` | query string parameters | ## Reactivity Marko's reactive `` and `` tags provide fine-grained state management and computed values. ```marko count * 2/>
${count}

Doubled: ${doubled()}

``` ## Validation Use Primate's `` tag from `@primate/marko/tags` to synchronize state with backend routes. ```marko // views/Counter.marko import { Field } from "@primate/marko/tags"; export interface Input { id: number; counter: number; }

Counter Example

${counter.value}

${counter.error.message}

``` Add corresponding backend validation in the route: ```ts // routes/counter.ts import Counter from "@/stores/Counter"; import route from "primate/route"; import response from "primate/response"; import p from "pema"; await Counter.create(); export default route({ async get() { const counters = await Counter.find({}); const counter = counters.length === 0 ? await Counter.insert({ counter: 10 }) : counters[0]; return response.view("Counter.marko", counter); }, async post(request) { const id = p.loose.u32.parse(request.query.get("id")); const counter = p.loose.number.parse(await request.body.json()); await Counter.update(id, { set: { counter } }); return null; }, }); ``` The `` tag automatically tracks loading states, captures validation errors, and posts updates on state changes. ## Document head Use Primate's `` tag to declare page metadata from a Marko page or layout. It renders metadata into the document head during SSR and keeps it synchronized during client navigation. ```marko import { Head } from "@primate/marko/tags"; ${input.title} ``` `Head` accepts `title`, `meta`, `link`, `style`, `script`, and `base` elements. It removes metadata owned by a page when that page is replaced, without disturbing elements supplied by the application template. Only one `title` may be declared across the active page and its layouts. ## Forms Use Primate's `
` tag from `@primate/marko/tags` to wire forms to backend routes with automatic field-level validation and error display. ```marko // views/LoginForm.marko import { Form } from "@primate/marko/tags";

${form.errors[0]}

${email.error}

${password.error}

``` Add the corresponding route: ```ts // routes/login.ts import route from "primate/route"; import response from "primate/response"; import p from "pema"; const LoginSchema = p({ email: p.string.email(), password: p.string.min(8), }); export default route({ get() { return response.view("LoginForm.marko"); }, async post(request) { const body = LoginSchema.parse(await request.body.form()); // implement authentication logic return null; }, }); ``` Validation errors from the server are automatically surfaced per-field via `form.field(name).error`. The `form.submitting` flag disables the submit button while the request is in flight. ### Form API | Property | Type | Description | | ------------------ | --------------------- | ----------------------------------- | | `form.id` | `string` | Unique form ID for the `id` attr | | `form.submit` | `(event?) => Promise` | Submit handler for `onSubmit` | | `form.submitting` | `boolean` | True while the request is in flight | | `form.submitted` | `boolean` | True after a successful submission | | `form.errors` | `string[]` | Form-level errors | | `form.field(name)` | `Field` | Access a named field | ### Field API | Property | Type | Description | | -------------- | -------------- | ------------------------------- | | `field.name` | `string` | Field name for the `name` attr | | `field.value` | `T` | Initial field value | | `field.error` | `string\|null` | First validation error or null | | `field.errors` | `string[]` | All validation errors for field | ## Layouts Create layout components that wrap your pages using `input.content`. ```marko // views/Layout.marko export interface Input { content: Marko.Body; }
<${input.content}/>
© 1996 My App
``` Next, register the layout via a `+layout.ts` file: ```ts // routes/+layout.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Layout.marko", { brand: "Primate Marko Demo" }); }, }); ``` Pages under this route subtree render inside the layout via `input.content`. ## Internationalization Create an i18n bridge file that adapts Primate's headless translator to Marko's reactivity model: ```ts // lib/i18n.ts import app from "@/config/app"; import i18n from "@primate/marko/i18n"; export default i18n(app.i18n); ``` Import and use the bridged translator directly in views: ```marko // views/i18n/Index.marko import t from "@/lib/i18n"; ${t.locale.get()} ${t("title")} ``` Primate's integration automatically subscribes to locale changes and triggers rerenders when switching languages. ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ------------ | ---------------------------- | | extensions | `string[]` | `[".marko"]` | Associated file extensions | | ssr | `boolean` | *production* | Enable server-side rendering | | csr | `boolean` | `true` | Enable client-side rendering | ### Example ```ts import marko from "@primate/marko"; import config from "primate/config"; export default config({ modules: [ marko({ // add `.marko.html` to associated file extensions extensions: [".marko", ".marko.html"], }), ], }); ``` ## Resources - [Documentation] - [Language guide](https://markojs.com/docs/syntax/) - [Reactivity](https://markojs.com/docs/reactivity/) [Documentation]: https://markojs.com Source: https://primate.run/docs/frontend/navigation # Client navigation Use `primate/client` to navigate from browser code without a full page reload. It is useful for interactions that happen outside a link, such as a desktop notification click. ```ts import client from "primate/client"; await client.navigate("/projects/42"); ``` `client.navigate()` is available only in browser code. Call it from a hydrated frontend component, event handler, or other client-side module; do not call it while rendering on the server. ## Destinations Pass either a relative or absolute URL: ```ts await client.navigate("projects/42"); await client.navigate("/projects/42?tab=activity"); await client.navigate("https://app.example.com/projects/42"); ``` Relative URLs resolve from the current page. Same-origin destinations use Primate's SPA navigation: Primate fetches and updates the current frontend without a full document reload, updates browser history, and scrolls to a hash target when one is present. External destinations use normal browser navigation instead: ```ts await client.navigate("https://example.com"); ``` This lets application code use the same method without checking whether a URL belongs to the current app. ## Notification example For example, navigate when a notification is clicked: ```ts import client from "primate/client"; notification.addEventListener("click", () => { void client.navigate(`/projects/${project.id}`); }); ``` Use `void` when the caller does not need to wait for the navigation to finish. ## Refreshing the current route Use `refresh` to fetch fresh server data for the current pathname and query without changing the URL, adding a history entry, scrolling, or reloading the document: ```ts import client from "primate/client"; const refreshed = await client.refresh(); ``` The promise resolves to `true` after Primate applies the fresh route data and dispatches the navigation completion event. It resolves to `false` if the request fails, is superseded by another navigation, redirects to another URL, or does not return a successful Primate route response. In those cases, the current view, URL, and history remain unchanged. `refresh` is distinct from navigating to the current URL. A same-URL `client.navigate()` call keeps its existing no-op behavior. ## Links and events Primate automatically intercepts ordinary same-origin links after hydration, so you usually do not need to call `client.navigate()` from an anchor's click handler. For custom link handling, pass the click event as the optional second argument: ```ts link.addEventListener("click", event => { void client.navigate(link.href, event); }); ``` For same-origin URLs, the event is prevented and the SPA navigation runs. For external URLs, the event is left untouched so the browser performs its normal link navigation. ## Native forms After hydration, Primate also enhances ordinary same-origin forms that target the current window. GET submissions encode successful controls in the query string without sending a request body. As in a native submission, this replaces any query string written directly in the form action. Repeated controls and the button that initiated submission are preserved. Submit-button overrides such as `formaction`, `formmethod`, and `formenctype` are honored. Forms with another target, an external action, or an unsupported encoding remain under browser control. A handler that calls `preventDefault()` also opts that submission out of Primate's enhancement. Frontend page responses are rendered even when they use an error status such as 400 or 422, allowing a route to display validation errors without a full-page load. JSON API responses are not treated as frontend pages; use `fetch` when an application needs to consume an API payload directly. ## Navigation completion Use `client.onNavigation` when application-wide browser behavior must run after Primate has mounted or updated a destination view. At that point the listener observes both the destination URL and its DOM: ```ts import client from "primate/client"; const unsubscribe = client.onNavigation(({ source, url }) => { analytics.page(url, { source }); }); // Stop listening when the owning integration is disposed. unsubscribe(); ``` `source` is `"navigate"` for enhanced links and `client.navigate()`, `"form"` for enhanced form navigation, `"history"` for Back and Forward, or `"refresh"` for `client.refresh()`. A successful navigation invokes listeners exactly once. Failed, superseded, and full-document navigation does not invoke them. Hash-only and shallow navigation do not invoke listeners because they do not mount or update a destination view. Back and Forward between shallow states likewise do not invoke them. Prefer your frontend's component lifecycle hooks for behavior local to one component. ## Query strings and hashes Query strings and hashes are preserved: ```ts await client.navigate("/search?q=primate#results"); ``` Navigating to a different query string on the current route performs an SPA navigation. Navigating to a hash on the current route updates history and scrolls to the matching element. Same-origin route redirects preserve the complete destination, including its query string and fragment. Primate updates history with that destination and scrolls after the redirected view mounts. This applies to enhanced links and forms, including redirect chains. Cross-origin or opaque redirects are left to the browser. ## Shallow navigation Use `shallow` to update the query string without fetching, remounting the current view, or scrolling: ```ts await client.navigate("/search?q=primate", { shallow: true }); ``` The public request store updates immediately. Back and Forward update it again without fetching while traversal remains within the same rendered view. Use `replace` when the new URL should replace the current history entry: ```ts await client.navigate("/search?q=framework", { replace: true, shallow: true, }); ``` Shallow navigation applies only when the destination has the current pathname. A different pathname performs normal SPA navigation. See [Frontends](/docs/frontend) for frontend setup and the available hydrated frontend integrations. Source: https://primate.run/docs/frontend/react # React Primate runs [React][Documentation] with server-side rendering, hydration, client navigation, layouts, validation and i18n. ## Setup ### Install ```bash npm install @primate/react react react-dom ``` ### Configure ```ts import config from "primate/config"; import react from "@primate/react"; export default config({ modules: [ react(), ], }); ``` ## Components Create React JSX components in `views` using TypeScript or JavaScript. ```tsx // views/PostIndex.tsx interface Post { title: string; excerpt?: string; } interface Props { title: string; posts: Post[]; } export default function PostIndex({ title, posts }: Props) { return (

{title}

{posts.map((post, index) => (

{post.title}

{post.excerpt &&

{post.excerpt}

}
))}
); } ``` Serve the component from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { title: "First Post", excerpt: "Introduction to Primate with React" }, { title: "Second Post", excerpt: "Building reactive applications" }, ]; return response.view("PostIndex.tsx", { title: "Blog", posts }); }, }); ``` ## Props Props passed to `response.view` map directly to component props. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("User.tsx", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the component: ```tsx interface User { name: string; role: string; } interface Props { user: User; permissions: string[]; } export default function User({ user, permissions }: Props) { return (

{user.name}

Role: {user.role}

    {permissions.map((permission, index) => (
  • {permission}
  • ))}
); } ``` ## Request Import the `useRequest` hook from `app:react` to access the current request inside any component. The hook re-renders the component automatically on client-side navigation. ```tsx import { useRequest } from "app:react"; export default function Page() { const request = useRequest(); return

Current path: {request.url.pathname}

; } ``` The `useRequest` hook returns a `RequestPublic` object. | Property | Type | Description | | --------- | -------------- | ----------------------- | | `url` | `URL` | current request URL | | `query` | `Dict` | query string parameters | ## Reactivity with hooks React's hooks provide state management and side effects for interactive components. ```tsx import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); const doubled = count * 2; return (
{count}

Doubled: {doubled}

); } ``` ## Validation Use Primate's validated state wrapper to synchronize with backend routes. ```tsx import client from "@primate/react/client"; interface Props { id: string; counter: number; } export default function Counter({ id, counter: initial }: Props) { const counter = client.field(initial).post(`/counter?id=${id}`); return (

Counter Example

{counter.value}
{counter.error && (

{counter.error.message}

)}
); } ``` Add corresponding backend validation in the route: ```ts // routes/counter.ts import Counter from "@/stores/Counter"; import route from "primate/route"; import response from "primate/response"; import p from "pema"; await Counter.create(); export default route({ async get() { const counters = await Counter.find({}); const counter = counters.length === 0 ? await Counter.insert({ counter: 10 }) : counters[0]; return response.view("Counter.tsx", { id: counter.id, counter: counter.counter, }); }, async post(request) { const id = p.string.parse(request.query.get("id")); const body = p.loose.number.parse(await request.body.json()); await Counter.update(id, { set: { counter: body } }); return null; }, }); ``` The wrapper automatically tracks loading states, captures validation errors, and posts updates on state changes. ## Forms Use `client.form` from `@primate/react/client` to wire forms to backend routes with automatic field-level validation and error display. See [Client forms](/docs/frontend/forms) for route actions, dynamic paths, and guidance on choosing between forms and `fetch`. ```tsx // views/LoginForm.tsx import route from "@/routes/login"; import client from "@primate/react/client"; export default function LoginForm() { const form = client.form(route.post, { initial: { email: "", password: "" }, }); return (
{form.errors.length > 0 && (

{form.errors[0]}

)}
{form.field("email").error && (

{form.field("email").error}

)}
{form.field("password").error && (

{form.field("password").error}

)}
); } ``` Add the corresponding route: ```ts // routes/login.ts import route from "primate/route"; import response from "primate/response"; import p from "pema"; const LoginSchema = p({ email: p.string.email(), password: p.string.min(8), }); export default route({ get() { return response.view("LoginForm.tsx"); }, async post(request) { const body = LoginSchema.parse(await request.body.form()); // implement authentication logic return null; }, }); ``` Validation errors from the server are automatically surfaced per-field via `form.field(name).error`. The `form.submitting` flag disables the submit button while the request is in flight. ### Form API | Property | Type | Description | | ------------------ | --------------------- | ----------------------------------- | | `form.id` | `string` | Unique form ID for the `id` attr | | `form.submit` | `(event?) => Promise` | Submit handler for `onSubmit` | | `form.submitting` | `boolean` | True while the request is in flight | | `form.errors` | `string[]` | Form-level errors | | `form.field(name)` | `Field` | Access a named field | ### Field API | Property | Type | Description | | -------------- | -------------- | ------------------------------- | | `field.name` | `string` | Field name for the `name` attr | | `field.value` | `T` | Initial field value | | `field.error` | `string\|null` | First validation error or null | | `field.errors` | `string[]` | All validation errors for field | ## Layouts Create layout components that wrap your components using `children`. ```tsx // views/Layout.tsx import { ReactNode } from "react"; interface Props { children: ReactNode; brand?: string; } export default function Layout({ children, brand = "My App" }: Props) { return (
{children}
© 1996 {brand}
); } ``` Next, register the layout via a `+layout.ts` file: ```ts // routes/+layout.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Layout.tsx", { brand: "Primate React Demo" }); }, }); ``` Pages under this route subtree render inside the layout as `children`. ## Internationalization Create an i18n bridge file that adapts Primate's headless translator to React's reactivity model: ```ts // lib/i18n.ts import app from "@/config/app"; import i18n from "@primate/react/i18n"; export default i18n(app.i18n); ``` Import and use the bridged translator directly in views: ```tsx import t from "@/lib/i18n"; export default function Welcome() { return (

{t("welcome")}

{t("current_locale")}: {t.locale.get()}

); } ``` Primate's integration automatically subscribes to locale changes and triggers rerenders when switching languages. ## Head Tags Use Primate's `Head` component to manage document head elements. ```tsx import Head from "@primate/react/Head"; export default function About() { return (
About Us - Primate React Demo

About Us

Welcome to our Primate React demo application. This page demonstrates how to manage document head elements including the title and meta tags for better SEO and social media sharing.

); } ``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ------------------ | ---------------------------- | | extensions | `string[]` | `[".tsx", ".jsx"]` | Associated file extensions | | ssr | `boolean` | *production* | Enable server-side rendering | | csr | `boolean` | `true` | Enable client-side rendering | ### Example ```ts import react from "@primate/react"; import config from "primate/config"; export default config({ modules: [ react({ // add `.react.tsx` to associated file extensions extensions: [".tsx", ".jsx", ".react.tsx"], }), ], }); ``` ## Resources - [Documentation] - [Components guide](https://react.dev/learn/your-first-component) - [Reference](https://react.dev/reference/react) [Documentation]: https://react.dev Source: https://primate.run/docs/frontend/solid # Solid Primate runs [Solid][Documentation] with server-side rendering, hydration, client navigation, layouts, validation and i18n. ## Setup ### Install ```bash npm install @primate/solid solid-js ``` ### Configure ```ts import config from "primate/config"; import solid from "@primate/solid"; export default config({ modules: [ solid(), ], }); ``` ## Components Create Solid JSX components in `views`. ```tsx // views/PostIndex.tsx import { For } from "solid-js"; interface Post { title: string; excerpt?: string; } interface Props { title: string; posts: Post[]; } export default function PostIndex(props: Props) { return (

{props.title}

{(post) => (

{post.title}

{post.excerpt &&

{post.excerpt}

}
)}
); } ``` Serve the component from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { title: "First Post", excerpt: "Introduction to Primate with Solid" }, { title: "Second Post", excerpt: "Building reactive applications" }, ]; return response.view("PostIndex.tsx", { title: "Blog", posts }); }, }); ``` ## Props Props passed to `response.view` map directly to component props. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("User.tsx", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the component: ```tsx import { For } from "solid-js"; interface User { name: string; role: string; } interface Props { user: User; permissions: string[]; } export default function User(props: Props) { return (

{props.user.name}

Role: {props.user.role}

    {(permission) =>
  • {permission}
  • }
); } ``` ## Request Import the `request` signal from `app:solid` to access the current request inside any component. The signal updates automatically on client-side navigation. ```tsx import { request } from "app:solid"; export default function Page() { return

Current path: {request().url.pathname}

; } ``` The `request` signal exposes a `RequestPublic` object. | Property | Type | Description | | --------- | -------------- | ----------------------- | | `url` | `URL` | current request URL | | `query` | `Dict` | query string parameters | ## Reactivity with signals Solid's signals provide fine-grained reactivity for state management and computed values. ```tsx import { createSignal, createMemo } from "solid-js"; export default function Counter() { const [count, setCount] = createSignal(0); const doubled = createMemo(() => count() * 2); return (
{count()}

Doubled: {doubled()}

); } ``` ## Validation Use Primate's validated state wrapper to synchronize with backend routes. ```tsx import client from "@primate/solid/client"; interface Props { id: string; counter: number; } export default function Counter(props: Props) { const counter = client.field(props.counter).post(`/counter?id=${props.id}`); return (

Counter Example

{counter.value()}
{counter.error() &&

{counter.error()!.message}

}
); } ``` Add corresponding backend validation in the route: ```ts // routes/counter.ts import Counter from "@/stores/Counter"; import route from "primate/route"; import response from "primate/response"; import p from "pema"; await Counter.create(); export default route({ async get() { const counters = await Counter.find({}); const counter = counters.length === 0 ? await Counter.insert({ counter: 10 }) : counters[0]; return response.view("Counter.tsx", { id: counter.id, counter: counter.counter, }); }, async post(request) { const id = p.string.parse(request.query.get("id")); const body = p.loose.number.parse(await request.body.json()); await Counter.update(id, { set: { counter: body } }); return null; }, }); ``` The wrapper automatically tracks loading states, captures validation errors, and posts updates on state changes. ## Forms Use `client.form` from `@primate/solid/client` to wire forms to backend routes with automatic field-level validation and error display. See [Client forms](/docs/frontend/forms) for route actions, dynamic paths, and guidance on choosing between forms and `fetch`. ```tsx // views/LoginForm.tsx import route from "@/routes/login"; import client from "@primate/solid/client"; export default function LoginForm() { const form = client.form(route.post, { initial: { email: "", password: "" }, }); return (
{form.errors().length > 0 && (

{form.errors()[0]}

)}
{form.field("email").error() && (

{form.field("email").error()}

)}
{form.field("password").error() && (

{form.field("password").error()}

)}
); } ``` Add the corresponding route: ```ts // routes/login.ts import route from "primate/route"; import response from "primate/response"; import p from "pema"; const LoginSchema = p({ email: p.string.email(), password: p.string.min(8), }); export default route({ get() { return response.view("LoginForm.tsx"); }, async post(request) { const body = LoginSchema.parse(await request.body.form()); // implement authentication logic return null; }, }); ``` Validation errors from the server are automatically surfaced per-field via `form.field(name).error()`. The `form.submitting()` signal disables the submit button while the request is in flight. ### Form API | Property | Type | Description | | ------------------ | --------------------- | ----------------------------------- | | `form.id` | `string` | Unique form ID for the `id` attr | | `form.submit` | `(event?) => Promise` | Submit handler for `onSubmit` | | `form.submitting` | `() => boolean` | True while the request is in flight | | `form.errors` | `() => string[]` | Form-level errors | | `form.field(name)` | `Field` | Access a named field | ### Field API | Property | Type | Description | | --------------- | ----------------- | ------------------------------- | | `field.name` | `string` | Field name for the `name` attr | | `field.value` | `T` | Initial field value | | `field.error` | `() => string\|null` | First validation error or null | | `field.errors` | `() => string[]` | All validation errors for field | ## Layouts Create layout components that wrap your pages using `children`. ```tsx // views/Layout.tsx import { JSX } from "solid-js"; interface Props { children: JSX.Element; brand?: string; } export default function Layout(props: Props) { return (
{props.children}
© 1996 {props.brand}
); } ``` Next, register the layout via a `+layout.ts` file: ```ts // routes/+layout.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Layout.tsx", { brand: "Primate Solid Demo" }); }, }); ``` Pages under this route subtree render inside the layout as `children`. ## Internationalization Create an i18n bridge file that adapts Primate's headless translator to Solid's reactivity model: ```ts // lib/i18n.ts import app from "@/config/app"; import i18n from "@primate/solid/i18n"; export default i18n(app.i18n); ``` Import and use the bridged translator directly in views: ```tsx import t from "@/lib/i18n"; export default function Welcome() { return (

{t("welcome")}

{t("current_locale")}: {t.locale.get()}

); } ``` Primate's integration automatically subscribes to locale changes and triggers rerenders when switching languages. ## Head Tags Use Primate's `Head` component to manage document head elements. ```tsx import Head from "@primate/solid/Head"; export default function About() { return (
About Us - Primate Solid Demo

About Us

Welcome to our Primate Solid demo application. This page demonstrates how to manage document head elements including the title and meta tags.

); } ``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ------------------ | ---------------------------- | | extensions | `string[]` | `[".tsx", ".jsx"]` | Associated file extensions | | ssr | `boolean` | *production* | Enable server-side rendering | | csr | `boolean` | `true` | Enable client-side rendering | ### Example ```ts import solid from "@primate/solid"; import config from "primate/config"; export default config({ modules: [ solid({ // add `.solid.tsx` to associated file extensions extensions: [".tsx", ".jsx", ".solid.tsx"], }), ], }); ``` ## Resources - [Documentation] - [Tutorial](https://www.solidjs.com/tutorial) - [Signals](https://www.solidjs.com/docs/latest/api#createsignal) - [Reactivity](https://www.solidjs.com/docs/latest/api#reactivity) [Documentation]: https://www.solidjs.com Source: https://primate.run/docs/frontend/svelte # Svelte Primate runs [Svelte][Documentation] with server-side rendering, hydration, client navigation, layouts, validation and i18n. ## Setup ### Install ```bash npm install @primate/svelte svelte ``` ### Configure ```ts import config from "primate/config"; import svelte from "@primate/svelte"; export default config({ modules: [ svelte(), ], }); ``` ## Components Create Svelte components in `views`. ```svelte

{title}

{#each posts as post}

{post.title}

{#if post.excerpt}

{post.excerpt}

{/if}
{/each}
``` Serve the component from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { title: "First Post", excerpt: "Introduction to Primate with Svelte" }, { title: "Second Post", excerpt: "Building reactive applications" }, ]; return response.view("PostIndex.svelte", { title: "Blog", posts }); }, }); ``` ## Props Props passed to `response.view` map directly to component props. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("User.svelte", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the component: ```svelte

{user.name}

Role: {user.role}

    {#each permissions as permission}
  • {permission}
  • {/each}
``` ## Request Import the `request` store from `app:svelte` to access the current request inside any component. The store is reactive — it updates automatically on client-side navigation. ```svelte

Current path: {$request.url.pathname}

``` The `request` store exposes a `RequestPublic` object. | Property | Type | Description | | --------- | -------------- | ----------------------- | | `url` | `URL` | current request URL | | `query` | `Dict` | query string parameters | ## Reactivity Svelte 5 uses runes for fine-grained reactivity. ```svelte
{count}

Doubled: {doubled}

``` ## Validation Use Primate's validated state wrapper to synchronize with backend routes. ```svelte

Counter Example

{$counter.value}
{#if $counter.error}

{$counter.error.message}

{/if}
``` Add corresponding backend validation in the route: ```ts // routes/counter.ts import Counter from "@/stores/Counter"; import route from "primate/route"; import response from "primate/response"; import p from "pema"; await Counter.create(); export default route({ async get() { const counters = await Counter.find({}); const counter = counters.length === 0 ? await Counter.insert({ counter: 10 }) : counters[0]; return response.view("Counter.svelte", { id: counter.id, value: counter.counter }); }, async post(request) { const id = p.string.parse(request.query.get("id")); const body = p.loose.number.parse(await request.body.json()); await Counter.update(id, { set: { counter: body } }); return null; }, }); ``` The wrapper automatically tracks loading states, captures validation errors, and posts updates on state changes. ## Forms Use `client.form` from `@primate/svelte/client` to wire forms to backend routes with automatic field-level validation and error display. See [Client forms](/docs/frontend/forms) for route actions, dynamic paths, and guidance on choosing between forms and `fetch`. ```svelte
{#if $form.errors.length}

{$form.errors[0]}

{/if}
{#if $form.field("email").error}

{$form.field("email").error}

{/if}
{#if $form.field("password").error}

{$form.field("password").error}

{/if}
``` Add the corresponding route with server-side validation: ```ts // routes/login.ts import route from "primate/route"; import response from "primate/response"; import p from "pema"; const LoginSchema = p({ email: p.string.email(), password: p.string.min(8), }); export default route({ get() { return response.view("LoginForm.svelte"); }, async post(request) { const body = LoginSchema.parse(await request.body.form()); // implement authentication logic return null; }, }); ``` Validation errors from the server are automatically surfaced per-field via `$form.field(name).error`. The `$form.submitting` flag disables the submit button while the request is in flight. ### Form API | Property | Type | Description | | --------------------- | ----------------------- | ---------------------------------- | | `$form.id` | `string` | Unique form ID for the `id` attr | | `$form.submit` | `(event?) => Promise` | Submit handler for `onsubmit` | | `$form.submitting` | `boolean` | True while the request is in flight | | `$form.errors` | `string[]` | Form-level errors | | `$form.field(name)` | `Field` | Access a named field | ### Field API | Property | Type | Description | | --------------------- | -------------- | ---------------------------------- | | `field.name` | `string` | Field name for the `name` attr | | `field.value` | `T` | Initial field value | | `field.error` | `string\|null` | First validation error or null | | `field.errors` | `string[]` | All validation errors for field | ## Layouts Create layout components that wrap your pages using `{@render children()}`. Create a layout component: ```svelte
{@render children()}
© 1996 {brand}
``` Next, register the layout via a `+layout.ts` file: ```ts // routes/+layout.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Layout.svelte", { brand: "Primate Svelte Demo" }); }, }); ``` Pages under this route subtree render inside the layout's `{@render children()}`. ## Internationalization Create an i18n bridge file that adapts Primate's headless translator to Svelte's reactivity model: ```ts // lib/i18n.ts import app from "@/config/app"; import i18n from "@primate/svelte/i18n"; export default i18n(app.i18n); ``` Import and use the bridged translator directly in views: ```svelte

{$t("welcome")}

{$t("current_locale")}: {t.locale.get()}

``` Primate's integration automatically subscribes to locale changes and triggers rerenders when switching languages. ## Head Tags Use Svelte's `` to manage document head elements. ```svelte About Us - Primate Svelte Demo

About Us

Welcome to our Primate Svelte demo application. This page demonstrates how to manage document head elements including the title and meta tags for better SEO and social media sharing.

``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ------------- | ---------------------------- | | extensions | `string[]` | `[".svelte"]` | Associated file extensions | | ssr | `boolean` | *production* | Enable server-side rendering | | csr | `boolean` | `true` | Enable client-side rendering | ### Example ```ts import svelte from "@primate/svelte"; import config from "primate/config"; export default config({ modules: [ svelte({ // add `.component.svelte` to associated file extensions extensions: [".svelte", ".component.svelte"], }), ], }); ``` ## Resources - [Documentation] - [Tutorial](https://svelte.dev/tutorial) - [Runes](https://svelte.dev/docs/svelte/what-are-runes) - [Stores](https://svelte.dev/docs#run-time-svelte-store) [Documentation]: https://svelte.dev Source: https://primate.run/docs/frontend/voby # Voby Primate runs [Voby][Documentation] with server-side rendering. ## Setup ### Install ```bash npm install @primate/voby ``` ### Configure ```ts import config from "primate/config"; import voby from "@primate/voby"; export default config({ modules: [ voby(), ], }); ``` ## Components Create Voby JSX components in `views`. ```jsx // views/post-index.jsx export default ({ posts, title }) => { return ( <>

{title}

{posts.map(({ id, title }) => (

{title}

))} ); }; ``` Serve the component from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" }, ]; return response.view("post-index.jsx", { posts, title: "Blog" }); }, }); ``` ## Props Props passed to `response.view` are available as component parameters. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("user.jsx", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the component: ```jsx // views/user.jsx export default ({ user, permissions }) => { return (

{user.name}

Role: {user.role}

    {permissions.map((permission, i) => (
  • {permission}
  • ))}
); }; ``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ------------------ | ---------------------------- | | extensions | `string[]` | `[".tsx", ".jsx"]` | Associated file extensions | | ssr | `boolean` | *production* | Enable server-side rendering | | csr | `boolean` | `true` | Enable client-side rendering | ### Example ```ts import voby from "@primate/voby"; import config from "primate/config"; export default config({ modules: [ voby({ // add .voby.tsx to associated file extensions extensions: [".jsx", ".tsx", ".voby.tsx"], }), ], }); ``` ## Resources - [Documentation] [Documentation]: https://github.com/vobyjs/voby Source: https://primate.run/docs/frontend/vue # Vue Primate runs [Vue][Documentation] with server-side rendering, hydration, client navigation, layouts, validation and i18n. ## Setup ### Install ```bash npm install @primate/vue vue ``` ### Configure ```ts import config from "primate/config"; import vue from "@primate/vue"; export default config({ modules: [ vue(), ], }); ``` ## Components Create Vue SFC components in `views`. ```vue ``` Serve the component from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { title: "First Post", excerpt: "Introduction to Primate with Vue" }, { title: "Second Post", excerpt: "Building reactive applications" }, ]; return response.view("PostIndex.vue", { title: "Blog", posts }); }, }); ``` ## Props Props passed to `response.view` map directly to component props. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("User.vue", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the component: ```vue ``` ## Request Import the `useRequest` composable from `app:vue` to access the current request inside any component. The composable updates automatically on client-side navigation. ```vue ``` The `useRequest` composable returns a `RequestPublic` object. | Property | Type | Description | | --------- | -------------- | ----------------------- | | `url` | `URL` | current request URL | | `query` | `Dict` | query string parameters | ## Reactivity with Composition API Vue's Composition API provides reactive state management with `ref` and `computed`. ```vue ``` ## Validation Use Primate's validated state wrapper to synchronize with backend routes. ```vue ``` Add corresponding backend validation in the route: ```ts // routes/counter.ts import Counter from "@/stores/Counter"; import route from "primate/route"; import response from "primate/response"; import p from "pema"; await Counter.create(); export default route({ async get() { const counters = await Counter.find({}); const counter = counters.length === 0 ? await Counter.insert({ counter: 10 }) : counters[0]; return response.view("Counter.vue", { id: counter.id, counter: counter.counter, }); }, async post(request) { const id = p.string.parse(request.query.get("id")); const body = p.loose.number.parse(await request.body.json()); await Counter.update(id, { set: { counter: body } }); return null; }, }); ``` The wrapper automatically tracks loading states, captures validation errors, and posts updates on state changes. ## Forms Use `client.form` from `@primate/vue/client` to wire forms to backend routes with automatic field-level validation and error display. See [Client forms](/docs/frontend/forms) for route actions, dynamic paths, and guidance on choosing between forms and `fetch`. ```vue ``` Add the corresponding route: ```ts // routes/login.ts import route from "primate/route"; import response from "primate/response"; import p from "pema"; const LoginSchema = p({ email: p.string.email(), password: p.string.min(8), }); export default route({ get() { return response.view("LoginForm.vue"); }, async post(request) { const body = LoginSchema.parse(await request.body.form()); // implement authentication logic return null; }, }); ``` Validation errors from the server are automatically surfaced per-field via `form.field(name).error`. The `form.submitting` flag disables the submit button while the request is in flight. ### Form API | Property | Type | Description | | ------------------ | --------------------- | ----------------------------------- | | `form.id` | `string` | Unique form ID for the `id` attr | | `form.submit` | `(event?) => Promise` | Submit handler for `@submit` | | `form.submitting` | `boolean` | True while the request is in flight | | `form.errors` | `string[]` | Form-level errors | | `form.field(name)` | `Field` | Access a named field | ### Field API | Property | Type | Description | | -------------- | -------------- | ------------------------------- | | `field.name` | `string` | Field name for the `name` attr | | `field.value` | `T` | Initial field value | | `field.error` | `string\|null` | First validation error or null | | `field.errors` | `string[]` | All validation errors for field | ## Layouts Create layout components that wrap your pages using ``. ```vue ``` Next, register the layout via a `+layout.ts` file: ```ts // routes/+layout.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Layout.vue", { brand: "Primate Vue Demo" }); }, }); ``` Pages under this route subtree render inside the layout's ``. ## Internationalization Create an i18n bridge file that adapts Primate's headless translator to Vue's reactivity model: ```ts // lib/i18n.ts import app from "@/config/app"; import i18n from "@primate/vue/i18n"; export default i18n(app.i18n); ``` Import and use the bridged translator directly in views: ```vue ``` Primate's integration automatically subscribes to locale changes and triggers rerenders when switching languages. ## Head Tags Use Vue's `onMounted` to manage document head elements. ```vue ``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ---------- | ---------------------------- | | extensions | `string[]` | `[".vue"]` | Associated file extensions | | ssr | `boolean` | *production* | Enable server-side rendering | | csr | `boolean` | `true` | Enable client-side rendering | ### Example ```ts import vue from "@primate/vue"; import config from "primate/config"; export default config({ modules: [ vue({ // add `.component.vue` to associated file extensions extensions: [".vue", ".component.vue"], }), ], }); ``` ## Resources - [Documentation] - [Composition API](https://vuejs.org/guide/extras/composition-api-faq.html) - [Single File Components](https://vuejs.org/guide/scaling-up/sfc.html) - [Reactivity Fundamentals](https://vuejs.org/guide/essentials/reactivity-fundamentals.html) [Documentation]: https://vuejs.org Source: https://primate.run/docs/frontend/web-components # Web Components Primate runs [Web Components][Documentation] with client-side rendering. ## Setup ### Install ```bash npm install @primate/webc ``` ### Configure ```ts import config from "primate/config"; import webc from "@primate/webc"; export default config({ modules: [ webc(), ], }); ``` ## Components Create Web Components in `views` with Primate's Component class. ```html ``` Serve the component from a route: ```ts // routes/posts.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const posts = [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" }, ]; return response.view("post-index.webc", { posts }); }, }); ``` ## Props Props passed to `response.view` are available as `this.props` in the view. Pass props from a route: ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("user.webc", { user: { name: "John", role: "Developer" }, permissions: ["read", "write"], }); }, }); ``` Access the props in the view: ```html ``` ## Lifecycle Web Components support lifecycle methods for managing component state and behavior. ```html ``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ----------- | -------------------------- | | extensions | `string[]` | `[".webc"]` | Associated file extensions | ### Example ```ts import config from "primate/config"; import webc from "@primate/webc"; export default config({ modules: [ webc({ // add `.webc.html` to associated file extensions extensions: [".webc", ".webc.html"], }), ], }); ``` ## Resources - [Documentation] [Documentation]: https://www.webcomponents.org Source: https://primate.run/docs/frontend # Frontends Primate supports multiple frontend frameworks and template engines. Use one or mix them in the same app. ## Overview | Frontend | SSR | Hydration | CSR | Layouts | Head | Forms | i18n | | --------------------------- | --- | --------- | --- | ------- | ---- | ----- | ---- | | *Reactive frontends* | | | | | | | | | [Angular] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | [React] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | [Solid] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | [Svelte] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | [Vue] | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | [Marko] | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | *Component-based frontends* | | | | | | | | | [HTML] | ✓ | | | | | | | | [HTMX] | ✓ | | | | | | | | [Voby] | ✓ | | | | | | | | [Web Components] | | ✓ | | | | | | | *Template engines* | | | | | | | | | [Eta] | ✓ | | | | | | | | [Handlebars] | ✓ | | | | | | | | [Markdown] | ✓ | | | | | | | [Angular]: /docs/frontend/angular [React]: /docs/frontend/react [Solid]: /docs/frontend/solid [Svelte]: /docs/frontend/svelte [Vue]: /docs/frontend/vue [HTML]: /docs/frontend/html [HTMX]: /docs/frontend/htmx [Marko]: /docs/frontend/marko [Voby]: /docs/frontend/voby [Web Components]: /docs/frontend/web-components [Eta]: /docs/frontend/eta [Handlebars]: /docs/frontend/handlebars [Markdown]: /docs/frontend/markdown For programmatic browser navigation and current-route refresh, see [Client navigation](/docs/frontend/navigation). For mutation patterns, dynamic paths, validation errors, and troubleshooting, see [Client forms](/docs/frontend/forms). ## Features * **SSR** — render HTML on the server and send as the initial response * **Hydration** — attach client code to SSR output for interaction * **CSR** — client routing that swaps views without full page reloads * **Layouts** — recursive wrappers with slots * **Head** — programmatic control of `` (title, meta, scripts) * **Validation** — value/schema validation on client and server * **i18n** — component-level translations ## Development rendering For frontends with client rendering, Primate renders in the browser during development by default to keep live reload builds small and fast. Production uses server-side rendering by default. Pass `ssr: true` to the frontend module to also use server-side rendering during development, or `ssr: false` to disable it in every mode. If `csr: false` is set and `ssr` is omitted, Primate enables SSR in development so the page is not blank. Setting both `csr: false` and `ssr: false` explicitly disables both rendering paths and therefore produces an empty app container. Development keeps packages external for fast rebuilds. Dependencies used by server-rendered views must therefore expose JavaScript that the runtime can load directly. ## Reactive frontends *Angular*, *React*, *Solid*, *Svelte*, *Vue* State-driven UI with SSR, hydration, routing, layouts, validation, and i18n in one model. Suited for complex state and frequent interactions. ## Component-based frontends *HTML*, *HTMX*, *Marko*, *Voby*, *Web Components* Components without CSR. Works well for server-driven pages and simple dashboards with minimal client JS. ## Template engines *Handlebars*, *Eta*, *Markdown* Static rendering for content-focused pages and templates. Good for docs, blogs, emails, and CMS output. ## Quickstart Install a frontend module: ```bash npm install @primate/react react react-dom ``` Configure it: ```ts // config/app.ts import config from "primate/config"; import react from "@primate/react"; export default config({ modules: [react()], }); ``` Create a route page next to your route: ```tsx // routes/index.tsx export default function Welcome({ name }: { name: string }) { return

Hello, {name}!

; } ``` Render it from the route: ```ts // routes/index.ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ name: "World" }); }, }); ``` ## Mixing frontends Multiple frontends can coexist; each handles its own files. ```ts import config from "primate/config"; import react from "@primate/react"; import markdown from "@primate/markdown"; import htmx from "@primate/htmx"; export default config({ modules: [react(), markdown(), htmx()], }); ``` Hydrated links can navigate between reactive frontend modules without replacing the document. Primate disposes the outgoing adapter before mounting the destination adapter. For independently built client partitions, the destination assets and inactive styles are loaded before this handoff; see [Client partitions](/docs/configuration#client-partitions). ## Forms with multiple actions Prefer a separate form for each mutation. If controls must share a form, store the selected operation in an explicit hidden field: ```html
``` Update and validate the intent deliberately rather than relying only on the browser's selected submitter. See [Client forms](/docs/frontend/forms) for the full decision guide. Source: https://primate.run/docs/i18n # I18N Internationalization (i18n) localizes your app. * **No hooks** — call `t("key", params?)` anywhere in frontend views * **Typed keys / params** — inferred from your default locale file * **SSR-safe / reactive** across all major frontends * **Persistence** — cookie (default), localStorage, sessionStorage, or none * **Intl-backed** — numbers, dates, currency, lists, relative time, units ## Setup Enable i18n in your app config and import your catalogs. %%% TypeScript:config/app.ts, JavaScript:config/app.ts ```ts import config from "primate/config"; import de from "../locales/de-DE.ts"; import en from "../locales/en-US.ts"; export default config({ i18n: { defaultLocale: "en-US", locales: { "en-US": en, "de-DE": de, }, } }) ``` ```js import config from "primate/config"; import de from "../locales/de-DE.js"; import en from "../locales/en-US.js"; export default config({ i18n: { defaultLocale: "en-US", locales: { "en-US": en, "de-DE": de, }, } }) ``` %%% ## Locales Locales are TypeScript modules in the `locales` directory that export a catalog via `i18n.locale(...)`. | File | Purpose | | -------------- | ------------------------------------------------------- | | `.ts` | catalog **matching** `defaultLocale` (**drives types**) | | `.ts` | any other catalog mapping keys -> translated strings | ### Example !!! In this example, `defaultLocale` is `"en-US"`, so `en-US.ts` is the default catalog. If you set `defaultLocale: "de-DE"`, then `de-DE.ts` becomes the default and drives type inference. !!! %%% English:locales/en-US.ts, German:locales/de-DE.ts ```ts import i18n from "primate/i18n"; export default i18n.locale({ switch_language: "Switch language", english: "English", german: "German", all_posts: "All posts", counter: "Counter: {count:n}", title: "Title", greet_user: "Hello, {name}", added_n_items: "{n:n|one item added|{n} items added}", price_line: "Total: {amount:currency}", last_seen: "Last seen {secs:ago}", when: "It happened on {d:date}", distance: "Distance: {km:unit(kilometer)}", list_example: "Tags: {tags:list}", }); ``` ```ts import i18n from "primate/i18n"; export default i18n.locale({ switch_language: "Sprache wechseln", english: "Englisch", german: "Deutsch", all_posts: "Alle Beiträge", counter: "Zähler: {count:n}", title: "Titel", greet_user: "Hallo, {name}", added_n_items: "{n:n|ein Eintrag hinzugefügt|{n} Einträge hinzugefügt}", price_line: "Summe: {amount:currency}", last_seen: "Zuletzt gesehen {secs:ago}", when: "Es geschah am {d:date}", distance: "Entfernung: {km:unit(kilometer)}", list_example: "Stichwörter: {tags:list}", }); ``` %%% !!! Key style is up to you (flat, dotted, underscored, nested). Flat keys with underscores make object access ergonomic in TS. !!! ## Frontend Each frontend requires a bridge file that adapts Primate's headless translator to the frontend's reactivity model. Create it once per project — conventionally at `lib/i18n.ts`: ```ts // lib/i18n.ts import app from "@/config/app"; import i18n from "@primate/svelte/i18n"; export default i18n(app.i18n); ``` The bridge import follows the same pattern for every frontend — `@primate/FRONTEND/i18n`. Views import `t` from wherever you place this file. Each frontend adapter exposes a translator `t` and a locale setter: | Frontend | Translator | Locale getter | Locale setter | Notes | | -------- | ---------- | ---------------- | ----------------- | ---------------------------------- | | React | `t` | `t.locale.get()` | `t.locale.set(l)` | Call `t()` anywhere | | Angular | `t` | `t.locale.get()` | `t.locale.set(l)` | Injectable or import as needed | | Vue | `t` | `t.locale.get()` | `t.locale.set(l)` | Works in setup/options | | Svelte | `$t` | `t.locale.get()` | `t.locale.set(l)` | `t` is a store; use `$t` in markup | | Solid | `t` | `t.locale.get()` | `t.locale.set(l)` | Signal-like; `t.subscribe()` | | Marko | `t` | `t.locale.get()` | `t.locale.set(l)` | | ### React ```tsx import t from "@/lib/i18n"; export function Profile({ user }: { user: { name: string; since: number } }) { return (

{t("profile_title")}

{t("greeting", { name: user.name })}

); } ``` ### Svelte ```svelte

{$t("dashboard_title")}

{$t("notifications", { count: 3 })}

``` ### Vue ```vue ``` ## Backend In route handlers, use `app.i18n` from the app facade. The locale is automatically resolved from the user's request cookie. ```ts // routes/api/greeting.ts import app from "@/config/app"; import route from "primate/route"; export default route({ get() { return { message: app.i18n("greeting", { name: "World" }) }; }, }); ``` A German user visiting `/api/greeting` receives `{ message: "Hallo, Welt!" }`. ### Translating in a specific locale Use `app.i18n.with(locale)` to get a translator bound to a specific locale, ignoring the active request locale. This is useful for bots, queues, or any context where the locale comes from application logic rather than a cookie. ```ts // routes/api/telegram.ts import app from "@/config/app"; import route from "primate/route"; export default route({ post(request) { const { language_code } = await request.body.json(); const t = app.i18n.with(language_code); return { message: t("greeting", { name: "World" }) }; }, }); ``` ### Limitations - `app.i18n` is read-only on the server — locale switching is client-only. - `app.i18n.with` throws if the locale is not in your catalogs. ### Use cases - **Localized API responses** — return translated error messages or labels - **Server-rendered content** — generate locale-aware HTML without client JS - **Email templates** — use `app.i18n()` when building notification emails ## Message syntax Messages are simple strings with `{placeholders}`. Each placeholder can specify a **format spec** after a colon. | Spec | Meaning | Param type | Example | | ----------------------- | ------------- | ---------------- | -------------- | | *(no spec)* | string | `string` | `{name}` | | `n` or `number` | numbers | `number` | `{count:n}` | | `d` or `date` | date | `Date \| number` | `{created:d}` | | `c` or `currency` | currency | `number` | `{total:c}` | | `o` or `ordinal` | ordinals | `number` | `{rank:o}` | | `a` or `ago` | relative time | `number` | `{delta:a}` | | `l` or `list` | item list | `string[]` | `{tags:l}` | | `u()` or `unit()` | units | `number` | `{size:u(MB)}` | ### Plural selection `{n:n|...}` optionally takes **2 or 3 choices**: * `n|one|other` — uses the locale's plural rules * `n|zero|one|other` — adds an explicit zero branch Inside choices you can **backreference** the formatted number using the same key. ```ts // en: "You have 1 item" / "You have 3 items" "You have {count:n|{count} item|{count} items}"; // de: "Du hast keine Artikel" / "Du hast 1 Artikel" / "Du hast 7 Artikel" "Du hast {num:n|keine Artikel|{num} Artikel|{num} Artikel}"; ``` ### Units `u(...)` accepts many aliases mapped to Intl units (length, area, volume, mass, temperature, speed, time, digital, energy, power, pressure, angle, frequency, concentration, electric, force, luminous). Examples: | Alias | Normalized Intl unit | | ------------------ | ----------------------------- | | `km/h` `kph` `kmh` | `kilometer-per-hour` | | `MB` `mb` | `megabyte`/`megabit` | | `°C` `celsius` | `celsius` | | `kWh` | `kilowatt-hour` | | `psi` | `pound-force-per-square-inch` | ## Locale switching Use `t.locale.set(locale)` to switch on the client. The active locale is reactive; subscribers are notified and the UI updates automatically. ```ts // read current const current = t.locale.get(); // switch and persist t.locale.set("de-DE"); ``` ### Persistence | Mode | Behavior | | -------------------- | ------------------------------------------- | | `"cookie"` (default) | Sends a `fetch("/")` and persists in cookie | | `"localStorage"` | Writes `__primate_locale` | | `"sessionStorage"` | Writes `__primate_locale` | | `false` | No persistence | Persistence only runs in the browser. `t.loading` is `true` while a cookie persistence request is inflight. ## Type inference Keys and parameter types are inferred from the **default** catalog: ```ts // locales/en-US.ts import i18n from "primate/i18n"; export default i18n.locale({ greeting: "Hello {name}", added: "Added {n:n|{n} item|{n} items}", created_at: "Created on {date:d}", }); ``` ```ts t("greeting", { name: "Ada" }); // ok t("added", { n: 3 }); // ok t("created_at", { date: Date.now() }); // ok (number as epoch) // t("greeting", {}); // TS error: missing "name" // t("created_at", { date: "2024-01-01" });// TS error: wrong type ``` ## FAQ * **Where do the plural categories come from?** `Intl.PluralRules` per active locale. When you pass 2 options, categories collapse to `one` vs `other`. * **Can I lazy-load catalogs?** Yes — provide `config.locales` with modules you control (the i18n core doesn't mandate how you import them). * **What if I mistype a key at runtime?** An unknown key returns the key name as a string. At compile time, TypeScript catches it in code that sees the inferred types. Source: https://primate.run/docs/index # What is Primate? Primate is **the universal web framework** for building full-stack applications. It doesn't tie you to a single stack — it lets you freely combine frontends, backends, databases, and runtimes into the mix that works best for you. ## Universal framework %%% React:views/Hello.tsx, Angular:views/Hello.component.ts, Vue:views/Hello.vue, Svelte:views/Hello.svelte, Solid:views/Hello.tsx ```tsx export default function Hello() { return

Hello from React!

; } ``` ```ts import { Component } from "@angular/core"; @Component({ template: "

Hello from Angular

", }) export default class Hello { } ``` ```vue ``` ```svelte

Hello from Svelte!

``` ```tsx export default function Hello() { return

Hello from Solid!

; } ``` %%% Web development today is fragmented. For almost every frontend framework, there's a corresponding *meta-framework*: React has Next, Vue has Nuxt, Svelte has SvelteKit. Angular tries to be both. Once you commit, you're locked in — backend code built for one meta-framework won't easily move to another. Primate avoids this trap. You can use your favorite frontend with a backend that works the same way across all of them. You can even combine different frontends in one project, or migrate gradually without rewriting the whole application. ## The power of WebAssembly %%% TypeScript:routes/index.ts, JavaScript:routes/index.js, Go:routes/index.go, Python:routes/index.py, Ruby:routes/index.rb ```ts import route from "primate/route"; export default route({ get() { return "Hello from TypeScript!"; }, }); ``` ```js import route from "primate/route"; export default route({ get() { return "Hello from JavaScript!"; }, }); ``` ```go package main import "github.com/primate-run/go/route" var _ = route.Get(func(_ route.Request) any { return "Hello from Go!" }) ``` ```py from primate import Route @Route.get def get(request): return "Hello from Python!" ``` ```rb require 'primate/route' Route.get do |request| "Hello from Ruby!" end ``` %%% On the backend, Primate offers the same flexibility — thanks to WebAssembly. You can write backend code in TypeScript, JavaScript, or other languages that compile to WebAssembly (Wasm). At runtime, Primate runs the compiled binary, not plain JS source. This lets you bring your language of choice — Go, Python and others — into a project while keeping full access to modern frontends. You're not forced to use JavaScript simply because your frontend does. ## Runtime agnostic %%% Node, Deno, Bun ```sh $ npx primate ``` ```sh $ deno run -A npm:primate ``` ```sh $ bunx --bun primate ``` %%% Primate runs natively on Node, Deno, and Bun. It doesn't rely on generic compatibility layers — it uses each runtime's native execution paths, through Primate's own compatibility layer. Because of its design, Primate is forward-compatible: code you write today will work on future runtimes as they emerge. ## Batteries included Primate extends beyond core HTTP handling into common full-stack needs. It ships with official packages under the `@primate` namespace. These extend the core with common capabilities: * [Databases and ORMs](/docs/stores) * [Events](/docs/events) * [Session management](/docs/sessions) * [Internationalization (i18n)](/docs/i18n) * [Native app builds (desktop)](/docs/target/native) Everything you need to build real apps is supported out of the box. ## How Primate compares | Feature | Meta-frameworks (Next, Nuxt, SvelteKit) | Primate | | ---------------| ----------------------------------------| --------------------------------| | Frontend | Tied to one (React, Vue, Svelte) | Any frontend, mix & match | | Backend | JavaScript / TypeScript only | JS/TS plus Wasm backends | | Runtime | Node, others (emulated) | Node, Deno, Bun — native paths | | Vendor lock-in | High | None | ## Why it matters Frameworks shift. Runtimes change. Primate stays stable through that churn. Instead of tying your application to a single ecosystem, it gives you a foundation that outlasts frontend fads and runtime shifts — so your stack evolves on your terms. Source: https://primate.run/docs/modules # Modules Modules are the extension mechanism for Primate. They let you hook into the application lifecycle — at build time, at startup, and on every request — to add capabilities that are not part of the core framework. All official `@primate/*` packages (frontends, databases, backends) are themselves modules, and you can write your own using exactly the same API. ## The `Module` interface A module is a plain object with two properties. ```ts import type { Module } from "primate"; const myModule: Module = { name: "my-module", setup(hooks) { // register lifecycle hooks here }, }; ``` | Property | Type | Description | | -------- | ---- | ----------- | | `name` | `string` | unique identifier used in logs and error messages | | `setup` | `(hooks: Setup) => void` | called once at startup; use it to register hooks | In practice, modules are usually exported as factory functions so they can accept configuration: ```ts import type { Module } from "primate"; type Options = { verbose?: boolean; }; export default (options: Options = {}): Module => ({ name: "my-module", setup(hooks) { // use options here }, }); ``` Register the module in `config/app.ts`: ```ts import config from "primate/config"; import myModule from "./my-module.ts"; export default config({ modules: [myModule({ verbose: true })], }); ``` ## Lifecycle hooks The `setup` function receives a `Setup` object that exposes lifecycle hooks. Each hook registers a callback that Primate calls at the corresponding point in the application lifecycle. | Hook | When it runs | Typical use | | ---- | ------------ | ----------- | | [`onInit`](#oninit) | Before anything else | validate config, set up external connections | | [`onBuild`](#onbuild) | During the build step | register esbuild plugins, precompute assets | | [`onServe`](#onserve) | When the HTTP server starts | capture server properties like `secure` | | [`onLog`](#onlog) | On every log call | capture or forward log entries — send to an external service, write to a file | | [`onError`](#onerror) | On an unexpected request failure | record incidents and attach correlation IDs | | [`onHandle`](#onhandle) | On every incoming request | middleware — auth, headers, request context | | [`onRoute`](#onroute) | After routing, before the handler | per-route pre/post processing | ### `onInit` Called once during startup, before any other lifecycle phase. Use it to validate options, establish database connections, or perform one-off initialisation that the rest of the module depends on. ```ts setup({ onInit }) { onInit(async app => { // app.path gives you access to project directories const config = await app.path.config.join("my-module.json").json(); // throw here to abort startup on misconfiguration }); } ``` ### `onBuild` Called during the build step. Use it to register esbuild plugins, precompute static assets, or write files into the build output. ```ts setup({ onBuild }) { onBuild(async app => { // register a custom esbuild plugin for the client bundle app.plugin("client", { name: "my-loader", setup(build) { build.onLoad({ filter: /\.txt$/ }, async args => ({ contents: await fs.readFile(args.path, "utf8"), loader: "text", })); }, }); // precompute a file into the run directory const data = await computeSomething(); await app.runpath("my-module-data.json").writeJSON(data); }); } ``` ### `onServe` Called once when the HTTP server starts, after the build is complete. Use it to capture runtime properties of the server, such as whether it is running over HTTPS. ```ts setup({ onServe }) { let secure = false; onServe(app => { secure = app.secure; }); } ``` ### `onLog` Called every time Primate emits a log entry, regardless of the configured log level. Use it to forward log output to an external service, write structured logs to a file, or feed entries into a monitoring pipeline. ```ts setup({ onLog }) { onLog(entry => { // entry.level — "error" | "warn" | "info" | "trace" // entry.message — message text externalService.log(entry.level, entry.message); }); } ``` The callback is synchronous and receives a `LogEntry` object. | Property | Type | Description | | --------- | ---- | ----------- | | `level` | `"error" \| "warn" \| "info" \| "trace"` | severity of the entry | | `message` | `string` | message text | !!! `onLog` fires for **every** log entry, even those below the application's configured log level. If you are writing to a noisy sink, filter by `level` inside the callback. !!! ### `onError` Called when an unexpected failure would produce a 500 response. The hook is awaited before Primate invokes the error route and receives the original thrown value, the request facade, the matched route template when known, and the request-processing phase. ```ts setup({ onError }) { onError(async ({ frames, phase, request, route }) => { const incident = await report({ frames, phase, route }); request.set("incident", incident.id); }); } ``` | Property | Type | Description | | -------- | ---- | ----------- | | `cause` | `unknown` | original thrown value | | `frames` | `readonly RequestErrorFrame[]` | application source frames with `file`, `line`, `column`, and an optional `function` | | `request` | `RequestFacade` | request associated with the failure | | `route` | `string \| undefined` | matched route template, never a concrete pathname | | `phase` | `"match" \| "handler" \| "layout" \| "render" \| "error-handler" \| "handle"` | processing phase that failed | The hook remains inside request context. Values attached with `request.set()` are therefore available to the application's `+error` route, which can show an opaque incident ID without exposing the cause. Each error observer runs in registration order. If one observer fails, Primate logs that failure and continues with the remaining observers. Observer return values cannot change the response, and observer failures are not reported back through `onError`. Production builds emit a linked private `build/server.js.map` without embedded source content. Primate uses it to map minified stack positions before invoking `onError`. Frame paths are relative to the application root, so they remain stable between deployments and do not expose build-machine paths. Server maps are runtime artifacts and are not included in public client assets or served by Primate. !!! Treat `cause` and the request as sensitive. Do not send exception messages, headers, bodies, cookies, query values, or path values to an incident service unless your application has explicitly classified them as safe. !!! ### `onHandle` Called on every incoming request, before routing. This is Primate's middleware hook. Return a `Response` to short-circuit the request; call `next(request)` to continue. ```ts setup({ onHandle }) { onHandle((request, next) => { const token = request.headers.try("Authorization"); if (token === undefined) { return new Response("Unauthorized", { status: 401 }); } // pass a modified request downstream using request context return next(request.set("auth.token", token)); }); } ``` The callback receives the same `RequestFacade` available in route handlers. Attach derived values (authenticated user, feature flags, locale) with `request.set()` so route handlers can read them with `request.get()`. !!! `onHandle` runs for every request, including static assets. Keep the callback fast. !!! ### `onRoute` Called after routing resolves but before the route handler runs. Useful for per-route pre- or post-processing. ```ts setup({ onRoute }) { onRoute((request, next) => { // add a header to every response the app produces return next(request).then(response => { response.headers.set("X-Powered-By", "my-module"); return response; }); }); } ``` ## A complete example Here is a minimal logging module that measures response time and prints it to the console. ```ts import type { Module } from "primate"; export default (): Module => ({ name: "request-logger", setup({ onHandle }) { onHandle(async (request, next) => { const start = Date.now(); const response = await next(request); const ms = Date.now() - start; console.log(`${request.method} ${request.url.pathname} ${response.status} ${ms}ms`); return response; }); }, }); ``` Register it in `config/app.ts`: ```ts import config from "primate/config"; import requestLogger from "./request-logger.ts"; export default config({ modules: [requestLogger()], }); ``` ## Sharing state between hooks Because a module factory is a regular function, any variable declared inside it is shared across all hooks for the lifetime of the application. This is the standard way to pass data between, say, `onServe` and `onHandle`. ```ts export default (): Module => { // shared across all hooks let apiKey = ""; return { name: "my-module", setup({ onInit, onHandle }) { onInit(async app => { apiKey = await loadApiKey(); }); onHandle((request, next) => { return next(request.set("apiKey", apiKey)); }); }, }; }; ``` Source: https://primate.run/docs/platform # Platforms Primate runs on **Node**, **Bun**, and **Deno** via `rcompat`. It avoids runtime-specific APIs and abstracts differences via `rcompat`. You build and run with your chosen runtime, and target **Web** or **Native**. ## Why `rcompat` Using `rcompat` keeps apps portable and future-proof: * No vendor lock-in to a single runtime * One codebase, three runtimes * Unified stable interfaces insulate you from runtime churn * Matches Primate's stance elsewhere (frontend, backend, database agnostic) ## Run ### Dev ```bash # Node npx primate # Bun bunx --bun primate # Deno deno run -A npm:primate ``` ### Build ```bash # Node npx primate build # Bun bunx --bun primate build # Deno deno run -A npm:primate build ``` The command exits nonzero when compilation, migration artifact generation, or another build step fails, so CI and deployment scripts can stop immediately. ### Serve a Build ```bash # Node npx primate serve # Bun bunx --bun primate serve # Deno deno run -A npm:primate serve ``` ## Unified APIs with `rcompat` Prefer `rcompat` packages over runtime-specific APIs. ### Filesystem ```ts import fs from "@rcompat/fs"; const base = fs.ref("static"); const file = base.join("logo.svg"); ``` ### HTTP Status ```ts import Status from "@rcompat/http/Status"; return new Response("ok", { status: Status.OK }); ``` ### MIME Types ```ts import mime from "@rcompat/mime/text/plain"; import resolve from "@rcompat/mime/resolve"; const known = mime; // "text/plain" const byExt = resolve(".svg"); // "image/svg+xml" ``` ## Runtime Support Primate aims for complete interoperability across various runtimes and targets. In the table below, some cells are left blank where a platform has not yet been wired up. ### Build Targets | Target | Node | Deno | Bun | | --------------------- | ---- | ---- | --- | | Web | ✓ | ✓ | ✓ | | Desktop (linux-x64) | | | ✓ | | Desktop (darwin-x64) | | | | | Desktop (darwin-arm64)| | | | | Desktop (windows-x64) | | | ✓ | ### Backends | Backend | Node | Deno | Bun (web) | Bun (native) | | ------------ | ---- | ---- | --------- | ------------ | | [Go] | ✓ | ✓ | | | | [Python] | ✓ | ✓ | | | | [Ruby] | ✓ | | | | | [TypeScript] | ✓ | ✓ | ✓ | ✓ | ### Databases | Database | Node | Deno | Bun (web) | Bun (native) | | ------------ | ---- | ---- | --------- | ------------ | | [MongoDB] | ✓ | ✓ | ✓ | | | [MySQL] | ✓ | ✓ | ✓ | | | [PostgreSQL] | ✓ | ✓ | ✓ | | | [SQLite] | ✓ | | ✓ | ✓ | [Go]: /docs/backend/go [Python]: /docs/backend/python [Ruby]: /docs/backend/ruby [TypeScript]: /docs/backend/typescript [MongoDB]: /docs/database/mongodb [MySQL]: /docs/database/mysql [PostgreSQL]: /docs/database/postgresql [SQLite]: /docs/database/sqlite Source: https://primate.run/docs/project-structure # Project structure Primate is convention over configuration. This page lists common directories and files used in a Primate app. ## Directories | Directory | Purpose | | ----------------------------- | ---------------------------------------------- | | `build` | build artefacts — add to `.gitignore` | | `client` | client entrypoints — CSS, JS, fonts | | [config](/docs/configuration) | configuration files | | [locales](/docs/i18n#locales) | I18N locales | | `node_modules` | install artefacts — add to `.gitignore` | | `templates` | app and error HTML templates | | [routes](/docs/routing) | filesystem-based routes | | `static` | static assets served as-is — images, downloads | | [stores](/docs/stores) | data stores | | [views](/docs/views) | frontend views | ## Files | File | Purpose | | ------------------- | ---------------------------------------------------- | | `package.json` | dependency management | | `.gitignore` | add `build`, `node_modules` and any other transients | | `tsconfig.json` | extend `primate/tsconfig` | | `package-lock.json` | lock file if using npm | | `pnpm-lock.yaml` | lock file if using pnpm | | `bun.lockb` | lock file if using Bun | | `yarn.lock` | lock file if using Yarn | ## Config files Reside inside `config`. May be authored in Javascript or TypeScript. | File | Purpose | | ------------------------------------------------- | ------------------------- | | [app.ts](/docs/configuration#app-ts) | app options | | [session.ts](/docs/configuration#session-ts) | session shape and options | | [db/\*.ts](/docs/configuration#db-ts) | database options | ## Route files Files inside the `routes` directory registered to a [backend](/docs/backend) based on their extension. You may use different backends in your app for different routes, but every route must be uniquely handled by one backend. | File | Purpose | | -------------------------------------- | ------------------------------------------------------------------------------ | | `index.ts` | matches `/` | | `user.ts` | matches `/user` | | [user/\[name\].ts] | matches `/user/*` — `*` is anything _except_ a `/` | | [user/\[\[name\]\].ts] | matches `/user` _and_ `/user/*` — `*` is anything _except_ a `/` | | [user/\[...name\].ts] | matches `/user/*` — `*` is anything _including_ zero or more `/` | | [user/\[\[...name\]\].ts] | matches `/user` _and_ `/user/*` — `*` is anything _including_ zero or more `/` | | [+layout.ts](/docs/routing#layouts) | route layouts for routes in same directory and below | | [+hook.ts](/docs/routing#hook) | route hook for routes in same directory and below | | [+error.ts](/docs/routing#error-files) | route error file for routes in same directory | [user/\[name\].ts]: /docs/routing#dynamic-routes [user/\[\[name\]\].ts]: /docs/routing#optional-routes [user/\[...name\].ts]: /docs/routing#rest-routes [user/\[\[...name\]\].ts]: /docs/routing#optional-rest-routes ## View files Files inside the `views` directory registered to a [frontend](/docs/frontend) based on their full extensions. You may use different frontends in your app for different views, but every view must be uniquely handled by one frontend. | File | Purpose | | ---------------- | ----------------------------------------------- | | `*.jsx` | [React], [Solid] or [Voby] views | | `*.tsx` | typed [React], [Solid] or [Voby] views | | `*.svelte` | [Svelte](/docs/frontend/svelte) views | | `*.vue` | [Vue](/docs/frontend/vue) views | | `*.component.ts` | [Angular](/docs/frontend/angular) views | | `*.marko` | [Marko](/docs/frontend/marko) views | | `*.eta` | [Eta](/docs/frontend/eta) views | | `*.html` | [HTML](/docs/frontend/html) views | | `*.htmx` | [HTMX](/docs/frontend/htmx) views | | `*.hbs` | [Handlebars](/docs/frontend/handlebars) views | | `*.webc` | [Web components](/docs/frontend/web-components) | [React]: /docs/frontend/react [Solid]: /docs/frontend/solid [Voby]: /docs/frontend/voby ## Client files Files inside the `client` directory are bundled by Primate and made available as named placeholders in `templates/app.html` via the [`entrypoints`](/docs/configuration#entrypoints) config option. Any file type supported by your active frontend modules works here — if you have `@primate/svelte` configured, you can point an entrypoint at a `.svelte` file and it will be bundled and injected just like CSS or plain TypeScript. ## Store files Files inside the `stores` directory, representing [data stores](/docs/stores). May be authored in JavaScript or TypeScript. | File | Purpose | | --------- | ---------------------------------------------------------- | | `User.ts` | ORM for `user` table (import with `@/stores/User` in routes) | | `Post.ts` | ORM for `post` table (import with `@/stores/Post` in routes) | ## Locale files Files inside the `locales` directory, for [I18N](/docs/i18n). | File | Purpose | | ---------- | ----------------------------------- | | `en-US.ts` | locale for English (United States) | | `en-UK.ts` | locale for English (United Kingdom) | | `de-DE.ts` | locale for German (Germany) | Source: https://primate.run/docs/quickstart # Quickstart The easiest way to get started with Primate is to run it. %%% Node, Deno, Bun ```sh $ npx primate ``` ```sh $ deno run -A npm:primate ``` ```sh $ bunx --bun primate ``` %%% This boots an app from the current directory and serves any _route_ files under `routes`. Let's create one: %%% TypeScript:routes/index.ts, JavaScript:routes/index.js ```ts import route from "primate/route"; // handle GET requests at this route export default route({ get() { return "Hello, world!"; }, }); ``` ```js import route from "primate/route"; // handle GET requests at this route export default route({ get() { return "Hello, world!"; }, }); ``` %%% Your app now greets you at http://localhost:6161. !!! Requests to `/` are handled by an `index` file. Primate uses [filesystem-based routing](/docs/routing). !!! ## Create config To customize your app, create a `config` directory and a config file: %%% TypeScript:config/app.ts, JavaScript:config/app.js ```ts import config from "primate/config"; export default config({ // config comes here }); ``` ```js import config from "primate/config"; export default config({ // config comes here }); ``` %%% ## Add frontend Install the Primate package for your frontend, plus the frontend itself: %%% React, Angular, Vue, Svelte, Solid ```sh $ npm install @primate/react react react-dom ``` ```sh $ npm install @primate/angular @angular/core @angular/common ``` ```sh $ npm install @primate/vue vue ``` ```sh $ npm install @primate/svelte svelte ``` ```sh $ npm install @primate/solid solid-js babel-preset-solid ``` %%% !!! All `@primate/*` packages are officially supported and versioned with Primate. !!! Add the frontend to your config: %%% React:config/app.ts, Angular:config/app.ts, Vue:config/app.ts, Svelte:config/app.ts, Solid:config/app.ts ```ts import react from "@primate/react"; import config from "primate/config"; export default config({ modules: [ react(), ], }); ``` ```ts import angular from "@primate/angular"; import config from "primate/config"; export default config({ modules: [ angular(), ], }); ``` ```ts import vue from "@primate/vue"; import config from "primate/config"; export default config({ modules: [ vue(), ], }); ``` ```ts import svelte from "@primate/svelte"; import config from "primate/config"; export default config({ modules: [ svelte(), ], }); ``` ```ts import solid from "@primate/solid"; import config from "primate/config"; export default config({ modules: [ solid(), ], }); ``` %%% Then create a route page next to your route — here is a simple counter: %%% React:routes/index.jsx, Angular:routes/index.component.ts, Vue:routes/index.vue, Svelte:routes/index.svelte, Solid:routes/index.jsx ```jsx import { useState } from "react"; export default function Counter(props) { const [counter, setCounter] = useState(props.start); return (

Counter Example

{counter}
); } ``` ```ts import { CommonModule } from "@angular/common"; import type { OnInit } from "@angular/core"; import { Component, Input } from "@angular/core"; @Component({ imports: [CommonModule], template: `

Counter Example

{{ count }}
`, }) export default class CounterComponent implements OnInit { @Input() start = 0; counter = 0; ngOnInit() { this.counter = this.start; } increment() { this.counter++; } decrement() { this.counter--; } } ``` ```vue ``` ```svelte

Counter Example

{counter}
``` ```jsx import { createSignal } from "solid-js"; export default function Counter(props) { const [counter, setCounter] = createSignal(props.start); return (

Counter Example

{counter()}
); } ``` %%% !!! Primate supports [many frontends](/docs/frontend); the quickstart only shows a few. !!! Serve the route page with `response.page`: %%% React:routes/index.ts, Angular:routes/index.ts, Vue:routes/index.ts, Svelte:routes/index.ts, Solid:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ start: 10 }); }, }); ``` ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ start: 10 }); }, }); ``` ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ start: 10 }); }, }); ``` ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ start: 10 }); }, }); ``` ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ start: 10 }); }, }); ``` %%% ## Switch backend By default, routes run in TypeScript/JavaScript. To add another backend, install its package: %%% Go, Python, Ruby ```sh $ npm install @primate/go ``` ```sh $ npm install @primate/python ``` ```sh $ npm install @primate/ruby ``` %%% !!! For some backends (e.g. Go), you'll need a compiler to produce Wasm. See the backend docs for setup details. !!! Then add it to your config: %%% Go:config/app.ts, Python:config/app.ts, Ruby:config/app.ts ```ts import go from "@primate/go"; import config from "primate/config"; export default config({ modules: [ go(), ], }); ``` ```ts import python from "@primate/python"; import config from "primate/config"; export default config({ modules: [ python(), ], }); ``` ```ts import ruby from "@primate/ruby"; import config from "primate/config"; export default config({ modules: [ ruby(), ], }); ``` %%% And create a route: %%% TypeScript:routes/index.ts, JavaScript:routes/index.js, Go:routes/index.go, Python:routes/index.py, Ruby:routes/index.rb ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ start: 10 }); }, }) ``` ```js import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ start: 10 }); }, }); ``` ```go package main import ( "github.com/primate-run/go/core" "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return response.Page(core.Dict{"start": 10}) }) ``` ```py from primate import Route, Response @Route.get def get(request): return Response.page({"start": 10}) ``` ```rb require 'primate/route' Route.get do |request| Primate.page({ :start => 10 }) end ``` %%% !!! Backends and frontends are fully interchangeable — combine them freely. !!! ## Add database To persist data across reloads, install a database package plus `pema` for validation: %%% Oracle, SQLite, PostgreSQL, MySQL, MongoDB ```sh $ npm install pema @primate/oracledb ``` ```sh $ npm install pema @primate/sqlite ``` ```sh $ npm install pema @primate/postgresql ``` ```sh $ npm install pema @primate/mysql ``` ```sh $ npm install pema @primate/mongodb ``` %%% Configure your database: %%% Oracle:config/db.ts, SQLite:config/db.ts, PostgreSQL:config/db.ts, MySQL:config/db.ts, MongoDB:config/db.ts ```ts import oracledb from "@primate/oracledb"; export default oracledb({ database: "app", }); ``` ```ts import sqlite from "@primate/sqlite"; export default sqlite({ database: "/tmp/app.db", }); ``` ```ts import postgresql from "@primate/postgresql"; export default postgresql({ database: "app", }); ``` ```ts import mysql from "@primate/mysql"; export default mysql({ database: "app", }); ``` ```ts import mongodb from "@primate/mongodb"; export default mongodb({ database: "app", }); ``` %%% !!! Except for SQLite, you need a running database server. !!! Create a store for your counter: %%% TypeScript:stores/Counter.ts, JavaScript:stores/Counter.ts ```ts import p from "pema"; import store from "primate/store"; import db from "../config/db.ts"; export default store({ table: "counter", db, schema: { id: store.key.primary(p.u32), value: p.i8.range(-20, 20), } }); ``` ```ts import p from "pema"; import store from "primate/store"; import db from "../config/db.ts"; export default store({ table: "counter", db, schema: { id: store.key.primary(p.u32), value: p.i8.range(-20, 20), } }); ``` %%% !!! Validation is applied before writes — databases handle structure and types, Primate adds higher-level rules. !!! Update your component to sync with the backend: %%% React:routes/index.tsx, Angular:routes/index.component.ts, Vue:routes/index.vue, Svelte:routes/index.svelte, Solid:routes/index.tsx ```tsx import client from "@primate/react/client"; interface Props { counter: number; id: string }; export default function Counter(props: Props) { const counter = client.field(props.counter).post( `/counter?id=${props.id}`, ); return (

Counter Example

{counter.value}
{counter.error &&

{counter.error.message}

}
); } ``` ```ts import { Component, Input } from "@angular/core"; import type { Validated } from "@primate/angular"; import client from "@primate/angular/client"; @Component({ template: `

Counter Example

{{ value }}
@if (error) {

{{ error.message }}

}
`, }) export default class CounterComponent { @Input() id: string = ""; @Input("counter") initial: number = 0; counter!: Validated; get value() { return this.counter.value(); } get loading() { return this.counter.loading(); } get error() { return this.counter.error(); } ngOnInit() { this.counter = client.field(this.initial) .post(`/counter?id=${this.id}`); } increment() { this.counter.update(n => n + 1); } decrement() { this.counter.update(n => n - 1); } } ``` ```vue ``` ```svelte

Counter Example

{$counter.value}
{#if $counter.error}

{$counter.error.message}

{/if}
``` ```tsx import client from "@primate/solid/client"; interface Props { counter: number; id: string }; export default function Counter(props: Props) { const counter = client.field(props.counter).post( `/counter?id=${props.id}`, ); return (

Counter Example

{counter.value()}
{counter.error() &&

{counter.error()!.message}

}
); } ``` %%% And wire it together with a route: %%% TypeScript:routes/index.ts, JavaScript:routes/index.js ```ts import Counter from "@/stores/Counter"; import p from "pema"; import response from "primate/response"; import route from "primate/route"; await Counter.create(); export default route({ async get() { const counters = await Counter.find({}); const counter = counters.length === 0 ? await Counter.insert({ value: 10 }) : counters[0]; return response.page(counter); }, async post(request) { // validate that an id was provided // request.query.get() will throw if id is missing const id = p.string.parse(request.query.get("id")); // validate body as a number, coercing from string first const body = p.loose({ value: p.number }).parse(await request.body.form()); // update the value in the database await Counter.update({ id }, { value: body.value }); // 204 no response return null; }, }); ``` ```js import Counter from "@/stores/Counter"; import p from "pema"; import response from "primate/response"; import route from "primate/route"; await Counter.create(); export default route({ async get() { const counters = await Counter.find({}); const counter = counters.length === 0 ? await Counter.insert({ value: 10 }) : counters[0]; return response.page(counter); }, async post(request) { // validate that an id was provided // request.query.get() will throw if id is missing const id = p.string.parse(request.query.get("id")); // validate body as a number, coercing from string first const body = p.loose({ value: p.number }).parse(await request.body.form()); // update the value in the database await Counter.update({ id }, { value: body.value }); // 204 no response return null; }, }); ``` %%% !!! Use Pema's `loose` namespace to turn web inputs into typed values before validation. !!! ## Wrap up With just a few files, you now have an app that can: - Map requests to routes - Render views with multiple frontends - Run code in different backends - Persist data in a database - Validate inputs end-to-end ## Next steps At this point, you've built a minimal but complete app. From here, you can scaffold a project, explore real examples, or dive deeper into the docs — and start building production-ready apps. - **Scaffold with init** — Run `npx primate init` for a guided setup. - **Explore examples** — Browse example apps in the docs and repo. - **Dive deeper** — Check out guides and API docs. Source: https://primate.run/docs/requests # Requests Route handlers receive a single `RequestFacade` object that bundles everything you need to handle an incoming HTTP request: the parsed `body`, `path` parameters, `query` parameters, `cookies`, `headers`, the original WHATWG `Request`, and a `URL` helper. This page walks through each part and shows how to access and validate it. |Property|Type|Description| |-|-|-| |[body](#body)|[RequestBody](#requestbody-reference)|parsed request body| |[path](#path)|[RequestBag](#requestbag-reference)|path parameters| |[query](#query)|[RequestBag](#requestbag-reference)|query parameters| |[headers](#headers)|[RequestBag](#requestbag-reference)|request headers| |[cookies](#cookies)|[RequestBag](#requestbag-reference)|request cookies (case-sensitive)| |[original](#original)|`Request`|original WHATWG `Request` object| |[url](#url)|`URL`|original request URL| |[forward](#forward)|`(to: string) => Promise`|forward the request| !!! `RequestFacade` also supports a request context store for sharing derived values between hooks and route handlers (see [Request context](#request-context)). ## Body The parsed request body. Primate decodes the body according to the `Content-Type` header sent with the request, and `request.body` offers different methods to retrieve the body. `request.body` enforces the correct accessor for the incoming `Content-Type` (e.g., `json()` for `application/json`) and throws on a mismatch. %%% TypeScript:routes/identify.ts, Go:routes/identify.go ```ts import p from "pema"; import route from "primate/route"; export default route({ async post(request) { const { name } = p({ name: p.string.min(1) }).parse(await request.body.json()); return `Hello, ${name}`; }, }); ``` ```go package main import ( "fmt" "github.com/primate-run/go/route" ) var _ = route.Post(func(request route.Request) any { json, err := request.Body.JSON() if err != nil { return map[string]any{"error": err.Error()} } name, _ := json["name"].(string) return fmt.Sprintf("Hello, %s", name) }) ``` %%% If a client sends a `POST` request to `/identify` using the content type `application/json` and `{"name": "John"}` as payload, this route responds with `200 OK` and the body `Hello, John`. |Content type|Method| |-|-| |`text/plain`|`text()`| |`application/json`|`json()`| |`application/x-www-form-urlencoded`|`form()` or `formEntries()`| |`multipart/form-data`|`multipart()`| |`application/octet-stream`|`blob()`| `form()` returns a `Record`. If a field name occurs more than once, the record keeps its last value. Use `formEntries()` to preserve every value and the original submission order as `[name, value][]`: ```ts const entries = await request.body.formEntries(); const categoryIds = entries .filter(([name]) => name === "categoryIds") .map(([, value]) => value); ``` For direct access to the platform's WHATWG `FormData`, use `request.original.formData()` instead. ## `RequestBody` reference ```ts import type JSONValue from "@rcompat/type/JSONValue"; interface RequestBody { text(): Promise; json(): Promise; form(): Promise>; formEntries(): Promise<[string, string][]>; multipart(): Promise<{ form: Record; files: Record; }>; blob(): Promise; } ``` ## Path Path parameters extracted from bracketed segments in your route path (e.g. `routes/users/[id]`, `routes/blog/[year]/[slug]`). Path parameters are exposed as a [RequestBag](#requestbag-reference), with `get()`, `try()`, `has()`, and schema-validated `as()`. %%% TypeScript:routes/user/[id].ts ```ts import route from "primate/route"; export default route({ get(request) { // throws if missing const id = request.path.get("id"); // "42" for /users/42 return `User #${id}`; }, }); ``` %%% If a parameter is optional in your route, prefer `try()` and handle `undefined`. %%% TypeScript:routes/blog/[year]/[[slug]].ts ```ts import route from "primate/route"; export default route({ get(request) { const year = request.path.get("year"); const slug = request.path.try("slug"); // string | undefined return slug ? `Post ${slug} from ${year}` : `All posts in ${year}`; }, }); ``` %%% !!! We're using `[[slug]]` to denote an [optional](/docs/routing#optional-routes) path segment. !!! You can validate all parameters at once with a schema. %%% TypeScript:routes/user/[id].ts ```ts import p from "pema"; import route from "primate/route"; const PathSchema = p({ id: p.string.regex(/^\d+$/) }); export default route({ get(request) { const { id } = PathSchema.parse(request.path); // id: string (digits only) return `User #${id}`; }, }); ``` %%% ## Query The query string is split into individual parameters and exposed as a `RequestBag`. Use it to read `?page=2&search=john`-style parameters. Query string parameters are matched case-insensitively. If a query parameter appears multiple times, Primate keeps only the last. %%% TypeScript:/search?page=2&search=john ```ts import route from "primate/route"; export default route({ get(request) { const page = Number(request.query.try("page") ?? "1"); // 1 if missing const term = request.query.get("search"); return `Searching '${term}' (page ${page})`; }, }); ``` %%% Schema-validate the entire query string: ```ts import p from "pema"; import route from "primate/route"; const QuerySchema = p({ page: p.int.loose.min(1).default(1), search: p.string.min(1), }); export default route({ get(request) { const { page, search } = QuerySchema.parse(request.query); return `Searching '${search}' (page ${page})`; }, }); ``` !!! `request.query` normalizes keys, `request.url.searchParams` does not. !!! ## Headers Request headers as a `RequestBag`. Header keys are matched case-insensitively. If a header appears multiple times, the last value is kept. ```ts import route from "primate/route"; export default route({ get(request) { const ua = request.headers.try("user-agent"); // may be undefined const contentType = request.headers.try("content-type"); // returned as JSON return { ua, contentType }; }, }); ``` Validate or transform headers with a schema. ```ts import http from "@rcompat/http"; import p from "pema"; import route from "primate/route"; const HeadersSchema = p({ "content-type": p.string.optional(), authorization: p.string.startsWith("Bearer ").optional(), }); export default route({ get(request) { const headers = HeadersSchema.parse(request.headers); const token = headers.authorization?.slice("Bearer ".length); const status = token ? http.Status.NO_CONTENT : http.Status.UNAUTHORIZED; return new Response(null, { status }); }, }); ``` ## Cookies Request cookies as a `RequestBag`. Cookie names are *case-sensitive* (unlike other request bags). If a cookie repeats, Primate keeps the last value. ```ts import http from "@rcompat/http"; import response from "primate/response"; import route from "primate/route"; export default route({ get(request) { const session = request.cookies.try("session"); // string | undefined if (!session) { return response.error({ body: "Unauthorized", status: http.Status.UNAUTHORIZED, }); } return `Hello (session ${session.slice(0, 8)}...)`; }, }); ``` As with other bags, you can parse/validate the whole set. ```ts import http from "@rcompat/http"; import p from "pema"; import response from "primate/response"; import route from "primate/route"; const CookieSchema = p({ session: p.uuid.optional(), }); export default route({ get(request) { const { session } = CookieSchema.parse(request.cookies); return response.text(session ? "OK" : "No session", { status: session ? http.Status.OK : http.Status.FORBIDDEN, }); }, }); ``` ## `RequestBag` reference ```ts interface RequestBag { get(key: string): string; try(key: string): string | undefined; has(key: string): boolean; as(schema: { parse(x: unknown): T }): T; toJSON(): Record; } ``` ## Original The original WHATWG `Request` object as received from the runtime. Use this for low-level capabilities like `clone()`, `signal`, or direct header access when needed. ```ts import route from "primate/route"; export default route({ get(request) { // Abort handling if the client disconnects request.original.signal.addEventListener("abort", () => { console.log("client disconnected"); }); // Access a raw header const lang = request.original.headers.get("Accept-Language"); return new Response(lang ?? "en-US"); }, }); ``` ## URL A `URL` instance representing the request URL. Handy for composing absolute or relative URLs, accessing the `searchParams` directly, etc. ```ts import route from "primate/route"; export default route({ get(request) { const url = request.url; // prefer request.query over url.searchParams const page = url.searchParams.get("page") ?? "1"; // build a new URL relative to the request const cdn = new URL("/assets/logo.svg", url); // returned as JSON return { page, cdn: cdn.href }; }, }); ``` ## Forward Forwards the **original WHATWG** `Request`. The method lives on `RequestFacade`, but what's forwarded is the underlying original request. If your handler will pass the request upstream and you don't need to read the body, disable body parsing on the route. Primate can't know whether you'll call `request.forward()` inside the handler, so you must explicitly **opt out** of body parsing on the route. Most other aspects of the original request are preserved. By default only the `Content-Type` header is forwarded to match the body — you can specify additional headers to forward. ```ts import route from "primate/route"; export default route({ get(request) { return request.forward("https://upstream.internal/service", { Authorization: request.headers.try("authorization") ?? "", Accept: request.headers.try("accept") ?? "", }); }, }); ``` Use this to implement simple reverse proxies, edge routing, or fan-out/fan-in patterns. ## Request context `RequestFacade` includes a **request context** store: request-scoped values that hooks can attach for downstream hooks and route handlers to read. Use request context for **derived server values** (e.g. authenticated user, locale, feature flags, request IDs). For client input, use `body`, `path`, `query`, `headers`, or `cookies`. Context is most commonly set in [hooks](/docs/routing#hooks). Request context is server-only. It is not included in hydration or client navigation data, so hooks and route handlers can use it for sensitive or non-serializable request-scoped values. Incoming request headers and cookies are likewise server-only. ### API |Method|Description| |--------|-------------| |`request.set(key, value)`| Set a context value | |`request.set(key, fn)`| Update a context value with a function | |`request.get(key)`| Get a context value (throws if missing) | |`request.try(key)`| Get a context value or `undefined` | |`request.has(key)` | Check if a context key exists | |`request.delete(key)`| Remove a context key | !!! Context is **not** part of the underlying WHATWG `Request`. `request.forward()` forwards the original request, not your attached context. !!! You can use hooks to authenticate in a hook, and then use in a route. ```ts // routes/+hook.ts import route from "primate/route"; export default route.hook(async (request, next) => { const user = await authenticate(request); return next(request.set("auth.user", user)); }); ``` ## `RequestFacade` reference ```ts import type { RequestBag, RequestBody } from "@primate/core/request"; type Dict = Record; interface RequestView { context: Dict; cookies: Dict; headers: Dict; path: Dict; query: Dict; url: URL; } interface RequestFacade { body: RequestBody; path: RequestBag; query: RequestBag; headers: RequestBag; cookies: RequestBag; original: Request; target: string; // pathname + querystring url: URL; forward(to: string, headers?: Dict): Promise; has(key: string): boolean; try(key: string): T | undefined; get(key: string): T; set(key: string, value: T | ((prev: T | undefined) => T)): RequestFacade; delete(key: string): RequestFacade; toJSON(): RequestView; } ``` Source: https://primate.run/docs/responses # Responses Route handlers return a `ResponseLike` value that Primate converts into a WHATWG `Response`. You can return simple values ("implicit" responses) or use explicit handlers from `primate/response` when you need to control all aspects of the response. |Return value|Handler|Response|Notes| |-|-|-|-| |`string`|[text](#text)|`200 text/plain`|Serve plain text| |`object`|[json](#json)|`200 application/json`|Serve JSON| |`Blob`·`File`·`FileRef`|[binary](#binary)|`200 application/octet-stream`|Stream contents| |`URL`|[redirect](#redirect)|`302`|Redirect to URL| |—|[view](#view)|`200 text/html`|Serve frontend component| |—|[page](#page)|`200 text/html`|Serve collocated route page| |—|[error](#error)|`404 text/html`|Show error page| |—|[abort](#abort)|as given|Terminate processing outside normal return flow| |—|[ws](#websocket)|`101`|WebSocket upgrade| |—|[sse](#server-sent-events)|`200 text/event-stream`|Server‑sent events| |`null`|—|`204`|`new Response(null)`| |[Response](#response)|—|as given|WHATWG `Response`| ## Text Return strings to serve `text/plain`. %%% TypeScript:routes/text.ts, JavaScript:routes/text.js, Go:routes/text.go, Python:routes/text.py, Ruby:routes/text.rb ```ts import route from "primate/route"; export default route({ get(request) { return "Hello from TypeScript!"; }, }); ``` ```js import route from "primate/route"; export default route({ get(request) { return "Hello from TypeScript!"; }, }); ``` ```go package main import "github.com/primate-run/go/route" var _ = route.Get(func(_ route.Request) any { return "Hello from Go!" }) ``` ```py from primate import Route @Route.get def get(request): return "Hello from Python!" ``` ```rb require 'primate/route' Route.get do |request| 'Hello from Ruby!' end ``` %%% Use the explicit `text` handler for more options. %%% TypeScript:routes/text.ts, JavaScript:routes/text.js, Go:routes/text.go, Python:routes/text.py, Ruby:routes/text.rb ```ts import http from "@rcompat/http"; import response from "primate/response"; import route from "primate/route"; export default route({ post(request) { return response.text("Hello TypeScript!", { status: http.Status.CREATED }); }, }); ``` ```js import http from "@rcompat/http"; import response from "primate/response"; import route from "primate/route"; export default route({ post(request) { return response.text("Hello JavaScript!", { status: http.Status.CREATED }); }, }); ``` ```go package main import ( "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Post(func(request route.Request) any { return response.Text("Hello from Go!", map[string]any{"status": 201}) }) ``` ```py from primate import Response, Route @Route.post def post(request): return Response.text("Hello from Python!", status=201) ``` ```rb require 'primate/route' require 'primate/response' Route.post do |request| Response.text("Hello from Ruby!", status: 201) end ``` %%% ## JSON Return JSON-serializable objects to serve `application/json`. %%% TypeScript:routes/json.ts, JavaScript:routes/json.ts, Go:routes/json.go, Python:routes/json.py, Ruby:routes/json.rb ```ts import route from "primate/route"; export default route({ get() { return [ { name: "Donald" }, { name: "John" }, ]; }, }); ``` ```ts import route from "primate/route"; export default route({ get() { return [ { name: "Donald" }, { name: "John" }, ]; }, }); ``` ```go package main import ( "github.com/primate-run/go/core" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return []core.Dict{{"name": "Donald"}, {"name": "John"}} }) ``` ```py from primate import Route @Route.get def get(request): return [{"name": "Donald"}, {"name": "John"}] ``` ```rb require 'primate/route' Route.get do |request| [{ name: 'Donald' }, { name: 'John' }] end ``` %%% Use the explicit `json` handler for more options. %%% TypeScript:routes/json.ts, JavaScript:routes/json.ts ```ts import response from "primate/response"; import http from "@rcompat/http"; import route from "primate/route"; export default route({ get() { return response.json([ { name: "Donald" }, { name: "John" }, ], { status: http.Status.CREATED }); }, }); ``` ```ts import response from "primate/response"; import http from "@rcompat/http"; import route from "primate/route"; export default route({ get() { return response.json([ { name: "Donald" }, { name: "John" }, ], { status: http.Status.CREATED }); }, }); ``` %%% ## Binary Return `Blob`, `File`, `ReadableStream` or any object exposing `{ stream(): ReadableStream }` to serve `application/octet-stream` (binary data). %%% TypeScript:routes/binary.ts, JavaScript:routes/binary.ts ```ts import route from "primate/route"; export default route({ get() { return new Blob(["data"]); }, }); ``` ```ts import route from "primate/route"; export default route({ get() { return new Blob(["data"]); }, }); ``` %%% Primate attempts to read the source's name and MIME type if available. Use the explicit `binary` handler for more options. %%% TypeScript:routes/binary.ts, JavaScript:routes/binary.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.binary(new Blob(["data"]), { // set filename manually headers: { "Content-Disposition": "attachment; filename=data.bin" }, }); }, }); ``` ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.binary(new Blob(["data"]), { // set filename manually headers: { "Content-Disposition": "attachment; filename=data.bin" }, }); }, }); ``` %%% Use rcompat's `FileRef` to conveniently load a file from disk and stream it out. %%% TypeScript:routes/binary.ts, JavaScript:routes/binary.ts ```ts import fs from "@rcompat/fs"; import route from "primate/route"; export default route({ get() { return fs.ref("/tmp/data.bin"); }, }); ``` ```ts import fs from "@rcompat/fs"; import route from "primate/route"; export default route({ get() { return fs.ref("/tmp/data.bin"); }, }); ``` %%% ## Redirect Return a `URL` to redirect to another address. %%% TypeScript:routes/redirect.ts, JavaScript:routes/redirect.ts ```ts import route from "primate/route"; export default route({ get() { return new URL("https://example.com/login"); }, }); ``` ```ts import route from "primate/route"; export default route({ get() { return new URL("https://example.com/login"); }, }); ``` %%% Use the explicit `redirect` handler to vary the status or for local redirects. %%% TypeScript:routes/redirect.ts, JavaScript:routes/redirect.ts, Go:routes/redirect.go, Python:routes/redirect.py, Ruby:routes/redirect.rb ```ts import Status from "@rcompat/http/Status"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.redirect("https://primate.run", Status.SEE_OTHER); }, post(request) { return response.redirect(`/login?next=${request.target}`); }, }); ``` ```ts import Status from "@rcompat/http/Status"; import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.redirect("https://primate.run", Status.SEE_OTHER); }, post(request) { return response.redirect(`/login?next=${request.target}`); }, }); ``` ```go package main import ( "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(_ route.Request) any { return response.Redirect("https://primate.run", 303) }) var _ = route.Post(func(_ route.Request) any { return response.Redirect("/login") }) ``` ```py from primate import Response, Route @Route.get def get(request): return Response.redirect("https://primate.run", 303) @Route.post def post(request): return Response.redirect("/login") ``` ```rb require 'primate/route' require 'primate/response' Route.get do |request| Response.redirect('https://primate.run', 303) end Route.post do |request| Response.redirect('/login') end ``` %%% ## View Render and serve [components](/docs/components) from the `views` directory as `text/html`. %%% TypeScript:routes/index.ts, JavaScript:routes/index.js, Go:routes/index.go, Python:routes/index.py, Ruby:routes/index.rb ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Counter.jsx"); }, }); ``` ```js import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Counter.jsx"); }, }); ``` ```go package main import ( "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return response.View("Counter.jsx") }) ``` ```py from primate import Response, Route @Route.get def get(request): return Response.view("Counter.jsx") ``` ```rb require 'primate/route' require 'primate/response' Route.get do |request| Response.view('Counter.jsx') end ``` %%% ### Props Populate the component with initial props. %%% TypeScript:routes/index.ts, JavaScript:routes/index.js, Go:routes/index.go, Python:routes/index.py, Ruby:routes/index.rb ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get(request) { return response.view("Counter.jsx", { start: 10 }); }, }); ``` ```js import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Counter.jsx", { start: 10 }); }, }); ``` ```go package main import ( "github.com/primate-run/go/core" "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return response.View("Counter.jsx", core.Dict{"start": 10}) }) ``` ```py from primate import Response, Route @Route.get def get(request): return Response.view("Counter.jsx", {"start": 10}) ``` ```rb require 'primate/route' require 'primate/response' Route.get do |request| Response.view('Counter.jsx', start: 10) end ``` %%% ### Page Components are embedded into your app's main HTML template at `templates/app.html`, with the component code replacing the `%body%` placeholder. If the app template doesn't exist, Primate falls back to its standard one. ```html Primate app %head% %body% ``` Pass a different `template` option to use another HTML template. ```ts import response from "primate/response"; import route from "primate/route"; // render into `templates/counter.html` export default route({ get() { return response.view("Counter.jsx", { start: 10 }, { template: "counter.html" }); }, }); ``` ### Placeholders You can use placeholders in your HTML templates. Placeholder names start with a letter or underscore and may contain letters, numbers, underscores, and hyphens. Literal percent signs and text that does not match this form are preserved. ```html %title% %head% %body% ``` Populate them in your routes. ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Counter.jsx", { start: 10 }, { placeholders: { title: "Counter", }, }); }, }); ``` ### Partial Pass a `partial: true` option to render the component without the enclosing HTML template. ```ts import response from "primate/response"; import route from "primate/route"; // will render Counter without embedding it into templates/app.html export default route({ get() { return response.view("Counter.jsx", { start: 10 }, { partial: true }); }, }); ``` This is useful for replacing parts of the page whilst retaining the HTML template. ## Page Render a frontend component collocated with the current route using `response.page`. This keeps the route handler and its page component next to each other. %%% TypeScript:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.page({ message: "Hello" }); }, }); ``` %%% The matching component uses the same basename as the route and a frontend extension. %%% TypeScript:routes/index.tsx ```tsx import type route from "./index"; export default function Page(props: typeof route.get.Page) { return

{props.message}

; } ``` %%% `response.page` accepts the same props and view options as `response.view`. Layouts can use `response.page` too. A `+layout.ts` route looks for a collocated `+layout` component, such as `+layout.tsx`, `+layout.svelte`, or `+layout.vue`. ## Error Serve a `404 Not Found` error page as `text/html`. %%% TypeScript:routes/not-found.ts, JavaScript:routes/not-found.js, Go:routes/not-found.go, Python:routes/not-found.py, Ruby:routes/not-found.rb ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.error({ body: "Not Found" }); }, }); ``` ```js import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.error({ body: "Not Found" }); }, }); ``` ```go package main import ( "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return response.Error(map[string]any{ "body": "Not Found", }) }) ``` ```py from primate import Response, Route @Route.get def get(request): return Response.error({"body": "Not Found"}) ``` ```rb require 'primate/route' require 'primate/response' Route.get do |request| Response.error(body: 'Not Found') end ``` %%% This handler uses the HTML file at `templates/error.html` or falls back to a standard one provided by Primate. ```html Error page %head%

Error page

%body%

``` You can pass a custom status to this handler. %%% TypeScript:routes/error.ts, JavaScript:routes/error.js, Go:routes/error.go, Python:routes/error.py, Ruby:routes/error.rb ```ts import Status from "@rcompat/http/Status"; import response from "primate/response"; import route from "primate/route"; export default route({ get(request) { return response.error({ status: Status.INTERNAL_SERVER_ERROR, }); }, }); ``` ```js import Status from "@rcompat/http/Status"; import response from "primate/response"; import route from "primate/route"; export default route({ get(request) { return response.error({ status: Status.INTERNAL_SERVER_ERROR, }); }, }); ``` ```go package main import ( "github.com/primate-run/go/core" "github.com/primate-run/go/response" "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return response.Error(core.Dict{"status": 500}) }) ``` ```py from primate import Response, Route @Route.get def get(request): return Response.error({"status": 500}) ``` ```rb require 'primate/route' require 'primate/response' Route.get do |request| Response.error(status: 500) end ``` %%% As with `view`, you can pass a different `template` option to use another HTML template. ```ts import response from "primate/response"; import route from "primate/route"; // use templates/custom-error.html instead of templates/error.html export default route({ get() { return response.error({ template: "custom-error.html" }); }, }); ``` ## Abort Use `response.abort` when code outside a handler's normal return flow must stop request processing with an intentional HTTP response. This includes path or query derivations, hooks, nested helpers, and layouts. ```ts import response from "primate/response"; const project = await Project.first({ where: { name } }); if (project === undefined) { response.abort({ body: "Project not found", headers: { "Content-Type": "text/plain" }, status: 404, }); } ``` `response.abort` throws internally and has return type `never`; do not write `throw response.abort(...)`. It accepts either an options object with a required HTTP status or a native `Response`. An abort is expected control flow. It bypasses `+error`, error logging, and `onError`, and does not cause the router to try a less-specific route. For an ordinary response from a route handler, continue to return a value such as `response.error({ status: 404 })`. Throw ordinary errors for unexpected failures that should remain observable. ## WebSocket Upgrade a `GET` request to `ws:` and handle `open`, `message`, and `close` events. ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.ws({ open(socket) { socket.send("hello"); }, message(socket, message) { // echo socket.send(String(message)); }, close(socket) { console.log("socket closed"); }, }); }, }); ``` ## Server‑sent events Push out events to the client as `text/event-stream`. ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.sse(source => { // push event to client source.send("open", "hi!"); }); }, }); ``` `response.sse` receives a setup function. Return a cleanup function to stop timers, unsubscribe from event channels, or release any other resources when the client disconnects. ```ts return response.sse(source => { const timer = setInterval(() => { source.send("tick", Date.now()); }, 1000); return () => clearInterval(timer); }); ``` Call `source.close()` to end the stream from the server. This closes the response cleanly and runs the returned cleanup function. Cleanup runs exactly once whether the server closes the stream or the client disconnects. For app-level pub/sub, use [`primate/events`](/docs/events) and return the unsubscribe function from the SSE setup. ## Response Return a custom `Response`. ```ts import http from "@rcompat/http"; import route from "primate/route"; export default route({ get() { return new Response("Hi!", { status: http.Status.ACCEPTED, headers: { "X-Custom": "1" }, }); }, }); ``` ## `ResponseLike` reference ```ts type ResponseLike = | string | Record | Record[] | Blob | ReadableStream | URL | null | Response ; ``` Source: https://primate.run/docs/routing # Routing Routing in Primate maps incoming HTTP requests to backend logic. Primate uses **filesystem-based routing**: it breaks down the request **path** and maps it to a file in `routes`. ## Filesystem-based routing In filesystem-based routing, files map directly to URL paths. - `/` matches `routes/index.ts` - `/user` matches `routes/user.ts` - `/user/profile` matches `routes/user/profile.ts` Route files may contain **path parameters** denoted with brackets. Resolved parameters are available to [route handlers](#route-handlers) as `request.path`. | Request path | Route file | `*` matches | |--------------------|-------------------------|------------------------| | `/` | [index.ts] | | | `/user` | [user.ts] | | | `/user/*` | [user/\[id\].ts] | anything except `/` | | `/user/*` | [user/\[...id\].ts] | anything including `/` | | `/user`, `/user/*` | [user/\[\[id\]\].ts] | anything except `/` | | `/user`, `/user/*` | [user/\[\[...id\]\].ts] | anything including `/` | [index.ts]: #static-routes [user.ts]: #static-routes [user/\[id\].ts]: #dynamic-routes [user/\[...id\].ts]: #rest-routes [user/\[\[id\]\].ts]: #optional-routes [user/\[\[...id\]\].ts]: #optional-rest-routes !!! Path parameters are URL-decoded. Greedy segments (`[...name]`) match across `/` and must be the **final** segment of a route path. !!! ### Path schemas Use `route.with({ path })` to validate and type all path parameters before the handler runs. ```ts // routes/user/[id].ts import p from "pema"; import route from "primate/route"; const Path = p({ id: p.string, }); export default route({ get: route.with({ path: Path }, request => { return request.path.get("id"); }), }); ``` Primate checks that the schema properties match the dynamic route segments. A route at `routes/user/[id].ts` must declare an `id` property, and must not declare unrelated path keys. For async normalization, use `p.async(...)` with `.derive(...)`. ```ts // routes/user/[id].ts import p from "pema"; import route from "primate/route"; const Path = p.async({ id: p.string, }).derive(async ({ id }) => ({ id: await resolveUserId(id), })); export default route({ get: route.with({ path: Path }, request => { return request.path.get("id"); }), }); ``` ## Path normalization Primate cleans up URLs before matching to make them consistent (e.g., collapsing slashes, ignoring trailing ones). | Request path | Normalized to | Why | |------------------|---------------|-----| | `/` | `/` | Root is special | | `/user/` | `/user` | Ignores trailing slash | | `/user//profile` | `/user/profile` | Collapses multiple slashes | | `/docs/index` | `/docs` | Treats explicit 'index' as parent | !!! Normalization doesn't decode parameters — that happens during matching if a dynamic route captures them. !!! ## Route resolution Primate matches the normalized path to files by traversing segments like a folder tree, prioritizing exact matches for predictability. - **Split into segments** — `/user/profile` becomes `["user", "profile"]`. - **Match step by step** - **Static first** — Exact file or directory names (`user.ts`). - **Then dynamic** — `[param].ts` or `[[param]].ts` for one segment. - **Rest if last** — `[...param].ts` or `[[...param]].ts` for the remainder. - **Optional fallback** — If no exact match at the end, try an optional param with an empty value. This ensures static > required > optional. If nothing matches, it's a 404. ### Rules for clean setups Primate checks for ambiguities at startup and errors out if found. - **One of each dynamic kind per level** — A single-segment route (`[id]`) may sit beside one rest route (`[...rest]`), but two single-segment routes or two rest routes at the same level are ambiguous. - **No overlaps** — Avoid `a.ts` + `a/index.ts`, or static + same-level optional (e.g., `user.ts` + `user/[[id]].ts`). - **Endpoints only** — Optionals and rests can't have subfiles; they're leaves. | Defined routes | Request | Resolved file | Notes | |--------------------------------------------------|--------------------|--------------------------------------|-------| | `routes/user.ts` | `/user` | `routes/user.ts` | Static wins | | `routes/user/[id].ts` | `/user/42` | `routes/user/[id].ts` | Captures one | | `routes/user/[...name].ts` | `/user/john/adams` | `routes/user/[...name].ts` | Captures rest | | `routes/[[id]].ts` (no index.ts) | `/` | `routes/[[id]].ts` | Empty optional | | `routes/docs.ts` | `/docs/index` | `routes/docs.ts` | Normalization | | `routes/a/[id].ts` + `routes/a/[...rest].ts` | `/a/x` | `routes/a/[id].ts` | Single segment wins | | `routes/a/[id].ts` + `routes/a/[...rest].ts` | `/a/x/y` | `routes/a/[...rest].ts` | Rest fallback | | `routes/user.ts` + `routes/user/[[id]].ts` | — | ✗ | Static shadows optional | | `routes/a.ts` + `routes/a/index.ts` | — | ✗ | Overlap | ## Virtual route groups A directory wrapped in parentheses organizes routes without adding a URL segment. For example: ```text routes/ (account)/ +layout.ts projects.ts settings.ts [namespace]/ (reserved)/ +hook.ts projects.ts ``` `routes/(account)/projects.ts` matches `/projects`, while `routes/[namespace]/(reserved)/projects.ts` matches `/:namespace/projects`. Groups may appear anywhere in the route tree and may be nested. Group names must contain only letters, numbers, `_`, or `-`. Special files remain scoped by the filesystem hierarchy. In the example, `(account)/+layout.ts` applies only to routes inside `(account)`, and the `(reserved)/+hook.ts` hook does not apply to sibling namespace routes. Because group directories are absent from URLs, Primate rejects collisions at startup and reports every source path. Two files such as `(a)/projects.ts` and `(b)/projects.ts` cannot both define `/projects`. The same applies to patterns whose matches overlap through groups: `index.ts` conflicts with `(a)/[[id]].ts`, and `[id].ts` conflicts with `(a)/[...rest].ts`. For client-rendered route pages, a virtual group is also a lazy bundle seam. The application and framework runtime stay in the partition's shared build graph, while the group's collocated pages, layouts, CSS, and route-related dependencies are emitted behind a group entrypoint. Direct responses preload the active group. Client navigation loads its JavaScript and CSS before disposing the active page; a failed load leaves the current URL, page, and styles intact. Navigation inside an already loaded group continues with JSON responses and does not download the group JavaScript again. For nested groups, the deepest group containing a route owns its entrypoint; ancestor layouts become dependencies and esbuild may place common code in shared chunks. Traditional top-level `views/` remain in the application bundle because route handlers can select them dynamically. Virtual groups are performance and organization boundaries, **not** authorization boundaries. Their files may be publicly addressable and share chunks. Use [client partitions](/docs/configuration#client-partitions) when assets require independently built output trees, and continue authorizing every protected request on the server. ## Static routes Static routes have the highest priority in route resolution. | Request path | Route file | |--------------|----------------| | `/` | `index.ts` | | `/user` | `user.ts` | | `/user/john` | `user/john.ts` | !!! You can also represent a route with an `index.ts` file inside a directory (e.g., `routes/user/index.ts` for `/user`). This is equivalent to `user.ts` — pick one style per route. Using directories with `index.ts` is handy for grouping routes to add [special files](#special-files) like layouts or hooks. !!! ## Dynamic routes Dynamic routes use single brackets for a single segment. The parameter matches any value **except** `/`. `routes/user/[name].ts` matches: | Request path | Matches | |--------------------|---------| | `/user/2` | ✓ `request.path.get("name")` is `"2"` | | `/user/john` | ✓ `request.path.get("name")` is `"john"` | | `/user` | ✗ parameter required | | `/user/john/adams` | ✗ doesn't match `/` | ## Rest routes Rest routes use single brackets with three dots. The parameter matches any value **including** `/`. `routes/user/[...name].ts` matches: | Request path | Matches | |--------------------|---------| | `/user/2` | ✓ `request.path.get("name")` is `"2"` | | `/user/john` | ✓ `request.path.get("name")` is `"john"` | | `/user` | ✗ parameter required | | `/user/john/adams` | ✓ `request.path.get("name")` is `"john/adams"` | !!! Because they match across `/`, **rest segments must be the final segment** of a route path. !!! ## Optional routes Optional routes use **double brackets**. The parameter matches any value **except** `/` and **can be empty** (i.e., absent). `routes/user/[[name]].ts` matches: | Request path | Matches | |--------------------|---------| | `/user/2` | ✓ `request.path.try("name")` is `"2"` | | `/user/john` | ✓ `request.path.try("name")` is `"john"` | | `/user` | ✓ `request.path.try("name")` is `undefined` | | `/user/john/adams` | ✗ doesn't match `/` | Optional parameters may only appear at the **end** of a route path. !!! For optional segments, prefer `.try("key")` (or a schema default). `.get("key")` throws when the parameter is absent. !!! ## Optional rest routes Optional rest routes use **double brackets with three dots**. The parameter matches any value **including** `/` and **can be empty**. `routes/user/[[...name]].ts` matches: | Request path | Matches | |--------------------|---------| | `/user/2` | ✓ `request.path.try("name")` is `"2"` | | `/user/john` | ✓ `request.path.try("name")` is `"john"` | | `/user` | ✓ `request.path.try("name")` is `undefined` | | `/user/john/adams` | ✓ `request.path.try("name")` is `"john/adams"` | ## Route handlers Route files may contain one or more route handlers that match the request's HTTP verb. Handlers map requests to responses. %%% TypeScript:routes/index.ts, JavaScript:routes/index.js, Go:routes/index.go, Python:routes/index.py, Ruby:routes/index.rb ```ts import route from "primate/route"; export default route({ get(request) { return "Hello from GET!"; }, post(request) { return "Hello from POST!"; }, }); ``` ```js import route from "primate/route"; export default route({ get(request) { return "Hello from GET!"; }, post(request) { return "Hello from POST!"; }, }); ``` ```go package main import ( "github.com/primate-run/go/route" ) var _ = route.Get(func(request route.Request) any { return "Hello from GET!" }) var _ = route.Post(func(request route.Request) any { return "Hello from POST!" }) ``` ```py from primate import Route @Route.get def get(request): return "Hello from GET!" @Route.post def post(request): return "Hello from POST!" ``` ```rb require 'primate/route' Route.get do |request| "Hello from GET!"; end Route.post do |request| "Hello from POST!"; end ``` %%% ## Special files Files whose names start with `+` are **special**: they do **not** map to HTTP paths. Instead, they influence how routes in their directory (and below) behave. | Special file | Purpose | Recursive | |---------------------------|--------------------------------------|-----------| | [+layout.ts](#layouts) | Wrap route output in a layout | ✓ | | [+hook.ts](#hooks) | Intercept and transform requests | ✓ | | [+error.ts](#error-files) | Handle errors thrown by routes | ✗ | !!! *Recursive* means the file affects the current directory **and all subdirectories**. Special files don't map to paths and don't stack arbitrarily — see each section for composition and precedence. !!! ### Layouts Layouts live in `+layout.ts` and **compose** from the route's directory upward. The route's content is rendered into the **nearest** layout (same directory, if present), which is then rendered into the next parent layout, and so on up to `routes/`. %%% TypeScript:routes/+layout.ts (top) ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("layout.jsx"); }, }); ``` %%% The rendered layout component must render the route content via a slot/children, depending on your frontend. %%% JSX:views/layout.jsx, Svelte:views/layout.svelte ```jsx export default function Layout({ children }) { return <>
{children}
; } ``` ```svelte
``` %%% Layouts may also render a collocated frontend component with `response.page`. For example, `routes/+layout.ts` can render `routes/+layout.tsx`, `routes/+layout.svelte`, `routes/+layout.vue`, or any other configured frontend extension. A layout handler for the request method takes precedence. If it is not defined, Primate falls back to the layout's `get` handler, so the usual GET-only layout also frames views returned from POST and other methods. Layout handlers run only when the route returns a frontend view that consumes them. They are not run for JSON, redirects, images, feeds, or other plain responses. !!! **Composition order**: the innermost layout (closest to the route) renders first; each parent layout wraps it. !!! ### Hooks Hooks live in `+hook.ts`. A hook intercepts **all routes in its directory and below**. Hooks execute **top-down**: the highest parent hook runs first; it calls `next(request)` to pass control to the next hook; finally the route runs. ```ts // routes/admin/+hook.ts import route from "primate/route"; export default route.hook((request, next) => { if (request.query.try("password") === "opensesame") { return next(request); } return "wrong"; }); ``` * Return `next(request)` to **continue** to the next hook or route handler. * Return any other response to **short-circuit** (e.g., redirect, error page). * You **must** return something — returning `undefined` is an error. ### Request context Hooks often compute values that downstream hooks and route handlers need (auth user, locale, feature flags, request IDs, etc.). Use **request context** to attach those derived values to the request as it flows through the hook chain. Set context in a hook with `request.set()`: ```ts // routes/+hook.ts import route from "primate/route"; export default route.hook((request, next) => { request.set("user", { id: 1, name: "John" }); return next(request); }); ``` Read it in nested hooks or route handlers with `request.get()` or `request.try()`: ```ts // routes/dashboard/index.ts import route from "primate/route"; export default route({ get(request) { const user = request.get<{ id: number; name: string }>("user"); return { greeting: `Hello, ${user.name}` }; }, }); ``` !!! `RequestFacade` context is **mutable**. `request.set(...)` updates the current request's context for downstream hooks and the route handler. !!! For the full API and semantics, see [Handling requests → Request context](/docs/requests#request-context). !!! **Execution order**: parent hooks run before child hooks. The route runs only after all applicable hooks have called `next(request)`. !!! ### Error files Error files live in `+error.ts`. An error handler applies to its directory **and all subdirectories**, but handlers do **not** compose: when an error is thrown, the **nearest** `+error.ts` up the directory tree handles it. Only one error handler runs. An error handler for the request method takes precedence. If it is not defined, Primate falls back to the error file's `get` handler. This also includes failures in application request hooks that run before route dispatch, such as session or database lookup failures, when the request matches a route. The matched route's nearest error file is used. Error handlers apply to API routes as well as browser routes; branch on the request path or headers when APIs should retain a plain or JSON error response. %%% TypeScript:routes/+error.ts (top) ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.redirect("/"); } }) ``` %%% !!! **Precedence**: the closest `+error.ts` (same directory, then parent, etc.) takes precedence. Unlike layouts and hooks, error handlers aren't layered. !!! Source: https://primate.run/docs/sessions # Sessions Sessions let you persist state across requests: authentication, user preferences, shopping carts, or any other server-side data you want tied to a client. Primate manages session cookies, validates session data, and ensures changes are committed only if your route succeeds. ## Configuration Sessions are configured in `config/session.ts`. By default Primate uses an in-memory store that accepts JSON-compatible object data, but you may also use any store defined in `stores` or define one directly in `config/session.ts`. ### Session options |Option|Default|Description| |-|-|-| |[cookie.httpOnly](#cookie-httponly)|`true`|mark cookie as `HttpOnly`| |[cookie.maxAge](#cookie-maxage)|none|persist cookie for this many seconds| |[cookie.name](#cookie-name)|`"session_id"`|name of the session cookie| |[cookie.path](#cookie-path)|`"/"`|path for which the cookie is valid| |[cookie.sameSite](#cookie-samesite)|`"Lax"`|`SameSite` cookie policy| |[cookie.secure](#cookie-secure)|app TLS setting|mark cookie as `Secure`| |[store](#store)|default in-memory store|store managing sessions| ### `cookie.httpOnly` Whether the session cookie should be marked `HttpOnly` (hidden from client-side JavaScript). ### `cookie.maxAge` The number of seconds for which the browser should retain the session cookie. When omitted, the cookie is a browser session cookie and is removed when the browser session ends. Set a non-negative integer to emit `Max-Age` and create a persistent cookie: ```ts export default session({ cookie: { maxAge: 60 * 60 * 24 * 30, }, store: Session, }); ``` This controls the browser cookie lifetime; ensure the session records in your store have an appropriate retention policy as well. ### `cookie.name` The name of the session cookie. ### `cookie.path` The path for which the cookie is valid. ### `cookie.sameSite` The `SameSite` cookie policy: `"Strict"`, `"Lax"`, or `"None"`. ### `cookie.secure` Whether the session cookie should be marked `Secure`. By default, this follows Primate's TLS configuration. Set it explicitly when TLS terminates at a reverse proxy and Primate receives plain HTTP from that trusted proxy: ```ts export default session({ cookie: { secure: true, }, store: Session, }); ``` The same setting is used when clearing a destroyed session cookie. ### `store` The session store. By default Primate uses a store managed in memory, but you can use any store defined in `stores` or define one directly in `config/session.ts`. The store will be used for both validating the data and persisting it to a database of your choice. The default store accepts JSON-compatible objects and resets whenever the app restarts. This makes it useful while developing an application. You can later switch to a custom store without changing route code; declare every session field in the custom store schema before switching. The store must include a `session_id` field of type UUID. Primate uses this to associate each stored record with the session cookie. ### Example First create the store inside `stores`. ```ts import db from "@/config/db"; import p from "pema"; import store from "primate/store"; export default store({ table: "session", db, schema: { id: store.key.primary(p.u32), session_id: p.uuid, user_id: p.number, last_active: p.date, // additional fields as needed }, }); ``` Then refer to it in your configuration file. ```ts import session from "primate/session"; import Session from "@/stores/Session"; export default session({ store: Session, }); ``` ## Session facade The session facade is the API used in routes to interact with session state. It hides cookie handling and persistence details, exposing a simple interface to create, read, update, and destroy sessions. | method / property | description | | ----------------- | ----------- | | `id` | current session ID, if one exists | | `exists` | whether a session is active | | `create(initial)` | start a session with initial data; generates a new ID | | `get()` | return current session data; throws if none | | `try()` | return data if a session exists, otherwise `undefined` | | `set(data)` | replace session data or derive from the previous state | | `destroy()` | end the session and clear the cookie | ### Usage in routes Import the session facade via `@/config/session`. It is bound to the session store you used in `config/session.ts`. ```ts import session from "@/config/session"; import route from "primate/route"; export default route({ get() { if (!session.exists) { session.create({ user_id: 42 }); } const data = session.get(); return `User ${data.user_id} last active at ${data.last_active.toISOString()}`; }, }); ``` ### `SessionFacade` reference ```ts interface SessionFacade { readonly id: string | undefined; readonly exists: boolean; create(initial?: T): void; get(): Readonly; try(): Readonly | undefined; set(next: ((previous: Readonly) => T) | T): void; destroy(): void; } ``` ## Validation Primate validates data passed to `create` and `set` using the provided session store. ```ts import db from "@/config/db"; import p from "pema"; import store from "primate/store"; export default store({ table: "session", db, schema: { id: store.key.primary(p.u32), session_id: p.uuid, token: p.string.min(10), }, }); ``` ```ts import session from "@/config/session"; import route from "primate/route"; export default route({ post() { // throws if token is shorter than 10 characters session.set({ token: "abc" }); }, }); ``` !!! Other operations like `get`, `try` or `destroy` don't take input and do not trigger validation. !!! This ensures your session store never contains malformed data. ## Concurrent requests Each request loads a snapshot of its session and persists changes after the request completes successfully. Concurrent requests for the same session do not observe one another's in-flight changes, and a later write can overwrite overlapping state written by an earlier request. Source: https://primate.run/docs/stores # Stores A **store** is a collection of methods for accessing data. Stores can be backed by APIs, filesystems, caches, or other sources. The most common store is the **`DatabaseStore`**, a collection of records with a schema and a common interface, backed by a relational or document database. This page focuses on database stores, the most widely used. ## Mapping | Driver | Maps to | Examples | | -------------- | ---------- | --------------------------------- | | SQL | table | SQLite, MySQL, PostgreSQL, Oracle | | Document/NoSQL | collection | JSONDB, MongoDB | Stores have: * **Schema** — field names and types (Pema) * **API** — type-safe CRUD and queries * **Driver** — the database connection to use ## Define a store Create a file under `stores` and export a store. Every store requires three fields: `table`, `db`, and `schema`. Stores may also define `migrate`, which defaults to `true`. Set `migrate: false` for stores that should not participate in migration generation. ```ts // stores/Post.ts import p from "pema"; import store from "primate/store"; import db from "@/config/db"; export default store({ table: "post", db, schema: { id: store.key.primary(p.u32), title: p.string.max(100), body: p.string, created: p.date.default(() => new Date()), }, }); ``` Passing an incorrect `table`, `db`, or `schema` throws immediately at construction time. Database stores require a primary key field. Pema types (`string`, `number`, `boolean`, `date`, etc.) enforce validation and map to database columns. ### Portability Because `table` and `db` are declared explicitly in the store file, stores are fully self-contained modules. You can import and use them anywhere — migration scripts, test suites, REPLs, or any other context outside a running Primate app — without any framework initialisation. ```ts // scripts/migrate.ts import Post from "../stores/Post.ts"; await Post.drop(); ``` ### Primary keys Define primary keys with `key.primary()`: ```ts import store from "primate/store"; import p from "pema"; // auto-generated integer PK (default) id: store.key.primary(p.u32) // auto-generated UUID PK id: store.key.primary(p.uuid) // bigint PK id: store.key.primary(p.u64) // manual PK (no auto-generation) id: store.key.primary(p.u32, { generate: false }) ``` Supported PK types: - **Unsigned integers**: `u8`, `u16`, `u32`, `u64`, `u128` - **UUIDs**: `p.uuid`, `p.uuid.v4()`, `p.uuid.v7()` ### Foreign keys Define foreign keys with `key.foreign()` to reference other stores: ```ts import store from "primate/store"; import p from "pema"; export default store({ table: "article", db, schema: { id: store.key.primary(p.u32), title: p.string, author_id: store.key.foreign(p.u32), // references User.id }, }); ``` Foreign keys should use the same scalar type as the referenced primary key, for example `key.foreign(p.u32)` or `key.foreign(p.uuid)`. ### Insertion type Every store exposes the type accepted by `insert` as `Store.Insert`. Generated and defaulted fields remain optional. ```ts import Post from "@/stores/Post"; type NewPost = typeof Post.Insert; const draft: NewPost = { title: "Primate 0.41", body: "...", }; await Post.insert(draft); ``` ## Relations Define relationships between stores inline in the schema using `store.relation`. ### One-to-many A user has many articles: ```ts // stores/User.ts import p from "pema"; import store from "primate/store"; import db from "@/config/db"; export default store({ table: "user", db, schema: { id: store.key.primary(p.uuid), name: p.string, articles: store.relation.many({ table: "article", by: "author_id" }), }, }); ``` ### One-to-one A user has one profile: ```ts // stores/User.ts import p from "pema"; import store from "primate/store"; import db from "@/config/db"; export default store({ table: "user", db, schema: { id: store.key.primary(p.uuid), name: p.string, profile: store.relation.one({ table: "profile", by: "user_id" }), }, }); ``` ### Reverse relations Query from the *many* side back to the *one* side using `reverse: true`: ```ts // stores/Article.ts import p from "pema"; import store from "primate/store"; import db from "@/config/db"; export default store({ table: "article", db, schema: { id: store.key.primary(p.u32), title: p.string, author_id: store.key.foreign(p.u32), author: store.relation.one({ table: "user", by: "author_id", reverse: true }), }, }); ``` ### Loading relations Use the `with` option to load related records. Pass the store directly: ```ts import Article from "@/stores/Article"; const users = await User.find({ with: { articles: Article }, }); ``` Pass a query object with `store` plus any sub-query options: ```ts import Article from "@/stores/Article"; const users = await User.find({ where: { active: true }, with: { articles: { store: Article, select: ["title", "created"], sort: { created: "desc" }, limit: 5, }, }, }); // result: // [ // { // id: "...", // name: "John", // articles: [ // { title: "First Post", created: ... }, // { title: "Second Post", created: ... }, // ] // } // ] ``` Each relation sub-query can specify: - `store` — the related store (required) - `select` — fields to include - `sort` — ordering - `where` — filter criteria - `limit` — max records per parent Primate validates that the store passed in `with` matches the table declared in the relation. Passing the wrong store throws a `relation_table_mismatch` error. ## Operators The `where` option supports operators for non-equality comparisons. ### Comparison operators Available on numbers, bigints, and dates: ```ts const posts = await Post.find({ where: { views: { $gt: 100 } }, }); ``` | Operator | Meaning | Types | | --------- | --------------------- | ------------------------ | | `$gt` | greater than | number, bigint | | `$gte` | greater than or equal | number, bigint | | `$lt` | less than | number, bigint | | `$lte` | less than or equal | number, bigint | | `$ne` | not equal | number, bigint | | `$after` | after | date | | `$before` | before | date | ### String operators ```ts const users = await User.find({ where: { name: { $like: "Ali%" } }, }); ``` | Operator | Meaning | | --------- | ---------------------------- | | `$like` | case-sensitive pattern match | | `$ilike` | case-insensitive pattern match | | `$ne` | not equal | Use `%` as a wildcard and `_` as a single-character wildcard. ### $in operator Match records whose field value is in a given list. Works on strings, numbers, bigints, dates, and UUIDs: ```ts const users = await User.find({ where: { name: { $in: ["John", "Bob", "Larry"] } }, }); const posts = await Post.find({ where: { status: { $in: ["draft", "published"] } }, }); ``` Passing an empty array throws — an empty `$in` can never match anything and is almost always a programmer mistake. ## Pagination ### Limit Cap the number of results with `limit`: ```ts const posts = await Post.find({ sort: { created: "desc" }, limit: 20, }); ``` ### Offset Skip a number of records with `offset`. Requires `limit` to be set: ```ts const page = await Post.find({ sort: { created: "desc" }, limit: 20, offset: 40, }); ``` Using `offset` without `limit` throws. All six database drivers support offset pagination natively. ## First matching record Use `first` when a query needs at most one record. It accepts `where`, `select`, `sort`, `offset`, and `with`, applies a limit of one internally, and returns `undefined` when no record matches. ```ts const latest = await Post.first({ where: { published: true }, sort: { created: "desc" }, select: ["id", "title"], }); ``` Unlike `find`, `first` does not accept a `limit` option. ## Configure a database Install a Primate database driver and create a `config/db` directory with your database file. %%% SQLite, PostgreSQL, MySQL, MongoDB ```sh $ npm install @primate/sqlite ``` ```sh $ npm install @primate/postgresql ``` ```sh $ npm install @primate/mysql ``` ```sh $ npm install @primate/mongodb ``` %%% %%% SQLite:config/db.ts, PostgreSQL:config/db.ts, MySQL:config/db.ts, MongoDB:config/db.ts ```ts import sqlite from "@primate/sqlite"; export default sqlite({ database: "/tmp/primate.db" }); ``` ```ts import postgresql from "@primate/postgresql"; export default postgresql({ database: "primate" }); ``` ```ts import mysql from "@primate/mysql"; export default mysql({ database: "primate" }); ``` ```ts import mongodb from "@primate/mongodb"; export default mongodb({ database: "primate" }); ``` %%% Name the file freely. For the main driver, use `index.ts` or `default.ts`. Import it into your stores as `db` from `@/config/db`. ## Migrations Primate can manage schema changes for database stores through an opt-in migration system. Enable it in `config/app.ts` by telling Primate which database should track applied migrations and which table to use: ```ts import config from "primate/config"; import db from "@/config/db"; export default config({ db: { migrations: { table: "migration", db, }, }, }); ``` If `db.migrations` is not configured, migrations are disabled. Once enabled, the workflow is: ```bash npx primate migrate:create --name="add posts" npx primate migrate:status npx primate migrate:apply ``` `migrate:create` compares your current stores to the live database and writes a new numbered migration file into `migrations/`. `migrate:status` shows which migrations are applied and which are pending. `migrate:apply` runs pending migrations in order and records them in the migration table. For production, `primate build` emits a self-contained migration entrypoint alongside the server. Run it before starting the server: ```bash node build/migrate.js status --json node build/migrate.js apply --json node build/server.js ``` The entrypoint contains the exact configuration, database adapter, and migrations bundled with that server build. It does not need the source tree or the Primate CLI. `status` exits with code 0 when clean, 2 when migrations are pending or the database is ahead, and 1 on an invalid command or operational error. `apply` exits with code 0 on success and 1 on failure. Explicitly applying migrations before startup is the recommended workflow. Primate does not apply migrations automatically when the server starts. ### Upgrading from 0.41 Version 0.42 removes `db.migrations.autoapply`. Remove that property from `config/app.ts`; leaving it configured produces an error with explicit migration instructions. Run `primate migrate:apply` during development. In production, replace automatic startup application with a deployment step that applies the exact migrations bundled with the build before starting its server: ```bash node build/migrate.js apply --json node build/server.js ``` Do not start the new server if migration application fails. Running `apply` again is safe when the database is already current. Primate is strict about unapplied migrations. If migration files exist that have not yet been applied, `migrate:create` refuses to generate another one on top. A built app checks its exact bundled migration list and exits with an error before listening if the database is behind or ahead. Migration IDs must be unique and append-only. The source commands and built migration artifact reject duplicate IDs, and they reject an unapplied migration whose ID is below the database's highest applied ID. Renumber a late migration above the current database version rather than applying it out of order. ## Lifecycle Create tables or collections at app startup, e.g. in a route file. ```ts import Post from "@/stores/Post"; await Post.create(); // ... later (e.g., tests/teardown) // await Post.drop(); ``` You can safely call `create()` multiple times; drivers treat it as idempotent. Good for prototyping. In production, prefer migrations to manage schema changes ahead of time. ## Usage in routes A typical route that reads (or creates) a record, then renders a route page: ```ts // routes/posts/index.ts import Post from "@/stores/Post"; import p from "pema"; import response from "primate/response"; import route from "primate/route"; export default route({ async get() { const posts = await Post.find({ where: {}, sort: { created: "desc" }, select: ["id", "title", "created"], limit: 20, }); return response.page({ posts }); }, async post(request) { const body = p({ title: p.string.max(100), body: p.string, }).parse(await request.body.form()); const created = await Post.insert(body); return response.page({ post: created }); }, }); ``` ## Extending stores Add custom methods to stores with `.extend()`. Extensions can be defined inline or in separate files for modularity. ```ts // stores/User.ts import p from "pema"; import store from "primate/store"; import db from "@/config/db"; export default store({ table: "user", db, schema: { id: store.key.primary(p.u32), name: p.string, age: p.u8.range(0, 120), lastname: p.string.optional(), }, }).extend(User => ({ findByAge(age: typeof User.Schema.age) { return User.find({ where: { age } }); }, async getAverageAge() { const users = await User.find({ where: {} }); return users.reduce((sum, u) => sum + u.age, 0) / users.length; }, })); ``` Extended stores are excluded from migration generation automatically; migrations operate on the base store definitions. ### Modular extensions Create a base store: ```ts // stores/User.ts import p from "pema"; import store from "primate/store"; import db from "@/config/db"; export default store({ table: "user", db, schema: { id: store.key.primary(p.u32), name: p.string, age: p.u8.range(0, 120), lastname: p.string.optional(), }, }); ``` Create an extended version: ```ts import Base from "@/stores/User"; export default Base.extend(User => { const Schema = User.Schema; return { findByAge(age: typeof Schema.age) { return User.find({ where: { age } }); }, findByNamePrefix(prefix: typeof Schema.name) { return User.find({ where: { name: { $like: `${prefix}%` } } }); }, async updateAge(id: typeof Schema.id, age: typeof Schema.age) { return User.update(id, { set: { age } }); }, async getAverageAge() { const users = await User.find({ where: {} }); return users.reduce((sum, u) => sum + u.age, 0) / users.length; }, }; }); ``` ## Bespoke SQL Store schemas can be passed directly to `db.sql` for typed raw SQL queries, letting you reuse your existing type definitions without duplication: ```ts import db from "@/config/db"; import User from "@/stores/User"; const findByAge = db.sql({ input: User.schema, query: "SELECT name FROM users WHERE age > :age", output: p.array(p({ name: p.string })), }); const results = await findByAge({ age: 18 }); ``` TypeScript enforces that every required field in the store schema has a matching placeholder in the query. Optional fields are exempt. See the [databases](/docs/database#typed-bespoke-sql) page for the full `db.sql` API. Source: https://primate.run/docs/target/native # Native The Native target compiles your Primate application into standalone desktop executables for Windows, macOS, and Linux. It embeds your app and uses a system **WebView** to render the UI. Under the hood, Primate runs an HTTP server **inside the executable** and points the WebView at it, so you ship a single desktop app without needing a separate web server. ## Setup Install the native module: ```bash npm install @primate/native ``` Configure your application: ```ts // config/app.ts import config from "primate/config"; import native from "@primate/native"; export default config({ modules: [native()], }); ``` ## How it works Primate starts a lightweight HTTP server in the app process and boots a WebView window to display your routes. Navigation, cookies, and fetch requests work as they do in the browser. * **Server**: serves your built app; you can still use routes, middleware, and modules. * **WebView**: renders your UI using the system WebView for each platform. !!! If you proxy to external services, keep in mind you're now a **desktop** process. Network access, TLS, and CORS behave like any other HTTP client. !!! ## Platform Support Currently supported platforms: | Platform | Runtime | Status | | ------------ | ------- | ------ | | linux-x64 | Bun | ✓ | | windows-x64 | Bun | ✓ | | darwin-x64 | Bun | | | darwin-arm64 | Bun | | ## Building ### Development Build Primate with the native target: ```bash bunx --bun primate build --target=desktop ``` This will create an executable `app` (or `app.exe` on Windows) inside `build`. ### Cross-compilation You can target specific platforms from your development machine: ```json { "scripts": { "build:linux-x64": "bunx --bun primate build --target=linux-x64", "build:darwin-x64": "bunx --bun primate build --target=darwin-x64", "build:darwin-arm64": "bunx --bun primate build --target=darwin-arm64", "build:windows-x64": "bunx --bun primate build --target=windows-x64" } } ``` Run, for example: ```bash bunx --bun primate build --target=windows-x64 ``` ### Build Output ``` build/ ├── app # Linux/MacOS executable └── app.exe # Windows executable ``` ## Configuration | Option | Type | Default | Description | | ------- | --------- | ------- | ------------------------------ | | `start` | `string` | `"/"` | Initial path when app launches | | `debug` | `boolean` | `false` | Enable debug logging | ### Example ```ts import native from "@primate/native"; export default config({ modules: [ native({ // start URL when app launches start: "/", // enable debug mode debug: false, }), ], }); ``` ## Distribution Shipping native apps usually involves platform packaging and signing: * **Windows**: package/sign your `app.exe` as needed for distribution. * **macOS**: wrap the executable into an `.app` bundle, then notarize/sign. * **Linux**: distribute the binary directly or via a package format of choice. These steps depend on your release tooling; integrate them after `primate build` completes. Source: https://primate.run/docs/target/web # Web The **Web** target serves your app over HTTP. It's the default target and runs on all runtimes (Node, Bun, Deno). `primate build` creates the server bundle and assets; `primate serve` runs the built app. ### Build output ``` build/ └── server.js # Production server entry ``` ## Serving ```sh npx primate serve # or node build/server.js ``` Source: https://primate.run/docs/validation # Validation In Primate, validation refers to making sure that input passed into your routes — typically from the frontend or API clients — is properly checked at runtime. This ensures your backend logic never executes on malformed or malicious data. Because the web is for the most part untyped, everything arrives as strings, binary blobs, or loosely structured JSON. Primate uses its own validation framework, Pema, to define schemas for these inputs. These schemas are applied in your routes to guarantee that inputs match the shapes your logic expects. !!! TypeScript already provides compile-time validation during development, but only Pema can enforce correctness at runtime, when real clients interact with your app. !!! Validation errors are surfaced as `ParseError`. Unlike regular errors, Primate automatically serializes them into a `400 Bad Request` JSON response and returns them to the client: ```json { "/email": { "message": "Expected valid email", "messages": ["Expected valid email"] } } ``` You can override this by catching the error yourself and returning a custom response. ## Summary | Input type | In Primate | Types | Use cases | | ------------------------------------- | --------------------- | ---------------- | ------------------------------------| | [Web forms](#web-forms) | `request.body.form` | `string\|File` | form submission, Authentication | | [JSON API calls](#json-api-calls) | `request.body.json` | `JSONValue` | REST APIs, client-side fetch calls | | [Binary uploads](#binary-uploads) | `request.body.blob` | `Blob` | File uploads | | [Query parameters](#query-parameters) | `request.query` | `string` | Pagination, filtering | | [Path parameters](#path-parameters) | `request.path` | `string` | REST resources, nested routes | | [Headers](#headers) | `request.headers` | `string` | Authentication, content negotiation | ## Web forms Form submissions are received as `request.body.form`, which optionally accepts a schema. Values arrive as strings (or `File` for file inputs). `form()` keeps the last value for repeated field names. To validate repeated checkbox or multi-select values, read `request.body.formEntries()`, select the entries by name, and validate the resulting array. ```ts import p from "pema"; import route from "primate/route"; const Login = p({ email: p.string.email(), password: p.string.min(8), }); export default route({ async post(request) { const form = Login.parse(await request.body.form()); // form.email and form.password are now validated return "Welcome back!"; }, }); ``` This ensures invalid email addresses or too-short passwords never reach your logic. !!! `request.body.form` isn't tied to Pema; it accepts any schema that exposes a `parse` function; but only Pema integrates directly with Primate to throw a `ParseError` that is intercepted and passed to the client. !!! ## String normalization Normalize strings before validating them with `trim()`, `lowercase()`, and `uppercase()`. Transforms run in declaration order before validators, and the normalized string is the parsed value. ```ts const Username = p.string .trim() .lowercase() .regex(/^[a-z][a-z0-9]{2,31}$/); Username.parse(" Primate41 "); // "primate41" ``` Use `check(predicate, message)` for validation that is more naturally expressed as a function. `check` is available on primitive Pema types. ```ts const EvenCode = p.u32.check( value => value % 2 === 0, "Code must be even", ); ``` String validators accept custom error messages as a string or message option: ```ts const Slug = p.string.startsWith("/", "Slug must start with a slash"); const Email = p.string.email({ message: "Enter a valid email address" }); ``` ## JSON API calls When clients send JSON data (e.g. via `fetch`), you access it with `request.body.json`. ```ts import p from "pema"; import route from "primate/route"; const CreateUser = p({ name: p.string, age: p.uint.min(13), // must be >= 13 }); export default route({ async post(request) { const user = CreateUser.parse(await request.body.json()); // user is now guaranteed to match schema return { id: 42, ...user }; }, }); ``` You can also attach a body schema directly to the route. Primate validates the body before returning it from `request.body.json()`. ```ts import p from "pema"; import route from "primate/route"; const CreateUser = p({ name: p.string, age: p.uint.min(13), }); export default route({ post: route.with( { body: CreateUser, contentType: "application/json" }, async request => { const user = await request.body.json(); return { id: 42, ...user }; }, ), }); ``` ## Derived values Use `.derive(...)` to transform a parsed value while keeping validation and normalization in one schema. ```ts import p from "pema"; const Name = p({ first: p.string, last: p.string, }).derive(({ first, last }) => `${first} ${last}`); const name = Name.parse({ first: "John", last: "Adams" }); // name is "John Adams" ``` Derived schemas work with route body schemas too. ```ts import p from "pema"; import route from "primate/route"; const Body = p({ name: p.string, }).derive(({ name }) => name.toUpperCase()); export default route({ post: route.with( { body: Body, contentType: "application/json" }, async request => { const name = await request.body.json(); return name; }, ), }); ``` ## Async object schemas Use `p.async(...)` when an object schema needs async normalization. The schema parses the object first, then awaits any derived value. ```ts import p from "pema"; const User = p.async({ id: p.string, }).derive(async ({ id }) => { return await loadUser(id); }); const user = await User.parse({ id: "john" }); ``` Async schemas can validate request bodies. ```ts import p from "pema"; import route from "primate/route"; const Body = p.async({ name: p.string, }).derive(async ({ name }) => name.toUpperCase()); export default route({ post: route.with( { body: Body, contentType: "application/json" }, async request => { const name = await request.body.json(); return name; }, ), }); ``` They can also validate path parameters while preserving the object properties Primate needs to match schema keys to route segments. ```ts // routes/user/[id].ts import p from "pema"; import route from "primate/route"; const Path = p.async({ id: p.string, }).derive(async ({ id }) => ({ id: await resolveUserId(id), })); export default route({ get: route.with({ path: Path }, request => { return request.path.get("id"); }), }); ``` ## Binary uploads Raw uploads (e.g. images) are available through `request.body.blob` as a `Blob`. ```ts import p from "pema"; import route from "primate/route"; const Icon = p.blob.max(1000).type("image/png"); export default route({ async post(request) { // this throws if body isn't a binary stream or doesn't pass validation const file = Icon.parse(await request.body.blob()); await FileRef.write("/tmp/1.png", file); return "File uploaded!"; }, }); ``` Use this for scenarios where the payload itself is the file, not just a form field. ## Query parameters Query parameters (e.g. `?page=2&filter=active`) are strings accessible at `request.query`. ```ts import p from "pema"; import route from "primate/route"; const Query = p({ page: p.loose.uint.default(1), filter: p.string.optional(), }); export default route({ get: route.with({ query: Query }, request => { // page is a validated number return `Showing page ${request.query.get("page")}`; }), }); ``` Importing this route in client code creates a typed stub. Its `query` option is serialized into the URL, and an `AbortSignal` can cancel the request: ```ts import search from "@/routes/search"; const controller = new AbortController(); const response = await search.get({ query: { filter: "active" }, signal: controller.signal, }); ``` Client stubs also accept `URLSearchParams`, which is useful for repeated query keys. Invalid query parameters produce a `400 Bad Request` before the route handler runs. Query lookup remains case-insensitive; schema keys preserve their declared casing, and requests or schemas with case-variant duplicate keys are rejected as ambiguous. ## Path parameters Path parameters are extracted from the route definition and exposed on `request.path`. ```ts import p from "pema"; import route from "primate/route"; const Path = p({ userId: p.loose.uint, }); export default route({ get(request) { const { userId } = Path.parse(request.path); return `Profile for user ${userId}`; }, }); ``` This is common in REST-style routes. ## Headers Request headers are strings, available via `request.headers`. ```ts import p from "pema"; import route from "primate/route"; const HeadersSchema = p({ authorization: p.string.startsWith("Bearer "), }); export default route({ get(request) { const { authorization } = HeadersSchema.parse(request.headers); const bearer = authorization.slice("Bearer ".length); /* actually validate the token */ return `Validated token: ${bearer}`; }, }); ``` Source: https://primate.run/docs/views # Views Frontend view components are placed in `views`. To use views, add a [frontend module](/docs/frontend). ## Serving views To serve views, install a frontend module, for example `@primate/html`. ```sh npm install @primate/html ``` Activate the module in your configuration. ```js import html from "@primate/html"; import config from "primate/config"; export default config({ modules: [html()], }); ``` Create an HTML view in `views`. ```html

Hello, world!

``` Serve it in a route with `response.view`, passing in the name of the file you just created. ```js // routes/hello.js import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("hello.html"); }, }); ``` `response.view` will use the `templates/app.html` to render a full HTML template, replacing `%body%` with the view's contents. If `templates/app.html` doesn't exist, Primate will use its default fallback file. ```html Primate app %head% %body% ``` The combination of the route's output and the page will result in the following HTML template served to a client requesting `GET /hello`. ```html Primate app

Hello, world!

``` ## Partials It is sometimes necessary to serve a bare view without a fully-fledged page, especially if you're replacing some parts of the page on the frontend (e.g., using HTMX). To this end, you can use the `partial` option of the `view` handler. ```js import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("hello.html", {}, { partial: true }); }, }); ``` Using the same `hello.html` view specified as above, a client requesting `GET /partial-hello` will see the following response. ```html

Hello, world!

``` Source: https://primate.run/guides/backends/add-a-go-backend Add Go routes with the `@primate/go` module. Write handlers in Go; Primate compiles them to WebAssembly and wires them like JS routes. !!! Make sure the Go executable is available in your `PATH`. Primate needs it to compile your routes to WebAssembly. !!! --- ### 1) Install Install the Primate Go package. ```sh $ npm install @primate/go ``` --- ### 2) Configure Load the Go module in your configuration. %%% TypeScript:config/app.ts ```ts import go from "@primate/go"; import config from "primate/config"; export default config({ modules: [go()] }); ``` %%% --- ### 3) Write a route Compose a route in Go. %%% Go:routes/index.go ```go package main import ( "github.com/primate-run/go/core" "github.com/primate-run/go/route" ) var _ = route.Get(func(_ route.Request) any { return "Hello from Go" }) var _ = route.Post(func(_ route.Request) any { return core.Dict{"ok": true} }) ``` %%% Source: https://primate.run/guides/backends/add-a-python-backend Add Python routes with the `@primate/python` module. Write routes in Python; Primate compiles them to WebAssembly and wires them like JS routes. !!! Manage your Primate Python app as you would a normal Python project. Create a `venv` and use `requirements.txt` to manage your dependencies to get a complete LSP experience. !!! --- ### 1) Install Install the Primate Python package. ```sh $ npm install @primate/python ``` --- ### 2) Configure Load the Python module in your configuration. %%% TypeScript:config/app.ts ```ts import config from "primate/config"; import python from "@primate/python"; export default config({ modules: [python()] }); ``` %%% --- ### 3) Write a route Compose a route in Python. %%% Python:routes/index.py ```py from primate import Route @Route.get def get(request): return "Hello from Python" @Route.post def post(request): return {"ok": True} ``` %%% Source: https://primate.run/guides/backends/add-a-ruby-backend Add Ruby routes with the `@primate/ruby` module. Write handlers in Ruby; Primate compiles them to WebAssembly and wires them like JS routes. !!! Manage your Primate Ruby app as you would a normal Ruby project. Make sure you install dependencies into `vendor` via `bundle config set --local path 'vendor/bundle'`, so Primate can pick them up. !!! --- ### 1) Install Install the Primate Ruby package as well as the `primate-run` gem. ```sh $ npm install @primate/ruby && bundle install primate-run ``` --- ### 2) Configure Load the Ruby module in your configuration. %%% TypeScript:config/app.ts ```ts import ruby from "@primate/ruby"; import config from "primate/config"; export default config({ modules: [ruby()] }); ``` %%% --- ### 3) Write a route Compose a route in Ruby. %%% Ruby:routes/index.rb ```rb require 'primate/route' Route.get do |request| 'Hello from Ruby' end Route.post do |request| { ok: true } end ``` %%% Source: https://primate.run/guides/components/serve-markdown Add Markdown rendering with the `@primate/markdown` module. Write content in Markdown; Primate renders it to HTML on the server. !!! Markdown is converted to HTML for display. !!! --- ### 1) Install Install the Primate Markdown package. ```sh npm i @primate/markdown ``` --- ### 2) Configure Load the Markdown module in your configuration. ```ts import config from "primate/config"; import markdown from "@primate/markdown"; export default config({ modules: [ markdown(), ], }); ``` --- ### 3) Write content Compose content in Markdown. ```md # Hello, World! This is **Markdown** content. ``` --- ### 4) Render the content Use `view()` in a route to render the Markdown. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.md"); }, }); ``` Source: https://primate.run/guides/components/use-angular Add Angular components with the `@primate/angular` module. --- ### 1) Install Install the Primate Angular package and Angular. ```sh npm install @primate/angular @angular/core @angular/common ``` --- ### 2) Configure Load the Angular module in your configuration. ```ts import config from "primate/config"; import angular from "@primate/angular"; export default config({ modules: [ angular(), ], }); ``` --- ### 3) Write a component Compose a component in TypeScript. ```ts // components/welcome.component.ts import { Component, Input } from "@angular/core"; @Component({ template: `

Hello, {{ name }}!

`, }) export class WelcomeComponent { @Input() name!: string; } ``` --- ### 4) Render the component Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("welcome.component.ts", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-eta Add Eta templating with the `@primate/eta` module. --- ### 1) Install Install the Primate Eta package. ```sh npm i @primate/eta ``` --- ### 2) Configure Load the Eta module in your configuration. ```ts import config from "primate/config"; import eta from "@primate/eta"; export default config({ modules: [ eta(), ], }); ``` --- ### 3) Write a template Compose a template in Eta. ```html

Hello, <%= it.name %>!

``` --- ### 4) Render the template Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.eta", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-handlebars Add Handlebars templating with the `@primate/handlebars` module. --- ### 1) Install Install the Primate Handlebars package. ```sh npm i @primate/handlebars ``` --- ### 2) Configure Load the Handlebars module in your configuration. ```ts import config from "primate/config"; import handlebars from "@primate/handlebars"; export default config({ modules: [ handlebars(), ], }); ``` --- ### 3) Write a template Compose a template in Handlebars. ```hbs

Hello, {{name}}!

``` --- ### 4) Render the template Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.hbs", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-html Add HTML templates with the `@primate/html` module. --- ### 1) Install Install the Primate HTML package. ```sh $ npm install @primate/html ``` --- ### 2) Configure Load the HTML module in your configuration. %%% TypeScript:config/app.ts ```ts import html from "@primate/html"; import config from "primate/config"; export default config({ modules: [html()] }); ``` %%% --- ### 3) Write a template Compose a template in HTML. %%% HTML:views/Welcome.html ```html

Hello, ${name}!

``` %%% --- ### 4) Render the template Use `response.view` in a route to render the template. %%% TypeScript:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Welcome.html", { name: "World" }); }, }); ``` %%% Source: https://primate.run/guides/components/use-htmx Add HTMX interactivity with the `@primate/htmx` module. --- ### 1) Install Install the Primate HTMX package along with HTMX. ```sh npm i @primate/htmx ``` --- ### 2) Configure Load the HTMX module in your configuration. ```ts import config from "primate/config"; import htmx from "@primate/htmx"; export default config({ modules: [ htmx(), ], }); ``` --- ### 3) Write a template Compose a template in HTML with HTMX. ```html

Hello, World!

``` --- ### 4) Render the template Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.htmx"); }, }); ``` Source: https://primate.run/guides/components/use-marko Add Marko components with the `@primate/marko` module. --- ### 1) Install Install the Primate Marko package. ```sh npm i @primate/marko ``` --- ### 2) Configure Load the Marko module in your configuration. ```ts import config from "primate/config"; import marko from "@primate/marko"; export default config({ modules: [ marko(), ], }); ``` --- ### 3) Write a component Compose a component in Marko. ```marko

Hello, ${input.name}!

``` --- ### 4) Render the template Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.marko", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-react Add React components with the `@primate/react` module. --- ### 1) Install Install the Primate React package along with React and React DOM. ```sh $ npm install @primate/react react react-dom ``` --- ### 2) Configure Load the React module in your configuration. %%% TypeScript:config/app.ts ```ts import react from "@primate/react"; import config from "primate/config"; export default config({ modules: [react()] }); ``` %%% --- ### 3) Write a component Compose a component in JSX or TSX. %%% TypeScript:views/Welcome.tsx ```tsx export default function Welcome({ name }: { name: string }) { return

Hello, {name}!

; } ``` %%% --- ### 4) Render the component Use `response.view` in a route to render the template. %%% TypeScript:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Welcome.tsx", { name: "World" }); }, }); ``` %%% Source: https://primate.run/guides/components/use-solid Add Solid components with the `@primate/solid` module. --- ### 1) Install Install the Primate Solid package along with Solid. ```sh npm i @primate/solid solid-js ``` --- ### 2) Configure Load the Solid module in your configuration. ```ts import config from "primate/config"; import solid from "@primate/solid"; export default config({ modules: [ solid(), ], }); ``` --- ### 3) Write a component Compose a component in JSX or TSX. ```tsx // components/Welcome.tsx import { Component } from "solid-js"; const Welcome: Component<{ name: string }> = (props) => { return

Hello, {props.name}!

; }; export default Welcome; ``` --- ### 4) Render the component Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.tsx", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-svelte Add Svelte components with the `@primate/svelte` module. --- ### 1) Install Install the Primate Svelte package along with Svelte. ```sh npm i @primate/svelte svelte ``` --- ### 2) Configure Load the Svelte module in your configuration. ```ts import config from "primate/config"; import svelte from "@primate/svelte"; export default config({ modules: [ svelte(), ], }); ``` --- ### 3) Write a component Compose a component in Svelte. ```svelte

Hello, {name}!

``` --- ### 4) Render the component Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.svelte", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-voby Add Voby components with the `@primate/voby` module. --- ### 1) Install Install the Primate Voby package along with Voby. ```sh npm i @primate/voby voby ``` --- ### 2) Configure Load the Voby module in your configuration. ```ts import config from "primate/config"; import voby from "@primate/voby"; export default config({ modules: [ voby(), ], }); ``` --- ### 3) Write a component Compose a component in JSX or TSX. ```tsx // components/Welcome.tsx import { $ } from "voby"; export default function Welcome({ name }: { name: string }) { return

Hello, {$(name)}!

; } ``` --- ### 4) Render the component Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.tsx", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-vue Add Vue components with the `@primate/vue` module. --- ### 1) Install Install the Primate Vue package along with Vue. ```sh npm i @primate/vue vue ``` --- ### 2) Configure Load the Vue module in your configuration. ```ts import config from "primate/config"; import vue from "@primate/vue"; export default config({ modules: [ vue(), ], }); ``` --- ### 3) Write a component Compose a component in Vue SFC. ```vue ``` --- ### 4) Render the component Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.vue", { name: "World" }); }, }); ``` Source: https://primate.run/guides/components/use-web-components Add Web Components with the `@primate/web-components` module. !!! Web Components are client-side only. !!! --- ### 1) Install Install the Primate Web Components package. ```sh npm i @primate/web-components ``` --- ### 2) Configure Load the Web Components module in your configuration. ```ts import config from "primate/config"; import WebComponents from "@primate/web-components"; export default config({ modules: [ WebComponents(), ], }); ``` --- ### 3) Write a component Compose a component as a custom element. ```ts ``` --- ### 4) Render the component Use `response.view` in a route to render the template. ```ts // routes/index.ts import route from "primate/route"; import response from "primate/response"; export default route({ get() { return response.view("Welcome.webc", { name: "World" }); }, }); ``` Source: https://primate.run/guides/databases/use-mongodb Add MongoDB as a database with the `@primate/mongodb` module. Configure it in `config/db`; Primate connects to it and provides a unified API. !!! Ensure MongoDB is running and accessible. !!! --- ### 1) Install Install the Primate MongoDB package. ```sh $ npm install @primate/mongodb ``` --- ### 2) Configure Create a database configuration file. %%% TypeScript:config/db.ts ```ts import mongodb from "@primate/mongodb"; export default mongodb({ host: "localhost", port: 27017, database: "app", // username: "user", // password: "pass", }); ``` %%% --- ### 3) Create a store MongoDB stores abstract a collection. Give the store a name and wire the database. %%% TypeScript:stores/User.ts ```ts import p from "pema"; import store from "primate/store"; import db from "../config/db.ts"; export default store({ table: "user", db, schema: { id: store.key.primary(p.uuid), name: p.string, email: p.string.email(), }, }); ``` %%% --- ### 4) Use the store Use the store in routes. %%% TypeScript:routes/users.ts ```ts import User from "@/stores/User"; import route from "primate/route"; export default route({ async get() { const users = await User.find({}); return users; }, async post(request) { const user = await User.insert(await request.body.form()); return user; }, }); ``` %%% Source: https://primate.run/guides/databases/use-mysql Add MySQL as a database with the `@primate/mysql` module. Configure it in `config/db`; Primate connects to it and provides a unified API. !!! Ensure MySQL is running and accessible. !!! --- ### 1) Install Install the Primate MySQL package. ```sh $ npm install @primate/mysql``` --- ### 2) Configure Create a database configuration file. %%% TypeScript:config/db.ts ```ts import mysql from "@primate/mysql"; export default mysql({ host: "localhost", port: 3306, database: "app", // username: "user", // password: "pass", }); ``` %%% --- ### 3) Create a store MySQL stores abstract a table. Give the store a name and wire the database. %%% TypeScript:stores/User.ts ```ts import p from "pema"; import store from "primate/store"; import db from "../config/db.ts"; export default store({ table: "user", db, schema: { id: store.key.primary(p.uuid), name: p.string, email: p.string.email(), }, }); ``` %%% --- ### 4) Use the store Use the store in routes. %%% TypeScript:routes/users.ts ```ts import User from "@/stores/User"; import route from "primate/route"; export default route({ async get() { const users = await User.find({}); return users; }, async post(request) { const user = await User.insert(await request.body.form()); return user; }, }); ``` %%% Source: https://primate.run/guides/databases/use-oracle Add Oracle as a database with the `@primate/oracle` module. Configure it in `config/db`; Primate connects to it and provides a unified API. !!! Ensure Oracle is running and accessible. !!! --- ### 1) Install Install the Primate Oracle package. ```sh $ npm install @primate/oracledb ``` --- ### 2) Configure Create a database configuration file. %%% TypeScript:config/db.ts ```ts import oracledb from "@primate/oracledb"; export default oracledb({ host: "localhost", port: 1521, database: "FREEPDB1", // username: "user", // password: "pass", }); ``` %%% --- ### 3) Create a store Oracle stores abstract a table. Give the store a name and wire the database. %%% TypeScript:stores/User.ts ```ts import p from "pema"; import store from "primate/store"; import db from "../config/db.ts"; export default store({ table: "user", db, schema: { id: store.key.primary(p.uuid), name: p.string, email: p.string.email(), }, }); ``` %%% --- ### 4) Use the store Use the store in routes. %%% TypeScript:routes/users.ts ```ts import User from "@/stores/User"; import route from "primate/route"; export default route({ async get() { const users = await User.find({}); return users; }, async post(request) { const user = await User.insert(await request.body.form()); return user; }, }); ``` %%% Source: https://primate.run/guides/databases/use-postgresql Add PostgreSQL as a database with the `@primate/postgresql` module. Configure it in `config/db`; Primate connects to it and provides a unified API. !!! Ensure PostgreSQL is running and accessible. !!! --- ### 1) Install Install the Primate PostgreSQL package. ```sh $ npm install @primate/postgresql ``` --- ### 2) Configure Create a database configuration file. %%% TypeScript:config/db.ts ```ts import postgresql from "@primate/postgresql"; export default postgresql({ host: "localhost", port: 3306, database: "app", // username: "user", // password: "pass", }); ``` %%% --- ### 3) Create a store PostgreSQL stores abstract a table. Give the store a name and wire the database. %%% TypeScript:stores/User.ts ```ts import p from "pema"; import store from "primate/store"; import db from "../config/db.ts"; export default store({ table: "user", db, schema: { id: store.key.primary(p.uuid), name: p.string, email: p.string.email(), }, }); ``` %%% --- ### 4) Use the store Use the store in routes. %%% TypeScript:routes/users.ts ```ts import User from "@/stores/User"; import route from "primate/route"; export default route({ async get() { const users = await User.find({}); return users; }, async post(request) { const user = await User.insert(await request.body.form()); return user; }, }); ``` %%% Source: https://primate.run/guides/databases/use-sqlite Add SQLite as a database with the `@primate/sqlite` module. Configure it in `config/db`; Primate connects to it and provides a unified API. !!! SQLite is file-based or in-memory, no server needed. !!! --- ### 1) Install Install the Primate SQLite package. ```sh $ npm install @primate/sqlite ``` --- ### 2) Configure Create a database configuration file. %%% TypeScript:config/db.ts ```ts import sqlite from "@primate/sqlite"; export default sqlite({ database: "/tmp/app.db", // or ":memory:" for in-memory }); ``` %%% --- ### 3) Create a store SQLite stores abstract a table. Give the store a name and wire the database. %%% TypeScript:stores/User.ts ```ts import p from "pema"; import store from "primate/store"; import db from "../config/db.ts"; export default store({ table: "user", db, schema: { id: store.key.primary(p.uuid), name: p.string, email: p.string.email(), }, }); ``` %%% --- ### 4) Use the store Use the store in routes. %%% TypeScript:routes/users.ts ```ts import User from "@/stores/User"; import route from "primate/route"; export default route({ async get() { const users = await User.find({}); return users; }, async post(request) { const user = await User.insert(await request.body.form()); return user; }, }); ``` %%% Source: https://primate.run/guides/requests/forward-requests Proxy requests to upstream services using `request.forward()`. !!! Make sure you disable body parsing on the forwarded route, to keep Primate from preparsing the body. !!! --- ### Simple proxy Forward request as-is. ```ts // routes/api.ts import route from "primate/route"; export default route({ get(request) { return request.forward("https://api.example.com"); }, }); ``` --- ### Add headers You can add headers to send along the forwarded request. ```ts export default route({ post(request) { return request.forward("https://api.example.com", { "X-Custom": "value" }); }, }); ``` Source: https://primate.run/guides/requests/read-body Access the request body by calling methods on `request.body`. Methods validate that the body is of the correct type. --- ### Text -> `string` Use `text()` to get the body as a `string`. ```ts // routes/api.ts import route from "primate/route"; export default route({ async post(request) { const received = await request.body.text(); return { received }; }, }); ``` --- ### JSON -> `Record` Use `json()` to get the body as JSON (`Record`). ```ts // routes/api.ts import route from "primate/route"; export default route({ async post(request) { const received = await request.body.json(); return { received }; }, }); ``` --- ### Form -> `Record` Use `form()` to get the body as a form (`Record`). ```ts import route from "primate/route"; export default route({ async post(request) { const received = await request.body.form(); return { received }; }, }); ``` `form()` keeps the last value when a field name occurs more than once. Use `formEntries()` when repeated fields or their submission order matter. It returns a JSON-compatible array of `[name, value]` pairs. ```ts const entries = await request.body.formEntries(); const categoryIds = entries .filter(([name]) => name === "categoryIds") .map(([, value]) => value); ``` --- ### Multipart -> `Record` Use `multipart()` to get the body as a multpart form (`Record`). ```ts import route from "primate/route"; export default route({ async post(request) { const { form, files } = await request.body.multipart(); return { form }; }, }); ``` --- ### Binary -> `Blob` Use `binary()` to get the body as binary (`Blob`). ```ts // routes/api.ts import route from "primate/route"; export default route({ async post(request) { const blob = await request.body.blob(); return { blob }; }, }); ``` Source: https://primate.run/guides/requests/use-cookies Access cookies from the request object. Cookies are parsed from the `Cookie` header. --- ### Read cookies Access cookies via `request.cookies`. ```ts // routes/index.ts import route from "primate/route"; export default route({ get(request) { const sessionId = request.cookies.get("session_id"); return { sessionId }; }, }); ``` --- ### Set cookies Use the `Set-Cookie` header to set cookies in responses. ```ts import route from "primate/route"; export default route({ get() { return new Response("Cookie set", { headers: { "Set-Cookie": "session_id=abc123; HttpOnly" } }); }, }); ``` Source: https://primate.run/guides/requests/use-headers Access HTTP headers from the request object. Headers are parsed from the request. --- ### Read headers Access headers via `request.headers`. ```ts // routes/index.ts import route from "primate/route"; export default route({ get(request) { const userAgent = request.headers.get("User-Agent"); const contentType = request.headers.get("Content-Type"); return { userAgent, contentType }; }, }); ``` --- ### Set headers in response Use `response` with headers option. ```ts import route from "primate/route"; export default route({ get() { return new Response("Hello", { headers: { "X-Custom": "value" } }); }, }); ``` Source: https://primate.run/guides/requests/use-path-parameters Access dynamic path parameters from the request object. Parameters are URL-decoded. !!! Use `.get()` for required params, `.try()` for optional. !!! --- ### Define route with params Use brackets in route filename. %%% TypeScript:routes/user/[id].ts ```ts import route from "primate/route"; export default route({ get(request) { const id = request.path.get("id"); return { id }; }, }); ``` %%% --- ### Access parameters Read params via `request.path`. %%% TypeScript:routes/user/[id].ts ```ts import route from "primate/route"; export default route({ get(request) { const id = request.path.get("id"); // throws if missing const name = request.path.try("name"); // null if missing return { id, name }; }, }); ``` %%% --- ### Multiple params Stack brackets for nested params. %%% TypeScript:routes/user/[id]/posts/[post_id].ts ```ts import route from "primate/route"; export default route({ get(request) { const id = request.path.get("id"); const postId = request.path.get("post_id"); return { id, postId }; }, }); ``` %%% --- ### Optional params Use double brackets for optional. %%% TypeScript:routes/user/[[name]].ts ```ts import route from "primate/route"; export default route({ get(request) { const name = request.path.try("name") ?? "guest"; return { name }; }, }); ``` %%% Source: https://primate.run/guides/requests/use-query-string Access query parameters from the request object. Query params are URL-decoded. !!! Use `.get()` for required params, `.try()` for optional. !!! --- ### Access query params Read query via `request.query`. %%% TypeScript:routes/search.ts ```ts // routes/search.ts import route from "primate/route"; export default route({ get(request) { const q = request.query.get("q"); const limit = request.query.try("limit") ?? 10; return { q, limit }; }, }); ``` %%% Source: https://primate.run/guides/responses/handle-validation-errors Use **Pema** schemas to validate inputs. When validation fails, Pema throws a `ParseError`. By default, Primate automatically turns this into a structured `400 Bad Request` JSON response. --- ### Default behavior If you don't catch the error, Primate responds with a specific JSON format describing what failed. !!! This is the kind of behavior you want in most cases, as Primate's frontends have [validation](/docs/frontend#validate) and [form](/docs/frontend#form) helpers to keep client values validated and synced with the server. !!! ```ts import p from "pema"; import route from "primate/route"; const Signup = p({ email: p.string.email() }); export default route({ async post(request) { // throws a `ParseError` if the `email` field does not contain a valid email const { email } = Signup.parse(await request.body.form()); return `Signed up with ${email}`; }, }); ``` --- ### Custom JSON error If you want custom behavior, wrap your validation in a `try/catch` block to override the default and return a custom response instead. ```ts import ParseError from "pema/ParseError"; export default route({ async post(request) { try { const { email } = Signup.parse(await request.body.form()); return `Signed up with ${email}`; } catch (error) { if (ParseError.is(error)) { // returned as JSON return { error: "Please provide a valid email address." }; } // will show an error page throw error; } }, }); ``` --- ### Custom error page For frontend routes, you might prefer rendering a page rather than JSON. !!! `primate/response` bundles all standard response handlers such as `json`, `text`, and `error`. !!! ```ts import ParseError from "pema/ParseError"; import response from "primate/response"; export default route({ async post(request) { try { const { email } = Signup.parse(await request.body.form()); return `Signed up with ${email}`; } catch (error) { if (ParseError.is(error)) { return response.error("error.jsx", { message: "Invalid signup details" }); } throw error; } }, }); ``` Source: https://primate.run/guides/responses/redirect-to-login To conditionally redirect to a login page based on session status, create a hook file inside `routes` and check if the user has a valid session. If not, redirect to `/login` with `request.target` (pathname and query string). !!! A hook file inside the top `routes` directory applies to all routes of your application. If you want to limit the hook to certain routes, place it in a subdirectory. !!! %%% TypeScript:routes/+hook.ts ```ts import session from "@/config/session"; import response from "primate/response"; import route from "primate/route"; export default route.hook((request, next) => { // if at login page or session exists, pass through if (request.url.pathname === "/login" || session.exists()) { return next(request); } return response.redirect(`/login?next=${request.target}`); }); ``` %%% --- Inside your `/login` route, read the `next` query parameter to redirect back to the page the user came from. %%% TypeScript:routes/login.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("Login.jsx"); }, post(request) { // do verification work, create session, etc. return response.redirect(request.query.get("next")); }, }); ``` %%% Source: https://primate.run/guides/responses/serve-json Return objects to serve JSON responses. Primate automatically serializes returned objects to JSON. !!! Objects are JSON-serialized; use `response.json()` for granular control. !!! --- ### Return object Primate sets `Content-Type: application/json` on returned objects and serializes them to JSON strings. %%% TypeScript:routes/api.ts ```ts import route from "primate/route"; export default route({ get() { return ({ message: "Hello", data: [1, 2, 3] }); }, }); ``` %%% --- ### Explicit JSON response Use `response.json()` for custom options. %%% TypeScript:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.json({ error: "Not found" }, { status: 404 }); }, }); ``` %%% Source: https://primate.run/guides/responses/serve-plain-text Return strings to serve plain text responses. Primate sets `Content-Type: text/plain` on returned string. !!! Strings are served as text; use `response.text()` for granular control. !!! --- ### Return string Primate sets `Content-Type: text/plain` on returned string. %%% TypeScript:routes/hello.ts ```ts import route from "primate/route"; export default route({ get() { return "Hello, World!"; }, }); ``` %%% --- ### Explicit text response Use `response.text()` for custom options. %%% TypeScript:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.text("Not found", { status: 404 }); }, }); ``` %%% --- ### Custom content type Specify MIME type. %%% TypeScript:routes/index.ts ```ts import route from "primate/route"; export default route({ get() { return new Response("Custom text", { headers: { "Content-Type": "text/custom" }, }); }, }); ``` %%% Source: https://primate.run/guides/responses/show-errors Return error responses with appropriate status codes. Use `Response` for custom errors. !!! Throw errors to trigger `+error.ts`; return responses for controlled errors. !!! --- ### Throw error Triggers error handler. ```ts // routes/api.ts import route from "primate/route"; export default route({ get() { throw new Error("Something went wrong"); }, }); ``` --- ### Return error response Controlled error response. ```ts import response from "primate/response"; export default route({ get() { return response.json({ error: "Bad request" }, { status: 400 }); }, }); ``` --- ### Error handler Use `+error.ts` for global handling. Routes placed alongside `+error.ts` or in subdirectories in its tree will trigger it upon throwing. ```ts // routes/+error.ts import route from "primate/route"; export default route({ get() { return response.json({ error: "Internal error" }, { status: 500 }); }, }); ``` Source: https://primate.run/guides/responses/stream-binary-data Serve binary data like files or images. Use the `binary` response. --- ### Serve file Read and return binary data. ```ts // routes/file.ts import fs from "@rcompat/fs"; import response from "primate/response"; import route from "primate/route"; const file = fs.ref("path/to/file.pdf"); export default route({ get() { return response.binary(file); }, }); ``` --- ### Serve stream Response back with the uploaded steam -- `Blob`s are automatically streamed. ```ts // routes/backstream.ts import route from "primate/route"; export default route({ post(request) { return request.body.binary(); }, }); ``` Source: https://primate.run/guides/responses/stream-events-with-sse Use **Server-Sent Events (SSE)** to push updates to the browser over a long-lived HTTP response (`text/event-stream`). The `sse` helper handles headers and the connection lifecycle. !!! This assumes you have [installed and loaded](/guides/load-modules) `@primate/html` in your config file. !!! --- ### 1) Client — subscribe to events Create an HTML component that subscribes to SSE via `EventSource`. %%% HTML:views/index.html ```html ``` %%% --- ### 2) Serve a page (route choice is flexible) You can serve this page from **any** route. We'll use `/` for brevity. %%% TypeScript:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("index.html"); }, }); ``` %%% --- ### 3) Server — SSE endpoint Expose `/sse` and periodically send events. %%% TypeScript:routes/sse.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { const start = Date.now(); return response.sse(source => { const timer = setInterval(() => { source.send("passed", Math.floor((Date.now() - start) / 1000)); }, 5000); // every 5s return () => clearInterval(timer); }); }, }); ``` %%% The function passed to `response.sse` runs when the stream starts. Return a cleanup function to clear timers or unsubscribe from event channels when the client disconnects. Call `source.close()` when the server needs to end the stream; the same cleanup function runs exactly once in either case. For keyed in-memory pub/sub, see [`primate/events`](/docs/events). Source: https://primate.run/guides/responses/websocket-chat You can use the `ws` handler to upgrade a `GET` route to `WebSocket`. To use this to implement a real-time chat application, first create a HTML client. !!! This assumes you have [installed and loaded](/guides/load-modules) `@primate/html` in your config file. !!! --- The HTML frontend will automatically extract `
``` %%% --- Serve the client from the index route (`/`). %%% TypeScript:routes/index.ts ```ts import response from "primate/response"; import route from "primate/route"; export default route({ get() { return response.view("index.html"); }, }); ``` %%% --- At the `/ws` route, upgrade with the `ws` handler, maintaining a list of active web socket connections and distributing messages to all active chat participants. %%% TypeScript:routes/ws.ts ```ts import p from "pema"; import response from "primate/response"; import route from "primate/route"; const sockets = new Set(); export default route({ get(request) { // limit the number of messages a client can send const limit = p.loose.uint.default(20).parse(request.query.get("limit")); let n = 1; return response.ws({ close(socket) { sockets.delete(socket); }, message(_, message) { [...sockets.values()].forEach(socket => { if (n > 0 && n < limit) { n++; socket.send(message); } }); }, open(socket) { sockets.add(socket); }, }); }, }); ``` %%% Source: https://primate.run/guides/routes/404-fallback-vs-error Use a **rest route** for "not found" pages. Use **`+error.ts`** to handle errors associated with matched routes, including failures in request hooks that run before route dispatch. Only the **nearest** `+error.ts` runs; error handlers don't compose. !!! 404 fallback ≠ error handler. The fallback only runs when nothing else matched. `+error.ts` runs when a matched route, layout, route hook, or earlier application request hook throws. !!! --- ### 1) Not-found fallback (handles unmatched URLs) Return a normal error response. ```ts // routes/[[...path]].ts import route from "primate/route"; export default route({ get() { return new Response("Not found", { status: 404 }); }, }); ``` --- ### 2) Error handler (handles errors associated with matched routes) `+error.ts` is triggered by errors in route processing and application request hooks. If browser and API routes need different representations, inspect the request in the error handler and return HTML, JSON, or a plain response as appropriate. ```ts // routes/+error.ts import route from "primate/route"; import response from "primate/response"; // or return a rendered error view export default route({ get() { return response.redirect("/"); }, }); ``` --- ### 3) When to use which - Use the **fallback route** to show a friendly 404. - Use **`+error.ts`** for exceptions, timeouts, validation errors, and pre-routing request-hook failures associated with routes that **did** match. Source: https://primate.run/guides/routes/nested-dynamic-params Capture multiple path parameters by stacking bracketed segments and read them via `request.path` (e.g., `routes/user/[id]/posts/[post].ts`). !!! Parameters are URL-decoded. For single-bracket segments like `[id]` or `[post]`, the param is **guaranteed to exist if the route matched**, so `request.path.get("id")` and `request.path.get("post")` are safe. Use `.try(key)` only with **optional** segments like `[[name]]`. !!! --- ### 1) Define nested params in the filesystem Create a route with multiple bracketed segments and read each one from `request.path.get(...)`. %%% TypeScript:routes/user/[id]/posts/[post].ts ```ts import route from "primate/route"; export default route({ get(request) { const id = request.path.get("id"); const post = request.path.get("post"); return `user ${id}, post ${post}`; }, }); ``` %%% --- ### 2) Validate or coerce as needed Coerce string params into the types you need and return a 4xx response when validation fails (e.g., ensure `id` is a number). %%% TypeScript:routes/user/[id]/posts/[post].ts ```ts import route from "primate/route"; export default route({ get(request) { const id = Number(request.path.get("id")); if (Number.isNaN(id)) return new Response("Invalid id", { status: 400 }); return { id, post: request.path.get("post") }; }, }); ``` %%% Source: https://primate.run/guides/routes/optional-params-with-try Use **double brackets** for optional segments and read them safely with `.try()` (or a schema default). !!! `.get(key)` throws when the parameter is absent; prefer `.try(key)` for optional segments. !!! --- ### 1) Define the optional segment Create a route file with an optional param. ```sh touch routes/user/[[name]].ts ``` --- ### 2) Support both `/user` and `/user/:name` Use `try` with a fallback in case the param is `undefined`. ```ts // routes/user/[[name]].ts import route from "primate/route"; export default route({ get(request) { const name = request.path.try("name") ?? "guest"; return `Hello, ${name}!`; }, }); ``` Source: https://primate.run/guides/routes/proxy-webhook-with-raw-body Forward inbound webhooks **unchanged** to an upstream service. --- ### Forward Use `request.forward` to forward the request as-is. ```ts // routes/webhook.ts import route from "primate/route"; export default route({ post(request) { return request.forward("https://upstream.example.com/webhooks/provider"), }, }); ``` --- ### Gate by provider IP/secret before forwarding Add a check before forwarding upstream. ```ts // routes/webhook.ts import route from "primate/route"; export default route({ post(request) { if (request.headers.try("X-Secret") !== process.env.WEBHOOK_SECRET) { return new Response("unauthorized", { status: 401 }); } return request.forward("https://upstream.example.com/webhooks/provider"); }, }); ``` Source: https://primate.run/guides/routes/respond-to-options APIs under `/api/*` might need to service CORS preflights. Reply with 204 and the appropriate `Access-Control-*` headers. !!! Adjust `Access-Control-Allow-Origin` and headers to your needs. !!! --- ### 1) Handle preflights for all /api/* paths Define an `OPTIONS` route function. ```ts // routes/api/[...segments].ts import route from "primate/route"; export default route({ options() { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Max-Age": "600", }, }); }, }); ``` --- ### 2) Add your actual handlers Add substantive handlers. ```ts import route from "primate/route"; export default route({ get() { return "ok"; }, post() { return "ok"; }, }); ``` Source: https://primate.run/guides/routes/single-handler-for-api Prototype quickly by fanning in your API under one file with a rest segment and branching on `request.method` and the captured path. !!! Great for small APIs; easy to split into dedicated files as the surface grows. !!! --- ### 1) Capture the remaining path Create a unified resolver for a request facade. ```ts // routes/api/[...segments].ts import route from "primate/route"; import type { RequestFacade } from "primate/request"; function path(request: RequestFacade) { return request.path.get("segments"); // e.g., "users/42" } ``` --- ### 2) Dispatch per method (and optionally by path) Expose API verbs. ```ts // Example branching by prefix: // if (path(request).startsWith("users/")) { ... } export default route({ get(req) { return `GET /api/${path(req)}`; }, post(req) { return `POST /api/${path(req)}`; }, put(req) { return `PUT /api/${path(req)}`; }, delete(req) { return `DELETE /api/${path(req)}`; }, }); ``` Source: https://primate.run/guides/validation/validate-request-bags Validate different parts of the request (query, params, headers). Use Pema schemas. !!! Validate all input sources for security. !!! --- ### Validate query params Schema for query string. ```ts // routes/search.ts import route from "primate/route"; import p from "pema"; const Query = p.loose({ q: p.string.min(1), limit: p.uint.max(100).default(10), }); export default route({ get(request) { const { q, limit } = Query.parse(request.query); return { q, limit }; }, }); ``` --- ### Validate path params Schema for path parameters. ```ts // routes/user/[id].ts import route from "primate/route"; import p from "pema"; export default route({ get(request) { const id = p.loose.uint.parse(request.path.get("id")); return { id }; }, }); ``` --- ### Validate headers Schema for headers. ```ts import route from "primate/route"; import p from "pema"; const IsBearer = p({ Authorization: p.string.startsWith("Bearer "), }); export default route({ get(request) { const { Authorization } = IsBearer.parse(request.headers); return { auth: Authorization }; }, }); ``` Source: https://primate.run/guides/validation/validate-request-body Validate incoming request body with schemas. Use Pema for type-safe validation. !!! Validation throws on failure; handle errors appropriately. !!! --- ### 1) Define schema Create validation schema. ```ts // routes/user.ts import p from "pema"; const UserSchema = p({ name: p.string.min(1), email: p.string.email(), }); ``` --- ### 2) Validate in route Attach the schema to the route. Primate validates the body before returning it from `request.body.form()`. ```ts // routes/user.ts import p from "pema"; import route from "primate/route"; const UserSchema = p({ name: p.string.min(1), email: p.string.email(), }); export default route({ post: route.with( { body: UserSchema, contentType: "application/x-www-form-urlencoded", }, async request => { const user = await request.body.form(); return { user }; }, ), }); ``` For JSON, pair the schema with `application/json` and read with `request.body.json()`. ```ts // routes/user.ts import p from "pema"; import route from "primate/route"; const UserSchema = p({ name: p.string.min(1), email: p.string.email(), }); export default route({ post: route.with( { body: UserSchema, contentType: "application/json", }, async request => { const user = await request.body.json(); return { user }; }, ), }); ``` --- ### 3) Derive a body value Use `.derive(...)` to validate an object and return a normalized value to the handler. ```ts // routes/user.ts import p from "pema"; import route from "primate/route"; const UserName = p({ name: p.string.min(1), }).derive(({ name }) => name.trim().toUpperCase()); export default route({ post: route.with( { body: UserName, contentType: "application/json", }, async request => { const name = await request.body.json(); return { name }; }, ), }); ``` --- ### 4) Use an async body schema Use `p.async(...)` when normalization needs to await work before the handler receives the body. ```ts // routes/user.ts import p from "pema"; import route from "primate/route"; const User = p.async({ email: p.string.email(), }).derive(async ({ email }) => { return await findOrCreateUser(email); }); export default route({ post: route.with( { body: User, contentType: "application/json", }, async request => { const user = await request.body.json(); return { user }; }, ), }); ``` --- ### 5) Handle validation errors Catch and respond to errors when you need custom output. If you don't catch the error, Primate returns a `400 Bad Request` validation response. ```ts import p from "pema"; import route from "primate/route"; import response from "primate/response"; const UserSchema = p({ name: p.string.min(1), email: p.string.email(), }); export default route({ post: route.with( { body: UserSchema, contentType: "application/json", }, async request => { try { const user = await request.body.json(); // save user return { success: true, user }; } catch (error) { return response.json({ error: error.message }, { status: 400 }); } }, ), }); ``` Source: https://primate.run/guides/validation/validate-with-pema Pema stands for **P**rimate sch**ema**. Use Pema for type-safe validation of data. !!! Pema provides composable validators for common types. !!! --- ### Basic usage Import and use validators. ```ts import p from "pema"; const Schema = p({ name: p.string.min(1), age: p.number.min(0), }); const data = Schema.parse({ name: "John", age: 30 }); ``` --- ### Built-in validators - `string`: String validation - `number`: Number validation - `boolean`: Boolean validation - `array`: Array validation - `object`: Object validation --- ### Chaining Chain validators for complex rules. ```ts import p from "pema"; const EmailSchema = p.string.min(1).max(255).email(); const AgeSchema = p.number.integer().min(0).max(150); ``` --- ### Error handling Validation throws `ParseError` with details. ```ts try { Schema.parse(input); } catch (error) { console.log(error.errors); // detailed errors } ```