Getting started with Samveys

Dari docs/guide/getting-started.md di repository framework.

Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.

This tutorial builds a small HTTP API with one validated POST route and a node:test test using the HTTP kernel’s Fetch API. It uses only packages and value exports present in this repository. playground/ is a scratch area you create yourself; it is not part of the repository. The snippets are type-checked against the package exports, and the test in step 4 runs in CI (tested).

1. Prepare a workspace example

From the monorepo root, create a local tutorial directory. Keep it inside this repository so TypeScript resolves the workspace packages. For a standalone project, use svs new instead (tutorial).

sh
mkdir -p playground/getting-started

The workspace already defines Node.js ≥ 26.13.0, ESM, and direct execution of .ts files. This guide assumes the monorepo dependencies are installed and its package links are available. Do not fetch these internal-only packages from the public npm registry.

2. Define the application and route

Create playground/getting-started/app.ts. The app uses the HTTP plugin, a controller, and a Standard Schema validator. The created method returns HTTP 201 and a JSON response.

ts
import { createApp } from '@samveys/core'
import { defineController, createHttpServer, http, post } from '@samveys/http'
import { schema } from '@samveys/validator'

const createMessage = schema.object({
  text: schema.string().trim().min(1),
})

export const messages = defineController('messages', {
  prefix: '/messages',
  routes: {
    create: post('/', { body: createMessage }, ({ body, response }) =>
      response.created({ id: 'm-1', text: body.text }),
    ),
  },
})

export const app = createApp({
  plugins: [http()],
  environment: 'test',
  processEnv: {},
})

export async function makeKernel() {
  await app.run()
  const router = app.container.make('http.router')
  router.controller(messages)
  return createHttpServer(app)
}

createHttpServer() returns both the server and its kernel. The tutorial test can use kernel.fetch() without opening a network port. A real entrypoint can call listen() on the returned server.

3. Test the validated POST route

Create playground/getting-started/app.test.ts. node:test runs this test directly with the repository’s Node 26 TypeScript runtime. A malformed body is rejected before the route handler runs and gets a Problem Details response.

ts
import assert from 'node:assert/strict'
import { after, test } from 'node:test'
import { app, makeKernel } from './app.ts'

const server = await makeKernel()
after(async () => {
  await server.close()
  await app.shutdown()
})

test('creates a validated message', async () => {
  const response = await server.kernel.fetch(
    new Request('http://localhost/messages', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ text: ' hello ' }),
    }),
  )

  assert.equal(response.status, 201)
  assert.deepEqual(await response.json(), { id: 'm-1', text: 'hello' })
})

test('rejects invalid input with Problem Details', async () => {
  const response = await server.kernel.fetch(
    new Request('http://localhost/messages', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ text: '' }),
    }),
  )

  assert.equal(response.status, 422)
  assert.equal(response.headers.get('content-type'), 'application/problem+json')
  const problem = await response.json() as { code: string; errors: { pointer: string }[] }
  assert.equal(problem.code, 'SVS_E042')
  assert.equal(problem.errors[0]?.pointer, '#/body/text')
})

Run the test from the monorepo root with Node’s test runner:

sh
node --test playground/getting-started/app.test.ts

4. The same test with @samveys/testing

createTestApp() from @samveys/testing removes the setup above: it boots a fresh app for each test, gives it an in-process HTTP client with assertions, and shuts it down at the end of the block through await using. Add "@samveys/testing": "workspace:*" to the dev dependencies of the package that holds the test.

ts
import { test } from 'node:test'
import { http } from '@samveys/http'
import { createTestApp } from '@samveys/testing'
import { messages } from './app.ts'

test('creates a validated message', async () => {
  await using testApp = await createTestApp({
    plugins: [http()],
    routes: (router) => router.controller(messages),
  })

  ;(await testApp.http.post('/messages', { json: { text: ' hello ' } }))
    .assertStatus(201)
    .assertJson({ id: 'm-1', text: 'hello' })

  ;(await testApp.http.post('/messages', { json: { text: '' } })).assertProblem(422, 'SVS_E042')
})

See packages/testing/README.md for fake time, event and hook recording, and swap().

5. Start the server

Create playground/getting-started/bin/server.ts to listen on the configured Node driver. The server closes gracefully when the app shuts down.

ts
import { app, makeKernel } from '../app.ts'

const server = await makeKernel()
await server.listen()
app.handleSignals()

The router now serves POST /messages. Invalid request bodies return 422 with an issue pointer; valid bodies are trimmed and receive a 201 JSON response.