@empire-builder-kit/containers 0.0.1-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +20 -0
  2. package/dist/executors.json +3 -0
  3. package/dist/generators/service/files-go/Dockerfile.template +11 -0
  4. package/dist/generators/service/files-go/README.md.template +11 -0
  5. package/dist/generators/service/files-go/cmd/main.go.template +34 -0
  6. package/dist/generators/service/files-go/docs/contracts.md.template +9 -0
  7. package/dist/generators/service/files-go/go.mod.template +3 -0
  8. package/dist/generators/service/files-python/Dockerfile.template +12 -0
  9. package/dist/generators/service/files-python/README.md.template +11 -0
  10. package/dist/generators/service/files-python/docs/contracts.md.template +9 -0
  11. package/dist/generators/service/files-python/pyproject.toml.template +15 -0
  12. package/dist/generators/service/files-python/src/main.py.template +20 -0
  13. package/dist/generators/service/files-rust/Cargo.toml.template +16 -0
  14. package/dist/generators/service/files-rust/Dockerfile.template +12 -0
  15. package/dist/generators/service/files-rust/README.md.template +11 -0
  16. package/dist/generators/service/files-rust/docs/contracts.md.template +9 -0
  17. package/dist/generators/service/files-rust/src/main.rs.template +29 -0
  18. package/dist/generators/service/files-typescript/Dockerfile.template +30 -0
  19. package/dist/generators/service/files-typescript/README.md.template +24 -0
  20. package/dist/generators/service/files-typescript/docs/architecture.md.template +18 -0
  21. package/dist/generators/service/files-typescript/docs/contracts.md.template +20 -0
  22. package/dist/generators/service/files-typescript/docs/operations.md.template +24 -0
  23. package/dist/generators/service/files-typescript/eslint.config.mjs.template +3 -0
  24. package/dist/generators/service/files-typescript/infra/service.ts.template +64 -0
  25. package/dist/generators/service/files-typescript/package.json.template +7 -0
  26. package/dist/generators/service/files-typescript/src/index.ts.template +2 -0
  27. package/dist/generators/service/files-typescript/src/main.ts.template +60 -0
  28. package/dist/generators/service/files-typescript/src/service.spec.ts.template +49 -0
  29. package/dist/generators/service/files-typescript/src/service.ts.template +57 -0
  30. package/dist/generators/service/files-typescript/tsconfig.app.json.template +12 -0
  31. package/dist/generators/service/files-typescript/tsconfig.json.template +7 -0
  32. package/dist/generators/service/files-typescript/tsconfig.spec.json.template +7 -0
  33. package/dist/generators/service/files-typescript/vitest.config.mts.template +17 -0
  34. package/dist/generators/service/schema.d.ts +8 -0
  35. package/dist/generators/service/schema.json +40 -0
  36. package/dist/generators/service/service.d.ts +5 -0
  37. package/dist/generators/service/service.d.ts.map +1 -0
  38. package/dist/generators/service/service.js +198 -0
  39. package/dist/generators/task/files-go/Dockerfile.template +10 -0
  40. package/dist/generators/task/files-go/README.md.template +11 -0
  41. package/dist/generators/task/files-go/cmd/main.go.template +22 -0
  42. package/dist/generators/task/files-go/docs/contracts.md.template +9 -0
  43. package/dist/generators/task/files-go/go.mod.template +3 -0
  44. package/dist/generators/task/files-python/Dockerfile.template +10 -0
  45. package/dist/generators/task/files-python/README.md.template +11 -0
  46. package/dist/generators/task/files-python/docs/contracts.md.template +9 -0
  47. package/dist/generators/task/files-python/pyproject.toml.template +12 -0
  48. package/dist/generators/task/files-python/src/main.py.template +26 -0
  49. package/dist/generators/task/files-rust/Cargo.toml.template +15 -0
  50. package/dist/generators/task/files-rust/Dockerfile.template +11 -0
  51. package/dist/generators/task/files-rust/README.md.template +11 -0
  52. package/dist/generators/task/files-rust/docs/contracts.md.template +9 -0
  53. package/dist/generators/task/files-rust/src/main.rs.template +12 -0
  54. package/dist/generators/task/files-typescript/Dockerfile.template +25 -0
  55. package/dist/generators/task/files-typescript/README.md.template +24 -0
  56. package/dist/generators/task/files-typescript/docs/architecture.md.template +18 -0
  57. package/dist/generators/task/files-typescript/docs/contracts.md.template +20 -0
  58. package/dist/generators/task/files-typescript/docs/operations.md.template +24 -0
  59. package/dist/generators/task/files-typescript/eslint.config.mjs.template +3 -0
  60. package/dist/generators/task/files-typescript/infra/task.ts.template +35 -0
  61. package/dist/generators/task/files-typescript/package.json.template +7 -0
  62. package/dist/generators/task/files-typescript/src/index.ts.template +6 -0
  63. package/dist/generators/task/files-typescript/src/main.ts.template +45 -0
  64. package/dist/generators/task/files-typescript/src/task.spec.ts.template +66 -0
  65. package/dist/generators/task/files-typescript/src/task.ts.template +71 -0
  66. package/dist/generators/task/files-typescript/tsconfig.app.json.template +13 -0
  67. package/dist/generators/task/files-typescript/tsconfig.json.template +19 -0
  68. package/dist/generators/task/files-typescript/tsconfig.spec.json.template +11 -0
  69. package/dist/generators/task/files-typescript/vitest.config.mts.template +9 -0
  70. package/dist/generators/task/schema.d.ts +8 -0
  71. package/dist/generators/task/schema.json +40 -0
  72. package/dist/generators/task/task.d.ts +5 -0
  73. package/dist/generators/task/task.d.ts.map +1 -0
  74. package/dist/generators/task/task.js +198 -0
  75. package/dist/generators.json +14 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +7 -0
  79. package/executors.json +3 -0
  80. package/generators.json +14 -0
  81. package/package.json +92 -0
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @empire-builder-kit/containers
2
+
3
+ Empire Builder Kit container workload generators.
4
+
5
+ ## Implemented Surface
6
+
7
+ - `@empire-builder-kit/containers:service`: scaffold a TypeScript-first backend service at `packages/<app>/apps/service`
8
+ - `@empire-builder-kit/containers:task`: scaffold a TypeScript-first backend task at `packages/<app>/apps/task`
9
+
10
+ ## Current Direction
11
+
12
+ This milestone focuses on a mergeable TypeScript-first backend slice: a minimal HTTP service plus a batch-oriented task with shared container build conventions, Nx targets, Vitest coverage, app-local docs, and generated SST/ECS composition templates with a consistent local-dev command contract. Broader language presets can land in follow-on milestones.
13
+
14
+ ## Building
15
+
16
+ Run `pnpm nx build @empire-builder-kit/containers` to build the library.
17
+
18
+ ## Running unit tests
19
+
20
+ Run `pnpm nx test @empire-builder-kit/containers` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,3 @@
1
+ {
2
+ "executors": {}
3
+ }
@@ -0,0 +1,11 @@
1
+ FROM golang:1-bookworm AS builder
2
+ WORKDIR /app
3
+ COPY go.mod go.sum* ./
4
+ RUN go mod download
5
+ COPY . .
6
+ RUN CGO_ENABLED=0 go build -o /service ./cmd/...
7
+
8
+ FROM gcr.io/distroless/static-debian12
9
+ COPY --from=builder /service /service
10
+ EXPOSE 3000
11
+ CMD ["/service"]
@@ -0,0 +1,11 @@
1
+ # <%= projectTitle %>
2
+
3
+ Go service for the `<%= app %>` app group.
4
+
5
+ ## Commands
6
+
7
+ - `pnpm nx run <%= projectName %>:build` — compile binary
8
+ - `pnpm nx run <%= projectName %>:dev` — run locally
9
+ - `pnpm nx run <%= projectName %>:test` — run tests
10
+ - `pnpm nx run <%= projectName %>:lint` — run golangci-lint
11
+ - `pnpm nx run <%= projectName %>:docker-build` — build container image
@@ -0,0 +1,34 @@
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "log/slog"
6
+ "net/http"
7
+ "os"
8
+ )
9
+
10
+ type healthResponse struct {
11
+ Status string `json:"status"`
12
+ Service string `json:"service"`
13
+ }
14
+
15
+ func main() {
16
+ logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
17
+ slog.SetDefault(logger)
18
+
19
+ mux := http.NewServeMux()
20
+ mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
21
+ w.Header().Set("Content-Type", "application/json")
22
+ json.NewEncoder(w).Encode(healthResponse{
23
+ Status: "ok",
24
+ Service: "<%= projectName %>",
25
+ })
26
+ })
27
+
28
+ addr := ":3000"
29
+ slog.Info("listening", "addr", addr)
30
+ if err := http.ListenAndServe(addr, mux); err != nil {
31
+ slog.Error("server failed", "error", err)
32
+ os.Exit(1)
33
+ }
34
+ }
@@ -0,0 +1,9 @@
1
+ # <%= projectTitle %> Contracts
2
+
3
+ ## Health Endpoint
4
+
5
+ - `GET /health` — returns `{ "status": "ok", "service": "<%= projectName %>" }`
6
+
7
+ ## Local Development
8
+
9
+ - `pnpm nx run <%= projectName %>:dev`
@@ -0,0 +1,3 @@
1
+ module github.com/org/<%= projectName %>
2
+
3
+ go 1.22
@@ -0,0 +1,12 @@
1
+ FROM python:3.12-slim AS builder
2
+ WORKDIR /app
3
+ COPY pyproject.toml ./
4
+ RUN pip install --no-cache-dir .
5
+
6
+ FROM python:3.12-slim
7
+ WORKDIR /app
8
+ COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
9
+ COPY --from=builder /usr/local/bin /usr/local/bin
10
+ COPY src/ src/
11
+ EXPOSE 3000
12
+ CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3000"]
@@ -0,0 +1,11 @@
1
+ # <%= projectTitle %>
2
+
3
+ Python (FastAPI) service for the `<%= app %>` app group.
4
+
5
+ ## Commands
6
+
7
+ - `pnpm nx run <%= projectName %>:dev` — run with auto-reload
8
+ - `pnpm nx run <%= projectName %>:test` — run pytest
9
+ - `pnpm nx run <%= projectName %>:lint` — run ruff
10
+ - `pnpm nx run <%= projectName %>:typecheck` — run mypy
11
+ - `pnpm nx run <%= projectName %>:docker-build` — build container image
@@ -0,0 +1,9 @@
1
+ # <%= projectTitle %> Contracts
2
+
3
+ ## Health Endpoint
4
+
5
+ - `GET /health` — returns `{ "status": "ok", "service": "<%= projectName %>" }`
6
+
7
+ ## Local Development
8
+
9
+ - `pnpm nx run <%= projectName %>:dev`
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "<%= projectName %>"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = [
6
+ "fastapi>=0.115",
7
+ "uvicorn[standard]>=0.34",
8
+ ]
9
+
10
+ [project.optional-dependencies]
11
+ dev = [
12
+ "pytest>=8",
13
+ "ruff>=0.8",
14
+ "mypy>=1.13",
15
+ ]
@@ -0,0 +1,20 @@
1
+ """<%= projectTitle %> — FastAPI service for the <%= app %> app group."""
2
+
3
+ import logging
4
+ import sys
5
+
6
+ from fastapi import FastAPI
7
+
8
+ logging.basicConfig(
9
+ stream=sys.stdout,
10
+ level=logging.INFO,
11
+ format="%(message)s",
12
+ )
13
+ logger = logging.getLogger("<%= projectName %>")
14
+
15
+ app = FastAPI(title="<%= projectTitle %>")
16
+
17
+
18
+ @app.get("/health")
19
+ async def health():
20
+ return {"status": "ok", "service": "<%= projectName %>"}
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "<%= projectName %>"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [[bin]]
7
+ name = "service"
8
+ path = "src/main.rs"
9
+
10
+ [dependencies]
11
+ axum = "0.8"
12
+ serde = { version = "1", features = ["derive"] }
13
+ serde_json = "1"
14
+ tokio = { version = "1", features = ["full"] }
15
+ tracing = "0.1"
16
+ tracing-subscriber = { version = "0.3", features = ["json"] }
@@ -0,0 +1,12 @@
1
+ FROM rust:1-bookworm AS builder
2
+ WORKDIR /app
3
+ COPY Cargo.toml Cargo.lock* ./
4
+ RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src
5
+ COPY src/ src/
6
+ RUN cargo build --release
7
+
8
+ FROM debian:bookworm-slim
9
+ RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
10
+ COPY --from=builder /app/target/release/service /usr/local/bin/service
11
+ EXPOSE 3000
12
+ CMD ["service"]
@@ -0,0 +1,11 @@
1
+ # <%= projectTitle %>
2
+
3
+ Rust service for the `<%= app %>` app group.
4
+
5
+ ## Commands
6
+
7
+ - `pnpm nx run <%= projectName %>:build` — compile release binary
8
+ - `pnpm nx run <%= projectName %>:dev` — run with cargo-watch
9
+ - `pnpm nx run <%= projectName %>:test` — run tests
10
+ - `pnpm nx run <%= projectName %>:lint` — run clippy
11
+ - `pnpm nx run <%= projectName %>:docker-build` — build container image
@@ -0,0 +1,9 @@
1
+ # <%= projectTitle %> Contracts
2
+
3
+ ## Health Endpoint
4
+
5
+ - `GET /health` — returns `{ "status": "ok", "service": "<%= projectName %>" }`
6
+
7
+ ## Local Development
8
+
9
+ - `pnpm nx run <%= projectName %>:dev`
@@ -0,0 +1,29 @@
1
+ use axum::{routing::get, Json, Router};
2
+ use serde::Serialize;
3
+ use std::net::SocketAddr;
4
+
5
+ #[derive(Serialize)]
6
+ struct HealthResponse {
7
+ status: &'static str,
8
+ service: &'static str,
9
+ }
10
+
11
+ async fn health() -> Json<HealthResponse> {
12
+ Json(HealthResponse {
13
+ status: "ok",
14
+ service: "<%= projectName %>",
15
+ })
16
+ }
17
+
18
+ #[tokio::main]
19
+ async fn main() {
20
+ tracing_subscriber::fmt().json().init();
21
+
22
+ let app = Router::new().route("/health", get(health));
23
+
24
+ let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
25
+ tracing::info!("listening on {}", addr);
26
+
27
+ let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
28
+ axum::serve(listener, app).await.unwrap();
29
+ }
@@ -0,0 +1,30 @@
1
+ # syntax=docker/dockerfile:1.7
2
+
3
+ FROM node:22-bookworm-slim AS build
4
+ WORKDIR /workspace
5
+
6
+ ENV CI=true
7
+
8
+ RUN corepack enable
9
+
10
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml nx.json tsconfig.base.json tsconfig.json eslint.config.mjs ./
11
+ COPY packages ./packages
12
+ COPY tools ./tools
13
+
14
+ RUN pnpm install --frozen-lockfile
15
+ RUN pnpm nx run <%= projectName %>:build
16
+
17
+ FROM node:22-bookworm-slim AS runtime
18
+ WORKDIR /app
19
+
20
+ ENV NODE_ENV=production
21
+ ENV PORT=3000
22
+ ENV SERVICE_NAME=<%= className %>Service
23
+
24
+ COPY --from=build /workspace/dist/<%= projectRoot %>/ ./
25
+
26
+ EXPOSE 3000
27
+
28
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || '3000') + '/health').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
29
+
30
+ CMD ["node", "main.js"]
@@ -0,0 +1,24 @@
1
+ # <%= projectTitle %>
2
+
3
+ TypeScript-first backend container service for the `<%= appName %>` app group.
4
+
5
+ ## Purpose
6
+
7
+ This package provides a first mergeable ECS-style service slice with a health endpoint, structured log output, and container build conventions that fit the Empire Builder Kit app-group layout.
8
+
9
+ ## Nx Targets
10
+
11
+ - `pnpm nx run <%= projectName %>:build`
12
+ - `pnpm nx run <%= projectName %>:lint`
13
+ - `pnpm nx run <%= projectName %>:typecheck`
14
+ - `pnpm nx run <%= projectName %>:test`
15
+ - `pnpm nx run <%= projectName %>:dev`
16
+ - `pnpm nx run <%= projectName %>:docker-build`
17
+
18
+ ## Current Conventions
19
+
20
+ - keep long-running backend code in `src/`
21
+ - expose health behavior through `GET /health`
22
+ - prefer structured JSON logs so ECS and CloudWatch parsing stay predictable
23
+ - keep SST composition scaffolding in `infra/service.ts` so app-group infra can opt into the workload without rewriting the local-dev contract
24
+ - treat this package as app-group-internal until a supported SDK or API contract is published
@@ -0,0 +1,18 @@
1
+ # <%= projectTitle %> Architecture
2
+
3
+ ## Scope
4
+
5
+ `<%= projectName %>` is the first TypeScript-first container workload slice for `<%= appName %>`. It gives the app group a long-running backend process under `packages/<%= appName %>/apps/service` without introducing language-preset sprawl in the first milestone.
6
+
7
+ ## Runtime Shape
8
+
9
+ - `src/main.ts` starts a small Node HTTP server
10
+ - `src/service.ts` keeps config, health payloads, and structured log formatting testable
11
+ - `GET /health` is the default readiness endpoint for local and container checks
12
+ - `infra/service.ts` captures the recommended SST `Cluster` + `Service` composition seam, image path, service registry, and `sst dev` command contract
13
+
14
+ ## Current Boundaries
15
+
16
+ - keep app-specific background processing and internal APIs here
17
+ - publish cross-app contracts through the owning app group's SDK instead of importing internals directly
18
+ - compose this workload into the owning app group's root `infra/` entrypoint instead of letting app code instantiate SST resources ad hoc
@@ -0,0 +1,20 @@
1
+ # <%= projectTitle %> Contracts
2
+
3
+ ## Local Development
4
+
5
+ - `pnpm nx run <%= projectName %>:dev` is the canonical local command for this workload.
6
+ - `infra/service.ts` passes the same command to SST via `dev.command` so `sst dev` and direct Nx usage stay aligned.
7
+ - `PORT`, `SERVICE_NAME`, and `SERVICE_VERSION` are the supported starter env overrides.
8
+
9
+ ## Infra Composition
10
+
11
+ - import `register<%= className %>Service` from `apps/service/infra/service` into the owning app group's root `infra/` entrypoint
12
+ - provide an existing `sst.aws.Cluster` from app-group infrastructure instead of constructing a cluster inside runtime code
13
+ - use the generated `GET /health` path as the default ECS and load balancer health contract
14
+ - pass linked SST resources through `link` so runtime code can consume them through `Resource.*`
15
+
16
+ ## Deployment Defaults
17
+
18
+ - image build context stays rooted at `packages/<%= appName %>/apps/service`
19
+ - local dev assumes `http://127.0.0.1:3000`
20
+ - load-balanced deployments default to forwarding `80/http` to `3000/http`
@@ -0,0 +1,24 @@
1
+ # <%= projectTitle %> Operations
2
+
3
+ ## Local Verification
4
+
5
+ - `pnpm nx run <%= projectName %>:build`
6
+ - `pnpm nx run <%= projectName %>:test`
7
+ - `pnpm nx run <%= projectName %>:dev`
8
+ - `node dist/<%= projectRoot %>/main.js`
9
+
10
+ Set `PORT`, `SERVICE_NAME`, or `SERVICE_VERSION` in the environment to override the defaults exposed by the starter service.
11
+
12
+ `<%= projectName %>:dev` uses `@swc-node/register` so the same command can be handed to `sst dev` from `infra/service.ts` without introducing a second local entrypoint contract.
13
+
14
+ ## Container Workflow
15
+
16
+ - `pnpm nx run <%= projectName %>:docker-build`
17
+ - image build runs the workspace install plus `pnpm nx run <%= projectName %>:build`
18
+ - the container exposes `GET /health` and ships with a matching Docker `HEALTHCHECK`
19
+ - `infra/service.ts` wires the same health path into SST load balancer and ECS health checks for a mergeable default
20
+
21
+ ## Next Steps
22
+
23
+ - replace the starter route handling with app-specific workload logic
24
+ - import `register<%= className %>Service` into the owning app group's `infra/` entrypoint when the workload is ready to deploy
@@ -0,0 +1,3 @@
1
+ import baseConfig from '<%= offsetFromRoot %>eslint.config.mjs';
2
+
3
+ export default [...baseConfig];
@@ -0,0 +1,64 @@
1
+ export interface Register<%= className %>ServiceArgs {
2
+ cluster: sst.aws.Cluster;
3
+ domain?: string | { name: string };
4
+ environment?: Record<string, string>;
5
+ link?: any[];
6
+ publicLoadBalancer?: boolean;
7
+ }
8
+
9
+ export function register<%= className %>Service({
10
+ cluster,
11
+ domain,
12
+ environment = {},
13
+ link = [],
14
+ publicLoadBalancer = false,
15
+ }: Register<%= className %>ServiceArgs) {
16
+ const service = new sst.aws.Service('<%= projectName %>', {
17
+ cluster,
18
+ image: {
19
+ context: './apps/service/<%= name %>',
20
+ dockerfile: 'Dockerfile',
21
+ },
22
+ dev: {
23
+ command: 'pnpm nx run <%= projectName %>:dev',
24
+ directory: '../../..',
25
+ url: 'http://127.0.0.1:3000',
26
+ },
27
+ environment: {
28
+ PORT: '3000',
29
+ SERVICE_NAME: '<%= className %>Service',
30
+ SERVICE_VERSION: '0.0.1',
31
+ ...environment,
32
+ },
33
+ health: {
34
+ command: [
35
+ 'CMD-SHELL',
36
+ 'node -e "fetch(\'http://127.0.0.1:3000/health\').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"',
37
+ ],
38
+ interval: '30 seconds',
39
+ timeout: '5 seconds',
40
+ retries: 3,
41
+ startPeriod: '10 seconds',
42
+ },
43
+ link,
44
+ logging: {
45
+ retention: '1 month',
46
+ },
47
+ serviceRegistry: {
48
+ port: 3000,
49
+ },
50
+ loadBalancer: {
51
+ public: publicLoadBalancer,
52
+ ...(domain ? { domain } : {}),
53
+ rules: [{ listen: '80/http', forward: '3000/http' }],
54
+ health: {
55
+ '3000/http': {
56
+ path: '/health',
57
+ successCodes: '200-399',
58
+ },
59
+ },
60
+ },
61
+ });
62
+
63
+ return { service };
64
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@<%= appName %>/service",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "main": "./dist/main.js"
7
+ }
@@ -0,0 +1,2 @@
1
+ export { createHealthResponse, formatLogEvent, readServiceConfig } from './service.js';
2
+ export type { HealthResponse, LogContext, ServiceConfig } from './service.js';
@@ -0,0 +1,60 @@
1
+ import { createServer } from 'node:http';
2
+ import { createLogger, createRequestContext } from '@empire-builder-kit/runtime';
3
+ import {
4
+ createHealthResponse,
5
+ readServiceConfig,
6
+ } from './service.js';
7
+
8
+ function readPort(env: NodeJS.ProcessEnv = process.env): number {
9
+ const value = env.PORT?.trim();
10
+
11
+ if (!value) {
12
+ return 3000;
13
+ }
14
+
15
+ const parsed = Number.parseInt(value, 10);
16
+
17
+ if (Number.isNaN(parsed) || parsed <= 0) {
18
+ throw new Error(`Invalid PORT value: ${value}`);
19
+ }
20
+
21
+ return parsed;
22
+ }
23
+
24
+ const config = readServiceConfig();
25
+ const port = readPort();
26
+ const logger = createLogger({ app: '<%= app %>', source: '<%= projectName %>' });
27
+
28
+ const server = createServer((request, response) => {
29
+ const method = request.method ?? 'GET';
30
+ const url = request.url ?? '/';
31
+ const ctx = createRequestContext({
32
+ app: '<%= app %>',
33
+ operation: `${method} ${url}`,
34
+ stage: process.env.SST_STAGE ?? process.env.EBK_STAGE ?? 'dev',
35
+ });
36
+
37
+ logger.info('request received', {
38
+ correlationId: ctx.correlationId,
39
+ method,
40
+ path: url,
41
+ });
42
+
43
+ if (method === 'GET' && url === '/health') {
44
+ response.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
45
+ response.end(JSON.stringify(createHealthResponse(config)));
46
+ return;
47
+ }
48
+
49
+ response.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
50
+ response.end(
51
+ JSON.stringify({
52
+ error: 'Not Found',
53
+ path: url,
54
+ })
55
+ );
56
+ });
57
+
58
+ server.listen(port, () => {
59
+ logger.info('service listening', { port });
60
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createHealthResponse,
4
+ formatLogEvent,
5
+ readServiceConfig,
6
+ } from './service.js';
7
+
8
+ describe('service helpers', () => {
9
+ it('falls back to service defaults', () => {
10
+ expect(readServiceConfig({})).toEqual({
11
+ serviceName: '<%= className %>Service',
12
+ version: '0.0.1',
13
+ });
14
+ });
15
+
16
+ it('builds a stable health payload', () => {
17
+ expect(
18
+ createHealthResponse({
19
+ serviceName: '<%= className %>Service',
20
+ version: '1.2.3',
21
+ })
22
+ ).toEqual({
23
+ service: '<%= className %>Service',
24
+ status: 'ok',
25
+ version: '1.2.3',
26
+ });
27
+ });
28
+
29
+ it('formats structured log output', () => {
30
+ expect(
31
+ formatLogEvent(
32
+ {
33
+ serviceName: '<%= className %>Service',
34
+ version: '1.2.3',
35
+ },
36
+ 'service.started',
37
+ { port: 3000 }
38
+ )
39
+ ).toBe(
40
+ JSON.stringify({
41
+ level: 'info',
42
+ message: 'service.started',
43
+ service: '<%= className %>Service',
44
+ version: '1.2.3',
45
+ port: 3000,
46
+ })
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,57 @@
1
+ export interface ServiceConfig {
2
+ serviceName: string;
3
+ version: string;
4
+ }
5
+
6
+ export interface HealthResponse {
7
+ service: string;
8
+ status: 'ok';
9
+ version: string;
10
+ }
11
+
12
+ export interface LogContext {
13
+ [key: string]: string | number | boolean | null;
14
+ }
15
+
16
+ const DEFAULT_SERVICE_NAME = '<%= className %>Service';
17
+ const DEFAULT_SERVICE_VERSION = '0.0.1';
18
+
19
+ function readOptionalEnv(
20
+ env: NodeJS.ProcessEnv,
21
+ key: string,
22
+ fallback: string
23
+ ): string {
24
+ const value = env[key]?.trim();
25
+ return value && value.length > 0 ? value : fallback;
26
+ }
27
+
28
+ export function readServiceConfig(
29
+ env: NodeJS.ProcessEnv = process.env
30
+ ): ServiceConfig {
31
+ return {
32
+ serviceName: readOptionalEnv(env, 'SERVICE_NAME', DEFAULT_SERVICE_NAME),
33
+ version: readOptionalEnv(env, 'SERVICE_VERSION', DEFAULT_SERVICE_VERSION),
34
+ };
35
+ }
36
+
37
+ export function createHealthResponse(config: ServiceConfig): HealthResponse {
38
+ return {
39
+ service: config.serviceName,
40
+ status: 'ok',
41
+ version: config.version,
42
+ };
43
+ }
44
+
45
+ export function formatLogEvent(
46
+ config: ServiceConfig,
47
+ message: string,
48
+ context: LogContext = {}
49
+ ): string {
50
+ return JSON.stringify({
51
+ level: 'info',
52
+ message,
53
+ service: config.serviceName,
54
+ version: config.version,
55
+ ...context,
56
+ });
57
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "<%= offsetFromRoot %>dist/<%= projectRoot %>",
6
+ "declaration": false,
7
+ "declarationMap": false,
8
+ "emitDeclarationOnly": false
9
+ },
10
+ "include": ["src/**/*.ts"],
11
+ "exclude": ["src/**/*.spec.ts"]
12
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "<%= offsetFromRoot %>tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["node"]
5
+ },
6
+ "include": ["src/**/*.ts", "vitest.config.mts"]
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["node", "vitest/globals"]
5
+ },
6
+ "include": ["src/**/*.spec.ts", "src/**/*.test.ts", "vitest.config.mts"]
7
+ }