A comprehensive tutorial series for building a Dino Runner game using Deno and TypeScript.
Stage 4 transforms our game into a full-featured web application with PostgreSQL database integration, global leaderboards, player customization, and persistent game data.
You can clone and deploy this project immediately to start building the Dino Runner game.
Once deployed, you can clone the created project to your local machine to work on it. Every time you commit and push changes to the GitHub repository, it will automatically deploy to Deno Deploy and you can see the changes live.
Runner Game/
โโโ src/ # Server-side source code
โ โโโ main.ts # Enhanced server with database middleware
โ โโโ middleware/ # Custom middleware
โ โ โโโ database.ts # Database connection middleware
โ โโโ database/ # Database layer
โ โ โโโ connection.ts # PostgreSQL connection management
โ โ โโโ schema.sql # Complete database schema
โ โ โโโ migrations.ts # Database initialization
โ โโโ routes/ # Route definitions
โ โโโ api.routes.ts # Health check endpoint
โ โโโ leaderboard.routes.ts # Leaderboard API endpoints
โ โโโ customization.routes.ts # Player customization API
โโโ public/ # Client-side static files
โ โโโ index.html # Enhanced UI with modals & leaderboard
โ โโโ js/
โ โ โโโ game.js # Database-integrated game client
โ โโโ css/
โ โโโ styles.css # Complete Stage 4 styling
โโโ deno.json # Deno configuration
โโโ README.md # Documentation
Note: Environment variables are configured in .env.example at the monorepo root (../../.env.example)
Neon is a serverless PostgreSQL database that provides a free tier for development, they will host your database online and handle scaling automatically.
- Create a Neon account, visit neon.tech and create a free account
- Set up a new PostgreSQL database project
- Copy the project connection string from the Neon dashboard
- Update your .env file with the connection string details
- Install PostgreSQL
- Create database and user manually
- Update your .env file with the database and user details
There is a .env.example provided in the root of the monorepo which you can
copy and update with your own variables. This shared environment file is used by
all stages:
# From the root directory (d:\game\)
cp .env.example .envYour DATABASE_URL should look like this:
# For Neon database
DATABASE_URL=postgresql://username:password@host.neon.tech/dino_runner?sslmode=require
# For local PostgreSQL
DATABASE_URL=postgresql://username:password@localhost:5432/dino_runnerOnce you have set up your database, you can integrate it with your Deno Deploy application. In your Deno Deploy dashboard, click the "Databases" tab and then "+ Add Database".
Add your database connection string to the form, and Deno Deploy will automatically configure the environment variables for your application. You can also manually add the Slug, Credentials and Port if needed. You do not need to include a PEM certificate for this example.
Once you have added the database details, you can test the connection and save it. Then you can assign the database to your application.
The application automatically loads environment variables from the .env file
and initializes the database schema on startup:
deno run --allow-net --allow-env --allow-read src/main.tsThe database will be initialized with:
- players
- high_scores
- player_settings
- game_sessions
We have now added a new dedicated leaderboard page at /leaderboard, alongside
the main game page, at /.
We have created a global leaderboard API that allows players to submit scores
and view rankings at /api/leaderboard. We can retrieve the top 10 scores and
submit new scores with GET and POST requests:
// Get global leaderboard (top 10)
GET /api/leaderboard
Response: [
{
"rank": 1,
"player_name": "DinoMaster",
"score": 15420,
"created_at": "2024-01-15T10:30:00Z"
}
]
// Submit new score
POST /api/scores
Body: {
"playerName": "YourName",
"score": 12500
}
// Get player's best scores
GET /api/scores/{playerName}We have implemented a player customization API that allows players to save their
preferences and retrieve available options at /api/customization. Players can
customize their dino color, background theme, difficulty preference, and sound
settings:
// Save player customization
PUT /api/customization/{playerName}
Body: {
"dinoColor": "#FF6B6B",
"backgroundTheme": "forest",
"difficultyPreference": "hard",
"soundEnabled": true
}
// Get player settings
GET /api/customization/{playerName}
// Get available customization options
GET /api/customization/optionsWe've added several new features and improvements in Stage 4, including database
integration and player customization. The database connection is managed in
src/database/connection.ts, and the API endpoints are defined in
src/routes/.
We manage the PostgreSQL connection using the npm:pg Pool module for optimal
Deno Deploy compatibility. The connection pool is established using either the
DATABASE_URL environment variable, Deno Deploy standard PostgreSQL environment
variables, or individual environment variables for local development:
import { Pool } from "npm:pg";
let pool: Pool | null = null;
export function getDatabase(): Pool {
if (!pool) {
// Try to use DATABASE_URL first (for Neon and other cloud providers)
const databaseUrl = Deno.env.get("DATABASE_URL");
if (databaseUrl) {
console.log("๐ง Using DATABASE_URL for connection pool");
pool = new Pool({
connectionString: databaseUrl,
max: 10, // 10 connections in pool
});
} else {
// Check if Deno Deploy standard PostgreSQL environment variables are available
const pgHost = Deno.env.get("PGHOST");
const pgUser = Deno.env.get("PGUSER");
if (pgHost && pgUser) {
console.log("๐ง Using Deno Deploy PostgreSQL environment variables");
const pgPassword = Deno.env.get("PGPASSWORD");
pool = new Pool({
host: pgHost,
user: pgUser,
password: pgPassword || undefined,
database: Deno.env.get("PGDATABASE") || "postgres",
port: parseInt(Deno.env.get("PGPORT") || "5432"),
max: 10,
});
} else {
// Fallback to custom environment variables for local development
console.log(
"๐ง Using custom DB environment variables (local development)",
);
const password = Deno.env.get("DB_PASSWORD");
pool = new Pool({
host: Deno.env.get("DB_HOST") || "localhost",
port: parseInt(Deno.env.get("DB_PORT") || "5432"),
database: Deno.env.get("DB_NAME") || "dino_runner",
user: Deno.env.get("DB_USER") || "postgres",
password: password || undefined,
max: 10,
});
}
}
console.log("๐๏ธ Database pool created successfully");
}
return pool;
}This code implements a connection pooling pattern optimized for Deno Deploy, ensuring efficient database connections that scale automatically. The pool manages up to 10 concurrent connections, reusing them efficiently to handle multiple requests.
The connection strategy uses a three-tier fallback approach:
- DATABASE_URL - preferred for cloud databases like Neon
- Deno Deploy standard PostgreSQL environment variables - PGHOST, PGUSER, etc.
- Custom environment variables - for local development
Database queries use the client connection pattern for proper resource management:
const client = await pool.connect();
try {
const result = await client.query("SELECT * FROM table WHERE id = $1", [id]);
// Process result.rows
} finally {
client.release(); // Always release client back to pool
}This approach ensures connections are properly returned to the pool and prevents connection leaks. The pool automatically handles connection lifecycle, reconnection on failures, and optimal resource utilization for cloud deployment.
We have enhanced the game client to submit scores to the server and handle leaderboard updates. The score submission function now includes detailed game statistics and error handling:
async submitScoreToDatabase(gameDuration) {
if (!this.playerName) return;
try {
const response = await fetch("/api/scores", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
playerName: this.playerName,
score: Math.floor(this.score),
obstaclesAvoided: this.obstaclesAvoided,
gameDuration: gameDuration,
maxSpeed: this.maxSpeedReached,
}),
});
if (response.ok) {
const data = await response.json();
if (data.isNewRecord) {
console.log("๐ NEW GLOBAL RECORD!");
this.showNewRecordMessage();
}
console.log(`๐ Score submitted! Global rank: #${data.globalRank}`);
// Refresh leaderboard
this.loadGlobalLeaderboard();
}
} catch (error) {
console.error("Failed to submit score:", error);
}
}The score submission function now tracks additional game metrics including obstacles avoided, game duration, and maximum speed reached. The server responds with global ranking information and indicates if the player achieved a new record.
The function gracefully handles network errors and provides detailed feedback about the submission status. On successful submission, it automatically refreshes the leaderboard display to show updated rankings.
Players can personalize their gaming experience with different themes, colors, and difficulty settings. The customization system allows players to select their preferred dino color and background theme, which are saved in the database:
applyCustomizations() {
// Update canvas background
const theme = this.themes[this.settings.backgroundTheme] || this.themes.desert;
this.canvas.style.background =
`linear-gradient(to bottom, ${theme.sky} 0%, ${theme.sky} 75%, ${theme.ground} 75%, ${theme.ground} 100%)`;
// Apply difficulty multiplier
const difficultyMultipliers = { easy: 0.8, normal: 1.0, hard: 1.3 };
this.initialGameSpeed = 3 * (difficultyMultipliers[this.settings.difficultyPreference] || 1.0);
this.gameSpeed = this.initialGameSpeed;
console.log(`๐จ Applied theme: ${this.settings.backgroundTheme}, difficulty: ${this.settings.difficultyPreference}`);
}The customization system dynamically applies themes using predefined color
schemes and adjusts game difficulty by modifying the initial game speed. The
applyCustomizations() method updates the canvas background with CSS gradients
and sets appropriate difficulty multipliers.
Settings are automatically saved to the database for registered players or localStorage for anonymous users. The system supports multiple themes (desert, forest, night, rainbow, space) and three difficulty levels (easy, normal, hard).
Players can access customization options through a modal interface with intuitive controls for color picking, theme selection, and difficulty adjustment.
We have enhanced the user interface with modals, responsive design, and a modern button system to improve the player experience. The interface includes:
- Player name entry and customization panels with clean, accessible modals
- Consistent button styling using a base
.btnclass with variants (.btn-primary,.btn-secondary, etc.) - Mobile-friendly design that adapts to different screen sizes
- Hover effects and animations for better user interaction
The CSS has been refactored to use a consolidated button system with CSS custom properties for consistent theming and maintainable styles.
PostgreSQL returns certain values as BigInt objects which cannot be serialized
to JSON by default. This has been resolved by explicitly converting database
values in the leaderboard.routes.ts file:
// Score submission with BigInt conversion
const rank = Number(rankResult.rows[0]?.rank) || 1;
const sanitizedScore = {
...insertedScore,
id: Number(insertedScore.id),
score: Number(insertedScore.score),
obstacles_avoided: Number(insertedScore.obstacles_avoided || 0),
};# Set up environment variables (from root directory)
cd ../../
cp .env.example .env
# Edit .env with your database credentials (Neon DATABASE_URL recommended)
# Navigate back to Stage 4 and start the server
cd packages/stage-4
deno run --allow-net --allow-env --allow-read src/main.tsNavigate to http://localhost:8000 and experience the complete database-integrated dino runner with global leaderboards and customization!
# Start the server (with automatic environment loading)
deno run --allow-net --allow-env --allow-read src/main.ts
# Test database connection
psql "postgresql://your_connection_string_here"
# Clear player data for testing (run in browser console)
resetPlayerData()
# Check server status
curl http://localhost:8000/api/healthBy completing Stage 4, you'll have:
- โ Integrated PostgreSQL database for persistent data storage
- โ Built global leaderboard system with real-time rankings
- โ Implemented player customization modal with theme and color options
- โ Created player profile system with persistent settings
- โ Added comprehensive API endpoints for data management
- โ Enhanced UI with modals, responsive design, and modern button system
- โ Implemented fallback systems for offline functionality
- โ Built scalable database schema for future features
- โ Added game analytics and session tracking with detailed metrics
- โ Created deployment-ready application with environment configuration
- โ Implemented connection pooling with automatic resource management
The game now provides a complete multiplayer experience with social features, personalization, and persistent data! ๐ฎ๐