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:

src/controllers/users.tsTypeScript
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:

src/controllers/posts.tsTypeScript
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:

src/controllers/orgs.tsTypeScript
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:

Bash
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:

src/controllers/items.tsTypeScript
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:

src/controllers/posts.tsTypeScript
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:

src/controllers/search.tsTypeScript
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:

src/controllers/users.tsTypeScript
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

src/controllers/users.tsTypeScript
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

src/controllers/blog.tsTypeScript
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

src/controllers/posts.tsTypeScript
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

If a request doesn't match any route pattern, JustScale returns a 404 automatically.
Bash
# 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