Skip to main content

Overview

The Custom Language Service is a Node.js/Express microservice that provides a simple invocation endpoint with configurable response codes. It’s primarily used for testing error handling, circuit breakers, and retry logic in the Gateway service.
Source code: node-services/custom-lang-service/app.jsEntry point: node-services/custom-lang-service/server.js

Technology Stack

  • Language: Node.js
  • Framework: Express
  • Database: PostgreSQL via pg driver
  • Observability: OpenTelemetry

Configuration

Environment Variables

PORT
string
default:"3000"
HTTP server port for the custom language service
DATABASE_URL
string
PostgreSQL connection stringExample: postgresql://devuser:devpass@postgres:5432/lang_dbService runs without database if not provided
OTEL_EXPORTER_OTLP_ENDPOINT
string
OpenTelemetry collector endpoint
OTEL_SERVICE_NAME
string
default:"custom-lang-service"
Service name for distributed tracing

Docker Compose Configuration

custom-lang-service:
  build:
    context: .
    dockerfile: deploy/docker/custom-lang-service/Dockerfile
  environment:
    PORT: "3000"
    DATABASE_URL: "postgresql://devuser:devpass@postgres:5432/lang_db"
    OTEL_SERVICE_NAME: "custom-lang-service"
  networks:
    - app
  depends_on:
    - postgres

API Reference

POST /invoke

Invoke the custom language service with a name parameter.
name
string
default:"World"
Name to include in the response messageSpecial values trigger error responses:
  • "unauthorized" → 401 Unauthorized
  • "forbidden" → 403 Forbidden
  • "notfound" → 404 Not Found
  • "conflict" → 409 Conflict
  • "ratelimit" → 429 Too Many Requests
  • "unavailable" → 503 Service Unavailable
Any other value returns a successful response.
message
string
Success message with the provided nameFormat: "Hello {name} from custom-lang-service!"

Example Request (Success)

curl -X POST http://localhost:3000/invoke \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice"}'

Example Response (Success)

{
  "message": "Hello Alice from custom-lang-service!"
}

Example Request (Error)

curl -X POST http://localhost:3000/invoke \
  -H "Content-Type: application/json" \
  -d '{"name": "unavailable"}'

Example Response (Error)

{
  "error": "service unavailable"
}

GET /healthz

Health check endpoint that verifies database connectivity.

Example Request

curl http://localhost:3000/healthz

Example Response (Healthy)

ok

Error Responses

  • 503 Service Unavailable: Database health check failed

Implementation Details

Invoke Logic

From node-services/custom-lang-service/app.js:16-69:
app.post('/invoke', async (req, res) => {
  const name = (req.body?.name || 'World').toString();

  let statusCode;
  let message;

  if (name === 'unauthorized') {
    statusCode = 401;
    message = null;
  } else if (name === 'forbidden') {
    statusCode = 403;
    message = null;
  } else if (name === 'notfound') {
    statusCode = 404;
    message = null;
  } else if (name === 'conflict') {
    statusCode = 409;
    message = null;
  } else if (name === 'ratelimit') {
    statusCode = 429;
    message = null;
  } else if (name === 'unavailable') {
    statusCode = 503;
    message = null;
  } else {
    statusCode = 200;
    message = `Hello ${name} from custom-lang-service!`;
  }

  if (pool) {
    try {
      await pool.query(
        'INSERT INTO executions (name, result_message, status_code) VALUES ($1, $2, $3)',
        [name, message, statusCode]
      );
    } catch (err) {
      console.error('failed to record execution:', err);
    }
  }

  if (statusCode === 200) {
    return res.status(200).json({ message });
  }

  const errorMessages = {
    401: 'unauthorized',
    403: 'forbidden',
    404: 'not found',
    409: 'conflict',
    429: 'rate limited',
    503: 'service unavailable',
  };
  return res.status(statusCode).json({ error: errorMessages[statusCode] });
});

Error Response Mapping

Input NameHTTP StatusResponse BodyUse Case
unauthorized401{"error": "unauthorized"}Test authentication errors
forbidden403{"error": "forbidden"}Test authorization errors
notfound404{"error": "not found"}Test resource not found
conflict409{"error": "conflict"}Test resource conflicts
ratelimit429{"error": "rate limited"}Test retry logic
unavailable503{"error": "service unavailable"}Test circuit breaker
Other200{"message": "Hello ..."}Normal operation

Database Schema

The service uses an executions table to log all invocations:
CREATE TABLE executions (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    result_message TEXT,
    status_code INT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Unlike the Go services, the Node.js services don’t include automatic migrations. You need to create the table manually.

Database Pattern

The service uses a synchronous write pattern:
if (pool) {
  try {
    await pool.query(
      'INSERT INTO executions (name, result_message, status_code) VALUES ($1, $2, $3)',
      [name, message, statusCode]
    );
  } catch (err) {
    console.error('failed to record execution:', err);
  }
}
  • Database write completes before sending response
  • Errors are logged but don’t fail the request
  • Records both successful and error responses

Testing Error Scenarios

The service is designed to help test error handling in the Gateway service:

Test Circuit Breaker

Trigger consecutive failures to open the circuit:
# Trigger 5+ consecutive failures
for i in {1..10}; do
  curl -X POST http://localhost:3000/invoke \
    -H "Content-Type: application/json" \
    -d '{"name": "unavailable"}'
  echo ""
done
After 5 consecutive 503 errors, the Gateway service’s circuit breaker should open.

Test Retry Logic

Trigger a retryable error:
curl -X POST http://localhost:3000/invoke \
  -H "Content-Type: application/json" \
  -d '{"name": "ratelimit"}'
The Gateway service should retry this request once (if retry budget allows).

Test Error Mapping

Verify HTTP status to gRPC code mapping:
# Test 401 → Code.UNAUTHENTICATED
curl -X POST http://localhost:3000/invoke \
  -H "Content-Type: application/json" \
  -d '{"name": "unauthorized"}'

# Test 403 → Code.PERMISSION_DENIED
curl -X POST http://localhost:3000/invoke \
  -H "Content-Type: application/json" \
  -d '{"name": "forbidden"}'

# Test 404 → Code.NOT_FOUND
curl -X POST http://localhost:3000/invoke \
  -H "Content-Type: application/json" \
  -d '{"name": "notfound"}'

Service Dependencies

Upstream Dependencies

  • PostgreSQL: Optional, service runs without DB but won’t persist logs

Downstream Consumers

  • Gateway Service: Primary consumer, uses for demonstrating resilience patterns

Observability

Logging

The service logs database errors:
console.error('failed to record execution:', err);

Distributed Tracing

OpenTelemetry instrumentation captures:
  • HTTP requests
  • Database queries
  • Response status codes
Tracing is configured in node-services/custom-lang-service/tracing.js.

Health Checks

From node-services/custom-lang-service/app.js:7-14:
app.get('/healthz', async (_req, res) => {
  try {
    await healthCheck();
    res.status(200).send('ok\n');
  } catch (_err) {
    res.status(503).json({ error: 'db health check failed' });
  }
});

Performance Characteristics

  • Latency: ~1-5ms (without database)
  • Latency (with DB): ~5-15ms
  • Throughput: High (no complex processing)
  • Database Write: Synchronous, adds ~5-10ms
  • Database Pool: 10 connections

Integration Example

Using the service from the Gateway service:
// From gateway service
result, err := s.breaker.Execute(func() (invokeResult, error) {
    return s.callCustom(ctx, name)
})

// callCustom makes HTTP POST to /invoke
payload, _ := json.Marshal(map[string]string{"name": name})
httpReq, _ := http.NewRequestWithContext(
    ctx, http.MethodPost, s.baseURL+"/invoke", bytes.NewReader(payload))
httpReq.Header.Set("content-type", "application/json")

httpResp, err := s.httpClient.Do(httpReq)
// ... handle response

Common Issues

Database Not Configured

Impact: Service runs but doesn’t persist execution logs Solution: Set DATABASE_URL environment variable

Database Table Missing

Error: Database query fails with “relation does not exist” Solution: Create the executions table manually:
CREATE TABLE executions (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    result_message TEXT,
    status_code INT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Gateway Circuit Breaker Not Opening

Symptom: Even after many failures, circuit stays closed Cause: Not enough consecutive failures (need 5+) Solution: Send more consecutive error requests:
for i in {1..10}; do
  grpcurl -plaintext -d '{"name": "unavailable"}' \
    localhost:8082 \
    gateway.v1.GatewayService/InvokeCustom
done

Use Cases

Development Testing

Test error handling without breaking real services:
// Frontend code
const response = await gatewayClient.invokeCustom({ name: "ratelimit" });
// Simulates rate limiting without actually rate limiting

Load Testing

Test Gateway service resilience under failures:
# Mix of success and failure
for i in {1..100}; do
  if [ $((i % 10)) -eq 0 ]; then
    NAME="unavailable"
  else
    NAME="test-$i"
  fi
  
  curl -X POST http://localhost:3000/invoke \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"$NAME\"}"
done

Circuit Breaker Demo

Demonstrate circuit breaker behavior:
  1. Send successful requests → Circuit closed
  2. Send 5+ failures → Circuit opens
  3. Wait 30 seconds → Circuit half-open
  4. Send successful request → Circuit closes

Testing

Test the service directly:
# Health check
curl http://localhost:3000/healthz

# Normal invocation
curl -X POST http://localhost:3000/invoke \
  -H "Content-Type: application/json" \
  -d '{"name": "Test"}'

# Test all error codes
for name in unauthorized forbidden notfound conflict ratelimit unavailable; do
  echo "Testing $name:"
  curl -X POST http://localhost:3000/invoke \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"$name\"}"
  echo ""
done

Gateway Service

Primary consumer with circuit breaker logic

Services Overview

Learn about service architecture

Build docs developers (and LLMs) love