Declarative API on Next.JS - a reality?

Hey! My name is Andrey, I am a Backend Node.JS developer in one of the foreign companies developing a system for office administration. Our application and its web version provide landlords with the ability to track office occupancy, connect IoT devices to track, for example, the amount of food in refrigerators or the amount of water in coolers, issue passes for employees in their building, and much more. One of the most important nodes in this system is the API for both internal users using an application or website and customers using our Whitelabel solution. In total, more than two hundred API endpoints are registered in our system, for the construction of which we used the NestJS framework. If for some reason you haven't heard of Nest yet,then I strongly recommend that you read the articleNestJS is the real nodejs backend . One of the main and most significant features of NestJS is native support for decorators, which in turn allows you to create endpoints declaratively.





@Get('/v2/:key')
@HttpCode(HttpStatus.OK)
async getContentFromCacheByKey(
	@Param('key') key: string,
): Promise<GenericRedisResponse> {
	const result = await this.cacheService.get(key);

	if (!result) {
		throw new NotFoundException(`There is no key ${key} in cache`);
	}

	return result;
}
      
      



Decorators are especially useful when it becomes necessary to accept different types of requests along the same path. For example, when we need not only to "take" data by key from the cache, but also to save the data under the key we need. The path remains the same, only the decorator and the contents of the method change.





@Post('/v2/:key')
@HttpCode(HttpStatus.NO_CONTENT)
async getContentFromCacheByKey(
	@Param('key') key: string,
  @Body() body: GenericRedisBody,
): Promise<void> {
    await this.cacheService.set(key, body.data, body.ex, body.validFor);
}
      
      



This is very convenient, if only because there is no need to create ornate methods with convoluted conditional statements. Not to mention the convenience of unit testing.





, NestJS, NextJS pet-. , NextJS API, , - NextJS , .





@storyofams/next-api-decorators

API routes NextJS. TypeScript, 100%, . API NextJS. :





// pages/api/user.ts
class User {
  // GET /api/user
  @Get()
  async fetchUser(@Query('id') id: string) {
    const user = await DB.findUserById(id);

    if (!user) {
      throw new NotFoundException('User not found.');
    }

    return user;
  }

  // POST /api/user
  @Post()
  @HttpCode(201)
  async createUser(@Body(ValidationPipe) body: CreateUserDto) {
    return await DB.createUser(body.email);
  }
}

export default createHandler(User);
      
      



, , :





export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'GET') {
    const user = await DB.findUserById(req.query.id);
    if (!user) {
      return res.status(404).json({
        statusCode: 404,
        message: 'User not found'
      })
    }

    return res.json(user);
  } else if (req.method === 'POST') {
    // Very primitive e-mail address validation.
    if (!req.body.email || (req.body.email && !req.body.email.includes('@'))) {
      return res.status(400).json({
        statusCode: 400,
        message: 'Invalid e-mail address.'
      })
    }

    const user = await DB.createUser(req.body.email);
    return res.status(201).json(user);
  }

  res.status(404).json({
    statusCode: 404,
    message: 'Not Found'
  });
}
      
      



. , :





@SetHeader('Content-Type', 'text/plain')
class UserHandler {
  @Get()
  users(@Header('Referer') referer: string) {
    return `Your referer is ${referer}`;
  }
  
  @Get('/json')
  @SetHeader('Content-Type', 'application/json')
  users(@Header('Referer') referer: string) {
    return { referer };
  }
}
      
      



, ! NestJS. class-validator class members :





import { IsNotEmpty, IsEmail } from 'class-validator';

export class CreateUserDTO {
  @IsEmail()
  email: string;

  @IsNotEmpty()
  fullName: string;
}
      
      



, 422 Unprocessable Entity



. , query :





@Get('/users')
@Query('isActive', ParseBooleanPipe({ nullable: true })) isActive?: boolean
      
      



isActive, URL , boolean. :





@Get('/users/:userId')
@Param('userId', ParseNumberPipe) userId: string,
      
      



middleware, , . NestJS , Guards (TLDR: ). , guard, JWT- , :





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

    next();
  }
);

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



middleware useMiddleware:





@UseMiddleware(() => ...)
class User {
// ...
      
      



, UnauthorizedException



. , , . :













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'







Catch. (, API- 503 ):





import { Catch } from '@storyofams/next-api-decorators';

function exceptionHandler(
  error: unknown,
  req: NextApiRequest,
  res: NextApiResponse
) {
  const message = error instanceof Error ? error.message : 'An unknown error occurred.';
  res.status(200).json({ success: false, error: message });
}

@Catch(exceptionHandler)
class Events {
  @Get()
  public events() {
    return 'Our events';
  }
}
      
      



This entire set of tools allows you to describe endpoints as declaratively as possible and break the code into smaller components, and this in turn greatly simplifies unit testing and debugging, which is critical to preserve the mental health and time of the developer :)





You can get acquainted with the documentation here , you can see the source code here . Thank you for reading to the end and not losing interest along the way!








All Articles