Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ SERVER_PORT=3000
# Database Configuration
# Replace with your actual PostgreSQL connection details
DATABASE_URL=postgresql://username:password@localhost:5432/elysia-boilerplate

# Enable migrations on server startup
DB_AUTO_MIGRATE=false
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added database migration error handling
- Added automatic database migrations on server startup

### Changed

- Updated dependencies to the latest version

## [0.4.1] - 2025-12-03

### Changed
Expand Down
12 changes: 6 additions & 6 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"@bogeychan/elysia-logger": "^0.1.10",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/openapi": "^1.4.11",
"drizzle-orm": "^0.44.7",
"drizzle-orm": "^0.45.0",
"drizzle-typebox": "^0.3.3",
"elysia": "^1.4.17",
"elysia": "^1.4.18",
"envalid": "^8.1.1",
"pg": "^8.16.3",
"pino": "^10.1.0",
Expand All @@ -19,7 +19,7 @@
"@biomejs/biome": "2.3.8",
"@types/bun": "^1.3.3",
"@types/pg": "^8.15.6",
"drizzle-kit": "^0.31.7",
"drizzle-kit": "^0.31.8",
"pino-pretty": "^13.1.3",
},
},
Expand Down Expand Up @@ -137,13 +137,13 @@

"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],

"drizzle-kit": ["[email protected].7", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A=="],
"drizzle-kit": ["[email protected].8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="],

"drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
"drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-lyd9VRk3SXKRjV/gQckQzmJgkoYMvVG3A2JAV0vh3L+Lwk+v9+rK5Gj0H22y+ZBmxsrRBgJ5/RbQCN7DWd1dtQ=="],

"drizzle-typebox": ["[email protected]", "", { "peerDependencies": { "@sinclair/typebox": ">=0.34.8", "drizzle-orm": ">=0.36.0" } }, "sha512-iJpW9K+BaP8+s/ImHxOFVjoZk9G5N/KXFTOpWcFdz9SugAOWv2fyGaH7FmqgdPo+bVNYQW0OOI3U9dkFIVY41w=="],

"elysia": ["[email protected].17", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "0.2.5", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-GcR7tgxk0+NgMCEqmXMs/xgND4XpmIzUdSdwchcQbYFeFisBcw9cmsvSpI10i160idwtlVyaRXX9K9IZBqnA7Q=="],
"elysia": ["[email protected].18", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "0.2.5", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-A6BhlipmSvgCy69SBgWADYZSdDIj3fT2gk8/9iMAC8iD+aGcnCr0fitziX0xr36MFDs/fsvVp8dWqxeq1VCgKg=="],

"end-of-stream": ["[email protected]", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],

Expand Down
10 changes: 5 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ services:
SERVER_PORT: 3000
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/elysia-boilerplate
depends_on:
postgres:
elysia-boilerplate-postgres:
condition: service_healthy
restart: unless-stopped
postgres:

elysia-boilerplate-postgres:
image: postgres:17-alpine
ports:
- 5432:5432
Expand All @@ -25,7 +25,7 @@ services:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: elysia-boilerplate
volumes:
- postgres_data:/var/lib/postgresql/data
- elysia-boilerplate-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d elysia-boilerplate"]
interval: 10s
Expand All @@ -34,4 +34,4 @@ services:
restart: unless-stopped

volumes:
postgres_data:
elysia-boilerplate-postgres-data:
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"@bogeychan/elysia-logger": "^0.1.10",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/openapi": "^1.4.11",
"drizzle-orm": "^0.44.7",
"drizzle-orm": "^0.45.0",
"drizzle-typebox": "^0.3.3",
"elysia": "^1.4.17",
"elysia": "^1.4.18",
"envalid": "^8.1.1",
"pg": "^8.16.3",
"pino": "^10.1.0"
Expand All @@ -37,7 +37,7 @@
"@biomejs/biome": "2.3.8",
"@types/bun": "^1.3.3",
"@types/pg": "^8.15.6",
"drizzle-kit": "^0.31.7",
"drizzle-kit": "^0.31.8",
"pino-pretty": "^13.1.3"
}
}
11 changes: 10 additions & 1 deletion src/common/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanEnv, port, str, url } from 'envalid';
import { bool, cleanEnv, port, str, url } from 'envalid';

const config = cleanEnv(Bun.env, {
NODE_ENV: str({
Expand All @@ -12,6 +12,15 @@ const config = cleanEnv(Bun.env, {
SERVER_HOSTNAME: str({ default: 'localhost' }),
SERVER_PORT: port({ default: 3000 }),
DATABASE_URL: url(),
/**
* Enable automatic database migrations on server startup.
*
* ⚠️ CAUTION: Disabled by default for safety.
* - In production, run migrations via CI/CD pipelines instead.
* - Enable in development/staging for convenience.
* - If migrations fail, the server will NOT start.
*/
DB_AUTO_MIGRATE: bool({ default: false }),
});

export default config;
15 changes: 14 additions & 1 deletion src/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Make sure to install the 'pg' package
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { log as logger } from 'src/common/logger';
import config from '../common/config';

const log = logger.child({ name: 'db' });

const db = drizzle({
connection: {
connectionString: config.DATABASE_URL,
Expand All @@ -10,4 +13,14 @@ const db = drizzle({
casing: 'snake_case',
});

/**
* Run all pending database migrations.
* @throws {Error} If migration fails (e.g., invalid SQL, connection issues)
*/
export async function migrateDb(): Promise<void> {
log.info('Running database migrations...');
await migrate(db, { migrationsFolder: 'src/db/migrations' });
log.info('Database migrations completed successfully');
}

export default db;
32 changes: 25 additions & 7 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import openapi from '@elysiajs/openapi';
import { Elysia, ElysiaCustomStatusResponse, status } from 'elysia';
import config from './common/config';
import { log } from './common/logger';
import { migrateDb } from './db';
import { users } from './modules/users';
import { gracefulShutdown } from './util/graceful-shutdown';

Expand Down Expand Up @@ -58,13 +59,30 @@ const app = new Elysia()
)
.use(users);

app.listen(config.SERVER_PORT, ({ development, hostname, port }) => {
log.info(
`🦊 Elysia is running at ${hostname}:${port} ${development ? '🚧 in development mode!🚧' : ''}`,
);
/**
* Bootstrap the application.
* Runs all initialization tasks before starting the server.
*/
async function bootstrap(): Promise<void> {
// Run database migrations before accepting any requests
if (config.DB_AUTO_MIGRATE) {
await migrateDb();
}

// Start the server only after all initialization is complete
app.listen(config.SERVER_PORT, ({ development, hostname, port }) => {
log.info(
`🦊 Elysia is running at http://${hostname}:${port} ${development ? '🚧 in development mode!🚧' : ''}`,
);
});

process.once('SIGINT', () => gracefulShutdown(app, 'SIGINT'));
process.once('SIGTERM', () => gracefulShutdown(app, 'SIGTERM'));
}

bootstrap().catch((error) => {
log.fatal({ err: error }, 'Failed to start application');
process.exit(1);
});

export type App = typeof app;

process.once('SIGINT', () => gracefulShutdown(app, 'SIGINT'));
process.once('SIGTERM', () => gracefulShutdown(app, 'SIGTERM'));
4 changes: 3 additions & 1 deletion src/modules/users/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Elysia, status } from 'elysia';
import { log } from 'src/common/logger';
import { log as logger } from 'src/common/logger';
import { type UsersModel, usersModelPlugin } from './model';
import { UsersService } from './service';

const log = logger.child({ name: 'users' });

export const users = new Elysia({ prefix: '/users', tags: ['Users'] })
.use(usersModelPlugin)
.post(
Expand Down
4 changes: 3 additions & 1 deletion src/util/graceful-shutdown.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { log } from 'src/common/logger';
import { log as logger } from 'src/common/logger';
import db from 'src/db';
import type { App } from 'src/main';

const log = logger.child({ name: 'test' });

let isShuttingDown = false;

export async function gracefulShutdown(
Expand Down