Skip to main content

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​

FeatureCloud FunctionsCloud Run
LanguageNode.js, Python, Go, JavaAny language
DeploymentSource codeDocker containers
ScalingAutomatic (0 to 1000+)Automatic (0 to 1000+)
Cold starts100-500ms50-200ms
Max timeout60 seconds (Hosting)60 minutes
PricingPer invocationPer CPU/memory second
Best forAPIs, webhooksComplex 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​

IssueCauseSolution
Function timeoutLong operationMove to Cloud Run
CORS errorsMissing headersConfigure CORS properly
Cold startsFirst invocationKeep warm or use min instances
Memory errorsLarge payloadIncrease memory allocation
Auth failuresToken expiredImplement 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​


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! 🚀