Typed routing, no regex
Literal segments, :params, optional and wildcard segments. Param names are inferred from the pattern, so a typo in params.id is a compile error, not a 500.
@samveys/http
Routing, validation, a database layer, a CLI and 47 packages that share one container and one config file. Typed from route params to response bodies.
npx @samveys/console new my-app
$ curl -i http://samveys.sadata-utility.apps.kb-prod.sadata.dev/_demo/subscribe \
-H 'content-type: application/json' \
-d '{"name":"Ada","email":"ada@","role":"backend"}'
HTTP/1.1 422 Unprocessable Entity
content-type: application/problem+json
content-security-policy: default-src 'self'; frame-ancestors 'none'
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: no-referrer
x-request-id: fdcde48b-6e2a-4cc0-b0c0-285e53fa8cef
{
"code": "SVS_E042",
"title": "Validation failed",
"status": 422,
"errors": [{ "pointer": "#/body/email", "detail": "Invalid email address" }]
}
The headers and the error body above come from the framework. The route only declares a schema; Samveys rejects the bad email before the handler runs.
Each of these is part of the packages a new project starts with.
Literal segments, :params, optional and wildcard segments. Param names are inferred from the pattern, so a typo in params.id is a compile error, not a 500.
@samveys/http
Body, query and params schemas implement Standard Schema v1. The handler receives the parsed output type; failures become 422 Problem Details with JSON pointers.
@samveys/validator
CSP, X-Frame-Options, Referrer-Policy and HSTS over HTTPS, a 1 MiB body limit, a 30 s handler timeout, prototype-less request data and stripped internal headers.
secure by default
An IoC container with inject(), providers and a plugin lifecycle. Every feature is a plugin you register in samveys.config.ts — and every plugin can be replaced.
@samveys/core
sse() streams with Last-Event-ID resume and keep-alive pings. The RFC 6455 server runs on the same port with origin allow-lists, auth and schema-validated messages.
realtime
Blade-like .tpl.html files with layouts, sections and <x-components> compile to ES modules. Expressions are parsed, never evaluated, and constructor access is rejected.
@samveys/template
createTestApp() boots a fresh app per test with an in-process HTTP client, fake time, event fakes and hook recording — and cleans itself up through await using.
@samveys/testing
Shipped by @samveys/console and installed per project. new, serve --watch, make:* generators, route:list, route:types, explain SVS_E042, check, build --docker and a REPL. Your own commands use the same defineCommand.
@samveys/console
Every request runs inside samveys:http.request with OpenTelemetry-style span attributes. An APM subscribes without importing a single Samveys module.
observability
Seven short files from a real project. Every API here exists in version 1.0.
Route files load lazily from samveys.config.ts. Param types come from the pattern itself.
import { defineController, defineRoutes, get, post } from '@samveys/http'
const users = defineController('users', {
prefix: '/users',
routes: {
show: get('/:id', ({ params }) => ({ id: params.id })),
create: post('/', ({ response }) => response.created({ created: true })),
},
})
export default defineRoutes((router) => {
router.get('/ping', () => 'pong')
router.controller(users)
})
The handler receives the parsed type. A bad body never reaches it.
import { schema } from '@samveys/validator'
const createUser = schema.object({
email: schema.string().email(),
age: schema.coerceNumber().int().optional(),
})
router.post('/users', { body: createUser }, ({ body }) => {
// body: { email: string; age?: number }
return { email: body.email }
})
// POST /users {"email":"nope"} →
// 422 application/problem+json
// { "code": "SVS_E042", "errors": [{ "pointer": "#/body/email" }] }
One file wires the whole app. Configuration is read at run time, never baked into a build.
import { defineConfig } from '@samveys/core'
import { database, sqlite } from '@samveys/database'
import { health } from '@samveys/health'
import { http } from '@samveys/http'
import { orm } from '@samveys/orm'
export default defineConfig({
name: 'my-app',
plugins: [
http({
port: Number(process.env.PORT ?? 3000),
routes: [() => import('./app/routes.js')],
}),
database({
default: 'main',
connections: { main: sqlite({ file: 'storage/app.db' }) },
}),
orm(),
health(),
],
})
This very page is rendered this way: a layout, sections, includes and components.
@layout('layouts.base')
@section('content')
<h1>{{ title }}</h1>
@each(user in users)
<x-user-card :user="user" />
@empty
<p>No users yet.</p>
@endeach
@endsection
In-process HTTP, real kernel, no port, no cleanup code.
import { test } from 'node:test'
import { http } from '@samveys/http'
import { createTestApp } from '@samveys/testing'
import { messages } from '../app/messages.js'
test('creates a validated message', async () => {
await using app = await createTestApp({
plugins: [http()],
routes: (router) => router.controller(messages),
})
;(await app.http.post('/messages', { json: { text: ' hello ' } }))
.assertStatus(201)
.assertJson({ id: 'm-1', text: 'hello' })
;(await app.http.post('/messages', { json: { text: '' } }))
.assertProblem(422, 'SVS_E042')
})
Server-Sent Events and WebSockets on the same port, without a dependency.
import { http, sse, websockets, ws } from '@samveys/http'
export const plugins = [
http(),
websockets({
routes: [
ws('/chat', { origins: ['https://app.example.com'] }, {
message: (socket, data) => socket.send(data as string),
}),
],
}),
]
router.get('/events', (ctx) =>
sse(ctx, async function* ({ lastEventId }) {
yield { id: Number(lastEventId ?? 0) + 1, event: 'tick', data: { at: Date.now() } }
}),
)
A plugin declares what it needs, binds into the container and hooks the lifecycle.
import { definePlugin } from '@samveys/core'
export const audit = definePlugin<{ channel?: string }>({
name: '@acme/audit',
requires: { plugins: ['@samveys/http'] },
boot({ container, config, app }) {
app.logger.info(`audit on ${config.channel ?? 'default'}`)
},
})
// Scope it to some routes only:
router.group({ prefix: '/admin', plugins: [audit] }, (admin) => {
admin.get('/overview', () => ({ ok: true }))
})
Everything a request touches lives in a few known files. Here is what goes where, then each piece with real code.
samveys.config.ts
Registers http({ port, routes, middleware, securityHeaders, … }) with every other plugin.
bin/server.ts
createApp(config) → app.run() → createHttpServer(app) → server.listen(). svs serve runs this file.
app/routes.ts
defineRoutes((router) => …): the route file http() loads lazily. Add more files to the routes array.
app/controllers/*.ts
defineController(name, { prefix, use, routes }): named routes grouped under one prefix.
app/middleware/*.ts
(ctx, next) => Response: global, group, controller or route level.
Patterns are literal, :param, :param? and a trailing * wildcard. No regular expressions, so a pattern always means one thing.
import { defineRoutes } from '@samveys/http'
export default defineRoutes((router) => {
router.get('/ping', () => 'pong') // text/plain
router.get('/users/:id', ({ params }) => ({ id: params.id }))
router.get('/posts/:slug?', ({ params }) => ({ slug: params.slug ?? 'latest' }))
router.get('/files/*path', ({ params }) => ({ path: params.path }))
router.delete('/users/:id', () => undefined) // 204 No Content
})
A controller is a named set of routes under one prefix. A group adds a prefix, middleware or route-scoped plugins to everything inside it.
import { defineController, del, get, post } from '@samveys/http'
import { requireAuth } from '../middleware/require-auth.js'
export const postController = defineController('posts', {
prefix: '/posts',
use: [requireAuth],
routes: {
index: get('/', () => ({ data: [] })), // name: posts.index
show: get('/:id', ({ params }) => ({ id: params.id })),
create: post('/', ({ response }) => response.created({ ok: true }, '/posts/1')),
destroy: del('/:id', ({ response }) => response.noContent()),
},
})
// app/routes.ts
router.controller(postController)
router.group({ prefix: '/admin', use: [requireAuth] }, (admin) => {
admin.get('/stats', () => ({ users: 42 }))
})
A handler gets one context object and returns a value. The kernel turns that value into a Response.
router.get('/search', async ({ query, headers, signal, requestId, clientIp, state, response }) => {
response.headers.set('cache-control', 'no-store') // merged into the final response
const hits = await fetch(`https://search.internal/?q=${query.q}`, { signal })
return { requestId, ip: clientIp, hits: await hits.json() } // → application/json
})
// What a handler may return:
// Response sent as-is
// string 200 text/plain
// undefined 204 No Content
// anything else 200 application/json
// Helpers: response.json() .text() .created() .noContent() .redirect() .stream() .problem()
Middleware is (ctx, next) => Response. Order: global → group → controller → route, then unwinding in reverse.
import type { Middleware } from '@samveys/http'
export const requireAuth: Middleware = async (ctx, next) => {
if (!ctx.headers.has('authorization')) {
return Response.json({ error: 'Unauthorized' }, { status: 401 }) // stop here
}
ctx.state.startedAt = performance.now()
const response = await next() // run the rest
response.headers.set('server-timing', `app;dur=${performance.now() - (ctx.state.startedAt as number)}`)
return response
}
// samveys.config.ts → global middleware
http({ middleware: [requireAuth], routes: [() => import('./app/routes.js')] })
params, query and body schemas run before the handler; a response schema runs after it. All Standard Schema v1.
import { schema } from '@samveys/validator'
const listQuery = schema.object({
page: schema.coerceNumber().int().positive().optional().default(1),
q: schema.string().trim().optional(),
})
const publicUser = schema.object({ id: schema.number(), name: schema.string() })
router.get('/users', { query: listQuery }, ({ query }) => ({ page: query.page }))
router.get('/me', { response: publicUser }, () => ({
id: 1, name: 'Ada', passwordHash: 'never sent', // stripped by the schema
}))
http() takes the server config; createHttpServer() starts it on the node driver, or on Fastify or Hono.
import { http } from '@samveys/http'
// import { fastify } from '@samveys/fastify'
http({
host: '0.0.0.0',
port: Number(process.env.PORT ?? 3000),
routes: [() => import('./app/routes.js'), () => import('./app/admin-routes.js')],
middleware: [],
requestTimeout: 30_000, // ms, 503 SVS_E044 after this
bodyLimit: 1_048_576, // bytes, 413 SVS_E043 above this
trustProxy: ['10.0.0.1'], // read X-Forwarded-* only from these
compression: { encodings: ['zstd', 'br', 'gzip'], threshold: 1024 },
securityHeaders: { contentSecurityPolicy: "default-src 'self'" },
// driver: fastify(),
})
Read from router.list(): the same router svs route:list prints. Add a route and it shows up here.
| Method | Pattern | Params | Validated |
|---|---|---|---|
| GET | / |
— | — |
| GET | /docs |
— | — |
| GET | /docs/:section/:slug |
section, slug | — |
| GET | /api |
— | — |
| GET | /api/:package |
package | — |
| GET | /ecosystem |
— | — |
| GET | /benchmarks |
— | — |
| GET | /support |
— | — |
| GET | /assets/:file |
file | — |
| POST | /_demo/subscribe |
— | body |
| GET | /_demo/stream |
— | — |
| GET | /_demo/explain |
— | query |
| GET | /ping |
— | — |
| GET | /health/live |
— | — |
| GET | /health/ready |
— | — |
Every framework error is application/problem+json (RFC 9457) with a stable code and the requestId.
| Status | Code | When |
|---|---|---|
| 404 | SVS_E040 |
No route matches the path. |
| 405 | SVS_E041 |
The path exists for other methods; Allow lists them. |
| 422 | SVS_E042 |
params, query or body failed its schema. |
| 413 | SVS_E043 |
The body is over bodyLimit (1 MiB by default). |
| 400 | SVS_E047 |
Unparseable body, unsafe param (e.g. encoded ../) or redirect. |
| 415 | SVS_E048 |
A body schema got a content type it cannot parse. |
| 503 | SVS_E044 |
The handler ran past requestTimeout; ctx.signal aborted. |
| 500 | SVS_E049 |
An unhandled error; details only with debug on. |
| 500 | SVS_E050 |
Handler output failed the response schema. |
21 commands built in, 23 more from packages, and your own written with the same defineCommand().
$ pnpm svs about
property value
------------ ---------------
Application samveys-landing
Environment local
Debug true
API Version 1.0.0
Node Version v26.13.0
Plugins 4
Bindings 10
$ pnpm svs plugin:list
order name kind requires extensions bindings
----- ----------------- ------ ------------- ------------------- ------------------------
0 @samveys/http plugin - - http.config, http.router
1 @samveys/view plugin - context: view, lazy view.registry
2 @samveys/template plugin @samveys/view - -
3 @samveys/health plugin - router: lazy -
$ pnpm svs make:controller Post
✔ Created controller Post:
app/controllers/post.ts
tests/post-controller.test.ts
$ pnpm svs route:list --json | jq ".[0]"
{
"method": "GET",
"pattern": "/",
"name": null,
"middleware": []
}
Real output of svs in this project. Add --json to any command for one machine-readable document.
svs new <name> [--kit=api] [--dir <dir>]
Create a project from a starter kit.
svs add <package> [--dry-run] [--yes]
Install a package and apply its recipe to samveys.config.ts.
svs upgrade --to <version> [--dry-run]
Move @samveys/* to a new major and run the codemods.
svs serve [--watch] [--entry <file>]
Run bin/server.ts from .ts with .env loaded; --watch restarts on change.
svs build [--out dist] [--docker] [--sea]
Compile to plain JS in dist/; --docker writes a Dockerfile, --sea one binary.
svs start [--permission --allow-write storage]
Run the production build, optionally under the Node permission model.
svs make:controller <Name>
app/controllers/<name>.ts and its test.
svs make:middleware <name>
app/middleware/<name>.ts and its test.
svs make:command <name:part>
app/commands/<name>.ts and its test.
svs make:provider <Name>
app/providers/<name>.ts and its test.
svs make:test <name>
tests/<name>.test.ts booting the app with createTestApp.
svs make:plugin <name> [--dir <dir>]
A publishable plugin package skeleton.
svs route:list [--json]
Every mounted route with its name and middleware.
svs route:types
Write .samveys/types/routes.d.ts for typed route().
svs about
Environment, versions, plugin and binding counts.
svs plugin:list
Plugins in boot order with hooks, extensions and bindings.
svs config:show [path]
Resolved configuration with secrets redacted.
svs env:check
Validate environment variables against their definitions.
svs check [--deploy] [--skip <id>]
System and deployment checks (APP_KEY, debug, database…).
svs explain <SVS_Exxx>
What an error code means and how to fix it.
svs repl
A REPL with the booted app, container and db.
Register the plugin in samveys.config.ts and its commands appear in svs --help.
| Package | Commands |
|---|---|
@samveys/database |
migratemigrate:rollbackmigrate:statusdb:seeddb:wipedb:import-laravel
|
@samveys/orm |
make:modelmake:migrationmodel:showmodel:prune
|
@samveys/queue |
queue:workqueue:failedqueue:retry
|
@samveys/scheduler |
schedule:runschedule:list
|
@samveys/openapi |
generate:openapigenerate:clientopenapi:diff
|
@samveys/env-crypt |
env:encryptenv:decrypt
|
@samveys/auth |
make:auth
|
@samveys/inspector |
inspector:prune
|
@samveys/mcp |
mcp
|
import { defineCommand } from '@samveys/console'
export const reportDailyCommand = defineCommand('report:daily', {
description: 'Print yesterday’s signups',
args: [{ name: 'team', description: 'Team slug', required: false }],
flags: {
days: { type: 'number', description: 'How many days back', default: 1 },
dry: { type: 'boolean', description: 'Do not send the email', short: 'd' },
},
async run({ args, flags, output, app }) {
const booted = await app() // boots samveys.config.ts only when asked
const db = booted.container.make('db') // the same binding request handlers use
const since = new Date(Date.now() - flags.days * 86_400_000).toISOString()
const signups = await db.from('users').where('created_at', '>', since).count()
output.table([{ team: args.team ?? 'all', days: flags.days, signups }])
output.result({ signups }) // what --json prints
return 0 // exit code
},
})
import { definePlugin } from '@samveys/core'
import { reportDailyCommand } from '../commands/report-daily.js'
export const commands = definePlugin({
name: 'app/commands',
boot({ extend }) {
extend.command(reportDailyCommand)
// or lazily: extend.command(() => import('../commands/report-daily.js'))
},
})
// samveys.config.ts
plugins: [http({ … }), commands()]
// now: pnpm svs --help lists report:daily
// pnpm svs report:daily web --days 7 --json
import assert from 'node:assert/strict'
import { it } from 'node:test'
import { createKernel } from '@samveys/console'
import { reportDailyCommand } from '../app/commands/report-daily.js'
it('prints signups as JSON', async () => {
let stdout = ''
const kernel = createKernel({ stdout: { write: (chunk) => void (stdout += chunk) } })
kernel.register(reportDailyCommand)
assert.equal(await kernel.run(['report:daily', '--json']), 0)
assert.ok('signups' in JSON.parse(stdout))
})
GET /_demo/explain?code=… runs createKernel().run(['explain', code, '--json']) from @samveys/console in this server process. The code is validated by a query schema first.
$ svs explain SVS_E044 --json
// press Run
bin/server.ts serves requests and svs runs commands, but both boot the same samveys.config.ts.
pnpm devsvs serve → bin/server.tscreateHttpServer(app)listens, runs the kernel per requestsamveys.config.ts
createApp → register → boot
pnpm svs <command>createKernel().run(argv)await app()boots once, shuts down aftersvs and bin/server.ts both load samveys.config.ts and boot the same plugins.
http() mounts routes on app:booted, so svs route:list shows exactly what the server serves.
A command’s app() and a request handler resolve the same bindings: db, cache, mail…
One plugin can add routes, middleware, ctx.* fields and svs commands at once.
Every request takes the same path through the kernel, whichever driver carries it: node:http, Fastify or Hono.
Internal x-samveys-* and x-middleware-subrequest headers are stripped; a request id is assigned.
Plugins may intercept and short-circuit with their own Response before routing.
Static routes before params before wildcards. No match → 404 SVS_E040.
Global → group → controller → route, then unwound in reverse after next() resolves.
params, query and body are checked together; all issues come back in one 422.
Runs with ctx.signal, aborted on client disconnect or timeout (503 SVS_E044).
Output is validated and unknown keys stripped; invalid output is 500 SVS_E050.
Security headers merged, then zstd → brotli → gzip compression, streamed, never buffered.
The form and the stream below call routes on the server that rendered this page.
POST /_demo/subscribe with a schema.object() body. Leave a field empty or type a bad email to get a 422.
// the response body appears here
GET /_demo/stream streams this process’s metrics once a second through sse(), with event ids for resume.
event: metrics …
Guides, tutorial and every package README, in one place.
Every public export of every package, with its signature.
All packages, their versions and the commands they add.
Throughput and latency, measured with a published method.
Versions, security reports, troubleshooting and contacts.
Nothing installed yet? Start at step 1. Every step says where to run it.
svs is not a program you already have. It is the command that ships inside the npm package @samveys/console. On a fresh machine, typing svs gives “command not found”. Get it through npx @samveys/console to create a project; after pnpm install every project has its own copy in node_modules/.bin, so you run it as pnpm svs ….
node -v && pnpm -v
Node must be 26.13.0 or newer, pnpm 12. No pnpm? Run npm i -g pnpm@12 (Node 26 no longer bundles Corepack).
npm config set @samveys:registry https://registry.sadata.dev/api/packages/sadata/npm/
Adds one line to ~/.npmrc. The registry is readable without a token. Skip this and npx answers 404, because @samveys/* is not on the public npm registry.
npx @samveys/console new my-app
There is no global svs on a fresh machine. npx downloads @samveys/console, the package that ships the svs command, and runs its new command once.
cd my-app && pnpm install && pnpm test
Installs @samveys/console as a devDependency. From here on, svs lives in node_modules/.bin of this project.
pnpm dev
Same as pnpm svs serve --watch: loads .env, sets APP_ENV=local, restarts on file changes. Open http://localhost:3000/ping → pong.
pnpm svs build --docker
dist/ plus a multi-stage, non-root Dockerfile. Configuration stays in the environment, never in the build.
| Where you are | Run | Example |
|---|---|---|
| Outside a project (no svs yet) | npx @samveys/console <command> | npx @samveys/console new my-app |
| Inside a project | pnpm svs <command> | pnpm svs make:model Note --migration |
| Inside a project, with npm | npx svs <command> | npx svs route:list |
| Bare svs everywhere (optional) | npm i -g @samveys/console | svs new my-app |
npx answers 404? Step 2 is missing: @samveys/* is not on the public npm registry, only on https://registry.sadata.dev/api/packages/sadata/npm/.
svs is not a global program; it is the bin of the @samveys/console package. Outside a project, run it through npx @samveys/console …. Inside a project it is a devDependency, so use pnpm svs … or npx svs …. If npx says 404, run the one-time npm config set @samveys:registry step first. Want a bare svs everywhere? npm i -g @samveys/console.
Node.js 26 LTS (26.13.0 or newer). There are no polyfills or compatibility paths; devEngines makes installs fail on older versions on purpose.
No. Development runs .ts files directly on Node 26 with erasable syntax only. svs build emits dist/ when you deploy.
Yes. @samveys/fastify and @samveys/hono are HTTP drivers: they carry the transport while the Samveys kernel still owns routing, middleware and errors. All three drivers pass the same 29-case conformance suite.
Templates are compiled to ES modules and loaded with import(). Expressions are a parsed subset — no assignments, no new, no functions — and constructor, prototype and __* access is refused at compile time and at run time.
Install it, then register its plugin in samveys.config.ts. svs add ships recipes for http, validator and testing today.
It is proprietary and internal: employees and contractors of PT Teknologi Sada Indonesia, for the organization’s work only.
One command creates the folders, the config, a test and a Dockerfile.
npx @samveys/console new my-app