Skip to content

Commit

Permalink
Merge pull request #131 from storyofams/docs
Browse files Browse the repository at this point in the history
Docs
  • Loading branch information
ggurkal authored Jun 14, 2021
2 parents 7cea419 + f779e24 commit 39ed0e6
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 412 deletions.
238 changes: 15 additions & 223 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@

---

This package contains a collection of decorators to create typed Next.js API routes, with easy request validation and transformation.
<div align="center">
A collection of decorators to create typed Next.js API routes, with easy request validation and transformation.

[View docs](https://next-api-decorators.vercel.app/)
</div>

---

Expand Down Expand Up @@ -53,7 +57,7 @@ class User {
export default createHandler(User);
```

💡 Read more about validation [here](#data-transfer-object)
💡 Read more about validation [here](https://next-api-decorators.vercel.app/docs/validation)

<details>
<summary>The code above without next-api-decorators</summary>
Expand Down Expand Up @@ -102,161 +106,23 @@ The structure is heavily inspired by NestJS, which is an amazing framework for a

## Installation

Add the package to your project:

```bash
$ yarn add @storyofams/next-api-decorators
```

Since decorators are still in proposal state, you need to add the following plugins to your `devDependencies` in order to use them:

```bash
$ yarn add -D @babel/core babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators babel-plugin-parameter-decorator
```
Visit https://next-api-decorators.vercel.app/docs/#installation to get started.

Make sure to add the following lines to the start of the `plugins` section in your babel configuration file:
```json5
{
"plugins": [
"babel-plugin-transform-typescript-metadata",
["@babel/plugin-proposal-decorators", { "legacy": true }],
"babel-plugin-parameter-decorator",
// ... other plugins
]
}
```
## Documentation

Your `tsconfig.json` needs the following flags:
Refer to our docs for usage topics:

```json5
"experimentalDecorators": true
```
[Validation](https://next-api-decorators.vercel.app/docs/validation)

[Route matching](https://next-api-decorators.vercel.app/docs/routing/route-matching)

## Usage
[Using middlewares](https://next-api-decorators.vercel.app/docs/middlewares)

### Data transfer object
[Custom middlewares](https://next-api-decorators.vercel.app/docs/middlewares#custom-middleware-decorators)

If you want to use `class-validator` to validate request body values and get them as DTOs, add it to your project by running:
[Pipes](https://next-api-decorators.vercel.app/docs/pipes)

```bash
$ yarn add class-validator class-transformer
```

Then you can define your DTOs like:

```ts
// pages/api/user.ts
import { createHandler, Post, HttpCode, Body, ValidationPipe } from '@storyofams/next-api-decorators';
import { IsNotEmpty, IsEmail } from 'class-validator';

class CreateUserDto {
@IsNotEmpty()
public name: string;

@IsEmail()
public email: string;
}

class User {
// POST /api/user
@Post()
@HttpCode(201)
public createUser(@Body(ValidationPipe) body: CreateUserDto) {
return User.create(body);
}
}

export default createHandler(User);
```

### Route matching

It is possible to use Express.js style route matching within your handlers. To enable the functionality add the `path-to-regexp` package to your project by running:
```bash
$ yarn add path-to-regexp
```

Then you can define your routes in your handler like:
```ts
// pages/api/user/[[...params]].ts
class User {
@Get()
public list() {
return DB.findAllUsers();
}

@Get('/:id')
public details(@Param('id') id: string) {
return DB.findUserById(id);
}

@Get('/:userId/comments')
public comments(@Param('userId') userId: string) {
return DB.findUserComments(userId);
}

@Get('/:userId/comments/:commentId')
public commentDetails(@Param('userId') userId: string, @Param('commentId') commentId: string) {
return DB.findUserCommentById(userId, commentId);
}
}
```

📖 File names are important for route matching. Read more at https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes

💡 It is possible to use pipes with `@Param`. e.g: `@Param('userId', ParseNumberPipe) userId: number`

⚠️ When `path-to-regexp` package is not installed and route matching is being used in handlers, the request will be handled by the method defined with the `/` path (keep in mind that using `@Get()` and `@Get('/')` do exactly the same thing).

## Middlewares

`next-api-decorators` is technically compatible with all Express.js middlewares. However, keep in mind that some middlewares may not be compatible with Next.js API routes. When using a 3rd party middleware in a Next.js API handler, it's advised to test the middleware thoroughly.

We provide the `@UseMiddleware` decorator to run a middleware **_before_** the handler. You can use the decorator either for a class or a class method

### Applying a middleware

```ts
const rateLimiter = rateLimit();

@UseMiddleware(rateLimiter)
class ArticleHandler {
@Get()
public articles() {
return 'My articles';
}
}
```

### Custom middleware decorators

In some cases, it may be beneficial to create a middleware decorator and use it throughout your app.

We provide the `createMiddlewareDecorator` function for you to create a decorator that fulfills your needs.

```ts
const JwtAuthGuard =
createMiddlewareDecorator((req: NextApiRequest, res: NextApiResponse, next: NextFunction) {
if (!validateJwt(req)) {
throw new UnauthorizedException();
// or
return next(new UnauthorizedException());
}

next();
});

class SecureHandler {
@Get()
@JwtAuthGuard()
public securedData(): string {
return 'Secret data';
}
}
```

💡 `NextFunction` type is exported from `@storyofams/next-api-decorators`.
[Exceptions](https://next-api-decorators.vercel.app/docs/exceptions)

## Available decorators

Expand Down Expand Up @@ -291,77 +157,3 @@ class SecureHandler {
| `@Param(key: string)` | Gets a route parameter value by key. |

\* Note that when you inject `@Res()` in a method handler you become responsible for managing the response. When doing so, you must issue some kind of response by making a call on the response object (e.g., `res.json(...)` or `res.send(...)`), or the HTTP server will hang.

## Built-in pipes

Pipes are being used to validate and transform incoming values. The pipes can be added to the `@Query`, `@Body` and `@Param` decorators like:

```ts
@Query('isActive', ParseBooleanPipe) isActive: boolean
```

⚠️ Beware that pipes throw when the value is `undefined` or invalid. Read about optional values [here](#handling-optional-values-in-conjunction-with-pipes)

| | Description |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ParseBooleanPipe` | Validates and transforms `Boolean` strings. Allows `'true'` and `'false'`. |
| `ParseDatePipe` | Validates and transforms `Date` strings. Allows valid `ISO 8601` formatted date strings. |
| `ParseNumberPipe` | Validates and transforms `Number` strings. Uses `parseFloat` under the hood. |
| `ValidateEnumPipe`* | Validates string based on `Enum` values. Allows strings that are present in the enum. |
| `ValidationPipe` | Validates the request body via `class-validator`. Works only when `class-validator` and `class-transformer` packages are installed. |
| `DefaultValuePipe`* | Assigns a default value to the parameter when its value is `null` or `undefined`. |

\* Note that bare function usage has no effect for `ValidateEnumPipe` and `DefaultValuePipe`. In other words, always use `@Query('step', DefaultValuePipe(1))` rather than `@Query('step', DefaultValuePipe)`.


### Handling optional values in conjunction with pipes

Pipes are non-nullable by default. However, the following pipes allow options to be passed as an argument and have the `nullable` property in their options:

- `ParseBooleanPipe`
- `ParseDatePipe`
- `ParseNumberPipe`
- `ValidateEnumPipe`

Usage:
```ts
@Query('isActive', ParseBooleanPipe({ nullable: true })) isActive?: boolean
```

## Exceptions

The following common exceptions are provided by this package.

| | Status code | Default message |
| ------------------------------ | ----------- | ------------------------- |
| `BadRequestException` | `400` | `'Bad Request'` |
| `UnauthorizedException` | `401` | `'Unauthorized'` |
| `NotFoundException` | `404` | `'Not Found'` |
| `PayloadTooLargeException` | `413` | `'Payload Too Large'` |
| `UnprocessableEntityException` | `422` | `'Unprocessable Entity'` |
| `InternalServerErrorException` | `500` | `'Internal Server Error'` |

### Custom exceptions

Any exception class that extends the base `HttpException` will be handled by the built-in error handler.

```ts
import { HttpException } from '@storyofams/next-api-decorators';

export class ForbiddenException extends HttpException {
public constructor(message?: string = 'Forbidden') {
super(403, message);
}
}
```

Then later in the app, we can use it in our route handler:

```ts
class Events {
@Get()
public events() {
throw new ForbiddenException();
}
}
```
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"commit": "./node_modules/cz-customizable/standalone.js",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint \"**/*.+(js|jsx|ts|tsx|mdx)\"",
"lint": "eslint \"!(examples|website)/**/*.+(js|jsx|ts|tsx|mdx)\"",
"ts-coverage": "typescript-coverage-report",
"semantic-release": "semantic-release",
"prepublishOnly": "pinst --disable && npm run build && npm run verify",
Expand Down Expand Up @@ -101,8 +101,8 @@
}
},
"lint-staged": {
"**/*.+(js|jsx|ts|tsx|mdx)": [
"eslint --fix"
"!(examples|website)/**/*.+(js|jsx|ts|tsx|mdx)": [
"eslint --fix"
]
}
}
31 changes: 31 additions & 0 deletions website/docs/api/create-param-decorator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: Custom parameter decorators
slug: /api/create-param-decorator
---

Parameter decorators are there to simplify the task of geting a specific data you need by using the request (`req`) object or by generating it. For example the `@Body` decorator we provide simply returns the `req.body` object.

By using the `createParamDecorator` function, you can create your own decorators that fulfill the needs of your application.

As a basic example, let's get the browser information of the client via a decorator.

First we create our decorator:
```ts
import { createParamDecorator } from '@storyofams/next-api-decorators';

export const UserAgent = createParamDecorator<string | undefined>(
req => req.headers['user-agent']
);
```

Later we can use the decorator in our handler:
```ts
...
class CommentHandler {
@Get()
public comments(@UserAgent() userAgent?: string) {
return `Someone requested the comments via "${userAgent ?? 'Unknown browser'}"`;
}
}
...
```
2 changes: 2 additions & 0 deletions website/docs/api/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ slug: /api/decorators
## Class decorators

* `@SetHeader(key: string, value: string)` Sets a header key valur pair for all routes in a handler class.
* `@UseMiddleware(...middlewares: Middleware[])` Registers one or multiple middlewares for all the routes defined in the class.
* `@Catch(handler: (error: unknown, req: NextApiRequest, res: NextApiResponse) => void | Promise<void>, exceptionType?: ClassConstructor)` Creates an exception handler for a handler class.

## Method decorators

* `@SetHeader(key: string, value: string)` Sets a header key value pair for the route that the decorator is applied to.
* `@HttpCode(code: number)` Defines the HTTP response code of the route.
* `@Download()` Marks the method as a download handler for the client, so the returned file can be downloaded by the browser.
* `@UseMiddleware(...middlewares: Middleware[])` Registers one or multiple middlewares for the handler.
* `@Catch(handler: (error: unknown, req: NextApiRequest, res: NextApiResponse) => void | Promise<void>, exceptionType?: ClassConstructor)` Creates an exception handler for a route in a handler class.

### HTTP method decorators
Expand Down
Loading

0 comments on commit 39ed0e6

Please sign in to comment.