Path Parameters
Extract typed parameters from URL paths
Path parameters allow you to capture dynamic segments from URLs. JustScale provides compile-time type safety for path parameters extracted from route patterns.
Basic Path Parameters
Define path parameters using the :paramName syntax:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const UsersController = createController('/users', {
routes: () => ({
getOne: Get('/:id').handle(({ params, res }) => {
// params.id is typed as string and guaranteed to exist
const userId = params.id;
res.json({ userId });
}),
}),
});When a request comes in to /users/123, the params object will be { id: '123' }.
Type Safety
TypeScript extracts the parameter names from your path pattern and makes them available in the params object with type safety:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const PostsController = createController('/posts', {
routes: () => ({
getComment: Get('/:postId/comments/:commentId').handle(({ params, res }) => {
// TypeScript knows params has both postId and commentId
const { postId, commentId } = params;
// This would be a TypeScript error:
// const invalid = params.nonexistent; // Error!
res.json({ postId, commentId });
}),
}),
});Multiple Path Parameters
You can have multiple parameters in a single route:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const OrgsController = createController('/orgs', {
routes: () => ({
getMember: Get('/:orgId/teams/:teamId/members/:userId').handle(({ params, res }) => {
const { orgId, teamId, userId } = params;
res.json({ orgId, teamId, userId });
}),
}),
});Example request:
curl http://localhost:3000/orgs/acme/teams/engineering/members/alice
# { "orgId": "acme", "teamId": "engineering", "userId": "alice" }Parameter Constraints
All Parameters Are Strings
Path parameters are always extracted as strings. If you need a number, parse it:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const ItemsController = createController('/items', {
routes: () => ({
getOne: Get('/:itemId').handle(({ params, res }) => {
const itemId = parseInt(params.itemId, 10);
if (isNaN(itemId)) {
res.error('Invalid item ID', 400);
return;
}
res.json({ itemId });
}),
}),
});Using populate() for Type Conversion
For entities, use the populate middleware with a transformfunction to convert the ID:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { populate } from '@justscale/http';
import { PostService } from '../services/post-service';
export const PostsController = createController('/posts', {
inject: { posts: PostService },
routes: (services) => ({
getOne: Get('/:postId')
.use(populate(
services.posts,
'post',
'postId',
{ transform: id => parseInt(id, 10) }
))
.handle(({ post, res }) => {
// post is already fetched with numeric ID
res.json({ post });
}),
}),
});URL Encoding
Parameters are automatically URL-decoded:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const SearchController = createController('/search', {
routes: () => ({
search: Get('/:query').handle(({ params, res }) => {
// Request: /search/hello%20world
console.log(params.query); // "hello world" (decoded)
res.json({ query: params.query });
}),
}),
});Validation
While path parameters are type-safe at compile time, you should validate them at runtime:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { validateParams } from '../middleware/validate-params';
import { ParamsSchema } from '../schemas/params';
export const UsersController = createController('/users', {
routes: () => ({
getOne: Get('/:id')
.use(validateParams(ParamsSchema))
.handle(({ params, res }) => {
// params.id is guaranteed to be a UUID string
res.json({ userId: params.id });
}),
}),
});Common Patterns
UUID Parameters
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const UsersController = createController('/users', {
routes: () => ({
getOne: Get('/:userId').handle(({ params, res }) => {
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(params.userId)) {
res.error('Invalid user ID format', 400);
return;
}
res.json({ userId: params.userId });
}),
}),
});Slug Parameters
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const BlogController = createController('/blog', {
routes: () => ({
getPost: Get('/:slug').handle(({ params, res }) => {
const slug = params.slug;
// slug might be: "my-first-post" or "hello-world"
res.json({ slug });
}),
}),
});Nested Resources
import { createController } from '@justscale/core';
import { Get, Delete } from '@justscale/http';
import { PostService } from '../services/post-service';
export const PostsController = createController('/posts', {
inject: { posts: PostService },
routes: (services) => ({
// GET /posts/:postId
getOne: Get('/:postId').handle(({ params, res }) => {
res.json({ postId: params.postId });
}),
// GET /posts/:postId/comments/:commentId
getComment: Get('/:postId/comments/:commentId').handle(({ params, res }) => {
res.json({ postId: params.postId, commentId: params.commentId });
}),
}),
});Non-Matching Routes
Info
# Route defined: GET /users/:id
curl http://localhost:3000/users/123 # ✓ Matches
curl http://localhost:3000/users # ✗ 404 Not Found
curl http://localhost:3000/users/123/foo # ✗ 404 Not Found