Your First Service
Learn how to create, register, and communicate with your first service using ScoutQuest. This tutorial covers both service registration and discovery.
Prerequisites
Before starting this tutorial, make sure you have:
- ScoutQuest server running (see Installation Guide)
- Node.js 16+ or Rust 1.75+ installed
- Basic understanding of HTTP APIs
Quick Server Check
Verify your ScoutQuest server is running:
curl http://localhost:8080/health
You should see:
{"status":"healthy","timestamp":"...","version":"..."}
Step 1: Create a Simple Web Service
Let's start by creating a simple web service that we'll register with ScoutQuest.
Create a Node.js Service
First, install the required dependencies:
mkdir my-first-service
cd my-first-service
npm init -y
npm install express scoutquest-js
Create server.js
:
const express = require('express');
const { ScoutQuestClient } = require('scoutquest-js');
const app = express();
const port = 3000;
const client = new ScoutQuestClient('http://localhost:8080');
app.use(express.json());
// Health check endpoint (required for ScoutQuest monitoring)
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
service: 'my-first-service'
});
});
// Simple API endpoint
app.get('/api/hello', (req, res) => {
res.json({
message: 'Hello from my first service!',
timestamp: new Date().toISOString()
});
});
// Get user data (example endpoint)
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
res.json({
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`,
service: 'my-first-service'
});
});
let serviceInstance = null;
// Start server and register with ScoutQuest
app.listen(port, async () => {
console.log(`Service running on port ${port}`);
try {
// Register service with ScoutQuest
serviceInstance = await client.registerService('my-first-service', 'localhost', port, {
tags: ['api', 'tutorial', 'v1'],
metadata: {
version: '1.0.0',
description: 'My first ScoutQuest service',
endpoints: ['/api/hello', '/api/users/:id']
},
healthCheck: {
path: '/health',
interval: 30,
timeout: 5
}
});
console.log(`ā
Service registered with ScoutQuest!`);
console.log(` Instance ID: ${serviceInstance.id}`);
console.log(` URL: http://localhost:${port}`);
console.log(` Health Check: http://localhost:${port}/health`);
} catch (error) {
console.error('ā Failed to register service:', error.message);
process.exit(1);
}
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('š Shutting down gracefully...');
if (serviceInstance) {
await client.deregisterService(serviceInstance.id);
console.log('ā
Service deregistered from ScoutQuest');
}
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('š Shutting down gracefully...');
if (serviceInstance) {
await client.deregisterService(serviceInstance.id);
console.log('ā
Service deregistered from ScoutQuest');
}
process.exit(0);
});
Run your service:
node server.js
Create a Rust Service
Create a new Rust project:
cargo new my-first-service
cd my-first-service
Add dependencies to Cargo.toml
:
[dependencies]
scoutquest-rust = "1.0"
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors"] }
Create src/main.rs
:
use axum::{
extract::Path,
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use scoutquest_rust::ServiceDiscoveryClient;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::signal;
use tower_http::cors::CorsLayer;
#[derive(Serialize)]
struct HealthResponse {
status: String,
timestamp: String,
service: String,
}
#[derive(Serialize)]
struct HelloResponse {
message: String,
timestamp: String,
}
#[derive(Serialize)]
struct UserResponse {
id: String,
name: String,
email: String,
service: String,
}
async fn health_check() -> Json {
Json(HealthResponse {
status: "healthy".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
service: "my-first-service".to_string(),
})
}
async fn hello() -> Json {
Json(HelloResponse {
message: "Hello from my first service!".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
})
}
async fn get_user(Path(user_id): Path) -> Json {
Json(UserResponse {
id: user_id.clone(),
name: format!("User {}", user_id),
email: format!("user{}@example.com", user_id),
service: "my-first-service".to_string(),
})
}
#[tokio::main]
async fn main() -> Result<(), Box> {
println!("š Starting my first service...");
// Create ScoutQuest client
let client = ServiceDiscoveryClient::new("http://localhost:8080")?;
// Create router
let app = Router::new()
.route("/health", get(health_check))
.route("/api/hello", get(hello))
.route("/api/users/:id", get(get_user))
.layer(CorsLayer::permissive());
// Start server
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
println!("š” Service running on port 3000");
// Register service with ScoutQuest
let mut metadata = HashMap::new();
metadata.insert("version".to_string(), "1.0.0".to_string());
metadata.insert("description".to_string(), "My first ScoutQuest service".to_string());
metadata.insert("endpoints".to_string(), "/api/hello,/api/users/:id".to_string());
let service_instance = client.register_service(
"my-first-service",
"localhost",
3000,
Some(metadata)
).await?;
println!("ā
Service registered with ScoutQuest!");
println!(" Instance ID: {}", service_instance.id);
println!(" URL: http://localhost:3000");
println!(" Health Check: http://localhost:3000/health");
// Start server with graceful shutdown
tokio::select! {
result = axum::serve(listener, app) => {
if let Err(err) = result {
eprintln!("ā Server error: {}", err);
}
}
_ = shutdown_signal() => {
println!("š Shutting down gracefully...");
// Deregister service
if let Err(err) = client.deregister_service(&service_instance.id).await {
eprintln!("ā ļø Failed to deregister service: {}", err);
} else {
println!("ā
Service deregistered from ScoutQuest");
}
}
}
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
Add chrono dependency for timestamps:
cargo add chrono --features serde
Run your service:
cargo run
Step 2: Test Your Service
Once your service is running, test it to ensure everything works correctly:
Test Direct Access
# Health check
curl http://localhost:3000/health
# Hello endpoint
curl http://localhost:3000/api/hello
# User endpoint
curl http://localhost:3000/api/users/123
Verify Registration in ScoutQuest
# List all registered services
curl http://localhost:8080/api/services
# Get specific service instances
curl http://localhost:8080/api/services/my-first-service/instances
You should see your service listed with status "healthy".
Step 3: Create a Client Service
Now let's create a second service that discovers and communicates with the first one.
Create client.js
:
const { ScoutQuestClient } = require('scoutquest-js');
async function clientDemo() {
const client = new ScoutQuestClient('http://localhost:8080');
console.log('š ScoutQuest Client Demo');
console.log('========================');
try {
// Discover the service
console.log('š Discovering "my-first-service"...');
const serviceInstance = await client.discoverService('my-first-service');
console.log('ā
Service found:');
console.log(` ID: ${serviceInstance.id}`);
console.log(` Address: ${serviceInstance.host}:${serviceInstance.port}`);
console.log(` Status: ${serviceInstance.status}`);
console.log(` Tags: ${serviceInstance.tags.join(', ')}`);
// Make HTTP calls through ScoutQuest
console.log('\nš” Making HTTP calls through ScoutQuest...');
// Call hello endpoint
const helloResponse = await client.get('my-first-service', '/api/hello');
console.log('ā
Hello response:', helloResponse.data);
// Call user endpoint
const userResponse = await client.get('my-first-service', '/api/users/456');
console.log('ā
User response:', userResponse.data);
// List all services
console.log('\nš All registered services:');
const services = await client.listServices();
services.forEach(service => {
console.log(` - ${service.name} (${service.instanceCount} instances)`);
});
// List instances of our service
console.log('\nš¢ Instances of "my-first-service":');
const instances = await client.listServiceInstances('my-first-service');
instances.forEach(instance => {
console.log(` - ${instance.id} at ${instance.host}:${instance.port} [${instance.status}]`);
});
} catch (error) {
console.error('ā Error:', error.message);
}
}
// Run the demo
clientDemo();
Run the client:
node client.js
Create src/bin/client.rs
:
use scoutquest_rust::ServiceDiscoveryClient;
#[tokio::main]
async fn main() -> Result<(), Box> {
println!("š ScoutQuest Client Demo");
println!("========================");
let client = ServiceDiscoveryClient::new("http://localhost:8080")?;
// Discover the service
println!("š Discovering \"my-first-service\"...");
match client.discover_service("my-first-service").await {
Ok(instance) => {
println!("ā
Service found:");
println!(" ID: {}", instance.id);
println!(" Address: {}:{}", instance.host, instance.port);
println!(" Status: {}", instance.status);
println!(" Tags: {:?}", instance.tags);
// Make HTTP calls through service discovery
println!("\nš” Making HTTP calls...");
// Call hello endpoint
match client.get_service("my-first-service", "/api/hello").await {
Ok(response) => {
println!("ā
Hello response: {}", response);
}
Err(e) => println!("ā Hello call failed: {}", e),
}
// Call user endpoint
match client.get_service("my-first-service", "/api/users/456").await {
Ok(response) => {
println!("ā
User response: {}", response);
}
Err(e) => println!("ā User call failed: {}", e),
}
}
Err(e) => {
println!("ā Service discovery failed: {}", e);
}
}
// List all services
println!("\nš All registered services:");
match client.list_services().await {
Ok(services) => {
for service in services {
println!(" - {} ({} instances)", service.name, service.instance_count);
}
}
Err(e) => println!("ā Failed to list services: {}", e),
}
Ok(())
}
Run the client:
cargo run --bin client
Step 4: Understanding What Happened
Congratulations! You've successfully:
ā What You Accomplished
- Service Registration: Your service automatically registered itself with ScoutQuest
- Health Monitoring: ScoutQuest monitors your service health every 30 seconds
- Service Discovery: Your client found the service without knowing its exact location
- Load-Balanced Communication: ScoutQuest handled routing your HTTP requests
- Graceful Shutdown: Services properly deregistered when stopped
Key Concepts Demonstrated
- Service Registry: Central place where services register themselves
- Health Checks: Automatic monitoring of service availability
- Service Discovery: Finding services by name instead of hardcoded addresses
- Metadata & Tags: Additional information for service filtering and routing
- HTTP Proxying: ScoutQuest routes requests to healthy service instances
Next Steps
Troubleshooting
Common Issues
- Service registration fails: Check if ScoutQuest server is running on port 8080
- Health check failures: Ensure your
/health
endpoint returns 200 status - Service not found: Wait a few seconds after registration for health checks to pass
- Connection refused: Verify firewall settings and port availability
For more detailed troubleshooting, see the Troubleshooting Guide.