Skip to content

Commit aa63dc2

Browse files
committed
feat: ✨ add automatic database migrations and error handling
- Introduced DB_AUTO_MIGRATE configuration to enable automatic migrations on server startup. - Implemented migration error handling to prevent server startup on failure. - Updated dependencies: drizzle-orm to 0.45.0, elysia to 1.4.18, and drizzle-kit to 0.31.8. - Modified docker-compose to rename postgres service and volume for clarity.
1 parent 8ca8669 commit aa63dc2

File tree

10 files changed

+81
-25
lines changed

10 files changed

+81
-25
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ SERVER_PORT=3000
1111
# Database Configuration
1212
# Replace with your actual PostgreSQL connection details
1313
DATABASE_URL=postgresql://username:password@localhost:5432/elysia-boilerplate
14+
15+
# Enable migrations on server startup
16+
DB_AUTO_MIGRATE=false

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Added database migration error handling
13+
- Added automatic database migrations on server startup
14+
15+
### Changed
16+
17+
- Updated dependencies to the latest version
18+
1019
## [0.4.1] - 2025-12-03
1120

1221
### Changed

bun.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"@bogeychan/elysia-logger": "^0.1.10",
99
"@elysiajs/cors": "^1.4.0",
1010
"@elysiajs/openapi": "^1.4.11",
11-
"drizzle-orm": "^0.44.7",
11+
"drizzle-orm": "^0.45.0",
1212
"drizzle-typebox": "^0.3.3",
13-
"elysia": "^1.4.17",
13+
"elysia": "^1.4.18",
1414
"envalid": "^8.1.1",
1515
"pg": "^8.16.3",
1616
"pino": "^10.1.0",
@@ -19,7 +19,7 @@
1919
"@biomejs/biome": "2.3.8",
2020
"@types/bun": "^1.3.3",
2121
"@types/pg": "^8.15.6",
22-
"drizzle-kit": "^0.31.7",
22+
"drizzle-kit": "^0.31.8",
2323
"pino-pretty": "^13.1.3",
2424
},
2525
},
@@ -137,13 +137,13 @@
137137

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

140-
"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=="],
140+
"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=="],
141141

142-
"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=="],
142+
"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=="],
143143

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

146-
"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=="],
146+
"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=="],
147147

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

docker-compose.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ services:
1212
SERVER_PORT: 3000
1313
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/elysia-boilerplate
1414
depends_on:
15-
postgres:
15+
elysia-boilerplate-postgres:
1616
condition: service_healthy
1717
restart: unless-stopped
18-
19-
postgres:
18+
19+
elysia-boilerplate-postgres:
2020
image: postgres:17-alpine
2121
ports:
2222
- 5432:5432
@@ -25,7 +25,7 @@ services:
2525
POSTGRES_PASSWORD: postgres
2626
POSTGRES_DB: elysia-boilerplate
2727
volumes:
28-
- postgres_data:/var/lib/postgresql/data
28+
- elysia-boilerplate-postgres-data:/var/lib/postgresql/data
2929
healthcheck:
3030
test: ["CMD-SHELL", "pg_isready -U postgres -d elysia-boilerplate"]
3131
interval: 10s
@@ -34,4 +34,4 @@ services:
3434
restart: unless-stopped
3535

3636
volumes:
37-
postgres_data:
37+
elysia-boilerplate-postgres-data:

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
"@bogeychan/elysia-logger": "^0.1.10",
2727
"@elysiajs/cors": "^1.4.0",
2828
"@elysiajs/openapi": "^1.4.11",
29-
"drizzle-orm": "^0.44.7",
29+
"drizzle-orm": "^0.45.0",
3030
"drizzle-typebox": "^0.3.3",
31-
"elysia": "^1.4.17",
31+
"elysia": "^1.4.18",
3232
"envalid": "^8.1.1",
3333
"pg": "^8.16.3",
3434
"pino": "^10.1.0"
@@ -37,7 +37,7 @@
3737
"@biomejs/biome": "2.3.8",
3838
"@types/bun": "^1.3.3",
3939
"@types/pg": "^8.15.6",
40-
"drizzle-kit": "^0.31.7",
40+
"drizzle-kit": "^0.31.8",
4141
"pino-pretty": "^13.1.3"
4242
}
4343
}

src/common/config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cleanEnv, port, str, url } from 'envalid';
1+
import { bool, cleanEnv, port, str, url } from 'envalid';
22

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

1726
export default config;

src/db/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
// Make sure to install the 'pg' package
21
import { drizzle } from 'drizzle-orm/node-postgres';
2+
import { migrate } from 'drizzle-orm/node-postgres/migrator';
3+
import { log as logger } from 'src/common/logger';
34
import config from '../common/config';
45

6+
const log = logger.child({ name: 'db' });
7+
58
const db = drizzle({
69
connection: {
710
connectionString: config.DATABASE_URL,
@@ -10,4 +13,14 @@ const db = drizzle({
1013
casing: 'snake_case',
1114
});
1215

16+
/**
17+
* Run all pending database migrations.
18+
* @throws {Error} If migration fails (e.g., invalid SQL, connection issues)
19+
*/
20+
export async function migrateDb(): Promise<void> {
21+
log.info('Running database migrations...');
22+
await migrate(db, { migrationsFolder: 'src/db/migrations' });
23+
log.info('Database migrations completed successfully');
24+
}
25+
1326
export default db;

src/main.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import openapi from '@elysiajs/openapi';
33
import { Elysia, ElysiaCustomStatusResponse, status } from 'elysia';
44
import config from './common/config';
55
import { log } from './common/logger';
6+
import { migrateDb } from './db';
67
import { users } from './modules/users';
78
import { gracefulShutdown } from './util/graceful-shutdown';
89

@@ -58,13 +59,30 @@ const app = new Elysia()
5859
)
5960
.use(users);
6061

61-
app.listen(config.SERVER_PORT, ({ development, hostname, port }) => {
62-
log.info(
63-
`🦊 Elysia is running at ${hostname}:${port} ${development ? '🚧 in development mode!🚧' : ''}`,
64-
);
62+
/**
63+
* Bootstrap the application.
64+
* Runs all initialization tasks before starting the server.
65+
*/
66+
async function bootstrap(): Promise<void> {
67+
// Run database migrations before accepting any requests
68+
if (config.DB_AUTO_MIGRATE) {
69+
await migrateDb();
70+
}
71+
72+
// Start the server only after all initialization is complete
73+
app.listen(config.SERVER_PORT, ({ development, hostname, port }) => {
74+
log.info(
75+
`🦊 Elysia is running at http://${hostname}:${port} ${development ? '🚧 in development mode!🚧' : ''}`,
76+
);
77+
});
78+
79+
process.once('SIGINT', () => gracefulShutdown(app, 'SIGINT'));
80+
process.once('SIGTERM', () => gracefulShutdown(app, 'SIGTERM'));
81+
}
82+
83+
bootstrap().catch((error) => {
84+
log.fatal({ err: error }, 'Failed to start application');
85+
process.exit(1);
6586
});
6687

6788
export type App = typeof app;
68-
69-
process.once('SIGINT', () => gracefulShutdown(app, 'SIGINT'));
70-
process.once('SIGTERM', () => gracefulShutdown(app, 'SIGTERM'));

src/modules/users/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Elysia, status } from 'elysia';
2-
import { log } from 'src/common/logger';
2+
import { log as logger } from 'src/common/logger';
33
import { type UsersModel, usersModelPlugin } from './model';
44
import { UsersService } from './service';
55

6+
const log = logger.child({ name: 'users' });
7+
68
export const users = new Elysia({ prefix: '/users', tags: ['Users'] })
79
.use(usersModelPlugin)
810
.post(

src/util/graceful-shutdown.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { log } from 'src/common/logger';
1+
import { log as logger } from 'src/common/logger';
22
import db from 'src/db';
33
import type { App } from 'src/main';
44

5+
const log = logger.child({ name: 'test' });
6+
57
let isShuttingDown = false;
68

79
export async function gracefulShutdown(

0 commit comments

Comments
 (0)