Tutorial: from svs new to a deployed app

Dari docs/tutorial/first-app.md di repository framework.

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

This tutorial takes a new employee from an empty folder to a running container. It takes about 30 minutes. You build a small notes API with a database table, a validated route, a test, and a Docker image. Each code block below is type-checked against the framework in CI. The block marked as a test also runs.

1. Create the project

@samveys/* comes from the Samveys registry on registry.sadata.dev, which anyone can read: no account or token is needed. The project .npmrc that svs new writes already points there.

sh
svs new notes --kit=api
cd notes
pnpm install
pnpm test        # the generated GET /ping test passes with no edits
pnpm dev         # svs serve --watch: http://localhost:3000/ping → pong

What you got:

  • samveys.config.ts with http(), database() (SQLite in storage/app.db, the default engine), orm() and health().
  • app/routes.ts with the routes.
  • tests/ with the tests.
  • A Dockerfile and a CI workflow.

2. Add a model and its table

sh
svs make:model Note --migration

Give the model its columns:

ts
import { Model, col } from '@samveys/orm'

export class Note extends Model.define('notes', {
  id: col.increments(),
  title: col.string(120),
  body: col.text().nullable(),
  createdAt: col.timestamp().autoCreate(),
}) {}

Then let the framework write the migration from the model, and run it:

sh
svs make:migration create_notes --diff   # compares app/models with the database
svs migrate

3. Add a validated route

ts
import { defineRoutes } from '@samveys/http'
import { schema } from '@samveys/validator'
import { Note } from './models/note.ts'

const NewNote = schema.object({ title: schema.string().min(1).max(120), body: schema.string().optional() })

export default defineRoutes((router) => {
  router.get('/ping', () => 'pong')
  router.get('/notes', async () => (await Note.query().orderBy('id', 'desc').limit(50).get()).map((note) => note.toJSON()))
  // The body is validator output, so create() accepts it. Unknown fields are already gone.
  router.post('/notes', { body: NewNote }, async ({ body, response }) => response.created((await Note.create(body)).toJSON()))
})

A body that fails the schema gets 422 with the failing fields in errors, without any code from you.

4. Test it

ts
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { createDatabaseManager, defineDatabaseConfig, sqlite } from '@samveys/database'
import { http } from '@samveys/http'
import { useDatabase } from '@samveys/orm'
import { createTestApp } from '@samveys/testing'
import routes from '../app/routes.ts'

test('POST /notes stores a note, and a bad body is a 422', async () => {
  const db = createDatabaseManager(defineDatabaseConfig({ default: 'main', connections: { main: sqlite({ file: ':memory:' }) } }))
  await db.rawQuery('create table notes (id integer primary key autoincrement, title varchar(120) not null, body text, created_at timestamptz)')
  useDatabase(db)
  await using app = await createTestApp({ plugins: [http()], routes: (router) => routes.register(router) })

  const created = await app.http.post('/notes', { json: { title: 'First', isAdmin: true } })
  created.assertStatus(201)
  assert.equal(created.json<{ title: string }>().title, 'First')
  assert.equal('isAdmin' in created.json<object>(), false, 'unknown fields never reach the model')

  const invalid = await app.http.post('/notes', { json: { title: '' } })
  invalid.assertStatus(422)

  assert.equal((await app.http.get('/notes')).json<unknown[]>().length, 1)
  await db.close()
})
sh
pnpm test

5. Check it before deploying

sh
svs check --deploy   # fails without APP_KEY, with debug on, or with an unreachable database
svs route:list

6. Build and run the container

sh
svs build --docker
docker build -t notes .
docker run --rm -p 3000:3000 -e APP_ENV=production -e APP_KEY=base64:… notes
curl localhost:3000/health/ready   # 200 once the database answers
  • The image. It runs as the non-root node user and contains no .env files. All configuration comes from env vars.
  • Shutdown. On SIGTERM, /health/ready turns 503 at once, and in-flight requests still finish.
  • A server database. To switch from SQLite, set DB_CONNECTION=mysql or DB_CONNECTION=postgres with a DATABASE_URL. No code changes. See Choose a database engine.

Next