├── bun.lockb ├── .gitignore ├── tsup.config.ts ├── tsconfig.json ├── .github └── workflows │ └── check.yml ├── biome.jsonc ├── LICENSE ├── package.json ├── lib └── index.ts └── README.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naruto-Leader/typed-route-handler/HEAD/bun.lockb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build artifacts 5 | dist 6 | 7 | # Test artifacts 8 | coverage 9 | 10 | # local env files 11 | .env 12 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig([ 4 | // Server APIs 5 | { 6 | entry: ["lib/index.ts"], 7 | format: ["cjs", "esm"], 8 | external: ["next"], 9 | dts: true, 10 | sourcemap: true 11 | } 12 | ]) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "esnext", 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "types": ["bun-types"], 13 | "skipLibCheck": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: oven-sh/setup-bun@v1 17 | with: 18 | bun-version: latest 19 | - uses: actions/setup-node@v4 20 | - run: bun install 21 | - run: bun run check 22 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "linter": { 9 | "enabled": true, 10 | "rules": { 11 | "recommended": true 12 | } 13 | }, 14 | "organizeImports": { 15 | "enabled": false 16 | }, 17 | "formatter": { 18 | "enabled": true, 19 | "indentStyle": "space", 20 | "indentWidth": 2 21 | }, 22 | "javascript": { 23 | "formatter": { 24 | "quoteStyle": "double", 25 | "semicolons": "asNeeded", 26 | "trailingCommas": "none" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matt Venables 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typed-route-handler", 3 | "version": "1.1.0", 4 | "description": "Type-safe route handlers for Next.js", 5 | "keywords": ["next", "route handler", "typescript", "api"], 6 | "homepage": "https://github.com/venables/typed-route-handler#readme", 7 | "bugs": { 8 | "url": "https://github.com/venables/typed-route-handler/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/venables/typed-route-handler.git" 13 | }, 14 | "license": "MIT", 15 | "author": "Matt Venables ", 16 | "main": "dist/index.js", 17 | "module": "dist/index.mjs", 18 | "types": "dist/index.d.ts", 19 | "files": ["dist/**/*"], 20 | "scripts": { 21 | "build": "tsup", 22 | "check": "biome check . && bun run typecheck && bun test", 23 | "clean": "git clean -xdf dist client", 24 | "format": "biome format --write .", 25 | "format:check": "biome format .", 26 | "lint": "biome lint .", 27 | "outdated": "npx npm-check-updates --interactive --format group", 28 | "prepublish": "bun run build", 29 | "typecheck": "tsc --noEmit --pretty" 30 | }, 31 | "dependencies": {}, 32 | "devDependencies": { 33 | "@biomejs/biome": "1.9.4", 34 | "@types/bun": "^1.1.14", 35 | "tsup": "^8.3.5", 36 | "tsx": "^4.19.2", 37 | "typescript": "^5.7.2" 38 | }, 39 | "peerDependencies": { 40 | "next": ">= 15" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest, NextResponse } from "next/server.js" 2 | 3 | /** 4 | * The default value of the next.js route parameter 5 | */ 6 | type NextRouteParams = Record 7 | 8 | /** 9 | * The Context parameter for route handlers, which is currently a `params` promise. 10 | * 11 | * See: https://nextjs.org/docs/app/api-reference/file-conventions/route#context-optional 12 | */ 13 | export interface NextRouteContext { 14 | params: Promise 15 | } 16 | 17 | /** 18 | * A typed Route Handler 19 | * 20 | * @example 21 | * ```ts 22 | * type ResponseData = { name: string } 23 | * type Context = NextRouteContext<{ userId: string, name: string }> 24 | * 25 | * export const GET: Handler = (req, context) => { 26 | * const { userId, name } = await context.params 27 | * if (!userId) { 28 | * unauthorized() 29 | * } 30 | * 31 | * return NextResponse.json({ name }) 32 | * } 33 | * ``` 34 | * 35 | * https://nextjs.org/docs/app/api-reference/file-conventions/route 36 | */ 37 | export type Handler< 38 | T = void, 39 | U = NextRouteContext, 40 | V extends Request = NextRequest 41 | > = ( 42 | // https://nextjs.org/docs/app/api-reference/file-conventions/route#request-optional 43 | request: V, 44 | // https://nextjs.org/docs/app/api-reference/file-conventions/route#context-optional 45 | context: U 46 | ) => NextResponse | Promise> 47 | 48 | /** 49 | * Wrap an API handler for types 50 | * 51 | * @example 52 | * ```ts 53 | * type ResponseData = { name: string } 54 | * type Context = NextRouteContext<{ id: string }> 55 | * 56 | * export const GET = handler((req, context) => { 57 | * const { id } = await context.params 58 | * if (!id) { 59 | * unauthorized() 60 | * } 61 | * 62 | * return NextResponse.json({ name: request.query.name }) 63 | * }) 64 | * ``` 65 | * 66 | * @param handler - the api handler 67 | * @returns a wrapped api handler 68 | */ 69 | export const handler = < 70 | T = void, 71 | U = NextRouteContext, 72 | V extends Request = NextRequest 73 | >( 74 | routeHandler: Handler 75 | ): Handler => { 76 | return routeHandler 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

typed-route-handler

2 | 3 |
4 | Type-safe Route Handlers for Next.js 5 |
6 | 7 | ```ts 8 | import type { Handler } from 'typed-route-handler' 9 | 10 | type ResponseData = { 11 | result: string 12 | over: number 13 | } 14 | 15 | export const GET: Handler = async (req) => { 16 | return NextResponse.json({ 17 | result: "this response is type-checked", 18 | over: 9000 19 | }) 20 | } 21 | ``` 22 | 23 | > [!NOTE] 24 | > This library is designed for **Next.js 15 and higher**. To use this library with Next.js 14 or earlier, use typed-route-handler version `0.3.0`. 25 | 26 | ## Features 27 | 28 | - ✅ **Type-safe** route handler responses 29 | - ✅ **Type-safe** route handler parameters 30 | - ✅ Full **zod compatibility** 31 | - ✅ Production ready 32 | 33 | ## Installation 34 | 35 | ```sh 36 | npm i -D typed-route-handler 37 | ``` 38 | 39 | This library can be installed as a devDependency when only used for types. If you'd like to use the [wrapper](#wrapper-function) function, you can install it as a regular dependency. 40 | 41 | ## Usage 42 | 43 | Typed handler is easy to use: In the simplest case, just add the type `Handler` to your route handler and you're good to go! 44 | 45 | ```diff 46 | + import type { Handler } from 'typed-route-handler' 47 | 48 | - export const GET = async (req: NextRequest) => { 49 | + export const GET: Handler = async (req) => { 50 | // ... 51 | } 52 | ``` 53 | 54 | ## Typed Responses 55 | 56 | The real magic comes when you add typing to your responses. 57 | 58 | ```ts 59 | import { NextResponse } from "next" 60 | import type { Handler } from 'typed-route-handler' 61 | 62 | type ResponseData = { 63 | name: string 64 | age: number 65 | } 66 | 67 | export const GET: Handler = (req) => { 68 | return NextResponse.json({ 69 | name: "Bluey", 70 | age: "seven", // <-- this will cause a type error 71 | }) 72 | } 73 | ``` 74 | 75 | ## Typed Parameters 76 | 77 | We can also add type verification to our parameters. Each parameter `Context` extends from `NextRouteContext`. 78 | 79 | ```ts 80 | // app/api/[name]/route.ts 81 | import { NextResponse } from "next/server" 82 | import { type Handler, type NextRouteContext } from "typed-route-handler" 83 | 84 | type ResponseData = { 85 | name: string 86 | } 87 | 88 | type Context = NextRouteContext<{ 89 | name: string 90 | }> 91 | 92 | export const GET: Handler = async (req, context) => { 93 | const { name } = await context.params // <-- this will be type-safe 94 | 95 | return NextResponse.json({ 96 | name 97 | }) 98 | } 99 | ``` 100 | 101 | Note that this does not perform any runtime type-checking. To do that, you can use the zod types: 102 | 103 | ```ts 104 | import { NextResponse } from "next/server" 105 | import { z } from "zod" 106 | import { type Handler } from "typed-route-handler" 107 | 108 | type ResponseData = { 109 | name: string 110 | } 111 | 112 | const contextSchema = z.object({ 113 | params: z.promise( // <-- note the promise here, for next.js 15+ 114 | z.object({ 115 | name: z.string() 116 | }) 117 | ) 118 | }) 119 | 120 | export const GET: Handler> = async (req, context) => { 121 | const { name } = await context.params // <-- this will still be type-safe 122 | 123 | // or you can parse the schema: 124 | const { params } = contextSchema.parse(context) 125 | const { name } = await params 126 | 127 | 128 | return NextResponse.json({ 129 | name 130 | }) 131 | } 132 | ``` 133 | 134 | ## Wrapper function 135 | 136 | In addition to providing these types, the library also provides a convenience wrapper function `handler` which simply applies the Handler type to the function. Since this is a no-op, it is recommended to use the `Handler` type directly. 137 | 138 | NOTE: If you use this method, you should install this package as a `dependency` 139 | 140 | ```ts 141 | import { handler } from "typed-route-handler" 142 | import { NextResponse } from 'next/server' 143 | 144 | type ResponseBody = { 145 | balance: number 146 | } 147 | 148 | export const GET = handler(async (req) => { 149 | return NextResponse.json({ 150 | balance: 9_000 151 | }) 152 | }) 153 | ``` 154 | 155 | ### Usage with modified `req`s (e.g. next-auth) 156 | 157 | When using this library with `next-auth` or other libraries which modify the `req` objects, you can pass a 3rd type to the `handler` call, representing modified Request object type. For example: 158 | 159 | ```ts 160 | import { auth } from '@/auth' 161 | import { type NextAuthRequest } from 'next-auth' 162 | import { handler, type type NextRouteContext } from 'typed-route-handler' 163 | 164 | export const GET = auth( 165 | handler((req, ctx) => { 166 | if (!req.auth?.user) { 167 | unauthorized() 168 | } 169 | 170 | // ... 171 | }) 172 | ) 173 | ``` 174 | 175 | ## 🏰 Production Ready 176 | 177 | Already widely used in high-traffic production apps in [songbpm](https://songbpm.com), [jog.fm](https://jog.fm), [usdc.cool](https://usdc.cool), as well as all [StartKit](https://github.com/startkit-dev/next) projects. 178 | 179 | ## ❤️ Open Source 180 | 181 | This project is MIT-licensed and is free to use and modify for your own projects. 182 | 183 | It was created by [Matt Venables](https://venabl.es). 184 | --------------------------------------------------------------------------------