Building a secure and scalable API gateway is crucial to modern software development. In this context, Node.js, Aptible Terraform, and Redis are three powerful tools that can be used to create a secure and scalable API gateway.
With Node.js, you can build fast and efficient server-side applications, while Aptible Terraform allows you to manage your Aptible resources directly from Terraform, enabling infrastructure as code (IaC).
Redis is an open-source, in-memory data structure store that can be used as a database, cache, and message broker.
Together, these tools can help you create a secure and scalable API gateway that can handle many requests while ensuring your data's security.
To build a secure and scalable API gateway using Node.js, Aptible Terraform, and Redis, you will need the following prerequisites:
Node.js: A powerful JavaScript runtime that forms the backend powerhouse with many authentication libraries.
Aptible: A serverless platform simplifying deployments, scaling, and security management for Node.js applications. Basic Aptible knowledge is required.
Redis: A lightning-fast in-memory database that acts as the cache and stores crucial authentication data like tokens and user sessions.
RedisInsight: A graphical user interface for Redis that allows you to visualize and interact with your Redis data. RedisInsight should be installed and running on your local machine.
Terraform: An open-source infrastructure as code (IaC) tool that allows you to manage your infrastructure safely and repeatedly. Basic Terraform knowledge is required.
Docker: A containerization platform that packages your application and its dependencies into a single container. Basic Docker knowledge is required.
Node.js
project by running this command:npm init -y
npm install express redis body-parser jsonwebtoken
const express = require('express');
Ensure Redis is installed and running on your system.
const redisClient = new Redis();
This code initializes a Redis client that connects to the default Redis server running on localhost
at port 6379
.
It uses the Redis()
constructor from the Redis library to create the client.
redisClient.on("connect", () => {...});
and redisClient.on("error", (err) => {...});
are event listeners that handle events when the Redis client successfully connects or encounters an error during the connection.
app.use(bodyParser.json());
This sets up the Express app to use the bodyParser.json()
middleware, allowing it to parse incoming JSON requests.
app.post("/register", (req, res) => {
const { username, password, email } = req.body;
// Perform validation checks
// Simulate storing user data in Redis
redisClient.hmset(
`user:${username}`,
["password", password, "email", email],
(err, reply) => {
if (err) {
console.error(err);
return res.status(500).json({ error: "Failed to register user" });
}
const token = jwt.sign({ username }, "your_secret_key", {
expiresIn: "1h",
});
return res.json({ message: "User registered successfully", token });
}
);
});
This endpoint handles the registration of a user. It retrieves username
, password
, and email
from the request body, performs validation checks, and then simulates storing user data in Redis using redisClient.hmset()
.
app.post("/login", (req, res) => {
const { username, password } = req.body;
// Validate user credentials
redisClient.hget(`user:${username}`, "password", (err, reply) => {
if (err || reply !== password) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Assuming successful authentication, generate a JWT token
const token = jwt.sign({ username }, "your_secret_key", {
expiresIn: "1h",
});
return res.json({ message: "Login successful", token });
});
});
This endpoint handles user login. It validates the user's credentials by retrieving the stored password from Redis using redisClient.hget()
.
If the credentials are valid, it generates a JWT token for authentication.
app.get("/protected", verifyToken, (req, res) => {
// If the token is verified, allow access to the protected route
return res.json({ message: "Access granted to protected route" });
});
This is an example of a protected route that requires a Bearer token for access. It uses the verifyToken
middleware function to verify the token before allowing access to the route.
function verifyToken(req, res, next) {
const bearerHeader = req.headers["authorization"];
if (typeof bearerHeader !== "undefined") {
const bearerToken = bearerHeader.split(" ")[1]; // Extract token from header
req.token = bearerToken;
jwt.verify(req.token, "your_secret_key", (err, authData) => {
if (err) {
return res.status(403).json({ error: "Forbidden" });
}
// If token is valid, proceed to the next middleware/route handler
next();
});
} else {
// If no token provided, return Forbidden
return res.status(403).json({ error: "Forbidden" });
}
}
This middleware function checks for a Bearer token in the Authorization
request's header. If a valid token is present, it verifies it using jwt.verify()
and allows access to the protected routes.
Otherwise, it returns a Forbidden error.
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
This code starts the Express server, listening on the specified PORT
, and logs a message to the console once the server is running.
Complete code:
const express = require("express");
const bodyParser = require("body-parser");
const Redis = require("ioredis");
const jwt = require("jsonwebtoken");
const app = express();
const PORT = process.env.PORT || 3000;
// Redis client setup to connect to a local Redis instance
const redisClient = new Redis(); // Connects to default localhost:6379
redisClient.on("connect", () => {
console.log("Connected to Redis...");
});
redisClient.on("error", (err) => {
console.error("Redis error:", err);
});
app.use(bodyParser.json());
// Routes
// Registration endpoint
app.post("/register", (req, res) => {
const { username, password, email } = req.body;
// Perform validation checks
// Simulate storing user data in Redis
redisClient.hmset(
`user:${username}`,
["password", password, "email", email],
(err, reply) => {
if (err) {
console.error(err);
return res.status(500).json({ error: "Failed to register user" });
}
const token = jwt.sign({ username }, "your_secret_key", {
expiresIn: "1h",
});
return res.json({ message: "User registered successfully", token });
}
);
});
// Login endpoint requiring a Bearer token for authentication
app.post("/login", (req, res) => {
const { username, password } = req.body;
// Validate user credentials
redisClient.hget(`user:${username}`, "password", (err, reply) => {
if (err || reply !== password) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Assuming successful authentication, generate a JWT token
const token = jwt.sign({ username }, "your_secret_key", {
expiresIn: "1h",
});
return res.json({ message: "Login successful", token });
});
});
// Protected route example - requires a Bearer token for access
app.get("/protected", verifyToken, (req, res) => {
// If the token is verified, allow access to the protected route
return res.json({ message: "Access granted to protected route" });
});
// Middleware function to verify the Bearer token in the Authorization header
function verifyToken(req, res, next) {
const bearerHeader = req.headers["authorization"];
if (typeof bearerHeader !== "undefined") {
const bearerToken = bearerHeader.split(" ")[1]; // Extract token from header
req.token = bearerToken;
jwt.verify(req.token, "your_secret_key", (err, authData) => {
if (err) {
return res.status(403).json({ error: "Forbidden" });
}
// If token is valid, proceed to the next middleware/route handler
next();
});
} else {
// If no token provided, return Forbidden
return res.status(403).json({ error: "Forbidden" });
}
}
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Start the Redis server:
redis-server
Response:
3444:C 07 Jan 2024 16:40:21.983 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
3444:C 07 Jan 2024 16:40:21.983 * Redis version=7.2.3, bits=64, commit=00000000, modified=0, pid=3444, just started
3444:C 07 Jan 2024 16:40:21.983 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
3444:M 07 Jan 2024 16:40:21.984 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 7.2.3 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 3444
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
3444:M 07 Jan 2024 16:40:21.985 # WARNING: The TCP backlog setting of 511 cannot be enforced because kern.ipc.somaxconn is set to the lower value of 128.
3444:M 07 Jan 2024 16:40:21.985 * Server initialized
3444:M 07 Jan 2024 16:40:21.986 * Loading RDB produced by version 7.2.3
3444:M 07 Jan 2024 16:40:21.986 * RDB age 67145 seconds
3444:M 07 Jan 2024 16:40:21.986 * RDB memory usage when created 1.38 Mb
3444:M 07 Jan 2024 16:40:21.986 * Done loading RDB, keys loaded: 3, keys expired: 0.
3444:M 07 Jan 2024 16:40:21.986 * DB loaded from disk: 0.001 seconds
3444:M 07 Jan 2024 16:40:21.986 * Ready to accept connections tcp
Open a new terminal, then start the Node.js server:
node app.js
Response:
Server running on port 3000
Connected to Redis...
Use tools like curl
or Postman to test the registration and login endpoints. Ensure you provide valid JSON data for registration (/register
) and login (/login
) endpoints.
API Documentation
Send a POST request to http://localhost:3000/register
with the following body (example):
{
"username": "example_user",
"password": "example_password",
"email": "[email protected]"
}
Response:
{
"message": "User registered successfully",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImV4YW1wbGVfdXNlciIsImlhdCI6MTcwNDU2ODA3OSwiZXhwIjoxNzA0NTcxNjc5fQ.tXernOwDwtYI3iJycBKYTcShalFEcFgE6VFFjbCqtsY"
}
Send a POST request to http://localhost:3000/login
with the user's credentials in the body:
{
"username": "example_user",
"password": "example_password"
}
Response:
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Indpc2RvbSIsImlhdCI6MTcwNDU2NzkzNSwiZXhwIjoxNzA0NTcxNTM1fQ.CZm0yLgA0BsfESGBORdrTLtQ5Kw3SIxSc8PuAaYBRV4"
}
RedisInsight offers a comprehensive view of the Redis database, allowing users to explore and analyze the database's contents.
Click on the Database Alias value that is 127.0.0.1:6379
For this tutorial, I clicked on the first value, that is user:example_user
Then, this screen will open with more details.
You need to have a Docker Hub account. If you haven't created one yet, sign up at Docker Hub.
Ensure you have Docker installed and running on your local machine.
Dockerfile
in the root directory of your Node.js project.# Use an official Node.js runtime as the base image
FROM node:16
# Set the working directory in the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install app dependencies
RUN npm install
# Copy the rest of the application files to the working directory
COPY . .
# Expose the port the app runs on
EXPOSE 3000
# Define the command to run the app
CMD ["node", "blog.js"]
The folder structure after adding the Dockerfile
Tag your Docker image: Open your terminal
, navigate to the root directory of your Node.js
application, where your Dockerfile
is located.
Run the following command to build your Docker image and tag it with your Docker Hub username and desired repository name:
docker build -t your-docker-username/auth-app:latest .
Replace your-docker-username
with your Docker Hub username and auth-app
with your desired repository name.
You will get a similar response like this:
[+] Building 8.4s (11/11) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 487B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:16 4.1s
=> [auth] library/node:pull token for registry-1.docker.io 0.0s
=> [1/5] FROM docker.io/library/node:16@sha256:f77a1aef2da8d83e45ec990f45df50f1a286c5fe8bbfb8c6e4246c6389705c0b 0.0s
=> [internal] load build context 0.3s
=> => transferring context: 4.55MB 0.3s
=> CACHED [2/5] WORKDIR /usr/src/app 0.0s
=> [3/5] COPY package*.json ./ 0.0s
=> [4/5] RUN npm install 3.5s
=> [5/5] COPY . . 0.2s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:23304094353243bf75417ea2db7794ddaf0c788b3695c9d2c5e9257887e113cf 0.0s
=> => naming to docker.io/wise4rmgod/auth-app:latest 1.0s
docker login
Enter your Docker Hub username
and password
When prompted, you will get a similar response if it is successful.
Authenticating with existing credentials...
Login Succeeded
Docker image
to your Docker Hub repository using the following command: docker push your-docker-username/auth-app:latest
This command uploads your local image to Docker Hub under your specified repository.
Response:
6198dc681375: Pushed
9214cb11fa0d: Pushed
0bec55053fbf: Pushed
dd387bc6b362: Mounted from wise4rmgod/blogpost-app
8ae0c889ca84: Mounted from wise4rmgod/blogpost-app
be36d2a441aa: Mounted from wise4rmgod/blogpost-app
c91ec53bcc27: Mounted from wise4rmgod/blogpost-app
03f6e3800bbe: Mounted from wise4rmgod/blogpost-app
ad3b30eb29d3: Mounted from wise4rmgod/blogpost-app
2a7587eb01b6: Mounted from wise4rmgod/blogpost-app
a10e482288d1: Mounted from wise4rmgod/blogpost-app
f9cfc9f6b603: Mounted from wise4rmgod/blogpost-app
latest: digest: sha256:8cde1cdf557e8f84b435ab7d8c07780e37049351cb1220b7e4df1a193fe84027 size: 2842
Verify the Push: Go to your Docker Hub account on the web and navigate to your repository to confirm that your Docker image has been successfully pushed.
This tutorial assumes you have a basic understanding of setting up an environment, application, endpoint, and database on the Aptible platform. The tutorial utilizes Redis as the database and employs Direct Docker for deployment.
aptible login
You will be prompted to enter your email
and password
. If successful, you will receive a response similar to this:
Token written to /Users/wisdomnwokocha/.aptible/tokens.json
This token will expire after 7 days (use --lifetime to customize)
Syntax:
aptible deploy --app <app name> --docker-image <docker image in cloud>
Example command:
aptible deploy --app reactgame --docker-image wise4rmgod/auth-app
You will receive a response similar to the following:
Deploying app...
INFO -- : Starting App deploy operation with ID: 62628758
INFO -- : Deploying without git repository
INFO -- : Writing .aptible.env file...
INFO -- : Fetching app image: wise4rmgod/auth-app...
INFO -- : Pulling from wise4rmgod/auth-app
INFO -- : 26ee4ff96582: Pulling fs layer
INFO -- : 446eab4103f4: Pulling fs layer
INFO -- : 2e3c22a0f840: Pulling fs layer
INFO -- : a7ab8ad9b408: Pulling fs layer
INFO -- : 3808fdf0c601: Pulling fs layer
INFO -- : ab9e4075c671: Pulling fs layer
INFO -- : 362360c8cef6: Pulling fs layer
INFO -- : 928b5d11ac66: Pulling fs layer
INFO -- : dc87e077ac61: Pulling fs layer
INFO -- : 8a5fcbf535dc: Pulling fs layer
INFO -- : 469401a1d41f: Pulling fs layer
INFO -- : eed607c017df: Pulling fs layer
INFO -- : 2e3c22a0f840: Downloading: 515 KB / 49.8 MB
INFO -- : 446eab4103f4: Downloading: 173 KB / 16.6 MB
INFO -- : 26ee4ff96582: Downloading: 495 KB / 47 MB
INFO -- : a7ab8ad9b408: Downloading: 527 KB / 175 MB
INFO -- : 2e3c22a0f840: Downloading: 25.2 MB / 49.8 MB
INFO -- : ab9e4075c671: Downloading: 352 KB / 33.4 MB
INFO -- : a7ab8ad9b408: Downloading: 35.4 MB / 175 MB
INFO -- : 26ee4ff96582: Pull complete
INFO -- : 446eab4103f4: Pull complete
INFO -- : 2e3c22a0f840: Pull complete
INFO -- : a7ab8ad9b408: Downloading: 70.8 MB / 175 MB
INFO -- : a7ab8ad9b408: Downloading: 106 MB / 175 MB
INFO -- : a7ab8ad9b408: Downloading: 140 MB / 175 MB
.
INFO -- : a7ab8ad9b408: Pull complete
INFO -- : 3808fdf0c601: Pull complete
INFO -- : ab9e4075c671: Pull complete
INFO -- : 362360c8cef6: Pull complete
INFO -- : 928b5d11ac66: Pull complete
INFO -- : dc87e077ac61: Pull complete
INFO -- : 8a5fcbf535dc: Pull complete
INFO -- : 469401a1d41f: Pull complete
INFO -- : eed607c017df: Pull complete
INFO -- : Digest: sha256:8cde1cdf557e8f84b435ab7d8c07780e37049351cb1220b7e4df1a193fe84027
INFO -- : Status: Downloaded newer image for wise4rmgod/auth-app:latest
INFO -- : No Procfile found in git directory or /.aptible/Procfile found in Docker image: using Docker image CMD
INFO -- : No .aptible.yml found in git directory or /.aptible/.aptible.yml found in Docker image: no before_release commands will run
INFO -- : Pushing image dualstack-v2-registry-i-0a7c34b9c3e849206.aptible.in:46022/app-65706/e305b9c9-c6e5-487b-a4f2-6b07306c5cd6:latest to private Docker registry...
INFO -- : The push refers to repository [dualstack-v2-registry-i-0a7c34b9c3e849206.aptible.in:46022/app-65706/e305b9c9-c6e5-487b-a4f2-6b07306c5cd6]
INFO -- : 8ae0c889ca84: Pushed
INFO -- : dd387bc6b362: Pushed
INFO -- : c91ec53bcc27: Pushing: 522 KB / 93.6 MB
INFO -- : 6198dc681375: Pushed
INFO -- : 0bec55053fbf: Pushed
INFO -- : 9214cb11fa0d: Pushed
INFO -- : 2a7587eb01b6: Pushing: 544 KB / 137 MB
INFO -- : ad3b30eb29d3: Pushing: 542 KB / 444 MB
INFO -- : c91ec53bcc27: Pushing: 31.3 MB / 93.6 MB
INFO -- : be36d2a441aa: Pushed
INFO -- : 03f6e3800bbe: Pushed
INFO -- : 2a7587eb01b6: Pushing: 34.9 MB / 137 MB
INFO -- : a10e482288d1: Pushing: 338 KB / 30.7 MB
INFO -- : f9cfc9f6b603: Pushing: 513 KB / 103 MB
INFO -- : c91ec53bcc27: Pushing: 62.7 MB / 93.6 MB
INFO -- : f9cfc9f6b603: Pushing: 35.1 MB / 103 MB
INFO -- : 2a7587eb01b6: Pushing: 69.4 MB / 137 MB
INFO -- : f9cfc9f6b603: Pushing: 69.2 MB / 103 MB
INFO -- : ad3b30eb29d3: Pushing: 45 MB / 444 MB
INFO -- : a10e482288d1: Pushed
INFO -- : 2a7587eb01b6: Pushing: 104 MB / 137 MB
INFO -- : c91ec53bcc27: Pushed
INFO -- : ad3b30eb29d3: Pushing: 90.5 MB / 444 MB
INFO -- : f9cfc9f6b603: Pushed
INFO -- : 2a7587eb01b6: Pushed
INFO -- : ad3b30eb29d3: Pushing: 135 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 178 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 223 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 267 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 311 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 357 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 401 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushed
INFO -- : latest: digest: sha256:8cde1cdf557e8f84b435ab7d8c07780e37049351cb1220b7e4df1a193fe84027 size: 2842
INFO -- : Pulling from app-65706/e305b9c9-c6e5-487b-a4f2-6b07306c5cd6
INFO -- : Digest: sha256:8cde1cdf557e8f84b435ab7d8c07780e37049351cb1220b7e4df1a193fe84027
INFO -- : Status: Image is up to date for dualstack-v2-registry-i-0a7c34b9c3e849206.aptible.in:46022/app-65706/e305b9c9-c6e5-487b-a4f2-6b07306c5cd6:latest
INFO -- : Image app-65706/e305b9c9-c6e5-487b-a4f2-6b07306c5cd6 successfully pushed to registry.
INFO -- : STARTING: Register service cmd in API
INFO -- : COMPLETED (after 0.27s): Register service cmd in API
INFO -- : STARTING: Derive placement policy for service cmd
INFO -- : COMPLETED (after 0.13s): Derive placement policy for service cmd
INFO -- : STARTING: Create new release for service cmd
INFO -- : COMPLETED (after 0.25s): Create new release for service cmd
INFO -- : STARTING: Schedule service cmd
...
INFO -- : COMPLETED (after 13.5s): Schedule service cmd
INFO -- : STARTING: Stop old app containers for service cmd
INFO -- : COMPLETED (after 0.0s): Stop old app containers for service cmd
INFO -- : STARTING: Start app containers for service cmd
INFO -- : WAITING FOR: Start app containers for service cmd
INFO -- : WAITING FOR: Start app containers for service cmd
INFO -- : COMPLETED (after 18.56s): Start app containers for service cmd
INFO -- : STARTING: Delete old containers for service cmd in API
INFO -- : COMPLETED (after 0.0s): Delete old containers for service cmd in API
INFO -- : STARTING: Commit app containers in API for service cmd
INFO -- : COMPLETED (after 0.26s): Commit app containers in API for service cmd
INFO -- : STARTING: Commit service cmd in API
INFO -- : COMPLETED (after 0.13s): Commit service cmd in API
INFO -- : STARTING: Cache maintenance page
INFO -- : COMPLETED (after 0.28s): Cache maintenance page
INFO -- : STARTING: Commit app in API
INFO -- : COMPLETED (after 0.18s): Commit app in API
INFO -- : App deploy successful.
Docker CMD
and click the Create Endpoint buttonThis will allow you to expose your database to the public internet.
Before you proceed with this section, you must have a Terraform cloud account and Terraform CLI installed.
The Aptible Terraform provider enables you to manage your Aptible resources directly from Terraform.
This allows you to use infrastructure as code (IaC) instead of manually initiating operations from the Aptible Dashboard or Aptible CLI.
With the Aptible Terraform provider
, you can automate setting up new environments, including creating, scaling, modifying, and de-provisioning apps and databases.
In your terminal, use the terraform login
command to create an API token and configure your CLI to use it.
terraform login
This is the structure of my folders now.
Create a new file in the directory you just created and name it main.tf
Add provider and resource information: In the main.tf
file, add the provider information for the cloud service you want.
For example, if you want to use AWS, add the following code:
resource "aptible_app" "example-app" {
env_id = data.aptible_environment.example.env_id
handle = "example-app"
config = {
"REDIS_URL": aptible_database.example-redis-db.default_connection_url,
"DATABASE_URL": aptible_database.example-pg-db.default_connection_url,
}
service {
process_type = "cmd"
container_count = 1
container_memory_limit = 1024
}
}
resource "aptible_database" "example-redis-b" {
env_id = data.aptible_environment.example.env_id
handle = "example-redis-db"
database_type = "redis"
container_size = 512
disk_size = 10
version = "5.0"
}
Initialize Terraform: In your terminal, navigate to the directory where you created your Terraform project and run the following command to initialize Terraform:
terraform init
Create an execution plan: Run the following command to create an execution plan:
terraform plan
Apply the changes: Finally, run the following command to apply the changes:
terraform apply
Integrating Node.js, Aptible Terraform, and Redis offers a robust and efficient framework for constructing a secure and scalable authentication API gateway.