Overview
TailStack uses Express Router for clean, modular routing. Routes are organized by feature, making it easy to maintain and scale your API.
Route Structure
Routes follow a hierarchical structure:
src/routes/
├── index.ts # Main router that combines all routes
└── weather.routes.ts # Feature-specific route definitions
Main Router
The main router combines all feature routes:
packages/core/source/Server/src/routes/index.ts
import { Router } from 'express' ;
import weatherRoutes from './weather.routes' ;
const router = Router ();
router . use ( '/weather' , weatherRoutes );
export default router ;
This router is mounted at /api in app.ts, so all routes are prefixed with /api.
Feature Routes Example
Here’s the weather routes implementation:
packages/core/source/Server/src/routes/weather.routes.ts
import { Router } from 'express' ;
import { WeatherController } from '../controller/weather.controller' ;
const router = Router ();
router . get ( '/location' , WeatherController . getWeatherByLocation );
router . get ( '/coordinates' , WeatherController . getWeatherByCoordinates );
export default router ;
Available Endpoints
With the current setup, the following endpoints are available:
Method Endpoint Description GET/api/weather/locationGet weather by location name GET/api/weather/coordinatesGet weather by coordinates
Creating New Routes
Create Route File
Create a new route file in the routes/ directory: src/routes/users.routes.ts
import { Router } from 'express' ;
import { UserController } from '../controller/user.controller' ;
const router = Router ();
router . get ( '/' , UserController . getAllUsers );
router . get ( '/:id' , UserController . getUserById );
router . post ( '/' , UserController . createUser );
router . put ( '/:id' , UserController . updateUser );
router . delete ( '/:id' , UserController . deleteUser );
export default router ;
Register Route
Add the route to the main router: import { Router } from 'express' ;
import weatherRoutes from './weather.routes' ;
import userRoutes from './users.routes' ;
const router = Router ();
router . use ( '/weather' , weatherRoutes );
router . use ( '/users' , userRoutes );
export default router ;
Create Controller
Implement the controller with your business logic: src/controller/user.controller.ts
import { Request , Response } from 'express' ;
export class UserController {
static async getAllUsers ( req : Request , res : Response ) : Promise < void > {
// Implementation
}
static async getUserById ( req : Request , res : Response ) : Promise < void > {
// Implementation
}
// ... other methods
}
Weather API Example
Let’s look at a complete example with the weather controller:
packages/core/source/Server/src/controller/weather.controller.ts
import { Request , Response } from 'express' ;
import { WeatherService } from '../services/weather.service' ;
export class WeatherController {
/**
* Get weather by location name
*/
static async getWeatherByLocation ( req : Request , res : Response ) : Promise < void > {
try {
const { location } = req . query ;
if ( ! location || typeof location !== 'string' ) {
res . status ( 400 ). json ({
error: 'Bad Request' ,
message: 'Location parameter is required' ,
});
return ;
}
const weatherData = await WeatherService . getWeatherByLocation ( location );
res . json ( weatherData );
} catch ( error ) {
const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
res . status ( 500 ). json ({
error: 'Internal Server Error' ,
message: errorMessage ,
});
}
}
/**
* Get weather by coordinates
*/
static async getWeatherByCoordinates ( req : Request , res : Response ) : Promise < void > {
try {
const { lat , lon } = req . query ;
if ( ! lat || ! lon ) {
res . status ( 400 ). json ({
error: 'Bad Request' ,
message: 'Both lat and lon parameters are required' ,
});
return ;
}
const latitude = parseFloat ( lat as string );
const longitude = parseFloat ( lon as string );
if ( isNaN ( latitude ) || isNaN ( longitude )) {
res . status ( 400 ). json ({
error: 'Bad Request' ,
message: 'Lat and lon must be valid numbers' ,
});
return ;
}
const weatherData = await WeatherService . getWeatherByCoordinates ( latitude , longitude );
res . json ( weatherData );
} catch ( error ) {
const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
res . status ( 500 ). json ({
error: 'Internal Server Error' ,
message: errorMessage ,
});
}
}
}
Example Requests
# Get weather by location
curl "http://localhost:5000/api/weather/location?location=London"
# Get weather by coordinates
curl "http://localhost:5000/api/weather/coordinates?lat=51.5074&lon=-0.1278"
Controller Pattern
TailStack uses static controller methods for route handlers:
Static methods are simpler and don’t require instantiation. They work well for stateless request handlers.
Controllers handle errors and return appropriate HTTP status codes (400 for validation errors, 500 for server errors).
TypeScript ensures type safety for request and response objects, preventing runtime errors.
Controllers handle HTTP concerns (request/response), while services contain business logic.
Route Parameters
Express supports several parameter types:
Query Parameters
// Route: /api/weather/location?location=London
const { location } = req . query ;
URL Parameters
// Route: /api/users/:id
const { id } = req . params ;
Request Body
// POST /api/users
const { name , email } = req . body ;
Best Practices
Use Controllers Separate route definitions from business logic using controllers
Validate Input Always validate request parameters before processing
Handle Errors Implement proper error handling with appropriate status codes
Type Safety Use TypeScript types for request/response objects
Group related routes together in feature-specific route files for better organization and maintainability.
Next Steps
Middleware Learn about middleware for authentication, validation, and more
Server Setup Understand how routes are mounted in the Express app