20 KiB
Tenant API Implementation (Tenant Management)
This document describes the implementation of the Tenant Management API endpoints for the MaplePress frontend, integrated with the MaplePress backend API.
Overview
The Tenant API endpoints manage tenants (organizations) in the multi-tenant system. Each tenant represents an organization with its own users, resources, and isolated data. Tenants are identified by both UUID and a URL-friendly slug.
Backend API Endpoints
Create Tenant
Endpoint: POST /api/v1/tenants
Authentication: Required (JWT token)
Get Tenant by ID
Endpoint: GET /api/v1/tenants/{id}
Authentication: Required (JWT token)
Get Tenant by Slug
Endpoint: GET /api/v1/tenants/slug/{slug}
Authentication: Required (JWT token)
Documentation: /cloud/maplepress-backend/docs/API.md (lines 416-558)
Request/Response Structures
Create Tenant Request
{
"name": "TechStart Inc",
"slug": "techstart"
}
Tenant Response
{
"id": "850e8400-e29b-41d4-a716-446655440000",
"name": "TechStart Inc",
"slug": "techstart",
"status": "active",
"created_at": "2024-10-24T00:00:00Z",
"updated_at": "2024-10-24T00:00:00Z"
}
Frontend Implementation
TenantService (src/services/API/TenantService.js)
Handles all tenant management operations with the backend API.
Key Features:
- Create new tenants
- Retrieve tenant by ID or slug
- Client-side validation (name and slug)
- Slug generation helper
- Response transformation (snake_case to camelCase)
- User-friendly error message mapping
Methods:
createTenant(tenantData)
Create a new tenant (organization).
import TenantService from './services/API/TenantService';
const tenant = await TenantService.createTenant({
name: "TechStart Inc",
slug: "techstart"
});
console.log(tenant);
// Output:
// {
// id: "850e8400-...",
// name: "TechStart Inc",
// slug: "techstart",
// status: "active",
// createdAt: Date object
// }
Parameters:
tenantData.name(string, required): Tenant/organization nametenantData.slug(string, required): URL-friendly identifier (lowercase, hyphens only)
Returns:
{
id: string, // Tenant ID (UUID)
name: string, // Tenant name
slug: string, // Tenant slug
status: string, // Tenant status (e.g., "active")
createdAt: Date // Creation timestamp
}
Throws:
- "Tenant data is required" - If tenantData is missing
- "Tenant name is required" - If name is missing
- "Tenant slug is required" - If slug is missing
- "Tenant slug must contain only lowercase letters, numbers, and hyphens" - Invalid slug format
- "Tenant slug already exists. Please choose a different slug." - Slug conflict (409)
- "Authentication required. Please log in to continue." - Missing/invalid token
getTenantById(tenantId)
Retrieve tenant information by ID.
const tenant = await TenantService.getTenantById("850e8400-...");
console.log(tenant.name); // "TechStart Inc"
Parameters:
tenantId(string, required): Tenant ID (UUID format)
Returns:
{
id: string,
name: string,
slug: string,
status: string,
createdAt: Date,
updatedAt: Date
}
Throws:
- "Tenant ID is required" - If ID is missing
- "Invalid tenant ID format" - If ID is not a valid UUID
- "Tenant not found." - If tenant doesn't exist (404)
- "Authentication required. Please log in to continue." - Missing/invalid token
getTenantBySlug(slug)
Retrieve tenant information by slug.
const tenant = await TenantService.getTenantBySlug("techstart");
console.log(tenant.id); // "850e8400-..."
Parameters:
slug(string, required): Tenant slug
Returns:
{
id: string,
name: string,
slug: string,
status: string,
createdAt: Date,
updatedAt: Date
}
Throws:
- "Tenant slug is required" - If slug is missing
- "Tenant slug cannot be empty" - If slug is empty after trimming
- "Tenant not found." - If tenant doesn't exist (404)
- "Authentication required. Please log in to continue." - Missing/invalid token
generateSlug(name)
Generate a URL-friendly slug from a tenant name.
const slug = TenantService.generateSlug("TechStart Inc!");
console.log(slug); // "techstart-inc"
const slug2 = TenantService.generateSlug("My Company");
console.log(slug2); // "my-company"
Parameters:
name(string): Tenant name
Returns: string - Generated slug (lowercase, hyphens, alphanumeric only)
Transformation Rules:
- Converts to lowercase
- Replaces spaces with hyphens
- Removes special characters
- Removes consecutive hyphens
- Removes leading/trailing hyphens
validateSlug(slug)
Validate a tenant slug format.
const result = TenantService.validateSlug("techstart");
console.log(result); // { valid: true, error: null }
const invalid = TenantService.validateSlug("Tech Start!");
console.log(invalid); // { valid: false, error: "Slug must contain..." }
Parameters:
slug(string): Slug to validate
Returns:
{
valid: boolean, // true if slug is valid
error: string|null // Error message if invalid, null if valid
}
Validation Rules:
- Required (non-empty)
- Length: 2-50 characters
- Format: lowercase letters, numbers, hyphens only
- Cannot start or end with hyphen
- Cannot contain consecutive hyphens
validateName(name)
Validate a tenant name.
const result = TenantService.validateName("TechStart Inc");
console.log(result); // { valid: true, error: null }
const invalid = TenantService.validateName("A");
console.log(invalid); // { valid: false, error: "Name must be at least 2 characters" }
Parameters:
name(string): Name to validate
Returns:
{
valid: boolean,
error: string|null
}
Validation Rules:
- Required (non-empty)
- Length: 2-100 characters
Data Flow
Create Tenant Flow
User provides tenant name and slug
↓
TenantService.createTenant()
↓
Validate name and slug (client-side)
↓
ApiClient.post() with JWT token
↓
Token automatically refreshed if needed
↓
POST /api/v1/tenants
↓
Backend validates (slug uniqueness, format)
↓
Backend creates tenant in database
↓
Backend returns tenant data
↓
TenantService transforms response
↓
Component receives tenant data
Get Tenant Flow
User requests tenant (by ID or slug)
↓
TenantService.getTenantById() or getTenantBySlug()
↓
Validate input format
↓
ApiClient.get() with JWT token
↓
Token automatically refreshed if needed
↓
GET /api/v1/tenants/{id} or /api/v1/tenants/slug/{slug}
↓
Backend retrieves tenant from database
↓
Backend returns tenant data
↓
TenantService transforms response
↓
Component receives tenant data
Error Handling
Error Types
| Error Condition | Response | Frontend Behavior |
|---|---|---|
| Missing authentication | 401 Unauthorized | "Authentication required. Please log in to continue." |
| Invalid tenant data | 400 Bad Request | Specific validation error |
| Slug already exists | 409 Conflict | "Tenant slug already exists. Please choose a different slug." |
| Tenant not found | 404 Not Found | "Tenant not found." |
| Server error | 500 Internal Server Error | Generic error message |
Error Message Mapping
// Backend error → Frontend error
"unauthorized" → "Authentication required. Please log in to continue."
"conflict" / "already exists" → "Tenant slug already exists. Please choose a different slug."
"not found" → "Tenant not found."
"slug" → "Invalid tenant slug. Must contain only lowercase letters, numbers, and hyphens."
"name" → "Invalid tenant name provided."
Validation Rules
Tenant Name
- Required: Cannot be empty
- Length: 2-100 characters
- Format: Any printable characters allowed
Tenant Slug
- Required: Cannot be empty
- Length: 2-50 characters
- Format: Lowercase letters, numbers, hyphens only
- Pattern:
^[a-z0-9-]+$ - Restrictions:
- Cannot start or end with hyphen
- Cannot contain consecutive hyphens
- Must be URL-safe
Usage Examples
Create a New Tenant
import React, { useState } from 'react';
import TenantService from '../../services/API/TenantService';
function CreateTenantForm() {
const [name, setName] = useState('');
const [slug, setSlug] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
// Auto-generate slug from name
const handleNameChange = (e) => {
const newName = e.target.value;
setName(newName);
// Generate slug automatically
const generatedSlug = TenantService.generateSlug(newName);
setSlug(generatedSlug);
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
// Validate before sending
const nameValidation = TenantService.validateName(name);
if (!nameValidation.valid) {
setError(nameValidation.error);
setLoading(false);
return;
}
const slugValidation = TenantService.validateSlug(slug);
if (!slugValidation.valid) {
setError(slugValidation.error);
setLoading(false);
return;
}
try {
const tenant = await TenantService.createTenant({ name, slug });
console.log("Tenant created:", tenant);
// Redirect or show success message
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Tenant Name:</label>
<input
type="text"
value={name}
onChange={handleNameChange}
placeholder="TechStart Inc"
maxLength={100}
/>
</div>
<div>
<label>Tenant Slug:</label>
<input
type="text"
value={slug}
onChange={(e) => setSlug(e.target.value.toLowerCase())}
placeholder="techstart"
maxLength={50}
/>
<small>URL: /tenants/{slug}</small>
</div>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Tenant'}
</button>
</form>
);
}
export default CreateTenantForm;
Display Tenant Information
import React, { useEffect, useState } from 'react';
import TenantService from '../../services/API/TenantService';
function TenantProfile({ tenantId }) {
const [tenant, setTenant] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const fetchTenant = async () => {
try {
const data = await TenantService.getTenantById(tenantId);
setTenant(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchTenant();
}, [tenantId]);
if (loading) return <div>Loading tenant...</div>;
if (error) return <div>Error: {error}</div>;
if (!tenant) return null;
return (
<div className="tenant-profile">
<h2>{tenant.name}</h2>
<p>Slug: {tenant.slug}</p>
<p>Status: {tenant.status}</p>
<p>Created: {tenant.createdAt.toLocaleDateString()}</p>
<p>Updated: {tenant.updatedAt.toLocaleDateString()}</p>
</div>
);
}
export default TenantProfile;
Search Tenant by Slug
import TenantService from '../../services/API/TenantService';
function TenantLookup() {
const [slug, setSlug] = useState('');
const [tenant, setTenant] = useState(null);
const handleSearch = async () => {
try {
const data = await TenantService.getTenantBySlug(slug);
setTenant(data);
} catch (error) {
console.error("Tenant not found:", error.message);
setTenant(null);
}
};
return (
<div>
<input
value={slug}
onChange={(e) => setSlug(e.target.value)}
placeholder="Enter tenant slug"
/>
<button onClick={handleSearch}>Search</button>
{tenant && (
<div>
<h3>{tenant.name}</h3>
<p>ID: {tenant.id}</p>
</div>
)}
</div>
);
}
Validate Slug in Real-Time
import React, { useState, useEffect } from 'react';
import TenantService from '../../services/API/TenantService';
function SlugInput({ value, onChange }) {
const [validation, setValidation] = useState({ valid: true, error: null });
useEffect(() => {
if (value) {
const result = TenantService.validateSlug(value);
setValidation(result);
}
}, [value]);
return (
<div>
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value.toLowerCase())}
className={validation.valid ? '' : 'error'}
/>
{!validation.valid && (
<span className="error-message">{validation.error}</span>
)}
{validation.valid && value && (
<span className="success-message">✓ Valid slug</span>
)}
</div>
);
}
Testing the Tenant API
1. Prerequisites
- Backend running at
http://localhost:8000 - Frontend running at
http://localhost:5173 - User logged in (valid JWT token)
2. Test Create Tenant
// In browser console after login
import TenantService from './services/API/TenantService';
// Generate slug from name
const slug = TenantService.generateSlug("My New Company");
console.log("Generated slug:", slug); // "my-new-company"
// Validate before creating
const validation = TenantService.validateSlug(slug);
console.log("Slug valid:", validation.valid); // true
// Create tenant
const tenant = await TenantService.createTenant({
name: "My New Company",
slug: slug
});
console.log("Created tenant:", tenant);
3. Test Get Tenant by ID
// Using tenant ID from creation
const tenant = await TenantService.getTenantById("850e8400-...");
console.log("Tenant by ID:", tenant);
4. Test Get Tenant by Slug
const tenant = await TenantService.getTenantBySlug("my-new-company");
console.log("Tenant by slug:", tenant);
5. Test with curl
# 1. Login and get access token
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!"
}' | jq -r '.access_token')
# 2. Create tenant
curl -X POST http://localhost:8000/api/v1/tenants \
-H "Content-Type: application/json" \
-H "Authorization: JWT $ACCESS_TOKEN" \
-d '{
"name": "Test Company",
"slug": "test-company"
}' | jq
# 3. Get tenant by ID (use ID from creation response)
TENANT_ID="850e8400-e29b-41d4-a716-446655440000"
curl -X GET "http://localhost:8000/api/v1/tenants/$TENANT_ID" \
-H "Authorization: JWT $ACCESS_TOKEN" | jq
# 4. Get tenant by slug
curl -X GET "http://localhost:8000/api/v1/tenants/slug/test-company" \
-H "Authorization: JWT $ACCESS_TOKEN" | jq
# 5. Test slug conflict (should fail with 409)
curl -X POST http://localhost:8000/api/v1/tenants \
-H "Content-Type: application/json" \
-H "Authorization: JWT $ACCESS_TOKEN" \
-d '{
"name": "Another Company",
"slug": "test-company"
}' | jq
# Expected: 409 Conflict
6. Test Slug Generation
// Test various slug generation scenarios
const tests = [
{ input: "TechStart Inc", expected: "techstart-inc" },
{ input: "My Company!", expected: "my-company" },
{ input: " Spaces Everywhere ", expected: "spaces-everywhere" },
{ input: "Multiple---Hyphens", expected: "multiple-hyphens" },
{ input: "123 Numbers", expected: "123-numbers" },
];
tests.forEach(({ input, expected }) => {
const result = TenantService.generateSlug(input);
console.log(`Input: "${input}"`);
console.log(`Expected: "${expected}"`);
console.log(`Got: "${result}"`);
console.log(`✓ Pass: ${result === expected}\n`);
});
Integration with AuthManager
The tenant created during registration is stored in AuthManager:
import { useAuth } from './services/Services';
import TenantService from './services/API/TenantService';
const { authManager } = useAuth();
// Get stored tenant from AuthManager
const storedTenant = authManager.getTenant();
console.log("Stored tenant:", storedTenant);
// { id: "...", name: "...", slug: "..." }
// Fetch fresh tenant data from API
const freshTenant = await TenantService.getTenantById(storedTenant.id);
console.log("Fresh tenant:", freshTenant);
// Compare
if (storedTenant.name !== freshTenant.name) {
console.warn("Tenant data has changed");
}
Multi-Tenant Context
Tenants provide isolation for multi-tenant applications:
// Get current user's tenant
import MeService from './services/API/MeService';
import TenantService from './services/API/TenantService';
const profile = await MeService.getMe();
const tenantId = profile.tenantId;
// Get full tenant details
const tenant = await TenantService.getTenantById(tenantId);
console.log("Current tenant:", tenant.name);
// Use tenant context for operations
console.log("Working in tenant:", tenant.slug);
Use Cases
1. Organization Switcher
Allow users to switch between tenants (if they belong to multiple).
2. Tenant Profile Display
Show tenant information in dashboard header or settings.
3. Tenant Creation Wizard
Guide users through creating a new tenant/organization.
4. Tenant Settings Page
Display and edit tenant information.
5. Multi-Tenant Routing
Use tenant slug in URLs (e.g., /t/acme-corp/dashboard).
Troubleshooting
"Tenant slug already exists" error
Possible causes:
- Slug is already taken by another tenant
- User trying to create duplicate tenant
Solution:
- Try a different slug
- Add numbers or suffix to make it unique
- Use the slug validation before submitting
"Tenant not found" error
Possible causes:
- Tenant ID is incorrect
- Tenant was deleted
- User doesn't have access to tenant
Solution:
- Verify tenant ID is correct UUID format
- Check if tenant still exists
- Verify user has proper access rights
Slug validation fails unexpectedly
Possible causes:
- Special characters in slug
- Uppercase letters
- Leading/trailing spaces or hyphens
Solution:
- Use
generateSlug()to auto-generate valid slug - Use
validateSlug()before submitting - Convert to lowercase and trim whitespace
Related Files
Created Files
src/services/API/TenantService.js
docs/TENANT_API.md
Backend Reference Files
cloud/maplepress-backend/docs/API.md (lines 416-558)
cloud/maplepress-backend/internal/usecase/tenant/
cloud/maplepress-backend/internal/repository/tenant/
Related Documentation
- REGISTRATION_API.md - Initial tenant creation during registration
- ME_API.md - User profile includes tenant ID
- FRONTEND_ARCHITECTURE.md - Architecture overview
- README.md - Documentation index
- Backend API Documentation - Complete API reference
Summary
The Tenant API implementation provides:
- Tenant Creation: Create new organizations with validation
- Tenant Retrieval: Get tenant by ID or slug
- Slug Generation: Auto-generate URL-friendly slugs
- Validation Helpers: Client-side validation before API calls
- Error Handling: Clear error messages and graceful failures
- Multi-Tenant Support: Foundation for multi-tenant architecture
This is essential for managing organizations in a multi-tenant SaaS application.
Last Updated: October 30, 2024 Frontend Version: 0.0.0 Documentation Version: 1.0.0