Quick start

gotsx is a Go module. An app = one Go package (routes, host modules, actions) + one app/ directory (pages, components, and islands written in the dialect). The compiler turns app/ into Go and JS under gen/, then go build.

1. Create an app

One go install, one gotsx new — the result runs right away

terminal
go install github.com/childrentime/gotsx/cmd/gotsx@latest
gotsx new hello && cd hello   # scaffold an app in its own Go module (host module, page, island, action, tsconfig)
gotsx dev                     # compile → go build → run; rebuild + browser reload on change
gotsx check                   # type-check only — the same diagnostics the editor LSP shows
go build -o hello .           # one self-contained binary
gotsx dev first runs cmd/hostgen to generate host types, then compiles the dialect, then go build and starts up; after that it watches app/ and goes live again about 2 seconds after a change — the browser reloads by itself. When a compile fails, the old version keeps running. The repo's own demos run with make dev-example / dev-shop.

2. Directory conventions

example/
hello/
├── go.mod               # module hello · require github.com/childrentime/gotsx  (gotsx.json is optional)
├── tsconfig.json        # editor support: app/.gen/gotsx.d.ts + host.d.ts are generated
├── main.go              # gotsx.Serve(gotsx.Options{Routes: gen.Routes, ...})
├── host/host.go         # host module (Go)
├── cmd/hostgen/         # reflect host.Registry → app/.gen/host.d.ts + host.json
├── app/
│   ├── pages/           # file routing: index.server.tsx → /, p/[id].server.tsx → /p/{id}, docs/[...slug].server.tsx → catch-all
│   ├── components/      # *.server.tsx → Go only
│   ├── islands/         # *.client.tsx → Go (SSR) + JS (signals)
│   ├── ui/              # no suffix = shared, compiled to both
│   └── tailwind.css     # present → Tailwind runs
├── public/              # static files → /public/*
└── gen/                 # generated output: *_gen.go, routes_gen.go, client/*.js

The suffix decides the compile target: .server.tsx is Go only, .client.tsx is Go + JS (an island), and no suffix is a shared component (both sides). Files under pages/ are routes, [id] is a path parameter and [...slug] a catch-all. Inside a page, redirect() / notFound() abort the render.

3. Write a page

A page is an export default component whose props are always PageProps

app/pages/index.server.tsx
import type { PageProps } from "gotsx";
import { models } from "host:data";        // Go-backed, zero marshalling
import Layout from "../components/Layout.server";
import ModelCard from "../components/ModelCard.server";

export default function Home({ query }: PageProps) {
  const q = query.q ?? "";
  const list = models.search(q);           // synchronous: concurrency from goroutines
  return (
    <Layout title="Products">
      <div class="grid">{list.map((m) => <ModelCard model={m} />)}</div>
      {list.length === 0 && <p class="empty">No matching products</p>}
    </Layout>
  );
}

Note there's no async: host calls are synchronous, and concurrency between requests comes from goroutines.

4. Write an island

Put the interactive parts into .client.tsx

app/islands/Counter.client.tsx
import { useState, useEffect } from "gotsx";

export default function Counter({ start }: { start: number }) {
  const [n, setN] = useState(start);
  const double = n * 2;                    // depends on n → auto memo
  useEffect(() => { console.log(n); });    // JS backend only
  return (
    <button onClick={() => setN(n + 1)}>
      {n} ×2 = {double}{n > 4 && <b> 🔥</b>}
    </button>
  );
}

Use it like any component inside a server component: <Counter start={0} />. Props must be JSON-serializable (they go into an HTML attribute); an island takes no children.

5. Expose Go capabilities

A host module = one Go value; fields map by json tag and methods by lowercased first letter

host/host.go
// host/host.go — the host module: the other side of import { models } from "host:data"
type ModelStore struct{ items []Model }

func (s *ModelStore) Search(q string) []Model {}
func (s *ModelStore) Get(id string) (Model, error) {   // error → 404 at the request layerreturn Model{}, fmt.Errorf("%w: %s", gotsx.ErrNotFound, id)
}

var Registry = map[string]gotsx.HostModule{
	"data": {Value: &DataModule{Models: store}, Go: "host.Data"},
}
app/.gen/host.d.ts (generated)
// app/.gen/host.d.ts — reflected from Go; what TSX sees = what Go exposes
declare module "host:data" {
  export interface Model { id: string; title: string; likes: number; tags: string[] }
  export interface ModelStore {
    search(arg0: string): Model[];
    get(arg0: string): Model;
  }
  export const models: ModelStore;
}

After compilation, models.search(q) is just host.Data.Models.Search(q): no marshalling, no reflection. Go's int and the dialect's number convert automatically; for a method returning error, the error becomes a panic recovered by the request layer, and one wrapping gotsx.ErrNotFound turns into a 404.

6. From an island back to Go: typed actions

List a Go method in Actions; the island imports it and awaits it — the compiler generates both halves of the HTTP call

host/host.go
// host/host.go — list the methods islands may call; hostgen types them as Promise<T>
func (d *DataModule) Like(id string) (int, error) { return d.Models.Like(id) }
func (d *DataModule) Rename(req *gotsx.Req, id, title string) error {} // *gotsx.Req: session / cookies

var Registry = map[string]gotsx.HostModule{
	"data": {Value: Data, Go: "host.Data", Actions: []string{"Like", "Rename"}},
}
app/islands/LikeButton.client.tsx
// app/islands/LikeButton.client.tsx — a typed action: no fetch, no untyped JSON
import { like } from "host:data";              // like(id: string): Promise<number>

export default function LikeButton({ id, likes }: { id: string; likes: number }) {
  const [n, setN] = useState(likes);
  return <button onClick={async () => setN(await like(id))}>{n}</button>;
}
// main.go: gotsx.Options{HostActions: gen.HostActions, …}
// errors: gotsx.Invalid(fields) → 422 (e.fields), gotsx.ErrNotFound → 404, else 500 — the call throws an Error with e.status

Sessions, flash messages and CSRF tokens come with it: an action with a *gotsx.Req parameter can read and write the signed session; pages receive props.session, props.flash and props.csrf for classic forms. A page can also export meta(props): Meta and the layout renders it into <head>.

7. Styling: Tailwind

If app/tailwind.css exists, the Tailwind standalone CLI runs on every build

app/tailwind.css
/* app/tailwind.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme { --color-brand-600: #1557d6; }

class="..." in the dialect is just a plain string; at build time Tailwind scans app/**/*.tsx to generate public/tailwind.css. Binary lookup order: $GOTSX_TAILWIND → the repo's .tools/tailwindcss → PATH. Also no Node. Every class on this site came from exactly this.