Routing bertipe, tanpa regex
Segmen literal, :param, segmen opsional dan wildcard. Nama param diambil dari pola, jadi salah ketik di params.id jadi error saat kompilasi, bukan 500.
@samveys/http
Routing, validasi, lapisan database, CLI dan 47 paket yang memakai satu container dan satu file config. Bertipe dari param route sampai body response.
npx @samveys/console new my-app
$ curl -i http://samveys.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
x-request-id: fde02d2a-6833-4e33-97b3-1056f870ef9a
{
"code": "SVS_E042",
"title": "Validation failed",
"errors": [{ "pointer": "#/body/email" }]
}
Semua ini sudah termasuk di paket yang dipakai proyek baru.
Segmen literal, :param, segmen opsional dan wildcard. Nama param diambil dari pola, jadi salah ketik di params.id jadi error saat kompilasi, bukan 500.
@samveys/http
Schema body, query dan params mengikuti Standard Schema v1. Handler menerima tipe hasil parsing; kegagalan menjadi 422 Problem Details dengan JSON pointer.
@samveys/validator
CSP, X-Frame-Options, Referrer-Policy dan HSTS lewat HTTPS, batas body 1 MiB, batas waktu handler 30 detik, data request tanpa prototype, dan header internal dibuang.
secure by default
Container IoC dengan inject(), provider dan siklus hidup plugin. Setiap fitur adalah plugin yang didaftarkan di samveys.config.ts — dan setiap plugin bisa diganti.
@samveys/core
sse() mengalirkan event dengan resume lewat Last-Event-ID dan ping keep-alive. Server RFC 6455 berjalan di port yang sama dengan daftar origin yang diizinkan, auth, dan pesan yang divalidasi schema.
realtime
File .tpl.html ala Blade dengan layout, section dan <x-components> dikompilasi menjadi modul ES. Ekspresi diparsing, tidak pernah dievaluasi, dan akses ke constructor ditolak.
@samveys/template
createTestApp() menjalankan app baru untuk setiap test dengan klien HTTP in-process, waktu palsu, event palsu dan perekam hook — dan membersihkan dirinya sendiri lewat await using.
@samveys/testing
Disediakan oleh @samveys/console dan dipasang per proyek. new, serve --watch, generator make:*, route:list, route:types, explain SVS_E042, check, build --docker dan REPL. Perintah buatanmu memakai defineCommand yang sama.
@samveys/console
Setiap request berjalan di dalam samveys:http.request dengan atribut span ala OpenTelemetry. APM bisa berlangganan tanpa meng-import satu modul Samveys pun.
observability
Tujuh file pendek dari proyek sungguhan. Semua API di sini ada di versi 1.0.
File route dimuat secara lazy dari samveys.config.ts. Tipe param berasal dari polanya sendiri.
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)
})
Handler menerima tipe hasil parsing. Body yang salah tidak pernah sampai ke sana.
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" }] }
Satu file menyambungkan seluruh app. Konfigurasi dibaca saat berjalan, tidak pernah ditanam ke hasil 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(),
],
})
Halaman ini sendiri dirender dengan cara ini: layout, section, include dan komponen.
@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
HTTP in-process, kernel asli, tanpa port, tanpa kode pembersihan.
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 dan WebSocket di port yang sama, tanpa dependensi.
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() } }
}),
)
Plugin menyatakan apa yang dibutuhkannya, mendaftar ke container dan memasang hook di siklus hidup.
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 }))
})
Semua yang disentuh sebuah request ada di beberapa file yang sudah pasti. Ini letak masing-masing, lalu setiap bagian dengan kode asli.
samveys.config.ts
Mendaftarkan http({ port, routes, middleware, securityHeaders, … }) bersama semua plugin lain.
bin/server.ts
createApp(config) → app.run() → createHttpServer(app) → server.listen(). svs serve menjalankan file ini.
app/routes.ts
defineRoutes((router) => …): file route yang dimuat http() secara lazy. Tambahkan file lain ke array routes.
app/controllers/*.ts
defineController(name, { prefix, use, routes }): route bernama yang dikelompokkan di bawah satu prefix.
app/middleware/*.ts
(ctx, next) => Response: di level global, group, controller atau route.
Pola berupa literal, :param, :param? dan wildcard * di akhir. Tanpa regular expression, jadi sebuah pola selalu berarti satu hal.
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
})
Controller adalah sekumpulan route bernama di bawah satu prefix. Group menambahkan prefix, middleware atau plugin berskala route ke semua yang ada di dalamnya.
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 }))
})
Handler menerima satu objek context dan mengembalikan sebuah nilai. Kernel mengubah nilai itu menjadi 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 adalah (ctx, next) => Response. Urutannya: global → group → controller → route, lalu dibalik urutannya.
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')] })
Schema params, query dan body berjalan sebelum handler; schema response berjalan setelahnya. Semuanya 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() menerima config server; createHttpServer() menjalankannya di driver node, atau di Fastify atau 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(),
})
Dibaca dari router.list(): router yang sama yang dicetak svs route:list. Tambahkan route dan route itu muncul di sini.
| Method | Pola | Params | Divalidasi |
|---|---|---|---|
| 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 |
— | — |
Setiap error framework berupa application/problem+json (RFC 9457) dengan code yang tetap dan requestId.
| Status | Kode | Kapan |
|---|---|---|
| 404 | SVS_E040 |
Tidak ada route yang cocok dengan path. |
| 405 | SVS_E041 |
Path ada untuk method lain; header Allow mendaftarkannya. |
| 422 | SVS_E042 |
params, query atau body tidak lolos schema-nya. |
| 413 | SVS_E043 |
Body melebihi bodyLimit (1 MiB secara default). |
| 400 | SVS_E047 |
Body tidak bisa diparsing, param tidak aman (mis. ../ yang di-encode) atau redirect tidak aman. |
| 415 | SVS_E048 |
Schema body menerima content type yang tidak bisa diparsing. |
| 503 | SVS_E044 |
Handler berjalan melewati requestTimeout; ctx.signal dibatalkan. |
| 500 | SVS_E049 |
Error yang tidak tertangani; detail hanya tampil saat debug aktif. |
| 500 | SVS_E050 |
Output handler tidak lolos schema response. |
21 perintah bawaan, 23 lagi dari paket, dan perintah buatanmu sendiri dengan defineCommand() yang sama.
$ 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": []
}
Output asli svs di proyek ini. Tambahkan --json ke perintah apa pun untuk satu dokumen yang bisa dibaca mesin.
svs new <name> [--kit=api] [--dir <dir>]
Membuat proyek dari starter kit.
svs add <package> [--dry-run] [--yes]
Memasang paket dan menerapkan recipe-nya ke samveys.config.ts.
svs upgrade --to <version> [--dry-run]
Memindahkan @samveys/* ke major baru dan menjalankan codemod-nya.
svs serve [--watch] [--entry <file>]
Menjalankan bin/server.ts dari .ts dengan .env dimuat; --watch me-restart saat ada perubahan.
svs build [--out dist] [--docker] [--sea]
Mengompilasi ke JS biasa di dist/; --docker menulis Dockerfile, --sea satu binary.
svs start [--permission --allow-write storage]
Menjalankan hasil build produksi, bisa di bawah permission model Node.
svs make:controller <Name>
app/controllers/<name>.ts beserta test-nya.
svs make:middleware <name>
app/middleware/<name>.ts beserta test-nya.
svs make:command <name:part>
app/commands/<name>.ts beserta test-nya.
svs make:provider <Name>
app/providers/<name>.ts beserta test-nya.
svs make:test <name>
tests/<name>.test.ts yang menjalankan app dengan createTestApp.
svs make:plugin <name> [--dir <dir>]
Kerangka paket plugin yang bisa dipublikasikan.
svs route:list [--json]
Setiap route yang terpasang beserta nama dan middleware-nya.
svs route:types
Menulis .samveys/types/routes.d.ts untuk route() yang bertipe.
svs about
Environment, versi, jumlah plugin dan binding.
svs plugin:list
Plugin sesuai urutan boot beserta hook, extension dan binding-nya.
svs config:show [path]
Konfigurasi akhir dengan rahasia disamarkan.
svs env:check
Memvalidasi variabel environment terhadap definisinya.
svs check [--deploy] [--skip <id>]
Pemeriksaan sistem dan deployment (APP_KEY, debug, database…).
svs explain <SVS_Exxx>
Arti sebuah kode error dan cara memperbaikinya.
svs repl
REPL dengan app, container dan db yang sudah di-boot.
Daftarkan plugin-nya di samveys.config.ts dan perintahnya muncul di svs --help.
| Paket | Perintah |
|---|---|
@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=… menjalankan createKernel().run(['explain', code, '--json']) dari @samveys/console di proses server ini. Kode divalidasi lebih dulu oleh schema query.
$ svs explain SVS_E044 --json
// tekan Jalankan
bin/server.ts melayani request dan svs menjalankan perintah, tapi keduanya mem-boot samveys.config.ts yang sama.
pnpm devsvs serve → bin/server.tscreateHttpServer(app)mendengarkan, menjalankan kernel per requestsamveys.config.ts
createApp → register → boot
pnpm svs <command>createKernel().run(argv)await app()di-boot sekali, dimatikan setelahnyasvs dan bin/server.ts sama-sama memuat samveys.config.ts dan mem-boot plugin yang sama.
http() memasang route saat app:booted, jadi svs route:list menampilkan persis yang dilayani server.
app() di sebuah perintah dan handler request mengambil binding yang sama: db, cache, mail…
Satu plugin bisa menambahkan route, middleware, field ctx.* dan perintah svs sekaligus.
Setiap request melewati jalur yang sama di kernel, driver apa pun yang membawanya: node:http, Fastify atau Hono.
Header internal x-samveys-* dan x-middleware-subrequest dibuang; request diberi id.
Plugin bisa mencegat dan menghentikan request dengan Response-nya sendiri sebelum routing.
Route statis dulu, lalu param, lalu wildcard. Tidak cocok → 404 SVS_E040.
Global → group → controller → route, lalu dibalik urutannya setelah next() selesai.
params, query dan body diperiksa bersamaan; semua masalah dikembalikan dalam satu 422.
Berjalan dengan ctx.signal, dibatalkan saat klien putus atau waktu habis (503 SVS_E044).
Output divalidasi dan key yang tidak dikenal dibuang; output tidak valid menjadi 500 SVS_E050.
Header keamanan digabungkan, lalu dikompresi zstd → brotli → gzip, dialirkan, tidak pernah ditampung.
Form dan stream di bawah memanggil route di server yang merender halaman ini.
POST /_demo/subscribe dengan body schema.object(). Kosongkan sebuah field atau tulis email yang salah untuk mendapat 422.
// body response muncul di sini
GET /_demo/stream mengalirkan metrik proses ini setiap detik lewat sse(), dengan id event untuk resume.
event: metrics …
Panduan, tutorial dan README setiap paket, di satu tempat.
Setiap ekspor publik dari setiap paket, lengkap dengan signature-nya.
Semua paket, versinya dan perintah yang ditambahkannya.
Throughput dan latensi, diukur dengan metode yang dipublikasikan.
Versi, laporan keamanan, pemecahan masalah dan kontak.
Belum ada yang terpasang? Mulai dari langkah 1. Setiap langkah menyebutkan di mana harus dijalankan.
svs bukan program yang sudah kamu punya. Itu adalah perintah yang ada di dalam paket npm @samveys/console. Di mesin yang masih kosong, mengetik svs menghasilkan “command not found”. Dapatkan lewat npx @samveys/console untuk membuat proyek; setelah pnpm install setiap proyek punya salinannya sendiri di node_modules/.bin, jadi kamu menjalankannya sebagai pnpm svs ….
node -v && pnpm -v
Node harus 26.13.0 atau lebih baru, pnpm 12. Belum ada pnpm? Jalankan npm i -g pnpm@12 (Node 26 tidak lagi menyertakan Corepack).
npm config set @samveys:registry https://registry.sadata.dev/api/packages/sadata/npm/
Menambahkan satu baris ke ~/.npmrc. Registry bisa dibaca tanpa token. Lewati langkah ini dan npx menjawab 404, karena @samveys/* tidak ada di registry npm publik.
npx @samveys/console new my-app
Di mesin baru belum ada svs global. npx mengunduh @samveys/console, paket yang membawa perintah svs, lalu menjalankan perintah new-nya sekali.
cd my-app && pnpm install && pnpm test
Memasang @samveys/console sebagai devDependency. Mulai dari sini, svs ada di node_modules/.bin proyek ini.
pnpm dev
Sama dengan pnpm svs serve --watch: memuat .env, mengatur APP_ENV=local, restart saat file berubah. Buka http://localhost:3000/ping → pong.
pnpm svs build --docker
dist/ plus Dockerfile multi-stage tanpa root. Konfigurasi tetap di environment, tidak pernah di hasil build.
| Kamu ada di | Jalankan | Contoh |
|---|---|---|
| Di luar proyek (belum ada svs) | npx @samveys/console <command> | npx @samveys/console new my-app |
| Di dalam proyek | pnpm svs <command> | pnpm svs make:model Note --migration |
| Di dalam proyek, dengan npm | npx svs <command> | npx svs route:list |
| svs langsung di mana saja (opsional) | npm i -g @samveys/console | svs new my-app |
npx menjawab 404? Langkah 2 terlewat: @samveys/* tidak ada di registry npm publik, hanya di https://registry.sadata.dev/api/packages/sadata/npm/.
svs bukan program global; itu bin dari paket @samveys/console. Di luar proyek, jalankan lewat npx @samveys/console …. Di dalam proyek ia adalah devDependency, jadi pakai pnpm svs … atau npx svs …. Kalau npx menjawab 404, jalankan dulu langkah sekali-saja npm config set @samveys:registry. Mau svs langsung di mana saja? npm i -g @samveys/console.
Node.js 26 LTS (26.13.0 atau lebih baru). Tidak ada polyfill atau jalur kompatibilitas; devEngines sengaja membuat instalasi gagal di versi yang lebih lama.
Tidak. Development menjalankan file .ts langsung di Node 26 dengan sintaks erasable saja. svs build menghasilkan dist/ saat kamu deploy.
Bisa. @samveys/fastify dan @samveys/hono adalah driver HTTP: keduanya membawa transport sementara kernel Samveys tetap memegang routing, middleware dan error. Ketiga driver lolos suite conformance 29 kasus yang sama.
Template dikompilasi menjadi modul ES dan dimuat dengan import(). Ekspresi adalah subset yang diparsing — tanpa assignment, tanpa new, tanpa fungsi — dan akses ke constructor, prototype dan __* ditolak saat kompilasi dan saat berjalan.
Pasang paketnya, lalu daftarkan plugin-nya di samveys.config.ts. svs add punya recipe untuk http, validator dan testing saat ini.
Samveys adalah perangkat lunak milik perusahaan dan internal: karyawan dan kontraktor PT Teknologi Sada Indonesia, hanya untuk pekerjaan perusahaan.
Satu perintah membuat folder, config, sebuah test dan Dockerfile.
npx @samveys/console new my-app