Skip to content

Latest commit

 

History

History
141 lines (108 loc) · 4.38 KB

README.md

File metadata and controls

141 lines (108 loc) · 4.38 KB

next-route-handler-pipe

Created by Kolby Sisk License

⚠️ This package is for projects using Next.js App Router. For projects using Pages Router see next-api-route-middleware.

Introduction

Piping functions allows us to abstract reusable code that runs before the route handler is invoked.

Example uses:

  • ✅ Validate body/query data
  • 🔒 Verifying signatures
  • 🪝 Catch errors
  • ✨ Add data to the req object

Getting started

1. Install'r

npm i next-route-handler-pipe

2. Create a pipe function

A pipe function is a function that accepts req, event, and next. It should return await next() when done, or return a NextResponse.

// PipeFunction accepts a generic when mutating req
export const withUser: PipeFunction<{ userId: string }> = async (req, event, next) => {
  const { userId } = await validateUserRequest();

  if (userId) {
    req.userId = userId;
    return await next();
  } else {
    return NextResponse.json({ message: 'Invalid auth token.' }, { status: 401 });
  }
};

3. Create a pipe

Import the pipe function and pass it pipe functions followed by a handler. If a pipe function adds data to the req you can compose an intersection type with NextRequest & the added data.

import { pipe } from 'next-route-handler-pipe';

async function handler(req: NextRequest & { userId: string }) {
  console.log(req.userId);
}

export const POST = pipe(withUser, handler);

Examples

validateBody

Combine with Zod to create an awesome validation pattern.

// validate-body.ts

// This is a pipe function that makes use of the factory pattern, meaning it returns a pipe function.
// This allows us to pass args into our pipe, like a zod schema.

export const validateBody = (zodSchema: z.ZodSchema): PipeFunction<{ data: any }> => {
  return async function (req, event, next) {
    // Get the body of the request and parse with Zod
    const body = await req.json();
    const validation = zodSchema.safeParse(typeof body === 'object' ? body : JSON.parse(body));

    // If validation fails we return early, preventing the handler from running.
    if (!validation.success) {
      console.error('Validation error from validateBody', validation.error);
      return NextResponse.json({ message: 'Validation error' }, { status: 400 });
    }
    // If validation passes we can add the parsed data to the req for easy access in our handler.
    else {
      req.data = validation.data;
      // Always return await next() in your pipe function.
      return await next();
    }
  };
};
// route.ts

// Create a Zod schema
const postSchema = z
  .object({
    postId: z.string(),
    postTitle: z.string(),
    authorName: z.string(),
  })

  // We can even add a Zod transform if we need
  .transform(({ postId, postTitle, authorName }) => ({
    post_id: postId,
    post_title: postTitle,
    author_first_name: authorName.split(' ')[0],
    author_last_name: authorName.split(' ')[1],
  }));

// Infer type from schema
type Post = z.infer<typeof postSchema>;

// Add intersection with `NextRequest` & your inferred type
async function handler(req: NextRequest & { data: Post }) {
  // We've validated that this data will always exist at this point
  const { post_id, post_title } = req.data;
}

// Notice we're calling the validateBody function - that's because it's a factory that will return a pipe function
export const POST = pipe(validateBody(postSchema), handler);

catchErrors

We can also perform actions with inner pipe functions. In this example we wrap the inner pipe functions in a try catch, allowing us to catch any errors that bubble up.

export const catchErrors: PipeFunction = async (req, event, next) => {
  try {
    return await next();
  } catch (error) {
    console.error(error);
    return NextResponse.json({ message: 'Server error!' }, { status: 500 });
  }
};