Adding Backend Logic to Firebase Hosting
While Firebase Hosting excels at serving static content, modern web applications often need server-side logic. This guide explains how to add dynamic backend functionality using Cloud Functions and Cloud Run.
Choosing Your Backend Solution​
Quick Comparison​
Feature | Cloud Functions | Cloud Run |
---|---|---|
Language | Node.js, Python, Go, Java | Any language |
Deployment | Source code | Docker containers |
Scaling | Automatic (0 to 1000+) | Automatic (0 to 1000+) |
Cold starts | 100-500ms | 50-200ms |
Max timeout | 60 seconds (Hosting) | 60 minutes |
Pricing | Per invocation | Per CPU/memory second |
Best for | APIs, webhooks | Complex apps, microservices |
Decision Tree​
Architecture Patterns​
Pattern 1: API Routes with Functions​
myapp.web.app/
├── / → Static (Hosting)
├── /about → Static (Hosting)
├── /api/users → Dynamic (Cloud Functions)
├── /api/products → Dynamic (Cloud Functions)
└── /assets/* → Static (Hosting)
Pattern 2: Server-Side Rendering​
myapp.web.app/
├── /_next/static/* → Static assets (Hosting)
├── /images/* → Static files (Hosting)
├── /* → Dynamic SSR (Cloud Run)
└── /api/* → API routes (Cloud Run)
Pattern 3: Hybrid Architecture​
myapp.web.app/ → Main app (Hosting)
api.myapp.web.app/ → API (Cloud Run)
admin.myapp.web.app/ → Admin panel (Hosting)
ws.myapp.web.app/ → WebSocket server (Cloud Run)
Setting Up Your Backend​
Prerequisites​
# Ensure you're on the Blaze (pay-as-you-go) plan
# Functions and Cloud Run require Blaze plan
# Install required tools
npm install -g firebase-tools
gcloud components install beta
Project Structure​
Recommended project organization:
my-project/
├── hosting/ # Static files
│ ├── public/
│ └── src/
├── functions/ # Cloud Functions
│ ├── src/
│ └── package.json
├── cloudrun/ # Cloud Run services
│ ├── Dockerfile
│ └── src/
└── firebase.json # Configuration
URL Rewriting Strategy​
Basic Rewrites​
{
"hosting": {
"rewrites": [
{
"source": "/api/**",
"function": "api"
},
{
"source": "/app/**",
"run": {
"serviceId": "app-service",
"region": "us-central1"
}
},
{
"source": "**",
"destination": "/index.html"
}
]
}
}
Advanced Routing​
{
"hosting": {
"rewrites": [
{
"source": "/api/v1/**",
"function": "apiV1"
},
{
"source": "/api/v2/**",
"function": "apiV2"
},
{
"source": "/graphql",
"run": {
"serviceId": "graphql-server"
}
},
{
"source": "/ws/**",
"run": {
"serviceId": "websocket-server"
}
}
]
}
}
Performance Considerations​
Cold Start Mitigation​
Cloud Functions:
// Keep functions warm
exports.keepWarm = functions.pubsub
.schedule('every 5 minutes')
.onRun(() => {
console.log('Keeping functions warm');
});
// Minimize dependencies
// Use lazy loading
const heavyLibrary = () => require('heavy-library');
Cloud Run:
# Set minimum instances
gcloud run deploy api-service \
--min-instances=1 \
--max-instances=100
Caching Strategy​
// Cache static responses
exports.api = functions.https.onRequest((req, res) => {
// Cache for 5 minutes
res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
// Vary by API key
res.set('Vary', 'X-API-Key');
res.json({ data: 'response' });
});
Security Best Practices​
Authentication​
// Cloud Functions with Firebase Auth
const admin = require('firebase-admin');
admin.initializeApp();
exports.secureApi = functions.https.onRequest(async (req, res) => {
// Verify Firebase ID token
const token = req.headers.authorization?.split('Bearer ')[1];
try {
const decodedToken = await admin.auth().verifyIdToken(token);
const uid = decodedToken.uid;
// Process authenticated request
res.json({ message: `Hello user ${uid}` });
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
});
CORS Configuration​
// Configure CORS for APIs
const cors = require('cors')({
origin: ['https://myapp.web.app', 'http://localhost:3000'],
credentials: true
});
exports.api = functions.https.onRequest((req, res) => {
cors(req, res, () => {
// Handle request
});
});
Development Workflow​
Local Development​
# Run all emulators
firebase emulators:start
# Run specific emulators
firebase emulators:start --only hosting,functions
# With Cloud Run local
docker build -t myapp .
docker run -p 8080:8080 myapp
Environment Configuration​
// functions/config.js
const functions = require('firebase-functions');
module.exports = {
apiKey: functions.config().api.key,
dbUrl: functions.config().database.url,
environment: functions.config().app.env || 'development'
};
// Set config
firebase functions:config:set api.key="YOUR_KEY"
Deployment Strategies​
Staged Deployments​
# Deploy to staging
firebase use staging
firebase deploy
# Test thoroughly
# Then deploy to production
firebase use production
firebase deploy
Blue-Green Deployment​
# Deploy new version to Cloud Run
gcloud run deploy api-service-green \
--image gcr.io/PROJECT/api:v2
# Test green deployment
# Then switch traffic
gcloud run services update-traffic api-service \
--to-latest=100
Monitoring and Debugging​
Logging​
// Structured logging
functions.logger.info('API request', {
method: req.method,
path: req.path,
userId: req.user?.uid,
duration: Date.now() - startTime
});
Performance Monitoring​
// Track custom metrics
const monitoring = require('@google-cloud/monitoring');
const client = new monitoring.MetricServiceClient();
async function recordMetric(value) {
const dataPoint = {
interval: {
endTime: {
seconds: Date.now() / 1000,
},
},
value: {
int64Value: value,
},
};
await client.createTimeSeries({
name: client.projectPath(projectId),
timeSeries: [{
metric: {
type: 'custom.googleapis.com/api/response_time',
},
points: [dataPoint],
}],
});
}
Cost Optimization​
Minimize Function Invocations​
// Batch operations
exports.batchProcess = functions.https.onRequest(async (req, res) => {
const items = req.body.items; // Process multiple items
const results = await Promise.all(
items.map(item => processItem(item))
);
res.json({ results });
});
Optimize Cloud Run​
# Multi-stage build for smaller images
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["npm", "start"]
Common Patterns​
RESTful API​
const express = require('express');
const app = express();
app.get('/api/users', getUsers);
app.post('/api/users', createUser);
app.put('/api/users/:id', updateUser);
app.delete('/api/users/:id', deleteUser);
exports.api = functions.https.onRequest(app);
GraphQL Server​
const { ApolloServer } = require('apollo-server-cloud-functions');
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
user: req.user,
}),
});
exports.graphql = server.createHandler();
Server-Side Rendering​
// Next.js with Cloud Run
const next = require('next');
const app = next({ dev: false });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
server.get('*', (req, res) => {
return handle(req, res);
});
server.listen(PORT);
});
Troubleshooting​
Common Issues​
Issue | Cause | Solution |
---|---|---|
Function timeout | Long operation | Move to Cloud Run |
CORS errors | Missing headers | Configure CORS properly |
Cold starts | First invocation | Keep warm or use min instances |
Memory errors | Large payload | Increase memory allocation |
Auth failures | Token expired | Implement token refresh |
Debug Tips​
# View function logs
firebase functions:log
# Stream logs
firebase functions:log --follow
# Cloud Run logs
gcloud run logs read --service=SERVICE_NAME
# Local debugging
firebase emulators:start --inspect-functions
Next Steps​
- Deep dive into Cloud Functions integration
- Explore Cloud Run for containers
- Learn about API Deployment patterns
- Set up Monitoring for your backend
Remember: Start with Cloud Functions for simple APIs. As your needs grow, Cloud Run offers more flexibility with containerized applications. Both integrate seamlessly with Firebase Hosting! 🚀