Skip to content

Chapter 10 - Manage the slideshows through an API

In this chapter, you will create an API to manage the slideshow.

The purpose of an API is to allow other interfaces to interact with your application. This client (= interface) could be a Telegram Bot, another UI made by one of your friends, a distant server that uses your application, etc.

In this chapter, you'll use the cURL command-line tool to get, create, update and delete your slideshows through your API.

Steps

Slideshows creation and the use of generated IDs

Up until now, you have always hardcoded the slideshows with their IDs and their slides.

In a real world application, these IDs are hardly ever defined by the end user. They are generated and stored by the application for you to avoid conflicts and data loss.

An UUID will be used for unique IDs when slideshows are created.

You will generate UUIDs with Node.js later.

Create the types to create and update a slideshow

Create two news CreateSlideshow and UpdateSlideshow types to create and update a slideshow. As mentioned earlier, the application is in charge to generate the IDs of your slideshows.

The only thing that differentiate a Slideshow types and the new CreateSlideshow and UpdateSlideshow types is the omission of the ID from the slideshow.

Thanks to TypeScript, omitting some fields from another type is very easy with the Omit utility type.

src/types/create-slideshow.type.ts
1
2
3
import { Slideshow } from "./slideshow.type";

export type CreateSlideshow = Omit<Slideshow, 'id'>;
src/types/update-slideshow.type.ts
1
2
3
import { Slideshow } from "./slideshow.type";

export type UpdateSlideshow = Omit<Slideshow, 'id'>;

Update the service to create, update, and delete slideshows

Update the AppService to allow to create, update, and delete slideshows.

You may notice the createSlideshow and updateSlideshow methods return the created/updated slideshow. It is good practice to return the created/updated object to the client so they knows what has changed on the API side and how to get the newly created object with its ID.

src/app.service.ts
import { Injectable } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { CreateSlideshow } from './types/create-slideshow.type';
import { Slideshow } from './types/slideshow.type';
import { UpdateSlideshow } from './types/update-slideshow.type';

const slideshows: Map<string, Slideshow> = new Map(); // (1)!

@Injectable()
export class AppService {
	getSlideshows() {
		return slideshows;
	}

	getSlideshow(slideshowId: string) {
		const slideshow = slideshows.get(slideshowId);

		return slideshow;
	}

	createSlideshow(createSlideshow: CreateSlideshow/* (2)! */) {
		const id = randomUUID(); // (3)!

		const newSlideshow: Slideshow = { // (4)!
			id: id,
			slides: createSlideshow.slides,
		};

		slideshows.set(id, newSlideshow); // (5)!

		return newSlideshow; // (6)!
	}

	updateSlideshow(slideshowId: string, updateSlideshow: UpdateSlideshow/* (7)! */) {
		const slideshow = this.getSlideshow(slideshowId); // (8)!

		if (!slideshow) {
			throw Error('Slideshow Not Found'); // (9)!
		}

		// Update the slideshow (10)
		slideshow.slides = updateSlideshow.slides;

		slideshows.set(slideshowId, slideshow);

		return slideshow;
	}

	deleteSlideshow(slideshowId: string) {
		const deletedSlideshow = slideshows.delete(slideshowId);

		return deletedSlideshow; // (11)!
	}
}
  1. The Map is now empty.
  2. The object is typed with the CreateSlideshow type for correctness.
  3. The ID of the slideshow is generated with the randomUUID function from the crypto Node.js module.
  4. The new slideshow is created with its ID and its slides.
  5. The slideshow is saved in the Map.
  6. The API returns the newly created object to the user.
  7. The object is typed with the UpdateSlideshow type for correctness.
  8. The keyword this refers to the class AppService. It means you can use other functions defined in the same class, such as the getSlideshow method defined earlier.
  9. If the requested slideshow was not found, it throws an error as it is not possible to update a non-existent slideshow.
  10. The slideshow is updated and returned to the end user.
  11. Return true if the element in the Map has been found and deleted, false if the element has not been found.

Update the controller to get, create, update and delete slideshows

In this update, a lot is going on.

When working with APIs on the Web, they often use the HTTP protocol.

The HTTP protocol defines a few "verbs" that are used to define specific actions with your API. For now, you'll use the following request methods:

  • GET: Get a resource from an endpoint. Actually, when you access all your webpages, a GET request is sent to the server that will give you an HTML file in return.
  • POST: Create a new resource on an endpoint. When submitting a form on a website, it often sends a POST request with the form data to be saved on the server.
  • PATCH: Update parts of a resource. The PUT request method should be used when updating the entire resource but is often used as PATCH as well.
  • DELETE: Delete a resource. As the deletion often means the resource doesn't exist anymore, the end user is only informed the resource was deleted without any more details.

For a list of all the available method request, check the MDN Web Docs HTTP request methods documentation.

The same path can have multiple endpoints for each request method.

For example, the /api/slideshows path offers two endpoints: one to get all the slideshows (with a GET request method) and a second to create a new slideshow (with a POST request method).

The most common format used with APIs is JSON. The API accepts and returns JSON payloads.

Each request can return a status code. The status codes are classified in five categories:

  1. Informational responses (100 – 199)
  2. Successful responses (200 – 299)
  3. Redirection messages (300 – 399)
  4. Client error responses (400 – 499)
  5. Server error responses (500 – 599)

The most common responses are:

  • 200 OK: The request succeeded.
  • 201 Created: The request succeeded, and a new resource was created as a result.
  • 204 No Content: There is no content to send for this request.
  • 301 Moved Permanently: The URL of the requested resource has been changed permanently. The new URL is given in the response.
  • 400 Bad Request: The server cannot or will not process the request due to something that is perceived to be a client error.
  • 401 Unauthorized: The client must authenticate itself to get the requested response.
  • 404 Not Found: The server cannot find the requested resource.
  • 409 Conflict: This response is sent when a request conflicts with the current state of the server.
  • 429 Too Many Requests: The user has sent too many requests in a given amount of time ("rate limiting").
  • 500 Internal Server Error: The server has encountered a situation it does not know how to handle.

For a list of all the available status codes, check the MDN Web Docs HTTP response status codes.

src/app.controller.ts
import {
	Get,
	Controller,
	Render,
	Param,
	Res,
	Post,
	Body,
	Patch,
	Delete,
	NotFoundException,
	HttpCode,
	Redirect,
} from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service';
import { CreateSlideshow } from './types/create-slideshow.type';
import { UpdateSlideshow } from './types/update-slideshow.type';

@Controller()
export class AppController {
	constructor(private readonly appService: AppService) {}

	@Get()
	@Redirect('/slideshows') // (1)!
	root() {}

	@Get('/slideshows') // (2)!
	@Render('index')
	getSlideshows() {
		const slideshows = this.appService.getSlideshows();

		// TODO: This can be improved when the following pull request is merged:
		// https://github.com/handlebars-lang/handlebars.js/pull/1679
		const slideshowsAsArray = Array.from(slideshows).map(([key, value]) => ({
			id: key,
			slideshow: value,
		}))

		return { slideshows: slideshowsAsArray };
	}

	@Get('/slideshows/:id') // (3)!
	getSlideshow(@Res() res: Response, @Param('id') id: string) {
		const slideshow = this.appService.getSlideshow(id);

		if (!slideshow) {
			return res.redirect('/slideshows'); // (4)!
		}

		return res.render(
			'slideshow',
			{ slideshow: slideshow },
		);
	}

	@Get('/api/slideshows') // (5)!
	getSlideshowsApi() {
		const slideshows = this.appService.getSlideshows(); // (6)!

		return Object.fromEntries(slideshows); // (7)!
	}

	@Get('/api/slideshows/:id')
	getSlideshowApi(@Param('id') id: string) {
		const slideshow = this.appService.getSlideshow(id);

		if (!slideshow) {
			throw new NotFoundException(); // (8)!
		}

		return slideshow;
	}

	@Post('/api/slideshows') // (9)!
	createSlideshowApi(@Body() createSlideshow: CreateSlideshow/* (10)! */) {
		const newSlideshow = this.appService.createSlideshow(createSlideshow);

		return newSlideshow;
	}

	@Patch('/api/slideshows/:id') // (11)!
	updateSlideshowApi(@Param('id') id: string, @Body() updateSlideshow: UpdateSlideshow) {
		try { // (12)!
			const updatedSlideshow = this.appService.updateSlideshow(id, updateSlideshow);

			return updatedSlideshow;
		} catch (error) { // (13)!
			throw new NotFoundException();
		}
	}

	@Delete('/api/slideshows/:id') // (14)!
	@HttpCode(204) // (15)!
	deleteSlideshowApi(@Param('id') id: string) {
		const deletedSlideshow = this.appService.deleteSlideshow(id);

		if (!deletedSlideshow) { // (16)!
			throw new NotFoundException();
		}
	}
}
  1. Add a redirection from the root path (/) to /slideshows.
  2. Change the old path to access slideshows (/) to /slideshows.
  3. Change the old path to access a slideshow (/:id) to /slideshows/:id.
  4. If a slideshow is not found, redirect to /slideshows.
  5. A new route /api/slideshows allows to get the slideshows as JSON.
  6. The service is used the same as with Handlebars.
  7. This transform the Map object to a standard JavaScript object that can be converted to JSON.
  8. The NotFoundException exception is a NestJS built-in exception to return a HTTP response with a 404 status code.
  9. The POST endpoint allows to create a new slideshow on the same endpoint path of the GET /api/slideshows endpoint.
  10. The Body decorator allows to extract the body from the request sent by the user.
  11. The PATCH endpoint allows to update a slideshow on the same endpoint path of the GET /api/slideshows/:id endpoint.
  12. The Try-Catch Statement allows to catch errors thrown from the try block.
  13. If an exception is thrown, the code in the catch block is executed.
  14. The DELETE endpoint allows to delete a slideshow on the same endpoint path of the GET /api/slideshows/:id endpoint.
  15. As the deletion of a slideshow doesn't return the deleted slideshow, a 204 response means no content is expected.
  16. If the deletion was unsuccessful, return a 404 response with the NotFoundException exception.

Update the main template

Update the main template to access the slideshow on /slideshows/{{this.id}}.

views/index.hbs
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>Media Player</title>
	</head>
	<body>
		{{!--
		TODO: This can be improved when the following pull request is merged:
		https://github.com/handlebars-lang/handlebars.js/pull/1679
		--}}
		{{#each slideshows}}
			<p>
				<a href="/slideshows/{{this.id}}">Access slideshow '{{this.id}}'.</a>
			</p>
		{{else}}
			<p>No slideshow.</p>
		{{/each}}
	</body>
</html>

Try out the API

Start the application and access http://localhost:3000.

You should be redirected to http://localhost:3000/slideshows with no slideshows.

Let's create a new slideshow with our API!

The following commands use cURL to make the requests on your API. After each command, the command is briefly explained with the lines to look at and the output of the command.

Create a slideshow

Let's create a new slideshow with the data from your first slideshow slideshow1.

In a terminal, execute the following command(s).
curl \
	http://localhost:3000/api/slideshows \
	--request POST \
	--verbose \
	--header 'Content-Type: application/json' \
	--data @- <<-EOF
	{
	  "slides": [
	    {
	      "src": "https://source.unsplash.com/random?1",
	      "type": "image",
	      "alt": "Random photo on Unsplash"
	    },
	    {
	      "src": "https://source.unsplash.com/random?2",
	      "type": "image",
	      "alt": "Random photo on Unsplash",
	      "interval": 2000
	    }
	  ]
	}
	EOF
  • 2 The URL to make the request to.
  • 3 The request method to use.
  • 4 Enable the verbose mode to have all details of the request.
  • 5 A header sent to the API. In this case, the Content-Type is set to application/json informing the API that the payload is in JSON format.
  • 6 Tells the request has a body.
  • 7-21 The body of the request with a JSON object representing the slideshow. You notice only the slides are set with the same data as your previous slideshow1.

The output should be similar to this.

Output of the cURL command.
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> POST /api/slideshows HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.85.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 288
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 247
< ETag: W/"f7-8+eluDQg6J57mOH3UfIjClO2xmk"
< Date: Sat, 04 Feb 2023 17:36:18 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"id":"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d","slides":[{"src":"https://source.unsplash.com/random?1","type":"image","alt":"Random photo on Unsplash"},{"src":"https://source.unsplash.com/random?2","type":"video","alt":"Random photo on Unsplash","interval":2000}]}%

The > lines are the ones from your request with all the HTTP request data. The < lines are the ones from the API.

  • 1 cURL informs you the POST is not necessary as you've defined a body. When a body is specified, cURL makes a POST request by default.
  • 4 The request method and path.
  • 5 The origin of the request.
  • 6 The name of the browser or tool that did the request.
  • 7 cURL accepts any kind of data that the API can send to it.
  • 8 The content type of the body.
  • 9 The content size in bytes.
  • 12 The response status code.
  • 13 A custom header stating the API is powered by Express.
  • 14 The content type of the response and its charset.
  • 15 The content size in bytes.
  • 17 The time of the response.
  • 22 The slideshow JSON object response from the API with its ID.

Access http://localhost:3000/api/slideshows. Your browser should display a JSON object of all slideshows, including your new slideshow.

Access http://localhost:3000/slideshows. You should see the list of the slideshows, including your new slideshow.

Get the slideshow

Using the ID from the previous command output, get the slideshow just created.

In a terminal, execute the following command(s).
1
2
3
4
curl \
	http://localhost:3000/api/slideshows/<id of the slideshow> \
	-X GET \
	-v
  • 2 The URL to make the request to.
  • 3 The request method to use.
  • 4 Enable the verbose mode to have all details of the request.

The output should be similar to this.

Output of the cURL command.
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /api/slideshows/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.85.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 247
< ETag: W/"f7-8+eluDQg6J57mOH3UfIjClO2xmk"
< Date: Sat, 04 Feb 2023 17:42:44 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"id":"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d","slides":[{"src":"https://source.unsplash.com/random?1","type":"image","alt":"Random photo on Unsplash"},{"src":"https://source.unsplash.com/random?2","type":"video","alt":"Random photo on Unsplash","interval":2000}]}%

The > lines are the ones from your request with all the HTTP request data. The < lines are the ones from the API.

  • 1 cURL informs you the GET is not necessary as there is no body to the request.
  • 4 The request method and path.
  • 5 The origin of the request.
  • 6 The name of the browser or tool that did the request.
  • 7 cURL accepts any kind of data that the API can send to it.
  • 10 The response status code.
  • 11 A custom header stating the API is powered by Express.
  • 12 The content type of the response and its charset.
  • 13 The content size in bytes.
  • 15 The time of the response.
  • 20 The slideshow JSON object response from the API.

Access http://localhost:3000/api/slideshows/{id}. Your browser should display a JSON object of the slideshow.

Access http://localhost:3000/slideshows/{id}. You should see the slideshow playing the two images.

Update the slideshow

Using the ID from the previous command output, update the slideshow with with the data from your second slideshow slideshow2.

In a terminal, execute the following command(s).
curl \
	http://localhost:3000/api/slideshows/<id of the slideshow> \
	-X PATCH \
	-v \
	-H 'Content-Type: application/json' \
	-d @- <<-EOF
	{
	  "slides": [
	    {
	      "src": "https://source.unsplash.com/random?3",
	      "type": "image",
	      "alt": "Random photo on Unsplash"
	    },
	    {
	      "src": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
	      "type": "video",
	      "alt": "Sintel: first open movie",
	      "interval": 10000
	    }
	  ]
	}
	EOF
  • 2 The URL to make the request to.
  • 3 The request method to use.
  • 4 Enable the verbose mode to have all details of the request.
  • 5 A header sent to the API. In this case, the Content-Type is set to application/json informing the API that the payload is in JSON format.
  • 6 Tells the request has a body.
  • 7-21 The body of the request with a JSON object representing the slideshow. You notice only the slides are set with the same data as your previous slideshow2.

The output should be similar to this.

Output of the cURL command.
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> PATCH /api/slideshows/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.85.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 328
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 287
< ETag: W/"11f-75AZJExQAWsRDVZdeQF0RGxr1ZQ"
< Date: Sat, 04 Feb 2023 18:07:28 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"id":"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d","slides":[{"src":"https://source.unsplash.com/random?3","type":"image","alt":"Random photo on Unsplash"},{"src":"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4","type":"video","alt":"Sintel: first open movie","interval":10000}]}%

The > lines are the ones from your request with all the HTTP request data. The < lines are the ones from the API.

  • 3 The request method and path.
  • 4 The origin of the request.
  • 5 The name of the browser or tool that did the request.
  • 6 cURL accepts any kind of data that the API can send to it.
  • 7 The content type of the body.
  • 8 The content size in bytes.
  • 11 The response status code.
  • 12 A custom header stating the API is powered by Express.
  • 13 The content type of the response and its charset.
  • 14 The content size in bytes.
  • 16 The time of the response.
  • 21 The slideshow JSON object response from the API with its ID.

Access http://localhost:3000/api/slideshows/{id}. Your browser should display a JSON object of the updated slideshow.

Access http://localhost:3000/slideshows/{id}. You should see the slideshow playing the image and the video.

Delete the slideshow

Using the ID from the previous command output, delete the slideshow.

In a terminal, execute the following command(s).
1
2
3
4
curl \
	http://localhost:3000/api/slideshows/<id of the slideshow> \
	-X DELETE \
	-v
  • 2 The URL to make the request to.
  • 3 The request method to use.
  • 4 Enable the verbose mode to have all details of the request.

The output should be similar to this.

Output of the cURL command.
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> DELETE /api/slideshows/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.85.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 204 No Content
< X-Powered-By: Express
< Date: Sat, 04 Feb 2023 18:11:45 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact

The > lines are the ones from your request with all the HTTP request data. The < lines are the ones from the API.

  • 3 The request method and path.
  • 4 The origin of the request.
  • 5 The name of the browser or tool that did the request.
  • 6 cURL accepts any kind of data that the API can send to it.
  • 9 The response status code.
  • 10 A custom header stating the API is powered by Express.
  • 11 The time of the response.

Access http://localhost:3000/api/slideshows. Your browser should display a JSON object of all slideshows. Your slideshow should not appear as it was deleted.

Access http://localhost:3000/slideshows. You should see the list of the slideshows. Your slideshow should not appear as it was deleted.

Get a non-existent slideshow

Try to access a non-existent slideshow.

In a terminal, execute the following command(s).
curl -v http://localhost:3000/api/slideshows/non-existing-slideshow

The output should be similar to this.

Output of the cURL command.
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /api/slideshows/non-existing-slideshow HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.85.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 40
< ETag: W/"28-oR73Vb/YPfDbgDgtA//MdHJasVY"
< Date: Sat, 04 Feb 2023 19:08:56 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"statusCode":404,"message":"Not Found"}%

The > lines are the ones from your request with all the HTTP request data. The < lines are the ones from the API.

  • 3 The request method and path.
  • 4 The origin of the request.
  • 5 The name of the browser or tool that did the request.
  • 6 cURL accepts any kind of data that the API can send to it.
  • 7 The content type of the body.
  • 8 The content size in bytes.
  • 11 The response status code.
  • 12 A custom header stating the API is powered by Express.
  • 13 The content type of the response and its charset.
  • 14 The content size in bytes.
  • 16 The time of the response.
  • 21 The NotFoundException exception message from the exception thrown by NestJS.

Access http://localhost:3000/api/slideshows/non-existing-slideshow. Your browser should display a JSON object with a 404 error.

Access http://localhost:3000/slideshows/non-existing-slideshow. You should be redirected to http://localhost:3000/slideshows.

Restart your application and notice the data loss

Create a new slideshow.

Stop your NestJS application with Ctrl+C in your terminal.

Restart your NestJS application with npm run start:dev.

Try to access the slideshows.

You should notice that the slideshow you created earlier has been lost. This is because all your slideshows are stored in memory. This means on a restart, everything is lost. You'll see how to store your slideshows in a database in the next chapter.

To stop your NestJS application, press Ctrl+C in your terminal.

Summary

Congrats! You can now manage your slideshows with the help of your API. The API allows you and others users to use your application with their own tools. It helps to separate the views rendering and the business logic of the application.