Modern web development often demands a blend of robust backend services, dynamic data handling, and efficient client-side delivery. This post demonstrates how to combine Express.js for server-side rendering (SSR), Supabase for a powerful backend, and TypeScript for a type-safe development experience into a single, cohesive application.
Why These Technologies?
- Express.js: A minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It's ideal for handling routes and serving SSR content. Express Documentation
- Supabase: An open-source Firebase alternative providing a PostgreSQL database, authentication, real-time subscriptions, and more. It simplifies backend development, letting you focus on your application logic. Supabase Documentation
- TypeScript: A strongly typed superset of JavaScript that compiles to plain JavaScript. It enhances code quality, readability, and maintainability, especially in larger projects. TypeScript Documentation
Project Setup
First, initialize your project and install the necessary dependencies:
npm init -y
npm install express @types/express ejs supabase-js dotenv
npm install -D typescript ts-node nodemon
npx tsc --init
Configure your tsconfig.json for a typical Node.js setup. Key settings include:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
Create a .env file in your project root to store your Supabase credentials:
SUPABASE_URL="YOUR_SUPABASE_PROJECT_URL"
SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
PORT=3000
Supabase Client Initialization
Create src/supabaseClient.ts to initialize and export your Supabase client. This centralizes your Supabase configuration and ensures environment variables are loaded correctly.
// src/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';
import dotenv from 'dotenv';
dotenv.config(); // Load environment variables
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Supabase URL and Anon Key are required in environment variables.');
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
Express Server with Server-Side Rendering (SSR)
Now, let's set up our Express server in src/server.ts. We'll use EJS as our templating engine to render dynamic content on the server before sending it to the client. This approach enhances initial page load performance and SEO.
// src/server.ts
import express, { Request, Response } from 'express';
import path from 'path';
import { supabase } from './supabaseClient'; // Import our Supabase client
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const PORT = process.env.PORT ? parseInt(process.env.PORT as string) : 3000;
// Set EJS as the templating engine and specify views directory
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../views')); // Assuming 'src' and 'views' are siblings
// Define a TypeScript interface for our product data for type safety
interface Product {
id: number;
name: string;
description: string;
price: number;
}
app.get('/', async (req: Request, res: Response) => {
try {
// Fetch data from your 'products' table in Supabase
const { data: products, error } = await supabase
.from<Product>('products') // Type assertion for type safety
.select('*');
if (error) {
console.error('Error fetching products:', error.message);
return res.status(500).send('Error fetching products.');
}
// Render the EJS template, passing the fetched products data
res.render('index', { products: products || [] });
} catch (err) {
console.error('Server error:', err);
res.status(500).send('An unexpected error occurred.');
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Creating the EJS Template
Create a views directory in your project root, and inside it, create index.ejs. This template will receive the products data from our Express route and render it dynamically.
<!-- views/index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Products from Supabase (SSR)</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
h1 { color: #0056b3; }
.product-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.product-card { background-color: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.product-card h2 { margin-top: 0; color: #333; }
.product-card p { font-size: 0.9em; color: #666; }
.product-card .price { font-weight: bold; color: #007bff; }
</style>
</head>
<body>
<h1>Our Awesome Products</h1>
<% if (products && products.length > 0) { %>
<div class="product-list">
<% products.forEach(product => { %>
<div class="product-card">
<h2><%= product.name %></h2>
<p><%= product.description %></p>
<p class="price">$<%= product.price.toFixed(2) %></p>
</div>
<% }); %>
</div>
<% } else { %>
<p>No products found.</p>
<% } %>
</body>
</html>
Running the Application
To run your application, you can use ts-node or compile it first:
# Using ts-node for development
npx nodemon --exec ts-node src/server.ts
# Or compile and run for production
npx tsc
node dist/server.js
Ensure you have a products table in your Supabase project with id, name, description, and price columns.
Best Practices and Insights
- Environment Variables: Always use
.envfiles anddotenvfor sensitive information like API keys. Never hardcode them. For production, ensure these are securely managed by your hosting provider. - Error Handling: Implement robust
try/catchblocks for asynchronous operations and consider global error handling middleware in Express for a better user experience and easier debugging. - Type Safety: Leverage TypeScript interfaces for data structures (like
Product) to catch potential data inconsistencies at compile-time, improving code reliability. - Scalability: For larger applications, consider caching strategies for Supabase data and potentially moving complex business logic into separate service layers.
Conclusion
By integrating Express.js for SSR, Supabase for a robust backend, and TypeScript for type safety, you can build powerful, performant, and maintainable web applications. This architecture provides the benefits of SEO and faster initial page loads from SSR, combined with the rapid development and scalability offered by Supabase, all within a strongly typed environment. This setup offers a solid foundation for a wide range of modern web projects.