claude/skills/effect-start-routing/SKILL.md
Use when creating routes in effect-start applications using Route and HyperRoute APIs.
npx skillsauth add nounder/dotfiles effect-start-routingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Organize routes in a directory hierarchy:
src/routes/
├── route.tsx # handles /
├── layer.tsx # middleware for all routes
├── users/
│ ├── route.tsx # handles /users
│ └── [id]/
│ └── route.tsx # handles /users/:id
└── [[404]]/
└── route.ts # catch-all fallback
In src/routes/tree.ts maintain the tree:
// src/routes/tree.ts
import { Route } from "effect-start"
export default Route.tree({
"*": await import("./layer.tsx").then(v => v.default),
"/": await import("./route.tsx").then(v => v.default),
"/users": await import("./users/route.tsx").then(v => v.default),
"/users/:id": await import("./users/[id]/route.tsx").then(v => v.default),
})
Each route.ts or route.tsx file exports a default route:
// src/routes/users/route.tsx
import { Route } from "effect-start"
export default Route.get(
Route.json({ users: [] })
)
Route.get(...) // GET
Route.post(...) // POST
Route.put(...) // PUT
Route.patch(...) // PATCH
Route.del(...) // DELETE
Route.head(...) // HEAD
Route.options(...) // OPTIONS
Route.use(...) // middleware (all methods)
Methods can be chained:
export default Route
.get(Route.json({ message: "get" }))
.post(Route.json({ message: "post" }))
Returns plain text (Content-Type: text/plain):
Route.get(Route.text("Hello, world!"))
Route.get(Route.text(function*(ctx) {
return "Dynamic text"
}))
Returns HTML (Content-Type: text/html):
Route.get(Route.html("<h1>Hello</h1>"))
Route.get(Route.html(function*(ctx) {
return `<h1>Hello ${ctx.name}</h1>`
}))
Returns JSON (Content-Type: application/json):
Route.get(Route.json({ message: "Hello" }))
Route.get(Route.json(function*(ctx) {
const data = yield* fetchData()
return { data }
}))
Returns binary data (Content-Type: application/octet-stream):
Route.get(Route.bytes(new Uint8Array([1, 2, 3])))
Full control over response format (no default Content-Type):
import { Entity, Route } from "effect-start"
Route.get(Route.render(function*(ctx) {
return Entity.make("<custom>data</custom>", {
headers: { "content-type": "application/xml" }
})
}))
Server-sent events streaming:
import { Stream } from "effect"
import { Route } from "effect-start"
Route.get(
Route.sse(
Stream.fromIterable([
{ event: "message", data: "Hello" },
{ event: "message", data: "World" },
])
)
)
Use Entity.make() for full control over status, headers, and body:
import { Entity, Route } from "effect-start"
// Redirect
Route.get(Route.render(function*() {
return Entity.make("", {
status: 301,
headers: { "location": "/new-path" }
})
}))
// Custom headers
Route.get(Route.json(function*() {
return Entity.make({ data: "value" }, {
status: 201,
headers: {
"x-custom": "header",
"cache-control": "no-cache"
}
})
}))
// Not found
Route.get(Route.render(function*() {
return Entity.make("Not found", {
status: 404,
headers: { "content-type": "text/plain" }
})
}))
layer.tsx files apply middleware to all nested routes:
// src/routes/layer.tsx
import { Route } from "effect-start"
import { BunRoute } from "effect-start/bun"
export default Route.use(
// Wrap HTML routes with a Bun HTML bundle (using a %children% placeholder)
Route.html(
BunRoute.bundle(() => import("../app.html"))
),
// wrap all JSON responses in data property
Route.json(function*(ctx, next) {
const value = yield* next()
return { data: value }
})
)
The next() function calls the inner route handler.
Handlers receive a context object with route descriptors and bindings:
Route.get(Route.json(function*(ctx) {
// ctx.method - HTTP method
// ctx.format - response format
// ctx.request - raw request object
return { method: ctx.method }
}))
import { Schema } from "effect"
import { Route } from "effect-start"
export default Route
.use(
Route.schemaHeaders(
Schema.Struct({
"authorization": Schema.String,
})
)
)
.get(
Route.json(function*(ctx) {
// ctx.authorization is typed as string
return { auth: ctx.authorization }
})
)
import { Schema } from "effect"
import { Route } from "effect-start"
// For route /users/:id
export default Route
.use(
Route.schemaUrlParams(
Schema.Struct({
"id": Schema.NumberFromString,
})
)
)
.get(
Route.json(function*(ctx) {
// ctx.id is typed as number
return { userId: ctx.id }
})
)
Route.tree({
"/": ..., // exact match
"/users": ..., // literal path
"/users/:id": ..., // named parameter
"/files/:path*": ..., // zero or more segments
"/docs/:slug+": ..., // one or more segments
"/:id?": ..., // optional parameter
"[[404]]": ..., // catch-all (optional)
"[...rest]": ..., // catch-all (required)
})
For JSX routes, use HyperRoute from effect-start/hyper:
import { Route } from "effect-start"
import { HyperRoute } from "effect-start/hyper"
export default Route.get(
HyperRoute.html(function*() {
return (
<div>
<h1>Hello, world!</h1>
</div>
)
})
)
Make sure jsxImportSource is set to effect-start in tsconfig.json
Combine multiple streams for complex real-time updates:
import { Effect, Option, Stream, SubscriptionRef } from "effect"
import { Route } from "effect-start"
Route.get(
Route.sse(
Effect
.gen(function*() {
return Stream.make(1, 2, 3, 4).pipe(
Stream.map((status) => ({
event: "status",
data: status,
})),
)
),
)
Use BunRoute.htmlBundle() to wrap routes in an HTML layout:
import { Route } from "effect-start"
import { BunRoute } from "effect-start/bun"
export default Route.tree({
"*": Route.use(
BunRoute.htmlBundle(() => import("./app.html")),
),
"/": Route.get(Route.html("<h1>Content goes here</h1>")),
})
testing
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
development
Use when writing Zig code. Contains Zig 0.15 API changes and patterns.
development
Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like "the xlsx in my downloads") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved.
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.