Skip to content

Latest commit

 

History

History
283 lines (195 loc) · 8.47 KB

README.md

File metadata and controls

283 lines (195 loc) · 8.47 KB

next-api-handler

install size CircleCI codecov next-api-handler Known Vulnerabilities

lightweight next.js api handler wrapper, portable & configurable for serverless environment

Introduction

next-api-handler contains helper functions and generic types to create next.js API routes with strong type declaration and type-checking.

next-api-handler will help in the following 3 aspects

  • sharable & express-like apis with middleware to build RESTful api route
  • simple response handler to transform response and error/exception in a predictable way
  • friendly generic types to build type-safe API interface shared between client & server

Getting Started

TL;DR

The following snippet will create /api/user following RESTful standard

// in new /pages/api/user.ts
import { RouterBuilder } from 'next-api-handler';

const router = new RouterBuilder();

router.post(async (req) => createUser(req.query.id));
router.get<{ name: string }>(() => ({ name: 'John Doe' }));

return router.build();

Requirements

Currently only tested with node.js v14+ with next.js v10+

Feel free to submit if you have issues regard to runtime environments

Installation

npm install next-api-handler
# or using yarn
yarn add next-api-handler

Features

Basic Usage

We can use express.js router-like builder

// in new /pages/api/user.js
import { RouterBuilder } from 'next-api-handler';

const router = new RouterBuilder();

router.post((req) => createUser(req.query.id));
router.get(() => ({ name: 'John Doe' }));

// or with async/await
// router.post(async (req) => {
//   const user = await createUser(req.query.id);
//   return user;
// });

return router.build();

Which is equivalent to the following next.js API routes

// in old /pages/api/user.js
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const user = await createUser(req.query.id); // some async function/services
    res.status(200).json({ success: true, data: user });
  } else if (req.method === 'GET') {
    res.status(200).json({ success: true, data: { name: 'John Doe' } });
  } else {
    res.setHeader('Allow', ['GET', 'POST']);
    res.status(405).json({
      success: false,
      message: `Method ${req.method} Not Allowed`,
    });
  }
}

Please see more examples in example folder, can compare /pages/api/before/* & /pages/api/after/*

More CRUD

Please see examples in before/crud/[id].ts and after/crud/[id].ts

Handling HTTP Error Response

There are some common built-in HTTP exceptions to shorten writing up error response, this can get particularly useful when writing up complex controllers

import { RouterBuilder, BadRequestException } from 'next-api-handler';

const router = new RouterBuilder();

router.get(() => throw new BadRequestException('something went wrong'));

export default router.build();

is equivalent to do

export default function handler(_req, res) {
  return res.status(400).json({
    success: false,
    message:
      process.env.NODE_ENV === 'production'
        ? 'Bad Request'
        : 'something went wrong ',
  });
}

We can disable process.env.NODE_ENV check by passing showMessage=true when creating the router

const router = new RouterBuilder({ showMessage: true });

API Middleware

If there's a piece of shared logic before handling the request, we can create middleware with typescript support

import { UnauthorizedException, NotFoundException } from 'next-api-handler';
import type { NextApiRequest, NextApiResponse } from 'next';

const authMiddleware = async (req: NextApiRequest, res: NextApiResponse) => {
  const apiKey = req.cookies['Authorization'];

  const isValid = await verify(apiKey); // some async service to check apiKey

  if (!isValid) {
    throw new UnauthorizedException();
  }

  const user = await getUserInfo(apiKey); // another async service to fetch user

  if (!user) {
    throw new NotFoundException();
  }

  return { user };
};

then we can reuse this user value in all other routes white enforcing type-checking

import { RouterBuilder, NextApiRequestWithMiddleware } from 'next-api-handler';

// some type returned from getUserInfo
type User = {
  id: string;
};

const router = new RouterBuilder();

// or router.inject(authMiddleware) if the order of adding middleware does not matter
router.use<User>(authMiddleware);

// all middleware values will stay in `req.middleware`,
router.get<string, User>(
  (req: NextApiRequestWithMiddleware<User>) => req.middleware.user.id
);

return router.build();

Utilities

Type declaration & Type-checking

This package is carefully designed for TypesScript users to correctly enforce type definitions

For server side, we can declare return types in any of route handler

router.get<{ name: string }>(() => ({ name: 'John Doe' }));

We can share the response type for client side to reuse, e.g. here as axios

// export type ApiResponse<T = unknown> = SuccessApiResponse<T> | ErrorApiResponse;
// export type SuccessApiResponse<T> = { success: true; data: T };
// export type ErrorApiResponse = { success: false; message: string };

import axios from 'axios';
import type { SuccessApiResponse } from 'next-api-handler';

type User = { name: string };

const result = await axios.get<SuccessApiResponse<User>>('/api/...');

or with native fetch provided by next.js, it is easy to determine whether it is a successful api response

type User = { name: string };

const fetchLocalUser = async (args): Promise<User> => {
  const response = await fetch('/api/...', args);
  const result = (await response.json()) as ApiResponse<User>;

  if (!result?.success) {
    throw new Error(result.message);
  }

  // Here ts should be able to figure out it is SuccessApiResponse
  return result.data;
};

Compatibility

As next-api-handler provides a general interface to generate next.js handler, it will be easy to work with other libraries like following:

import { RouterBuilder } from 'next-api-handler'

const router = new RouterBuilder()

router.get( ... )
router.post( ... )

// here handler has type 'NextApiHandler' exposed from 'next'
const handler = router.build()

// ... work with wrapper or other plugins

A quick example with iron-session:

// pages/api/user.ts

import { withIronSessionApiRoute } from 'iron-session/next';
import { RouterBuilder } from 'next-api-handler';

const router = new RouterBuilder();
const handler = router.get((req) => req.session.user).build();

export default withIronSessionApiRoute(handler, {
  cookieName: 'myapp_cookiename',
  password: 'complex_password_at_least_32_characters_long',
  // secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
  cookieOptions: {
    secure: process.env.NODE_ENV === 'production',
  },
});

Reference

License

MIT