Skip to main content

Backend from Scratch

This is the smallest useful backend shape for W7S.

Repo structure

my-app/
backend/
src/
health.ts
notes.ts
db/
app/
migrations/
0001_init.sql
w7s.json

Define routes

Create w7s.json:

[
{
"type": "webhook",
"path": "/health",
"method": "GET",
"handler": "./backend/src/health.ts",
"auth": "none"
},
{
"type": "webhook",
"path": "/notes",
"method": "ANY",
"handler": "./backend/src/notes.ts",
"auth": "none"
}
]

Add a health handler

export default async function handle() {
const deployMeta =
(globalThis as { __W7S_DEPLOY_META?: Record<string, unknown> }).__W7S_DEPLOY_META ?? {};

return {
status: 200,
body: {
ok: true,
service: "my-app",
branch: deployMeta.branch ?? null,
commit: deployMeta.commitHash ?? null,
},
};
}

Add a database migration

Create db/app/migrations/0001_init.sql:

create table if not exists notes (
id integer primary key autoincrement,
title text not null,
body text,
created_at_ms integer not null
);

Add a note handler

Use Drizzle rather than raw SQL in request handlers.

import { desc } from "drizzle-orm";
import { db, schema } from "./db/client";

export default async function handle(payload: {
method?: string;
body?: unknown;
}) {
const method = String(payload?.method ?? "GET").toUpperCase();

if (method === "GET") {
const rows = await db
.select()
.from(schema.notes)
.orderBy(desc(schema.notes.createdAtMs))
.limit(20);

return { status: 200, body: { ok: true, items: rows } };
}

if (method === "POST") {
const input = (payload.body ?? {}) as { title?: string; body?: string };
if (!input.title?.trim()) {
return { status: 400, body: { ok: false, error: "Missing title." } };
}

await db.insert(schema.notes).values({
title: input.title.trim(),
body: input.body?.trim() || null,
createdAtMs: Date.now(),
});

return { status: 201, body: { ok: true } };
}

return { status: 405, body: { ok: false, error: "Method not allowed." } };
}

Deploy workflow

Minimal backend-only deploy workflow:

name: Deploy Backend

on:
push:
branches: [main]
paths:
- "backend/**"
- "db/**"
- "w7s.json"
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: zip -qr repo.zip . -x ".git/*" -x ".github/*"
- run: |
OWNER_SLUG="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
REPO_SLUG="$(basename "${{ github.repository }}")"
curl --fail --show-error \
-X POST "https://${OWNER_SLUG}.w7s.cloud/${REPO_SLUG}/_deploy?artifact=source&scope=backend" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "x-github-repository: ${{ github.repository }}" \
-H "x-github-sha: ${{ github.sha }}" \
-H "x-github-branch: ${{ github.ref_name }}" \
-H "content-type: application/zip" \
--data-binary "@repo.zip"

Result

After deploy:

  • GET /health confirms the deploy is live
  • migrations are already applied
  • GET /notes and POST /notes operate on the repo-scoped database

For the DB layer, continue with Database and Drizzle.