Skip to content

Introduction to RESTful APIs with NestJS

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Welcome to an introduction to RESTful APIs with NestJS. Understanding JavaScript and TypeScript will make it easier to follow the directions in this article, but you don't necessarily need to be proficient.

NestJS is one of the most rapidly growing frameworks for building Node.js server-side applications. Companies such as Roche, Adidas, and Autodesk, all trust NestJS when it comes to building efficient and scalable server-side applications.

NestJS is based heavily on Angular, and uses Angular-like modules, services, controllers, pipes, and decorators. This allows NestJS to help developers create scalable, testable, loosely coupled, and easily maintainable applications. NestJS was built with TypeScript, but it can also support pure JavaScript development.

NestJS offers a level of abstraction above two very popular Node.js frameworks, either Express.js or Fastify. This means that all of the great middleware that are available for Express.js and Fastify can also be used with NestJS.

The best way to get familiar with NestJS is to build a basic RESTful API with CRUD (Create, Read, Update, and Delete) functionality. This is exactly what we'll be doing together in this article. We'll be building a simple RESTful API for a Blog, with endpoints to handle CRUD operations on blog posts.

Getting Started

Code editor

Using Visual Studio Code as a code editor can help speed up NestJS development because of its smart IntelliSense and great TypeScript support.

In Visual Studio Code, make sure that you have the following user setting by going to File / Preferences / Settings, and searching for the user setting named typescript.preferences.importModuleSpecifier. Make sure to set it to relative as seen below.

"typescript.preferences.importModuleSpecifier": "relative"

This will allow Visual Studio Code to use relative paths rather than absolute paths when auto-importing. Using absolute path imports in our application can lead to problems if and when our code ends up in a different directory.

Insomnia

Insomnia is a useful API testing tool that we will use to test the NestJS API that we will be building.

The NestJS CLI

To get started with NestJS, let's install the Nest CLI. The Nest CLI is a command-line interface tool that makes it easy to develop, and maintain NestJS applications. It allows us to run our application in development mode, and to build and bundle it for a production-ready release.

npm i -g @nestjs/cli

Creating a new NestJS project

With the Nest CLI now installed, we can use it to create a new project.

nest new rest-api

This command will create a new project directory called rest-api. It will create a base structure for our project, and add in the following core Nest files:

  • app.controller.ts: A controller with a single route.
  • app.controller.spec.ts: The controller's unit tests.
  • app.module.ts: The root module of our application.
  • app.service.ts: A service for the AppModule's business logic.
  • main.ts: The entry file of our application.

The initial project structure created by the Nest CLI encourages us to follow the common convention of keeping each module in its own directory.

Testing the sample endpoint

The installation of NestJS comes with a sample API endpoint that we can test by making a request to it. If we open app.controller.ts, we can see that there is a GET endpoint that was created for us with the @Get() decorator. It returns a 'Hello World!' string.

@Get()
getHello(): string {
  return this.appService.getHello();
}

Let's run npm run start:dev from our project folder. This will run our NestJS app in watch mode, which provides live-reload support when application files are changed.

Once NestJS is running, let's open http://localhost:3000 in our web browser. We should see a blank page with the Hello World! greeting.

We can also use API testing tools such as Insomnia to make a GET request to http://localhost:3000. We should get the same Hello World! greeting as our result.

Let's remove this endpoint since it was only added by the Nest CLI for demo purposes. Go ahead and delete app.controller.ts, app.service.ts, and app.controller.spec.ts. Also, delete all references to AppController and AppService in app.module.ts.

Creating a feature module

The architectural design of NestJS encourages feature modules. This feature-based design groups the functionality of a single feature in one folder, registered in one module. This design simplifies the codebase and makes code-splitting very easy.

Module

We create modules in NestJS by decorating a class with the @Module decorator. Modules are needed to register controllers, services, and any other imported sub-modules. Imported sub-modules can have their own controllers, and services, registered.

Let's use the Nest CLI to create the module for our blog posts.

nest generate module posts

This gives us an empty PostsModule class in the posts.module.ts file.

Interface

We will use a TypeScript interface to define the structure of our JSON object that will represent a blog post.

An interface is a virtual or abstract structure that only exists in TypeScript. Interfaces are used only for type-checking purposes by the TypeScript compiler. TypeScript interfaces do not produce any JavaScript code during the transpilation of TypeScript to JavaScript.

Let's use the Nest CLI to create our interface.

cd src/posts 
nest generate interface posts

These commands allow us to create a posts.interface.ts file in the feature-based folder for our blog posts, which is /src/posts.

The TypeScript interface keyword is used to define our interface. Let's make sure to prefix it with export to allow this interface to be used throughout our application

export interface PostModel {
  id?: number;
  date: Date;
  title: string;
  body: string;
  category: string;
}

From the command-line prompt, let's reset our current working directory back to the root folder of our project by using the following command.

cd ../..

Service

Services are classes that handle business logic. The PostsService that we will be creating will handle the business logic needed to manage our blog posts.

Let's use the Nest CLI to create the service for our blog posts.

nest generate service posts

This will give us an empty PostsService class in the posts.service.ts file.

The @Injectable() decorator marks the PostsService class as a provider that we can register in the providers array of our PostsModule, and then inject into our controller class. More on this later.

Controller

Controllers are classes that handle incoming requests and return responses to the client. A controller can have more than one route or endpoint. Each route or endpoint can implement its own set of actions. The NestJS routing mechanism handles the routing of requests to the right controller.

Let's use the Nest CLI to create the controller for our blog posts.

nest generate controller posts

This will give us an empty PostsController class in the posts.controller.ts file.

Let's inject our PostsService into the constructor of the PostsController class.

import { Controller } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}
}

NestJS uses dependency injection to set up a reference to PostsService from within our controller. We can now use the methods provided by PostsService by referencing this.postsService from within our controller class.

Registering our controller and service

Let's make sure that our PostsModule registers our PostsController and PostsService. The nest generate service post and nest generate controller post commands that we ran earlier have automatically registered the PostsService and PostsController classes in the PostsModule for us.

@Module({
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

NestJS uses the term providers to refer to service classes, middleware, guards, and more.

If the PostsService class needs to be made available to other modules within our application, we can export it from PostsModule by using the exports array. This way, any module importing the PostsModule will be able to use the PostsService.

@Module({
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}  

Importing PostsModule into AppModule

We now have one cohesive and well-organized module for all the functionality related to our blog posts. However, all the functionality that will be provided by our PostsModule is not available to our application unless the AppModule imports it.

If we revisit the AppModule, we can see that the PostsModule has been automatically added to the imports array by the nest generate module post command that we ran earlier.

@Module({
  imports: [PostsModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

Adding Service and Controller logic

Our PostsService and PostsController have no functionality implemented at the moment. Let's now implement our CRUD endpoints and their corresponding logic while adhering to RESTful standards.

NestJS makes it easy to use MongoDB (using the @nestjs/mongoose package), or PostgreSQL (using Prisma or TypeORM) to persist and manage our application's data. However, for the sake of simplicity, let's create a local array in our service to mock a database.

import { Injectable } from '@nestjs/common';
import { PostModel } from './posts.interface';

@Injectable()
export class PostsService {
  private posts: Array<PostModel> = [];
}

A service, or provider, as NestJS refers to them, can have its scope configured. By default, a service has a singleton scope. This means that a single instance of a service is shared across our entire application. The initialization of our service will occur only once during the startup of our application. The default singleton scope of services means that any class that injects the PostsService will share the same posts array data in memory.

Get all posts

Let's add a method to our PostsService that will return all of our blog posts.

public findAll(): Array<PostModel> {
  return this.posts;
}

Let's add a method to our PostsController that will make the logic of the service's findAll() method available to client requests.

@Get()
public findAll(): Array<PostModel> {
  return this.postsService.findAll();
}

The @Get decorator is used to create a GET /posts endpoint. The /posts path of the request comes from the @Controller('posts') decorator that was used in order to define our controller.

Get one post

Let's add a method to our PostsService that will return a specific blog post that a client may be looking for. If the id of the requested post is not found in our list of posts, let's return an appropriate 404 NOT FOUND HTTP error.

public findOne(id: number): PostModel {
  const post: PostModel = this.posts.find(post => post.id === id);

  if (!post) {
    throw new NotFoundException('Post not found.');
  }

  return post;
}

Let's add a method to our PostsController that will make the logic of the service's findOne() method available to client requests.

@Get(':id')
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
  return this.postsService.findOne(id);
}

The @Get decorator is used with a parameter as @Get(':id') here to create a GET /post/[id] endpoint, where [id] is an identification number for a blog post.

@Param, from the @nestjs/common package, is a decorator that makes route parameters available to us as properties in our method.

@Param values are always of type string. Since we defined id to be of type number in TypeScript, we need to do a string to number conversion. NestJS provides a number of pipes that allow us to transform request parameters. Let's use the NestJS ParseIntPipe to convert the id to a number.

Create a post

Let's add a method to our PostsService that will create a new blog post, assign the next sequential id to it, and return it. If the title is already being used by an existing blog post, we will throw a 422 UNPROCESSABLE ENTITY HTTP error.

public create(post: PostModel): PostModel {
  // if the title is already in use by another post
  const titleExists: boolean = this.posts.some(
    (item) => item.title === post.title,
  );
  if (titleExists) {
    throw new UnprocessableEntityException('Post title already exists.');
  }

  // find the next id for a new blog post
  const maxId: number = Math.max(...this.posts.map((post) => post.id), 0);
  const id: number = maxId + 1;

  const blogPost: PostModel = {
    ...post,
    id,
  };

  this.posts.push(blogPost);

  return blogPost;
}

Let's add a method to our PostsController that will make the logic of the service's create method available to client requests.

@Post()
public create(@Body() post: PostModel): PostModel {
  return this.postsService.create(post);
}

The @Post decorator is used to create a POST /post endpoint.

When we use the NestJS decorators for the POST, PUT, and PATCH HTTP verbs, the HTTP body is used to transfer data to the API, typically in JSON format.

We can use the @Body decorator to parse the HTTP body. When this decorator is used, NestJS will run JSON.parse() on the HTTP body and provide us with a JSON object for our controller method. Within this decorator, we declare post to be of type Post because this is the data structure that we are expecting the client to provide for this request.

Delete a post

Let's add a method to our PostsService that will delete a blog post from our in-memory list of posts using the JavaScript splice() function. A 404 NOT FOUND HTTP error will be returned if the id of the requested post is not found in our list of posts.

public delete(id: number): void {
  const index: number = this.posts.findIndex(post => post.id === id);

  // -1 is returned when no findIndex() match is found
  if (index === -1) {
    throw new NotFoundException('Post not found.');      
  }

  this.posts.splice(index, 1);
}

Let's add a method to our PostsController that will make the logic of the service's delete method available to client requests.

@Delete(':id')
public delete(@Param('id', ParseIntPipe) id: number): void {  
  this.postsService.delete(id);
}

Update a post

Let's add a method to our PostsService that will find a blog post by id, update it with newly submitted data, and return the updated post. A 404 NOT FOUND HTTP error will be returned if the id of the requested post is not found in our list of posts. A 422 UNPROCESSABLE ENTITY HTTP error will be returned if the title is being used for another blog post.

public update(id: number, post: PostModel): PostModel {
  this.logger.log(`Updating post with id: ${id}`);

  const index: number = this.posts.findIndex((post) => post.id === id);

  // -1 is returned when no findIndex() match is found
  if (index === -1) {
    throw new NotFoundException('Post not found.');
  }

  // if the title is already in use by another post
  const titleExists: boolean = this.posts.some(
    (item) => item.title === post.title && item.id !== id,
  );
  if (titleExists) {
    throw new UnprocessableEntityException('Post title already exists.');
  }

  const blogPost: PostModel = {
    ...post,
    id,
  };

  this.posts[index] = blogPost;

  return blogPost;
}

Let's add a method to our PostsController that will make the logic of the service's update method available to client requests.

@Put(':id')
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): PostModel {
  return this.postsService.update(id, post);
}

We use the @Put decorator to make use of the HTTP PUT request method. PUT can be used to both create and update the state of a resource on the server. If we know that a resource already exists, PUT will replace the state of that resource on the server.

Testing our feature module

Let's start our development server using npm run start:dev. Then, let's open the Insomnia application to test the API endpoints that we created for the PostsModule.

Get all posts

Let's make a GET request to http://localhost:3000/posts. The result should be a 200 OK success code with an empty array.

Create a post

Let's make a POST request to http://localhost:3000/posts using the following as our JSON body.

{
  "date": "2021-08-16",
  "title": "Intro to NestJS",
  "body": "This blog post is about NestJS",
  "category": "NestJS"
}

We should get a successful 201 Created response code, along with a JSON representation of the post that was created, including the id field that has been automatically generated.

Get a post

Let's make a GET request to http://localhost:3000/posts/1. The result should be a successful 200 OK response code, along with the data for the post with an id of 1.

Update a post

Let's make a PUT request to http://localhost:3000/posts/1 using the following as our JSON body.

{
  "date": "2021-08-16",
  "title": "Intro to TypeScript",
  "body": "An intro to TypeScript",
  "category": "TypeScript"
}

The result should be a successful 200 OK response code, along with a JSON representation of the post that was updated.

Delete a post

Let's make a DELETE request to http://localhost:3000/posts/1. The result should be a successful 200 OK response code with no JSON object returned.

Logging

NestJS makes it easy to add logging to our application. Rather than using console.log() statements, we should use the Logger class provided by NestJS. This will provide us with nicely formatted log messages in the Terminal.

Let's add logging to our API. The first step is to define the logger in our service class.

import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PostModel } from './posts.interface';

@Injectable()
export class PostsService {
  private posts: Array<PostModel> = [];
  private readonly logger = new Logger(PostsService.name);

  // ...
}

With the logger now defined, we can add log statements in our service class. Here is an example of a log statement that we can add as the first line of the findAll() method in the PostsService class.

this.logger.log('Returning all posts');

Such statements provide us with a convenient log message in the Terminal every time a service method is called during a client's request to our API.

When a GET /posts request is sent, we should see the following message in the Terminal.

[PostsService] Returning all posts.

Swagger

NestJS makes it easy to add the OpenAPI specification to our API using the NestJS Swagger package. The OpenAPI specification is used to describe RESTful APIs for documentation and reference purposes.

Swagger setup

Let's install Swagger for NestJS.

npm install --save @nestjs/swagger swagger-ui-express

Swagger configuration

Let's update the main.ts file that bootstraps our NestJS application by adding Swagger configuration to it.

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Blog API')
    .setDescription('Blog API')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

With our NestJS app running, we can now go to http://localhost:3000/api to view the Swagger documentation for our API. Notice that default is displayed above the routes for our posts.

Let's change that by adding @ApiTags('posts') right below @Controller('posts') in our PostsController class. This will replace default with posts to indicate that this set of endpoints belongs to the posts feature model.

@Controller('posts')
@ApiTags('posts')

ApiProperty

Let's make the properties of our PostModel interface visible to Swagger. We do so by annotating fields with the ApiProperty() decorator, or the ApiPropertyOptional() decorator for optional fields.

To use these decorators, we must first change our interface to a class in the posts.interface.ts file.

export class PostModel {
  @ApiPropertyOptional({ type: Number })
  id?: number;
  @ApiProperty({ type: String, format: 'date-time' })
  date: Date;
  @ApiProperty({ type: String })
  title: string;
  @ApiProperty({ type: String })
  body: string;
  @ApiProperty({ type: String })
  category: string;
}

Within the ApiProperty() decorator applied to each field, we describe the field type. The id field is optional since we don't know of a new blog post's id when creating it. A date-time string format is used for the date field.

These changes allow the PostModel structure to be documented within Swagger. With our NestJS app running, we can now go to http://localhost:3000/api to view the documentation of the PostModel.

ApiResponse

Let's use the @ApiResponse() decorator for Swagger to document all the possible responses from our API's endpoints. This will be helpful for informing users of our API what responses they can expect to receive by calling a given endpoint. We will be making these changes in the PostsController class.

For the findAll method, let's use the @ApiOkResponse() decorator to document a 200 OK success response.

@Get()
@ApiOkResponse({ description: 'Posts retrieved successfully.'})
public findAll(): Array<PostModel> {
  return this.postsService.findAll();
}

For the findOne method, let's use the @ApiOkResponse() decorator to document a 200 OK response when the post is found. Let's use the @ApiNotFoundResponse() decorator to document a 404 NOT FOUND HTTP error when a post is not found.

@Get(':id')
@ApiOkResponse({ description: 'Post retrieved successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
  return this.postsService.findOne(id);
}

For the create method, let's use the @ApiCreatedResponse() decorator to document a 201 CREATED response when a post is created. Let's use the @ApiUnprocessableEntityResponse() decorator to document a 422 UNPROCESSABLE ENTITY HTTP error when a duplicate post title is found.

@Post()
@ApiCreatedResponse({ description: 'Post created successfully.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public create(@Body() post: PostModel): void {
  return this.postsService.create(post);
}

For the delete method, let's use the @ApiOkResponse() decorator to document a 200 OK response when a post is deleted. Let's use the @ApiNotFoundResponse() decorator to document a 404 NOT FOUND HTTP error when the post to be deleted is not found.

@Delete(':id')
@ApiOkResponse({ description: 'Post deleted successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public delete(@Param('id', ParseIntPipe) id: number): void {
  return this.postsService.delete(id);
}

For the update method, let's use the @ApiOkResponse() decorator to document a 200 OK response when a post is updated. Let's use the @ApiNotFoundResponse() decorator to document a 404 NOT FOUND HTTP error when the post to be updated is not found. Let's use the @ApiUnprocessableEntityResponse() decorator to document a 422 UNPROCESSABLE ENTITY HTTP error when a duplicate post title is found.

@Put(':id')
@ApiOkResponse({ description: 'Post updated successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): void {
  return this.postsService.update(id, post);
}

After saving these changes, all response codes and their related descriptions should now be listed for each endpoint on the Swagger web page at http://localhost:3000/api.

We could also use Swagger instead of Insomnia to test our API. By clicking on the "Try it out" button that appears under each endpoint on the Swagger web page, we can see if they are working as expected.

Exception filter

Exception filters give us full control over the exceptions layer of NestJS. We can use an exception filter to add custom fields to a HTTP exception response body or to print out logs of every HTTP exception that occurs to the Terminal.

Let's create a new filters folder in the /src folder, as well as a new file called http-exception.filter.ts within it. Let's add the following class in this file.

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const statusCode = exception.getStatus();
    const message = exception.message || null;

    const body = {
      statusCode,
      message,      
      timestamp: new Date().toISOString(),
      endpoint: request.url,
    };

    this.logger.warn(`${statusCode} ${message}`);

    response
      .status(statusCode)
      .json(body);
  }
}

This class makes use of the NestJS logger to print a warning message to the Terminal whenever a HTTP exception occurs. It also returns two custom fields within the response body whenever a HTTP exception occurs. The timestamp field reports when the exception occurred, and the endpoint field reports which route triggered the exception.

In order to apply this filter, we need to decorate the PostsController with @UseFilters(new HttpExceptionFilter()).

@Controller('posts')
@UseFilters(new HttpExceptionFilter())
export class PostsController {
  constructor(private readonly postsService: PostsService) {}
}

After saving these changes, NestJS will reload our application for us. If we use Insomnia to send a PUT /posts/1 request to our API, it should trigger a 404 NOT FOUND HTTP error, since no blog posts exist for updating in our application when it starts. The HTTP exception response body that is returned to Insomnia should now contain the timestamp and endpoint fields.

{
  "statusCode": 404,
  "message": "Post not found.",
  "timestamp": "2021-08-23T21:05:29.497Z",
  "endpoint": "/posts/1"
}

We should also see the following line printed out to the Terminal.

WARN [HttpExceptionFilter] 404 Post not found.

Summary

In this article, we saw how NestJS makes back-end API development fast, simple, and effective. The NestJS application structure has helped us build a very well-organized project.

We covered a lot, so let's recap what we learned:

  • How to use the NestJS CLI.
  • How to build feature modules in NestJS.
  • How to use services and controllers.
  • How to test an API with Insomnia.
  • How to add logging to NestJS apps.
  • How to use Swagger for documenting and previewing NestJS APIs.
  • How to get full control of HTTP exceptions in NestJS.

Happy developing with NestJS!

Code

Visit the following GitHub repository in order to access the code featured in this article.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Understanding Sourcemaps: From Development to Production cover image

Understanding Sourcemaps: From Development to Production

What Are Sourcemaps? Modern web development involves transforming your source code before deploying it. We minify JavaScript to reduce file sizes, bundle multiple files together, transpile TypeScript to JavaScript, and convert modern syntax into browser-compatible code. These optimizations are essential for performance, but they create a significant problem: the code running in production does not look like the original code you wrote. Here's a simple example. Your original code might look like this: ` After minification, it becomes something like this: ` Now imagine trying to debug an error in that minified code. Which line threw the exception? What was the value of variable d? This is where sourcemaps come in. A sourcemap is a JSON file that contains a mapping between your transformed code and your original source files. When you open browser DevTools, the browser reads these mappings and reconstructs your original code, allowing you to debug with variable names, comments, and proper formatting intact. How Sourcemaps Work When you build your application with tools like Webpack, Vite, or Rollup, they can generate sourcemap files alongside your production bundles. A minified file references its sourcemap using a special comment at the end: ` The sourcemap file itself contains a JSON structure with several key fields: ` The mappings field uses an encoding format called VLQ (Variable Length Quantity) to map each position in the minified code back to its original location. The browser's DevTools use this information to show you the original code while you're debugging. Types of Sourcemaps Build tools support several variations of sourcemaps, each with different trade-offs: Inline sourcemaps: The entire mapping is embedded directly in your JavaScript file as a base64 encoded data URL. This increases file size significantly but simplifies deployment during development. ` External sourcemaps: A separate .map file that's referenced by the JavaScript bundle. This is the most common approach, as it keeps your production bundles lean since sourcemaps are only downloaded when DevTools is open. Hidden sourcemaps: External sourcemap files without any reference in the JavaScript bundle. These are useful when you want sourcemaps available for error tracking services like Sentry, but don't want to expose them to end users. Why Sourcemaps During development, sourcemaps are absolutely critical. They will help avoid having to guess where errors occur, making debugging much easier. Most modern build tools enable sourcemaps by default in development mode. Sourcemaps in Production Should you ship sourcemaps to production? It depends. While security by making your code more difficult to read is not real security, there's a legitimate argument that exposing your source code makes it easier for attackers to understand your application's internals. Sourcemaps can reveal internal API endpoints and routing logic, business logic, and algorithmic implementations, code comments that might contain developer notes or TODO items. Anyone with basic developer tools can reconstruct your entire codebase when sourcemaps are publicly accessible. While the Apple leak contained no credentials or secrets, it did expose their component architecture and implementation patterns. Additionally, code comments can inadvertently contain internal URLs, developer names, or company-specific information that could potentially be exploited by attackers. But that’s not all of it. On the other hand, services like Sentry can provide much more actionable error reports when they have access to sourcemaps. So you can understand exactly where errors happened. If a customer reports an issue, being able to see the actual error with proper context makes diagnosis significantly faster. If your security depends on keeping your frontend code secret, you have bigger problems. Any determined attacker can reverse engineer minified JavaScript. It just takes more time. Sourcemaps are only downloaded when DevTools is open, so shipping them to production doesn't affect load times or performance for end users. How to manage sourcemaps in production You don't have to choose between no sourcemaps and publicly accessible ones. For example, you can restrict access to sourcemaps with server configuration. You can make .map accessible from specific IP addresses. Additionally, tools like Sentry allow you to upload sourcemaps during your build process without making them publicly accessible. Then configure your build to generate sourcemaps without the reference comment, or use hidden sourcemaps. Sentry gets the mapping information it needs, but end users can't access the files. Learning from Apple's Incident Apple's sourcemap incident is a valuable reminder that even the largest tech companies can make deployment oversights. But it also highlights something important: the presence of sourcemaps wasn't actually a security vulnerability. This can be achieved by following good security practices. Never include sensitive data in client code. Developers got an interesting look at how Apple structures its Svelte codebase. The lesson is that you must be intentional about your deployment configuration. If you're going to include sourcemaps in production, make that decision deliberately after considering the trade-offs. And if you decide against using public sourcemaps, verify that your build process actually removes them. In this case, the public repo was quickly removed after Apple filed a DMCA takedown. (https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md) Making the Right Choice So what should you do with sourcemaps in your projects? For development: Always enable them. Use fast options, such as eval-source-map in Webpack or the default configuration in Vite. The debugging benefits far outweigh any downsides. For production: Consider your specific situation. But most importantly, make sure your sourcemaps don't accidentally expose secrets. Review your build output, check for hardcoded credentials, and ensure sensitive configurations stay on the backend where they belong. Conclusion Sourcemaps are powerful development tools that bridge the gap between the optimized code your users download and the readable code you write. They're essential for debugging and make error tracking more effective. The question of whether to include them in production doesn't have a unique answer. Whatever you decide, make it a deliberate choice. Review your build configuration. Verify that sourcemaps are handled the way you expect. And remember that proper frontend security doesn't come from hiding your code. Useful Resources * Source map specification - https://tc39.es/ecma426/ * What are sourcemaps - https://web.dev/articles/source-maps * VLQ implementation - https://github.com/Rich-Harris/vlq * Sentry sourcemaps - https://docs.sentry.io/platforms/javascript/sourcemaps/ * Apple DMCA takedown - https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md...

Incremental Hydration in Angular cover image

Incremental Hydration in Angular

Incremental Hydration in Angular Some time ago, I wrote a post about SSR finally becoming a first-class citizen in Angular. It turns out that the Angular team really treats SSR as a priority, and they have been working tirelessly to make SSR even better. As the previous blog post mentioned, full-page hydration was launched in Angular 16 and made stable in Angular 17, providing a great way to improve your Core Web Vitals. Another feature aimed to help you improve your INP and other Core Web Vitals was introduced in Angular 17: deferrable views. Using the @defer blocks allows you to reduce the initial bundle size and defer the loading of heavy components based on certain triggers, such as the section entering the viewport. Then, in September 2024, the smart folks at Angular figured out that they could build upon those two features, allowing you to mark parts of your application to be server-rendered dehydrated and then hydrate them incrementally when needed - hence incremental hydration. I’m sure you know what hydration is. In short, the server sends fully formed HTML to the client, ensuring that the user sees meaningful content as quickly as possible and once JavaScript is loaded on the client side, the framework will reconcile the rendered DOM with component logic, event handlers, and state - effectively hydrating the server-rendered content. But what exactly does "dehydrated" mean, you might ask? Here's what will happen when you mark a part of your application to be incrementally hydrated: 1. Server-Side Rendering (SSR): The content marked for incremental hydration is rendered on the server. 2. Skipped During Client-Side Bootstrapping: The dehydrated content is not initially hydrated or bootstrapped on the client, reducing initial load time. 3. Dehydrated State: The code for the dehydrated components is excluded from the initial client-side bundle, optimizing performance. 4. Hydration Triggers: The application listens for specified hydration conditions (e.g., on interaction, on viewport), defined with a hydrate trigger in the @defer block. 5. On-Demand Hydration: Once the hydration conditions are met, Angular downloads the necessary code and hydrates the components, allowing them to become interactive without layout shifts. How to Use Incremental Hydration Thanks to Mark Thompson, who recently hosted a feature showcase on incremental hydration, we can show some code. The first step is to enable incremental hydration in your Angular application's appConfig using the provideClientHydration provider function: ` Then, you can mark the components you want to be incrementally hydrated using the @defer block with a hydrate trigger: ` And that's it! You now have a component that will be server-rendered dehydrated and hydrated incrementally when it becomes visible to the user. But what if you want to hydrate the component on interaction or some other trigger? Or maybe you don't want to hydrate the component at all? The same triggers already supported in @defer blocks are available for hydration: - idle: Hydrate once the browser reaches an idle state. - viewport: Hydrate once the component enters the viewport. - interaction: Hydrate once the user interacts with the component through click or keydown triggers. - hover: Hydrate once the user hovers over the component. - immediate: Hydrate immediately when the component is rendered. - timer: Hydrate after a specified time delay. - when: Hydrate when a provided conditional expression is met. And on top of that, there's a new trigger available for hydration: - never: When used, the component will remain static and not hydrated. The never trigger is handy when you want to exclude a component from hydration altogether, making it a completely static part of the page. Personally, I'm very excited about this feature and can't wait to try it out. How about you?...

Next.js Conf 2021 Highlights cover image

Next.js Conf 2021 Highlights

Given how well-presented it was, it's hard to believe that Next.js Conf 2021 was only the second edition of Vercel's Next.js Conf. Vercel is the creator of the industry-leading React and JavaScript framework for full-stack web development called Next.js. This year also marked a milestone birthday celebration for Next.js as it turned 5 years old. Guillermo Rauch, CEO of Vercel and co-creator of Next.js, started off the conference with a keynote talk. Guillermo announced Next.js version 12, the biggest release ever. Guillermo highlighted the following features of Next.js 12. A new Rust-based compiler Next.js 12 uses a Rust compiler built with SWC, which stands for Speedy Web Compiler. It's an open platform for fast tooling. This compiler improves both local and production environments. This new Rust-based compiler achieves around 5 times faster builds and around 3 times faster Hot Module Replacement (or HMR). Zero code updates are required to benefit from these improvements. Simply update your Next.js app to version 12 to begin enjoying the benefits of this new compiler. ES Modules Next.js 11.1 added experimental support for ES modules. In Next.js 12, ES modules are now the default. ES modules are the official, standardized module system for JavaScript. They are supported by all major browsers and Node.js. This standardized module system allows for smaller package sizes. URL Imports Next.js 12 includes experimental support for URL Imports. This allows developers to use any package directly via a URL. There is no install or build step required. Remote resources can be processed exactly like local dependencies. Middleware & Edge Functions Over the past few years, Next.js has provided tremendous support for serverless deployments, thereby eliminating the DevOps burden for developers. Next.js continued on this mission by introducing *middleware* and *edge functions* on Vercel serverless deployments. Middleware Next.js middleware allow developers to run code before a request is completed. The response to an incoming request can be modified by rewriting, redirecting, adding headers, or streaming HTML. This allows middleware to be used for things like authentication, feature flags, A/B testing, logging, server-side analytics, and more. Next.js middleware provide the same flexibility on serverless platforms that is available with traditional web servers. Next.js middleware support Web APIs like fetch and they work out of the box in Next.js 12, or deployed as edge functions on Vercel. Edge functions Edge functions allow developers to run code close to their users. Vercel now supports edge functions for all users on their platform. It's important to note that other hosting providers can also be configured to support middleware at the edge, and not just Vercel. Multi-region deployment is often avoided due to cost and complexity. With edge functions, server-side logic can now be moved to the Edge, close to the geographic location of the website visitor. This results in improved loading times for visitors, no matter where they visit from. As developers, we typically serve dynamic content for visitor personalization reasons and serve static content for faster load times. However, with a Next.js middleware deployed as an edge function, dynamic content can be served at the speed of static content. Edge functions help to solve the common issues with serverless deployments because they boot up instantly, deploy globally, and support streaming data. The future of React Next.js 12 brings the future of React to Next.js. It includes experimental support for React 18 and React server components. It was reassuring to hear how closely Next.js is working with the React team to prepare Next.js for React 18, server-side streaming, and React server components. React server components React server components allow individual components to be rendered on the server-side. It's different from *Server-Side Rendering (SSR)*, which pre-generates the HTML of an entire web page on the server. Server-Side Rendering renders all the HTML on the page or nothing at all. With React server components, a web page can progressively update at the component level as data comes in. With React server components, data fetching can be done at the component level. This simplifies things, because getServerSideProps and getStaticProps will no longer be needed. This aligns more closely to the React hooks model, which advocates for combining data fetching with components. Using server components React server components can be used in Next.js 12 with an experimental flag. To try out React server components, we can simply give any page a .server.js or .server.ts suffix. Client-side rendered components can be used within the server component. These client components will hydrate and become interactive when the server component is rendered. This can be useful for adding client-side functionality to a page, like upvoting and downvoting behaviors. Benefits of server components React server components require no client-side JavaScript and they improve page rendering speeds, as well as the user experience. React server components provide greater flexibility, allowing the developer to choose where a component renders, either on the client or on the server. Granular component caching The Next.js team showcased a new exploration that is currently underway using a new Data component that works like a React suspense boundary. This new component supports granular component caching. It's similar to *Incremental Static Regeneration (ISR)*. Next.js Analytics Last year, Next.js announced Next.js Analytics to help monitor web vitals. Since then, it has seen amazing adoption, processing over 7 billion data points. This year, Next.js announced Checks, automated performance checks that start automatically and report results when a deployment finishes. A preview deployment is checked to ensure that there are no runtime errors and that good performance makes its way into production. This is all thanks to the end-to-end reliability checks and *Web Vitals* performance checks that are done. Next.js Live During the conference, we learned about the evolution of Next.js Live, which was presented at last year's Next.js Conf. Next.js Live provides an instant development and real-time collaboration experience for Next.js. All it takes to collaborate with Next.js Live is a link. Next.js Live makes it possible to code, chat, draw, and edit, directly from a web browser. Next.js Live now provides new Git integrations. Next.js Live runs natively in web browsers, and now boots up instantly thanks to innovations like ES Modules and Web Assembly, which the new compiler takes advantage of. Notable Talks Next.js 12 Overview Developer Experience of the Future by Lee Robinson. Learn more about the features included in Next.js 12. Edge functions Next.js at the Edge by Suzanne Aldrich and Lee Robinson. Learn more about Edge functions with Next.js and Vercel. Edge Functions Explained by Kelsey Hightower and Lee Robinson. See a demo of edge functions and learn how they're different from the traditional way of building for the web. Next.js and databases Next.js and Prisma: Databases in Serverless Made Easy by Daniel Norman. Learn how Prisma makes building database-driven applications with Next.js a breeze. Streaming Streaming in Next.js by Kara Erickson. Learn about the benefits of streaming with Next.js. Getting Started Getting Started with Next.js by Delba de Oliveira. Learn the fundamentals of React and Next.js. All Talks To view all the talks that were part of Next.js Conf 2021, visit the Vercel YouTube page....

Vercel BotID: The Invisible Bot Protection You Needed cover image

Vercel BotID: The Invisible Bot Protection You Needed

Nowadays, bots do not act like “bots”. They can execute JavaScript, solve CAPTCHAs, and navigate as real users. Traditional defenses often fail to meet expectations or frustrate genuine users. That’s why Vercel created BotID, an invisible CAPTCHA that has real-time protections against sophisticated bots that help you protect your critical endpoints. In this blog post, we will explore why you should care about this new tool, how to set it up, its use cases, and some key considerations to take into account. We will be using Next.js for our examples, but please note that this tool is not tied to this framework alone; the only requirement is that your app is deployed and running on Vercel. Why Should You Care? Think about these scenarios: - Checkout flows are overwhelmed by scalpers - Signup forms inundated with fake registrations - API endpoints draining resources with malicious requests They all impact you and your users in a negative way. For example, when bots flood your checkout page, real customers are unable to complete their purchases, resulting in your business losing money and damaging customer trust. Fake signups clutter the app, slowing things down and making user data unreliable. When someone deliberately overloads your app’s API, it can crash or become unusable, making users angry and creating a significant issue for you, the owner. BotID automatically detects and filters bots attempting to perform any of the above actions without interfering with real users. How does it work? A lightweight first-party script quickly gathers a high set of browser & environment signals (this takes ~30ms, really fast so no worry about performance issues), packages them into an opaque token, and sends that token with protected requests via the rewritten challenge/proxy path + header; Vercel’s edge scores it, attaches a verdict, and checkBotId() function simply reads that verdict so your code can allow or block. We will see how this is implemented in a second! But first, let’s get started. Getting Started in Minutes 1. Install the SDK: ` 1. Configure redirects Wrap your next.config.ts with BotID’s helper. This sets up the right rewrites so BotID can do its job (and not get blocked by ad blockers, extensions, etc.): ` 2. Integrate the client on public-facing pages (where BotID runs checks): Declare which routes are protected so BotID can attach special headers when a real user triggers those routes. We need to create instrumentation-client.ts (place it in the root of your application or inside a src folder) and initialize BotID once: ` instrumentation-client.ts runs before the app hydrates, so it’s a perfect place for a global setup! If we have an inferior Next.js version than 15.3, then we would need to use a different approach. We need to render the React component inside the pages or layouts you want to protect, specifying the protected routes: ` 3. Verify requests on your server or API: ` - NOTE: checkBotId() will fail if the route wasn’t listed on the client, because the client is what attaches the special headers that let the edge classify the request! You’re all set - your routes are now protected! In development, checkBotId() function will always return isBot = false so you can build without friction. To disable this, you can override the options for development: ` What happens on a failed check? In our example above, if the check failed, we return a 403, but it is mostly up to you what to do in this case; the most common approaches for this scenario are: - Hard block with a 403 for obviously automated traffic (just what we did in the example above) - Soft fail (generic error/“try again”) when you want to be cautious. - Step-up (require login, email verification, or other business logic). Remember, although rare, false positives can occur, so it’s up to you to determine how you want to balance your fail strategy between security, UX, telemetry, and attacker behavior. checkBotId() So far, we have seen how to use the property isBot from checkBotId(), but there are a few more properties that you can leverage from it. There are: isHuman (boolean): true when BotID classifies the request as a real human session (i.e., a clear “pass”). BotID is designed to return an unambiguous yes/no, so you can gate actions easily. isBot (boolean): We already saw this one. It will be true when the request is classified as automated traffic. isVerifiedBot (boolean): Here comes a less obvious property. Vercel maintains and continuously updates a comprehensive directory of known legitimate bots from across the internet. This directory is regularly updated to include new legitimate services as they emerge. This could be helpful for allowlists or custom logic per bot. We will see an example in a sec. verifiedBotName? (string): The name for the specific verified bot (e.g., “claude-user”). verifiedBotCategory? (string): The type of the verified bot (e.g., “webhook”, “advertising”, “ai_assistant”). bypassed (boolean): it is true if the request skipped BotID check due to a configured Firewall bypass (custom or system). You could use this flag to avoid taking bot-based actions when you’ve explicitly bypassed protection. Handling Verified Bots - NOTE: Handling verified bots is available in botid@1.5.0 and above. It might be the case that you don’t want to block some verified bots because they are not causing damage to you or your users, as it can sometimes be the case for AI-related bots that fetch your site to give information to a user. We can use the properties related to verified bots from checkBotId() to handle these scenarios: ` Choosing your BotID mode When leveraging BotID, you can choose between 2 modes: - Basic Mode: Instant session-based protection, available for all Vercel plans. - Deep Analysis Mode: Enhanced Kasada-powered detection, only available for Pro and Enterprise plan users. Using this mode, you will leverage a more advanced detection and will block the hardest to catch bots To specify the mode you want, you must do so in both the client and the server. This is important because if either of the two does not match, the verification will fail! ` Conclusion Stop chasing bots - let BotID handle them for you! Bots are and will get smarter and more sophisticated. BotID gives you a simple way to push back without slowing your customers down. It is simple to install, customize, and use. Stronger protection equals fewer headaches. Add BotID, ship with confidence, and let the bots trample into a wall without knowing what’s going on....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co