Building Serverless API Gateways with Node.js Express and GitHub Actions CI/CD
Microservices architectures often require a central API Gateway to manage authentication, rate limiting, logging, and routing to downstream services. Running a traditional, always-on API Gateway on EC2 or Docker containers can lead to idle compute costs and maintenance overhead.
By leveraging Serverless architecture (AWS Lambda & API Gateway) with Node.js Express and GitHub Actions, you can build a highly resilient, auto-scaling API Gateway that costs virtually zero when idle and automates deployments seamlessly.
In this tutorial, we will construct a production-ready Serverless API Gateway from scratch using Node.js Express, wrap it for serverless deployment with serverless-http, and establish a zero-downtime CI/CD pipeline via GitHub Actions.
ποΈ Architecture Overview
The API Gateway acts as the single point of entry for client requests:
[ Client Request ]
β
βΌ
[ AWS API Gateway / Lambda (Express.js Gateway) ]
βββ 1. Security Headers (Helmet) & CORS
βββ 2. Rate Limiting (express-rate-limit)
βββ 3. JWT & API Key Authentication
βββ 4. Dynamic Proxy Dispatcher -> [ Downstream Services ]
Key Components:
- Express.js: Core routing engine and middleware handling.
-
Serverless Framework (
serverless-http): Adapts Express request/response objects into AWS Lambda event handlers. - GitHub Actions: Automated linting, testing, and deployment to AWS on code pushes.
π Project Structure
serverless-api-gateway/
βββ .github/
β βββ workflows/
β βββ deploy.yml
βββ src/
β βββ middleware/
β β βββ auth.js
β β βββ rateLimiter.js
β βββ routes/
β β βββ gateway.js
β βββ app.js
βββ tests/
β βββ gateway.test.js
βββ handler.js
βββ serverless.yml
βββ package.json
βββ README.md
π» 1. Setting Up package.json
First, initialize the Node.js project and install the required dependencies:
{
"name": "serverless-api-gateway",
"version": "1.0.0",
"description": "Production-ready Serverless API Gateway with Express.js and GitHub Actions",
"main": "handler.js",
"scripts": {
"start": "serverless offline",
"test": "jest --detectOpenHandles"
},
"dependencies": {
"axios": "^1.6.8",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "^7.2.0",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"serverless-http": "^3.2.0"
},
"devDependencies": {
"jest": "^29.7.0",
"serverless": "^3.38.0",
"serverless-offline": "^13.3.3",
"supertest": "^6.3.4"
}
}
π 2. Authentication & Rate Limiting Middleware
Authentication Middleware (src/middleware/auth.js)
const jwt = require('jsonwebtoken');
const API_KEY_SECRET = process.env.API_KEY_SECRET || 'super-secret-api-key';
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-jwt-key';
function authenticateRequest(req, res, next) {
const apiKey = req.headers['x-api-key'];
const authHeader = req.headers['authorization'];
// 1. API Key Validation
if (apiKey && apiKey === API_KEY_SECRET) {
req.user = { role: 'service-account' };
return next();
}
// 2. JWT Bearer Token Validation
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
return next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired authentication token' });
}
}
return res.status(401).json({ error: 'Unauthorized: Missing API Key or Bearer Token' });
}
module.exports = { authenticateRequest };
Rate Limiter Middleware (src/middleware/rateLimiter.js)
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: {
status: 429,
error: 'Too many requests from this IP, please try again after 15 minutes.'
}
});
module.exports = { apiLimiter };
π 3. Core Gateway Express App (src/app.js)
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const { apiLimiter } = require('./middleware/rateLimiter');
const gatewayRoutes = require('./routes/gateway');
const app = express();
// Security and utility middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Global Rate Limiter
app.use('/api', apiLimiter);
// Health Check Endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Gateway Proxy Routes
app.use('/api/v1', gatewayRoutes);
// Fallback 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found on API Gateway' });
});
module.exports = app;
π 4. Routing & Proxying Engine (src/routes/gateway.js)
const express = require('express');
const axios = require('axios');
const { authenticateRequest } = require('../middleware/auth');
const router = express.Router();
// Downstream service endpoints registry
const SERVICES = {
USERS: process.env.USERS_SERVICE_URL || 'https://jsonplaceholder.typicode.com/users',
POSTS: process.env.POSTS_SERVICE_URL || 'https://jsonplaceholder.typicode.com/posts'
};
// Protected User Microservice Proxy
router.get('/users', authenticateRequest, async (req, res) => {
try {
const response = await axios.get(SERVICES.USERS, { timeout: 5000 });
res.status(200).json({
gateway: 'Serverless-Node-Gateway',
service: 'users-microservice',
data: response.data
});
} catch (error) {
res.status(502).json({ error: 'Bad Gateway: Microservice response failed', details: error.message });
}
});
// Protected Posts Microservice Proxy
router.get('/posts', authenticateRequest, async (req, res) => {
try {
const response = await axios.get(SERVICES.POSTS, { timeout: 5000 });
res.status(200).json({
gateway: 'Serverless-Node-Gateway',
service: 'posts-microservice',
data: response.data.slice(0, 5) // Return sample posts
});
} catch (error) {
res.status(502).json({ error: 'Bad Gateway: Microservice response failed', details: error.message });
}
});
module.exports = router;
β‘ 5. Serverless Handler (handler.js) & Config (serverless.yml)
Lambda Entrypoint (handler.js)
const serverless = require('serverless-http');
const app = require('./src/app');
module.exports.handler = serverless(app);
Serverless Framework Configuration (serverless.yml)
service: serverless-api-gateway
provider:
name: aws
runtime: nodejs18.x
region: us-east-1
memorySize: 256
timeout: 10
environment:
API_KEY_SECRET: ${env:API_KEY_SECRET, 'super-secret-api-key'}
JWT_SECRET: ${env:JWT_SECRET, 'super-secret-jwt-key'}
functions:
api:
handler: handler.handler
events:
- httpApi:
path: '*'
method: '*'
plugins:
- serverless-offline
βοΈ 6. GitHub Actions CI/CD Pipeline (.github/workflows/deploy.yml)
Automate testing and deployment whenever changes are pushed to main:
name: Serverless API Gateway CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test-and-lint:
name: Run Tests & Audit
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Run Unit Tests
run: npm test
deploy:
name: Deploy to AWS Lambda
needs: test-and-lint
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Install Dependencies
run: npm ci
- name: Deploy Serverless Service
uses: serverless/github-action@v3.2
with:
args: deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
API_KEY_SECRET: ${{ secrets.API_KEY_SECRET }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
π§ͺ 7. Automated Unit Testing with Jest & Supertest (tests/gateway.test.js)
const request = require('supertest');
const app = require('../src/app');
describe('Serverless API Gateway Endpoints', () => {
it('GET /health - should return status 200 OK', async () => {
const res = await request(app).get('/health');
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('status', 'healthy');
});
it('GET /api/v1/users - should reject unauthorized requests with 401', async () => {
const res = await request(app).get('/api/v1/users');
expect(res.statusCode).toEqual(401);
});
it('GET /api/v1/users - should succeed with valid X-API-KEY header', async () => {
const res = await request(app)
.get('/api/v1/users')
.set('x-api-key', 'super-secret-api-key');
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('service', 'users-microservice');
});
});
π― Conclusion
By wrapping Express.js with serverless-http and managing deployments via GitHub Actions, you achieve:
- Zero Cold-Start Overhead for Light Proxies: Lightweight Node.js routing.
- Built-in Protection: Helmet security headers, rate-limiting, and centralized JWT validation.
- Automated CI/CD: Automatic test execution and deployment to AWS Lambda on every git push.
This setup offers enterprise-grade API management with serverless cost efficiency.
Top comments (0)