Skip to content

Commit

Permalink
Create typed-route-handler project
Browse files Browse the repository at this point in the history
  • Loading branch information
venables committed Jan 9, 2024
1 parent c96a809 commit f480bf7
Show file tree
Hide file tree
Showing 24 changed files with 585 additions and 100 deletions.
13 changes: 11 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
module.exports = {
root: true,
extends: [
require.resolve("@vercel/style-guide/eslint/browser"),
require.resolve("@vercel/style-guide/eslint/next"),
require.resolve("@vercel/style-guide/eslint/node"),
require.resolve("@vercel/style-guide/eslint/typescript")
require.resolve("@vercel/style-guide/eslint/typescript"),
"next/core-web-vitals"
],
parserOptions: {
tsconfigRootDir: __dirname,
Expand Down Expand Up @@ -45,12 +48,18 @@ module.exports = {
}
],
rules: {
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports", fixStyle: "inline-type-imports" }
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-invalid-void-type": [
"error",
{
allowInGenericTypeArguments: true
}
],
"@typescript-eslint/no-misused-promises": [
"error",
{
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- uses: actions/setup-node@v3
- run: bun run setup
- uses: actions/setup-node@v4
- run: bun install
- run: bun run check
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Matt Venables <matt@venabl.es>
Copyright (c) 2024 Matt Venables <matt@venabl.es>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
236 changes: 206 additions & 30 deletions README.md
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).
6 changes: 0 additions & 6 deletions bin/setup

This file was deleted.

Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./dist/client"
24 changes: 0 additions & 24 deletions lib/cli.ts

This file was deleted.

40 changes: 40 additions & 0 deletions lib/client/index.ts
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")
}
}
Loading

0 comments on commit f480bf7

Please sign in to comment.