{"id":16528351,"url":"https://github.com/danvk/crosswalk","last_synced_at":"2025-09-12T18:31:54.509Z","repository":{"id":40317589,"uuid":"312606283","full_name":"danvk/crosswalk","owner":"danvk","description":"Typed express router for TypeScript","archived":false,"fork":false,"pushed_at":"2022-05-15T13:22:37.000Z","size":729,"stargazers_count":87,"open_issues_count":3,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-12-25T20:03:02.041Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danvk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-11-13T15:02:12.000Z","updated_at":"2024-12-08T03:58:14.000Z","dependencies_parsed_at":"2022-08-09T17:10:37.582Z","dependency_job_id":null,"html_url":"https://github.com/danvk/crosswalk","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrosswalk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrosswalk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrosswalk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danvk%2Fcrosswalk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danvk","download_url":"https://codeload.github.com/danvk/crosswalk/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":232621285,"owners_count":18551716,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T17:39:49.184Z","updated_at":"2025-09-12T18:31:54.281Z","avatar_url":"https://github.com/danvk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Crosswalk: safe routes for Express and TypeScript\n\n[![codecov](https://codecov.io/gh/danvk/crosswalk/branch/master/graph/badge.svg?token=L4VL0FB46U)](https://codecov.io/gh/danvk/crosswalk)\n\nThis library helps you build type-safe REST APIs using Express using\nTypeScript.\n\nHere's what you have to do:\n\n- Define your API using TypeScript types (see below).\n- Run `ts-json-schema-generator` to produce a JSON Schema for your API.\n\nHere's what you get in return:\n\n- Type-safe API implementations (for your server)\n- Type-safe API requests (for your client code)\n- Runtime request validation (for your server, using [ajv][])\n- Interactive API documentation (via [swagger-ui-express][suie])\n\nRequirements:\n\n- TypeScript 4.1+\n- Express\n\nThere is an optional requirement of [`ts-json-schema-generator`][tsjs] if you\nwant runtime request validation or API docs. (You probably do!)\n\nFor a full example of a project using crosswalk, see this [demo repo][demo].\n\n## Usage\n\nFirst install `crosswalk` and its peer dependencies (if you haven't already):\n\n    # if needed\n    npm install express\n    npm install -D typescript @types/express\n\n    npm install crosswalk\n\nThen define your API in `api.ts`:\n\n```ts\nimport type {Endpoint, GetEndpoint} from 'crosswalk/dist/api-spec';\n\nexport interface API {\n  '/users': {\n    get: GetEndpoint\u003cUsersResponse, {query?: string}\u003e;  // Response/query parameter types\n    post: Endpoint\u003cCreateUserRequest, User\u003e;\n  };\n  '/users/:userId': {\n    get: GetEndpoint\u003cUser\u003e;\n  }\n}\n```\n\nThen implement the API (`users.ts`):\n\n```ts\nimport {API} from './api';\nimport {TypedRouter} from 'crosswalk';\n\nexport function registerAPI(router: TypedRouter\u003cAPI\u003e) {\n  router.get('/users', async ({}, req, res, {query}) =\u003e filterUsersByName(users, query));\n  router.post('/users', async ({}, userInput) =\u003e createUser(userInput));\n  router.get('/users/:userId', async ({userId}) =\u003e getUserById(userId));\n}\n```\n\nFinally, register it on your Express server (`server.ts`):\n\n```ts\nconst app = express();\napp.use(bodyParser.json());\nconst typedRouter = new TypedRouter\u003cAPI\u003e(app);\nregisterAPI(typedRouter);\napp.listen(4567);\n```\n\nThere are a few things you get by doing this:\n\n- A definition of your API's shape in one place using TypeScript's type system.\n- A check that you've only implemented endpoints that are in the API definition.\n- Types for route parameters (via TypeScript 4.1's template literal types)\n- Types for query parameters (and automatic coercion of non-string parameters)\n- A check that each endpoint's implementation returns a Promise for the\n  expected response type.\n\nWhile not required, it's not much extra work to get runtime request validation\nand this is highly recommended. See \"Runtime request validation\" below.\n\nFor a complete example, check out the [crosswalk-demo repo][demo].\n\n## Type-safe API usage\n\nIn your client-side code, you can make type-safe API requests:\n\n```ts\nimport {typedApi} from 'crosswalk';\nimport {API} from './api';\n\nconst api = typedApi\u003cAPI\u003e();\nconst getUserById = api.get('/users/:userId');\nconst createUser = api.post('/users');\n\nasync function demo () {\n  const newUser = await createUser({}, {\n    id: 'fred',\n    name: 'Fred Flinstone'\n  });  // Request body is type checked\n\n  const user = await getUserById({userId: 'fred'});\n  // Route parameters are type checked!\n  // user's TypeScript type is User\n  console.log(user.name);\n}\n```\n\nThis uses the `fetch` API under the hood, but you can plug in your own fetch\nfunction if you need to pass extra headers or prefer to use Axios.\n\nYou can also construct API URLs for mocking or requesting yourself. The path\nparameters will be type checked. No more `/path/to/undefined/null`!\n\n```ts\nimport {apiUrlMaker} from 'crosswalk';\nconst urlMaker = apiUrlMaker\u003cAPI\u003e('/api/v0');\nconst getUserByIdUrl = urlMaker('/users/:userId');\nconst fredUrl = getUserByIdUrl({userId: 'fred'});\n// /api/v0/users/fred\nconst userUrl = urlMaker('/users');\nconst fredSearchUrl = userUrl(null, {query: 'fred'});\n// /api/v0/users?query=fred\n```\n\n## Runtime request validation\n\nTo ensure that your users hit API endpoints with the correct payloads, use\n`ts-json-schema-generator` to convert your API definition to JSON Schema:\n\n    ts-json-schema-generator --no-ref-encode --no-top-ref --path api.ts API --out api.schema.json\n\nThen pass this to the `TypeRouter` when you create it in `server.ts`:\n\n```ts\nconst apiSchema = require('./api.schema.json');\nconst typedRouter = new TypedRouter\u003cAPI\u003e(app, apiSchema);\n```\n\nNow if the user hits an API endpoint with an incorrect payload, they'll get a\nfriendly error message:\n\n    $ http POST :/user name=\"Fred\"\n    {\n        error: `data should have required property 'age'`,\n    }\n\nYou now have two representations of your API: `api.ts` and `api.schema.json`.\nThe recommended way to keep them in sync is to run `ts-json-schema-generator` as\npart of your continuous integration workflow and fail if there are any diffs.\nThe TypeScript definition (`api.ts`) is the source of truth, not the JSON\nSchema (`api.schema.json`).\n\n## Bells and Whistles\n\n### Error handling\n\nYou may `throw` an `HTTPError` in a handler to produce an error response.\nIn `users.ts`:\n\n```ts\nimport {API} from './api';\nimport {TypedRouter, HTTPError} from 'crosswalk';\n\nfunction getUserById(userId: string): User | null {\n  // ...\n}\n\nexport function registerAPI(router: TypedRouter\u003cAPI\u003e) {\n  router.get('/users/:userId', async ({userId}) =\u003e {\n    const user = getUserById(userId);\n    if (!user) {\n      throw new HTTPError(404, `No such user ${userId}`);\n    }\n    return user;\n  });\n}\n```\n\n### Verifying implementation completeness\n\nWith JSON Schema for your API (see above), the typed router can also check\nthat you've implemented all the endpoints you declared. In `server.ts`:\n\n```ts\nconst apiSchema = require('./api.schema.json');\nconst typedRouter = new TypedRouter\u003cAPI\u003e(app, apiSchema);\nregisterAPI(typedRouter);\ntypedRouter.assertAllRoutesRegistered();\n// will throw unless all endpoints are registered\n```\n\n### Route-aware middleware\n\nExpress middlware runs before you know which route is going to match.\nThis means that you can't access route params, or the path that was matched.\nYou _can_ access these properties if you register a [`finish`][finish] handler, but by that point\nthe request has been served and you can't do anything about it.\n\ncrosswalk lets you register middleware that runs after a route has been matched, but before the\nrequest has been handled. This is often a convenient place to apply access controls.\n\nFor example:\n\n```ts\ntypedRouter.useRouterMiddleware((req, res, next) =\u003e {\n  const {params} = req;\n  if ('userId' in params \u0026\u0026 params.userId === 'badguy') {\n    res.status(403).send('Forbidden');\n  } else {\n    next();\n  }\n});\n```\n\nYou can also access `req.route` in this context.\nFor example, `req.route.path` might be `/users/:userId` in the previous example.\n\n### Generating API docs\n\nYou can convert your API definition into Swagger form to get interactive\nHTML documentation.\n\nFirst, install `swagger-ui-express`:\n\n    yarn add swagger-ui-express\n\nThen convert your API schema to Open API format and serve it up:\n\n```ts\nimport swaggerUI from 'swagger-ui-express';\nimport {TypedRouter, apiSpecToOpenApi} from 'crosswalk';\n\napp.use('/docs', swaggerUI.serve, swaggerUI.setup(apiSpecToOpenApi(apiSchema)));\n```\n\nThen visit `/docs`. You may need to pass some additional options to\n`apiSpecToOpenApi` to get query execution from the Swagger docs to work.\n\n## Options\n\nThe `TypedRouter` class takes the following options.\n\n### invalidRequestHandler\n\nBy default, if request validation fails, crosswalk returns a 400 status code and a descriptive\nerror. If you'd like to do something else, you may specify your own `invalidRequestHandler`. For\nexample, you might like to log the error or omit validation details from the response in prod.\n\nThis is the default implementation (`crosswalk.defaultInvalidRequestHandler`):\n\n```ts\nnew TypedRouter\u003cAPI\u003e(app, apiSchema, {\n  handleInvalidRequest({response, payload, ajv, errors}) {\n    response.status(400).json({\n      error: ajv.errorsText(errors),\n      errors,\n      invalidRequest: payload,\n    });\n  }\n});\n```\n\n## Questions\n\n**GraphQL has types, why not use that?**\n\nIf you want to use GraphQL, that's great! Go for it! But there are many reasons\nthat REST APIs are still around. If you're already using REST and want to get\ntype safety without havin to do a full GraphQL conversion, then Crosswalk can\nhelp.\n\n**Why do I have to define my API in TypeScript _and_ JSON Schema?**\n\n[TypeScript types get erased at runtime][1], so if you want to use TypeScript\ntypes as your source of truth, you need to have some way of accessing them at\nruntime. For Crosswalk, that way is JSON Schema. This is a convenient form\nsince there are many JSON Schema validators (such as [ajv][]) and JSON Schema\nis also used by OpenAPI, which makes it easy to generate documentation.\n\nWhy not use JSON Schema as the source of truth? Some developers choose to do\nthis, but personally I find TypeScript's type declaration syntax much\nfriendlier to use. And you'd still need some way to get TypeScript types out\nof it to get static type checking.\n\nThere are several tools for defining types that are available both at runtime\nand for TypeScript. If running `ts-json-schema-generator` bothers you, you might\nwant to look into [iots][] or [zod][].\n\n**How do I keep `api.ts` and `api.schema.json` in sync?**\n\nI recommend adding a check to your continuous integration system that runs\n`typescript-json-schema` and then `git diff` to make sure there are no changes.\nYou could also do this as a prepush or precommit hook.\n\n**How do I use middleware with this?**\n\ncrosswalk is a thin wrapper around calling `app.get`, `app.post`, etc. Your middleware should work exactly as it did without crosswalk.\n\n**How do I register my API under a prefix?**\n\nMake a new router, wrap it with `TypedRouter`, and mount it wherever you like:\n\n```ts\nconst app = express();\nconst rawApiRouter = express.Router();\nconst apiRouter = new TypedRouter\u003cAPI\u003e(rawApiRouter, apiJsonSchema);\n// ... register API endpoints ...\napiRouter.assertAllRoutesRegistered();\napp.use('/api/v0', rawApiRouter);\n```\n\n**Why does this require TypeScript 4.1 or later?**\n\nBecause it has a hard dependency on [template literal types][ts41]. These are\nused to [generate types based on Express paths][tweet].\n\nIf you get errors about `Type 'any' is not assignable to type 'never'.`, it\nmight be because you're using an old version of TypeScript, either in your\nproject or in your editor.\n\n**Should I set `--additional-properties` with `ts-json-schema-generator`?**\n\nThere are many options you can set when you run [`ts-json-schema-generator`][tsjs]. You should think\ncarefully about these as they have an impact on the runtime behavior of your code.\n\nThe `--additional-properties` option is more interesting. TypeScript uses a \"structural\" or \"duck\" typing\nsystem. This means that an object may have the declared properties in its type, _but it could have\nothers, too_!\n\n```ts\ninterface Hero {\n  heroName: string;\n}\nconst superman = {\n  heroName: 'Superman',\n  alterEgo: 'Clark Kent',\n};\n\ndeclare function getHeroDetails(hero: Hero): string;\ngetHeroDetails(superman);  // ok!\n```\n\nThis is simply the way that TypeScript works, and so it must be the way that crosswalk statically\nenforces your request types. If you're comfortable with this behavior, set `--additional-properties`.\n\nIf you _do not_ specify `--additional-properties`, additional properties on a request will result in the request\nbeing rejected at runtime with a 400 HTTP response. This has pros and cons. The pros are that it\nwill catch more user errors (e.g. misspelling an optional property name) and allows the server to\nbe more confident about the shape of its input (`Object.keys` won't produce surprises). The con\nis that your runtime behavior is divergent from the static type checking, so client code that\npasses the type checker might produce failing requests at runtime. Until TypeScript gets\n[\"exact\" types][exact], it will not be able to fully model `--additional-properties=false` statically.\n\n**What's with the name?**\n\nA crosswalk is a _safe route_ across a road. Also a nod to [Sidewalk Labs][swl],\nwhere this project was originally developed.\n\n## Related projects\n\n- [GraphQL][] achieves type-safe APIs by ditching the REST structure altogether. If you want to use GraphQL, there are many tools to help, e.g. [Apollo][].\n- [swagger-typescript-api][] starts with a Swagger (OpenAPI) schema as a source of truth and generates TypeScript types from it (crosswalk does the reverse).\n- [blitzjs][] and [trpc][] also give you end-to-end type safety, but without modeling a REST API explicitly in the way that crosswalk does.\n\n## Development setup\n\nAfter cloning, run:\n\n    yarn\n\nYou may get some warnings about peer dependencies, but these can be ignored.\n\nThen:\n\n    yarn test\n    yarn test --coverage\n\nTo test with the [demo repo][demo],\n\n    yarn tsc\n    cd ../crosswalk-demo\n    yarn add ../crosswalk\n\nTo publish:\n\n    yarn tsc\n    yarn publish\n\n[tsjs]: https://github.com/vega/ts-json-schema-generator\n[ajv]: https://ajv.js.org/\n[1]: https://effectivetypescript.com/2020/07/27/safe-queryselector/\n[zod]: https://github.com/colinhacks/zod\n[iots]: https://github.com/gcanti/io-ts\n[demo]: https://github.com/danvk/crosswalk-demo\n[suie]: https://github.com/scottie1984/swagger-ui-express\n[ts41]: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1\n[tweet]: https://twitter.com/danvdk/status/1301707026507198464\n[swl]: https://sidewalklabs.com/\n[exact]: https://github.com/microsoft/TypeScript/issues/12936\n[swagger-typescript-api]: https://github.com/acacode/swagger-typescript-api\n[trpc]: https://github.com/trpc/trpc\n[blitzjs]: https://blitzjs.com/\n[graphql]: https://graphql.org/\n[apollo]: https://www.apollographql.com/\n[finish]: https://nodejs.org/api/http.html#http_event_finish\n\n## File Upload (multipart/form-data) Support\n\nCrosswalk supports type-safe file upload endpoints using `multipart/form-data` and integrates smoothly with middleware like [multer](https://github.com/expressjs/multer).\n\n### How to Define File Upload Endpoints\n\n1. **Mark file fields in your API spec:**\n   - Use a type file that conforms to `{__type: 'file'}` interface to indicate file field in the form.\n   - Use MultipartEndpoint.\n   - Example:\n\n```ts\n// api-spec.ts\nexport type File = {__type: 'file'};\n\nexport interface API {\n  '/upload': {\n    post: MultipartEndpoint\u003c{ file: File; description: string }, { success: boolean }\u003e;\n  };\n}\n```\n\n2. **Implement the endpoint using multer:**\n   - Register a route-aware middleware for the upload endpoint that uses `multer` to handle file parsing.\n   - In your handler, access the file via `req.file` and the rest of the form data via `req.body`.\n\n```ts\nimport multer from 'multer';\nconst upload = multer({ storage: multer.memoryStorage() });\n\nrouter.useRouterMiddleware((req, res, next) =\u003e {\n  if (req.route.path === '/upload' \u0026\u0026 req.method === 'POST') {\n    upload.single('file')(req as any, res as any, () =\u003e {\n      // Optionally inject a dummy value for the file field so validation passes\n      if (req.file) req.body.file = 'uploaded';\n      next();\n    });\n  } else {\n    next();\n  }\n});\n\nrouter.post('/upload', async (_, body, req) =\u003e {\n  const file = req.file;\n  return {\n    success: true,\n    filename: file?.originalname || 'unknown.txt',\n    size: file?.size || 0,\n  };\n});\n```\n\n### How Validation Works\n- For `multipart/form-data` endpoints, Crosswalk automatically removes all fields of type `File` from the request body before validation.\n- This allows you to use type-safe request validation for the rest of the form fields, while letting `multer` handle the file(s).\n- In your handler, the `body` parameter will only include non-file fields; use `req.file` or `req.files` for the uploaded file(s).\n\n### OpenAPI/Swagger Generation\n- File fields are automatically documented as `type: string, format: binary` in the generated OpenAPI 3.0 schema.\n- Example:\n\n```yaml\nrequestBody:\n  content:\n    multipart/form-data:\n      schema:\n        type: object\n        properties:\n          file:\n            type: string\n            format: binary\n          description:\n            type: string\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanvk%2Fcrosswalk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanvk%2Fcrosswalk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanvk%2Fcrosswalk/lists"}