-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
585 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,237 @@ | ||
# startkit-typescript | ||
<div align="center"> | ||
<h2 align="center">typed-route-handler</h2> | ||
<p>Build powerful, type-safe Route Handlers in Next.js</p> | ||
</div> | ||
|
||
> A sane starting point for Typescript projects. | ||
## Features | ||
|
||
## Getting started | ||
- ✅ **Type-safe** route handler responses | ||
- ✅ **Type-safe** route handler parameters | ||
- ✅ Extended Next.js **error handling** | ||
- ✅ Full **zod compatibility** | ||
- ✅ Route handler **timing** | ||
- ✅ Request **logging** | ||
- ✅ Production ready | ||
|
||
To get started simply run the following command. | ||
## Installation | ||
|
||
```sh | ||
bun run setup | ||
npm i typed-route-handler | ||
``` | ||
|
||
## Local Development | ||
## Usage | ||
|
||
```sh | ||
bun dev | ||
Typed handler is easy to use: In the simplest case, just wrap your Route Handler with `handler` and you're good to go! | ||
|
||
```diff | ||
+ import { handler } from 'typed-route-handler' | ||
|
||
- export const GET = async (req: NextRequest) => { | ||
+ export const GET = handler(async (req) => { | ||
// ... | ||
- } | ||
+ }) | ||
``` | ||
|
||
## Building | ||
## Typed Responses | ||
|
||
```sh | ||
bun build | ||
The real magic comes when you add typing to your responses. | ||
|
||
```ts | ||
import { NextResponse } from "next" | ||
|
||
type ResponseData = { | ||
name: string | ||
age: number | ||
} | ||
|
||
export const GET = handler<ResponseData>((req) => { | ||
// ... | ||
|
||
return NextResponse.json({ | ||
name: "Bluey", | ||
age: 7, | ||
something: "else" // <-- this will cause a type error | ||
}) | ||
}) | ||
``` | ||
|
||
## Linting / Checking the codebase | ||
## Typed Parameters | ||
|
||
To run a full check of the codebase (type-check, lint, prettier check, test), run: | ||
We can also add type verification to our parameters. | ||
|
||
```sh | ||
bun check | ||
```ts | ||
import { NextResponse } from "next" | ||
|
||
type ResponseData = { | ||
name: string | ||
} | ||
|
||
type Context = { | ||
params: { | ||
userId: string | ||
} | ||
} | ||
|
||
export const GET = handler<ResponseData, Context>((req, context) => { | ||
// ... | ||
const userId = context.params.userId // <-- this will be type-safe | ||
|
||
return NextResponse.json({ | ||
name: "Bluey" | ||
}) | ||
}) | ||
``` | ||
|
||
### Linting | ||
This can get even more powerful with `zod` | ||
|
||
```sh | ||
bun lint | ||
```ts | ||
import { NextResponse } from "next" | ||
import { z } from "zod" | ||
|
||
type ResponseData = { | ||
name: string | ||
} | ||
|
||
const contextSchema = z.object({ | ||
params: z.object({ | ||
id: z.string() | ||
}) | ||
}) | ||
|
||
export const GET = handler<ResponseData, z.infer<typeof contextSchema>>( | ||
(req, context) => { | ||
// ... | ||
const userId = context.params.userId // <-- this will still be type-safe | ||
|
||
// or you can parse the schema: | ||
const { params } = contextSchema.parse(context) | ||
|
||
return NextResponse.json({ | ||
name: "Bluey" | ||
}) | ||
} | ||
) | ||
``` | ||
|
||
### Type Checking | ||
## Typed request bodies | ||
|
||
```sh | ||
bun type-check | ||
Similarly, you can use `zod` to parse request bodies: | ||
|
||
```ts | ||
import { NextResponse } from "next" | ||
import { z } from "zod" | ||
|
||
type ResponseData = { | ||
name: string | ||
} | ||
|
||
const bodySchema = z.object({ | ||
username: z.string() | ||
}) | ||
|
||
export const PUT = handler<ResponseData>((req, context) => { | ||
const body = bodySchema.parse(await req.json()) | ||
|
||
// If the body does not satisfy `bodySchema`, the route handler will catch | ||
// the error and return a 400 error with the error details. | ||
|
||
return NextResponse.json({ | ||
name: body.username | ||
}) | ||
}) | ||
``` | ||
|
||
### Formatting with Prettier | ||
## Automatic `zod` issue handling | ||
|
||
When a zod error is thrown in the handler, it will be caught automatically and | ||
converted to a Validation Error with a 400 status code. | ||
|
||
Example: | ||
|
||
```json | ||
{ | ||
"error": "Validation Error", | ||
"issues": [ | ||
{ | ||
"code": "invalid_type", | ||
"expected": "string", | ||
"received": "undefined", | ||
"path": ["name"], | ||
"message": "Required" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
```sh | ||
bun format | ||
## Extended Next.js errors | ||
|
||
This library adds the following convenience methods to Route Handlers. | ||
|
||
Similar to how Next.js offers `notFound()` and `redirect()`, typed-route-handler offers: | ||
|
||
- `unauthorized()` | ||
- `validationError()` | ||
|
||
For example: | ||
|
||
```ts | ||
export const GET = handler(async (req) => { | ||
const session = await auth() | ||
|
||
if (!session) { | ||
unauthorized() | ||
} | ||
}) | ||
``` | ||
|
||
to check for format errors, run: | ||
This will return the following HTTP 401 Unauthorized body: | ||
|
||
```sh | ||
bun format:check | ||
```json | ||
{ | ||
"error": "Unauthorized" | ||
} | ||
``` | ||
|
||
### Testing via Jest | ||
## Client-side Usage | ||
|
||
```sh | ||
bun test | ||
`typed-route-handler` comes with a client library that extends the traditional `fetch` API with type information. | ||
|
||
The `typedFetch` function will automatically parse the response as JSON, and apply the proper types. On an error response, it will throw. | ||
|
||
```ts | ||
import { typedFetch } from "typed-route-handler/client" | ||
|
||
const data = await typedFetch<{ id: number; username: string }>("/api/user") | ||
|
||
data.id // <-- number | ||
data.username // <-- string | ||
``` | ||
|
||
If there's an API error, it will be thrown by the client: | ||
|
||
```ts | ||
import { typedFetch } from "typed-route-handler/client" | ||
|
||
try { | ||
await typedFetch("/api/user") | ||
} catch (e) { | ||
e.message // <-- Validation Error, etc | ||
} | ||
``` | ||
|
||
## Roadmap | ||
|
||
- [ ] Add support for streaming responses (generic `Response` type) | ||
- [ ] Add support for custom API response formats | ||
- [ ] Client-side error handling with zod issues | ||
|
||
## 🏰 Production Ready | ||
|
||
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](htts://github.com/startkit-dev/startkit-next) projects. | ||
|
||
## ❤️ Open Source | ||
|
||
This project is MIT-licensed and is free to use and modify for your own projects. | ||
|
||
It was created by [Matt Venables](https://venabl.es). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./dist/client" |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { type ApiResponseError } from "../types" | ||
|
||
/** | ||
* Simple check to see if the response is an error. This method could be | ||
* expanded to check for other types of errors. | ||
* | ||
* This method adds a type hint to the `json` parameter, which is otherwise | ||
* unknown. | ||
*/ | ||
function isError( | ||
response: Response, | ||
json?: unknown | ||
): json is ApiResponseError | undefined { | ||
return !response.ok | ||
} | ||
|
||
/** | ||
* | ||
*/ | ||
export async function typedFetch<T>(url: string, options?: RequestInit) { | ||
const response = await fetch(url, { | ||
...options, | ||
headers: { | ||
"Content-Type": "application/json", | ||
...options?.headers | ||
} | ||
}) | ||
|
||
try { | ||
const json = await response.json() | ||
|
||
if (isError(response, json)) { | ||
throw new Error(json?.error ?? response.statusText) | ||
} | ||
|
||
return json as T | ||
} catch (e) { | ||
throw new Error("Invalid JSON response") | ||
} | ||
} |
Oops, something went wrong.