Skip to content

Dynamic Routes

Dynamic routes allow you to capture URL segments as parameters with type safety. StratusTS uses a typed parameter syntax that makes routes explicit and predictable.

What’s Working:

  • ✅ Typed parameter syntax (<id:int>, <slug:str>)
  • ✅ Multiple parameters in single route
  • ✅ Route matching and registration

Under Development:

  • 🚧 Parameter value extraction in controllers
  • 🚧 Automatic type conversion (int, str, etc.)
  • 🚧 Parameter validation

Not tested yet!

  • Wildcard routes ⏳

Planned Features:

  • 📋 Optional parameters
  • 📋 Parameter constraints (min, max, pattern)

StratusTS uses an explicit typed syntax for parameters:

<parameter_name:type>

Supported types:

  • int - Integer numbers (e.g., 1, 42, 999)
  • str - Strings (e.g., “hello”, “my-slug”, “user-123”)
  • bool - Boolean (spacific: true,false) its a nish case
  • * - Any (e.g., “hello”, “my-slug”, 123) but it’s Returns as String

Examples:

<id:int> // Matches: 1, 42, 999
<slug:str> // Matches: hello, my-post, about-us
<userId:int> // Matches: 1, 2, 3
<username:str> // Matches: john, alice, bob-smith
<is-active:bool> // Matches: true, false
<any:*> // Matches: john, alice, 123

Route definition:

src/users/routes.ts
import { Router } from 'stratus-ts';
import { getUser } from './controllers';
const { routes, route } = Router();
route.get('/<id:int>', getUser);
export default routes;

Matches:

Terminal window
/1 Matches (id = 1)
/42 Matches (id = 42)
/999 Matches (id = 999)
/abc No match (not an integer)
/1.5 No match (not an integer)

Route definition:

src/blog/routes.ts
route.get('/<slug:str>', getPost);

Matches:

Terminal window
/hello-world Matches (slug = "hello-world")
/my-first-post Matches (slug = "my-first-post")
/about Matches (slug = "about")
/123 Matches (slug = "123")

StratusTS supports multiple parameters in a single route.

Route definition:

src/post/routes.ts
route.get('/<userId:int>/posts/<postId:int>', getUserPost);

Matches:

Terminal window
/1/posts/5 Matches (userId=1, postId=5)
/42/posts/123 Matches (userId=42, postId=123)
/1/posts/abc No match (postId not int)

Route definition:

src/post/routes.ts
route.get('/<year:int>/<month:int>/<slug:str>', getBlogPost);

Matches:

Terminal window
/2024/01/my-post Matches
/2024/12/hello-world Matches
/2024/abc/my-post No match (month not int)

Controller (when parameter extraction is implemented):

Section titled “Controller (when parameter extraction is implemented):”
src/post/routes.ts
const getBlogPost: ControllerType<{
year: number
month: number
slug: string
}> = async (question, reply) => {
const { year, month, slug } = question.params();
// all the params
const post = await Post.findOne({
where: {
year,
month,
slug,
},
});
reply.status(ok()).json({ data: post });
};

The typed parameter syntax provides several benefits:

src/app/routes.ts
// others way (ambiguous)
route.get('/:id', getUser); // Is id a number? String? UUID?
// stratus-ts way (explicit)
route.get('/<id:int>', getUser); // Clearly an integer

When parameter extraction is implemented, types will be automatically converted:

src/app/routes.ts
// others way:
const id = parseInt(request.params.id); // Manual conversion
// No manual parsing needed
const { id } = question.params(); // Already a number!

Routes that don’t match the type won’t trigger the controller:

src/app/routes.ts
route.get('/<id:int>', getUser);
// /users/abc → 404 (doesn't match route)
// /users/123 → Calls getUser with id=123

More specific routes should come before general ones:

src/app/routes.ts
// ✅ Correct order
route.get('/featured', getFeaturedPosts); // Static route first
route.get('/recent', getRecentPosts); // Static route
route.get('/<slug:str>', getPost); // Dynamic last
// ❌ Wrong order
route.get('/<slug:str>', getPost); // Matches everything!
route.get('/featured', getFeaturedPosts); // Never reached

Rule: Define routes from most specific to least specific.

Planned syntax:

src/post/routes.ts
route.get('/posts/<id:int>?', listOrGetPost);

Intended behavior:

src/app/routes.ts
/posts → List all posts (id is undefined)
/posts/5 → Get post 5 (id = 5)

Planned syntax:

route.get('/<id:uuid>', getUser);
route.get('/<date:date>', getPostsByDate);

Intended matching:

<id:uuid> → Matches UUID format only
<date:date> → Matches date format (YYYY-MM-DD)

Planned syntax:

src/app/routes.ts
route.get('/<id:int:min=1:max=9999>', getUser);
route.get('/<slug:str:pattern=[a-z-]+>', getPost);

Planned syntax:

src/app/routes.ts
route.get('/files/<path:path>', serveFile);

Would match:

Terminal window
/files/guide.pdf
/files/logo.png
/files/file.txt
src/app/routes.ts
// ✅ Good - clear what the parameter is
route.get('/<userId:int>/posts/<postId:int>', getPost);
// ❌ Bad - unclear parameter names
route.get('/<a:int>/posts/<b:int>', getPost);
src/app/routes.ts
// ✅ Good - use int for numeric IDs
route.get('/<id:int>', getUser);
// ✅ Good - use str for slugs
route.get('/<slug:str>', getPost);
// ❌ Bad - using str for numeric ID
route.get('/<age:str>', getUser); // Should be int
src/app/routes.ts
// ✅ Good - reasonable nesting
route.get('/<userId:int>/posts/<postId:int>', getPost);
// ⚠️ Consider simplifying - too nested
route.get(
'/<userId:int>/posts/<postId:int>/comments/<commentId:int>/replies/<replyId:int>',
getReply
);

Here’s a complete example using typed dynamic routes:

src/app/routes.ts
// src/blog/routes.ts
import { Router } from 'stratus-ts';
import {
listPosts,
getPost,
getPostsByYear,
getPostsByYearMonth,
getUserPosts,
} from './controllers';
const { routes, route } = Router();
// Static routes first
route.get('/', listPosts);
route.get('/featured', getFeaturedPosts);
// Dynamic routes (more specific first)
route.get('/<year:int>/<month:int>/<slug:str>', getPostsByYearMonth);
route.get('/<year:int>/<slug:str>', getPostsByYear);
route.get('/user/<userId:int>', getUserPosts);
route.get('/<slug:str>', getPost);
export default routes;
src/app/controllers.ts
// src/blog/controllers.ts
import { type ControllerType, ok } from 'stratus-ts';
export const getPostsByYearMonth: ControllerType<{
year: number
month: number
slug: string
}> = async (question, reply) => {
const { year, month, slug } = question.params();
// year: number, month: number, slug: string
const post = await Post.findOne({
where: { year, month, slug },
});
reply.status(ok()).json({ data: post });
};
  • ✅ Typed parameter syntax implemented
  • ✅ Route matching works
  • ✅ Wildcard routes
  • 🚧 Parameter extraction in development
  • 🚧 Complete parameter extraction
  • 🚧 Automatic type conversion
  • 🚧 Better error messages
  • 📋 Optional parameters
  • 📋 Custom types
  • 📋 Parameter constraints

We value your input on dynamic routing!

Found a bug? Open an issue

Include:

  • Route definition
  • Expected behavior
  • Actual behavior
  • Example code

Want a feature? Start a discussion

Share:

  • Your use case
  • Example of desired syntax
  • Why it’s important
  • GitHub: Watch the repository for updates
  • Changelog: Check release notes
  • This page: Updates as features land

Dynamic routing is actively evolving. Have questions?

  • Documentation: Check back for updates
  • GitHub Issues: Report problems
  • Discussions: Ask questions
  • Examples: See Examples for working code

Thank you for your patience as we complete this feature!