Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 6 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ services:
LOG_LEVEL: info
SERVER_HOSTNAME: 0.0.0.0
SERVER_PORT: 3000
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/elysia-boilerplate
DATABASE_URL: postgresql://postgres:postgres@elysia-boilerplate-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;
23 changes: 11 additions & 12 deletions src/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import config from './config';

export const log = createPinoLogger({
level: config.LOG_LEVEL,
transport:
config.NODE_ENV === 'development'
? {
targets: [
{
target: 'pino-pretty',
options: {
colorize: true,
},
transport: ['development', 'test'].includes(config.NODE_ENV)
? {
targets: [
{
target: 'pino-pretty',
options: {
colorize: true,
},
],
}
: undefined,
},
],
}
: undefined,
});
20 changes: 19 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,19 @@ 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...');
try {
await migrate(db, { migrationsFolder: 'src/db/migrations' });
log.info('Database migrations completed successfully');
} catch (error) {
log.error({ error }, 'Database migration failed');
throw error;
}
}

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: 'graceful-shutdown' });

let isShuttingDown = false;

export async function gracefulShutdown(
Expand Down