At Docker, we’re incredibly proud of our vibrant, diverse and creative community. From time to time, we feature cool contributions from the community on our blog to highlight some of the great work our community does. Are you working on something awesome with Docker? Send your contributions to Ajeet Singh Raina (@ajeetraina) on the Docker Community Slack and we might feature your work!
Over the last five years, TypeScript’s popularity has surged among enterprise developers. In Stack Overflow’s 2022 Developer Survey, TypeScript ranked third in the “most wanted” category. Stack Overflow reserves this distinction for developers who aren’t developing with a specific language or technology, but have expressed interest in doing so.
TypeScript’s incremental adoption is attributable to enhancements in developer code quality and comprehensibility. Overall, Typescript encourages developers to thoroughly document their code and inspires greater confidence through ease of use. TypeScript offers every modern JavaScript feature while introducing powerful concepts like interfaces, unions, and intersection types. It improves developer productivity by clearly displaying syntax errors during compilation, rather than letting things fail at runtime.
However, remember that every programming language comes with certain drawbacks, and TypeScript is no exception. Long compilation times and a steeper learning curve for new JavaScript users are most noteworthy. The good news is that TypeScript’s pros outweigh its cons for ideal use cases. We’ll tackle one of those now.
Building Your Application
In this tutorial, you’ll learn how to build a basic URL shortener from scratch using TypeScript and Nest.
First, you’ll create a basic application in Nest without using Docker. You’ll see how the application lets you build a simple URL shortening service in Nest and TypeScript, with a Redis backend. Next, you’ll learn how Docker Compose can help you jointly run a Nest.js, TypeScript, and Redis backend to power microservices. Let’s jump in.
Getting Started
The following key components are essential to completing this walkthrough:
Before starting, make sure you have Node installed on your system. Then, follow these steps to build a simple web application with TypeScript.
Creating a Nest Project
Nest is currently the fastest growing server-side development framework in the JavaScript ecosystem. It’s ideal for writing scalable, testable, and loosely-coupled applications. Nest provides a level of abstraction above common Node.js frameworks and exposes their APIs to the developer. Under the hood, Nest makes use of robust HTTP server frameworks like Express (the default) and can optionally use Fastify as well! It supports databases like PostgreSQL, MongoDB, and MySQL. NestJS is heavily influenced by Angular, React, and Vue — while offering dependency injection right out of the box.
For first-time users, we recommend creating a new project with the Nest CLI. First, enter the following command to install the Nest CLI.
npm install -g @nestjs/cli
Next, let’s create a new Nest.js project directory called backend
.
mkdir backend
It’s time to populate the directory with the initial core Nest files and supporting modules. From your new backend directory, run Nest’s bootstrapping command. We’ll call our new application link-shortener
:
nest new link-shortener ⚡ We will scaffold your app in a few seconds.. CREATE link-shortener/.eslintrc.js (665 bytes) CREATE link-shortener/.prettierrc (51 bytes) CREATE link-shortener/README.md (3340 bytes) CREATE link-shortener/nest-cli.json (118 bytes) CREATE link-shortener/package.json (1999 bytes) CREATE link-shortener/tsconfig.build.json (97 bytes) CREATE link-shortener/tsconfig.json (546 bytes) CREATE link-shortener/src/app.controller.spec.ts (617 bytes) CREATE link-shortener/src/app.controller.ts (274 bytes) CREATE link-shortener/src/app.module.ts (249 bytes) CREATE link-shortener/src/app.service.ts (142 bytes) CREATE link-shortener/src/main.ts (208 bytes) CREATE link-shortener/test/app.e2e-spec.ts (630 bytes) CREATE link-shortener/test/jest-e2e.json (183 bytes) ? Which package manager would you ❤️ to use? (Use arrow keys) ❯ npm yarn pnpm
All three packages managers are usable, but we’ll choose npm
for the purposes of this walkthrough.
Which package manager would you ❤️ to use? npm ✔ Installation in progress... ☕ 🚀 Successfully created project link-shortener 👉 Get started with the following commands: $ cd link-shortener $ npm run start Thanks for installing Nest 🙏 Please consider donating to our open collective to help us maintain this package. 🍷 Donate: https://opencollective.com/nest
Once the command is executed successfully, it creates a new link-shortener
project directory with node modules and a few other boilerplate files. It also creates a new src/
directory populated with several core files as shown in the following directory structure:
tree -L 2 -a . └── link-shortener ├── dist ├── .eslintrc.js ├── .gitignore ├── nest-cli.json ├── node_modules ├── package.json ├── package-lock.json ├── .prettierrc ├── README.md ├── src ├── test ├── tsconfig.build.json └── tsconfig.json 5 directories, 9 files
Let’s look at the core files ending with .ts
(TypeScript) under /src
directory:
src % tree . ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts └── main.ts 0 directories, 5 files
Nest embraces modularity. Accordingly, two of the most important Nest app components are controllers and providers. Controllers determine how you handle incoming requests. They’re responsible for accepting incoming requests, performing some kind of operation, and returning the response. Meanwhile, providers are extra classes which you can inject into the controllers or to certain providers. This grants various supplemental functionality. We always recommend reading up on providers and controllers to better understand how they work.
The app.module.ts
is the root module of the application and bundles up a couple of controllers and providers that the controller uses.
cat app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule {}
As shown in the above file, AppModule is just an empty class. Nest’s @Module
decorator is responsible for providing the config that lets Nest build a functional application from it.
First, app.controller.ts
exports a basic controller with a single route. The app.controller.spec.ts
is the unit test for the controller. Second, app.service.ts
is a basic service with a single method. Third, main.ts
is the entry file of the application. It bootstraps the application by calling NestFactory.create
, then starts the new application by having it listen for inbound HTTP requests on port 3000.
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();
Running the Application
Once the installation is completed, run the following command to start your application:
npm run start > [email protected] start > nest start [Nest] 68686 - 05/31/2022, 5:50:59 PM LOG [NestFactory] Starting Nest application... [Nest] 68686 - 05/31/2022, 5:50:59 PM LOG [InstanceLoader] AppModule dependencies initialized +24ms [Nest] 68686 - 05/31/2022, 5:50:59 PM LOG [RoutesResolver] AppController {/}: +4ms [Nest] 68686 - 05/31/2022, 5:50:59 PM LOG [RouterExplorer] Mapped {/, GET} route +2ms [Nest] 68686 - 05/31/2022, 5:50:59 PM LOG [NestApplication] Nest application successfully started +1ms
This command starts the app with the HTTP server listening on the port defined in the src/main.ts
file. Once the application is successfully running, open your browser and navigate to http://localhost:3000
. You should see the “Hello World!” message:
Add a Repository
A repository is a layer that is in charge of storing stuff. Here, we would want a repository layer to store the mapping between the hashes and their original URLs.
Let’s first create an interface for the repository. Create a file named app.repository.ts
and fill it up as follows:
import { Observable } from 'rxjs'; export interface AppRepository { put(hash: string, url: string): Observable<string>; get(hash: string): Observable<string>; } export const AppRepositoryTag = 'AppRepository'; [javascript]</pre> <p class="c9"><span class="c6">Now, let's create a simple repository that stores the mappings in a hashmap in the memory. Create a file named <code>app.repository.hashmap.ts</code>:</span></p> <pre>[javascript] import { AppRepository } from './app.repository'; import { Observable, of } from 'rxjs'; export class AppRepositoryHashmap implements AppRepository { private readonly hashMap: Map<string, string>; constructor() { this.hashMap = new Map<string, string>(); } get(hash: string): Observable<string> { return of(this.hashMap.get(hash)); } put(hash: string, url: string): Observable<string> { return of(this.hashMap.set(hash, url).get(hash)); } }
Now, let’s instruct Nest.js that if one asked for AppRepositoryTag
provide them with AppRepositoryHashMap
. First, let’s do it in the app.module.ts
:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AppRepositoryTag } from './app.repository'; import { AppRepositoryHashmap } from './app.repository.hashmap'; @Module({ imports: [], controllers: [AppController], providers: [ AppService, { provide: AppRepositoryTag, useClass: AppRepositoryHashmap }, // <-- here ], }) export class AppModule {}
Let’s now add a new test for our new endpoint in app.service.spec.ts
:
import { Test, TestingModule } from "@nestjs/testing"; import { AppService } from "./app.service"; import { AppRepositoryTag } from "./app.repository"; import { AppRepositoryHashmap } from "./app.repository.hashmap"; import { mergeMap, tap } from "rxjs"; describe('AppService', () => { let appService: AppService; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ providers: [ { provide: AppRepositoryTag, useClass: AppRepositoryHashmap }, AppService, ], }).compile(); appService = app.get<AppService>(AppService); }); describe('retrieve', () => { it('should retrieve the saved URL', done => { const url = 'docker.com'; appService.shorten(url) .pipe(mergeMap(hash => appService.retrieve(hash))) .pipe(tap(retrieved => expect(retrieved).toEqual(url))) .subscribe({ complete: done }) }); }); });
Before running our tests, let’s implement the function in app.service.ts
:
import { Inject, Injectable } from '@nestjs/common'; import { map, Observable } from 'rxjs'; import { AppRepository, AppRepositoryTag } from './app.repository'; @Injectable() export class AppService { constructor( @Inject(AppRepositoryTag) private readonly appRepository: AppRepository, ) {} getHello(): string { return 'Hello World!'; } shorten(url: string): Observable<string> { const hash = Math.random().toString(36).slice(7); return this.appRepository.put(hash, url).pipe(map(() => hash)); // <-- here } retrieve(hash: string): Observable<string> { return this.appRepository.get(hash); // <-- and here } }
Run these tests once more to confirm that everything passes, before we begin storing the data in a real database.
Add a Database
So far, we’re just storing our mappings in memory. That’s fine for testing, but we’ll need to store them somewhere more centralized and durable in production. We’ll use Redis, a popular key-value store available on Docker Hub.
Let’s install this Redis client by running the following command from the backend/link-shortener
directory:
npm install [email protected] --save
Inside /src
, create a new version of the AppRepository interface that uses Redis. We’ll call this file app.repository.redis.ts
:
import { AppRepository } from './app.repository'; import { Observable, from, mergeMap } from 'rxjs'; import { createClient, RedisClientType } from 'redis'; export class AppRepositoryRedis implements AppRepository { private readonly redisClient: RedisClientType; constructor() { const host = process.env.REDIS_HOST || 'redis'; const port = +process.env.REDIS_PORT || 6379; this.redisClient = createClient({ url: `redis://${host}:${port}`, }); from(this.redisClient.connect()).subscribe({ error: console.error }); this.redisClient.on('connect', () => console.log('Redis connected')); this.redisClient.on('error', console.error); } get(hash: string): Observable<string> { return from(this.redisClient.get(hash)); } put(hash: string, url: string): Observable<string> { return from(this.redisClient.set(hash, url)).pipe( mergeMap(() => from(this.redisClient.get(hash))), ); } }
Finally, it’s time to change the provider in app.module.ts
to our new Redis repository from the in-memory version:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AppRepositoryTag } from './app.repository'; import { AppRepositoryRedis } from "./app.repository.redis"; @Module({ imports: [], controllers: [AppController], providers: [ AppService, { provide: AppRepositoryTag, useClass: AppRepositoryRedis }, // <-- here ], }) export class AppModule {}
Finalize the Backend
Head back to app.controller.ts
and create another endpoint for redirect:
import { Body, Controller, Get, Param, Post, Redirect } from '@nestjs/common'; import { AppService } from './app.service'; import { map, Observable, of } from 'rxjs'; interface ShortenResponse { hash: string; } interface ErrorResponse { error: string; code: number; } @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } @Post('shorten') shorten(@Body('url') url: string): Observable<ShortenResponse | ErrorResponse> { if (!url) { return of({ error: `No url provided. Please provide in the body. E.g. {'url':'https://google.com'}`, code: 400 }); } return this.appService.shorten(url).pipe(map(hash => ({ hash }))); } @Get(':hash') @Redirect() retrieveAndRedirect(@Param('hash') hash): Observable<{ url: string }> { return this.appService.retrieve(hash).pipe(map(url => ({ url }))); } }
Click here to access the code previously developed for this example.
Containerizing the TypeScript Application
Docker helps you containerize your TypeScript app, letting you bundle together your complete TypeScript application, runtime, configuration, and OS-level dependencies. This includes everything needed to ship a cross-platform, multi-architecture web application.
Let’s see how you can easily run this app inside a Docker container using a Docker Official Image. First, you’ll need to download Docker Desktop. Docker Desktop accelerates the image-building process while making useful images more discoverable. Complete the installation process once your download is finished.
Docker uses a Dockerfile to specify an image’s “layers.” Each layer stores important changes building upon the base image’s standard configuration. Create the following empty Dockerfile
in your Nest project.
touch Dockerfile
Use your favorite text editor to open this Dockerfile
. You’ll then need to define your base image. Let’s also quickly create a directory to house our image’s application code. This acts as the working directory for your application:
WORKDIR /app
The following COPY
instruction copies the files from the host machine to the container image:
COPY . .
Finally, this closing line tells Docker to compile and run your application packages:
CMD["npm", "run", "start:dev"]
Here’s your complete Dockerfile
:
FROM node:16 COPY . . WORKDIR /app RUN npm install EXPOSE 3000 CMD ["npm", "run", "start:dev"]
You’ve effectively learned how to build a Dockerfile
for a sample TypeScript app. Next, let’s see how to create an associated Docker Compose file for this application. Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you’ll use a YAML file to configure your services. Then, with a single command, you can create and start every service from your configuration.
Defining Services Using a Compose File
It’s time to define your services in a Docker Compose file:
services: redis: image: 'redis/redis-stack' ports: - '6379:6379' - '8001:8001' networks: - urlnet dev: build: context: ./backend/link-shortener dockerfile: Dockerfile environment: REDIS_HOST: redis REDIS_PORT: 6379 ports: - '3000:3000' volumes: - './backend/link-shortener:/app' depends_on: - redis networks: - urlnet networks: urlnet:
Your example application has the following parts:
- Two services backed by Docker images: your frontend
dev
app and your backend databaseredis
- The
redis/redis-stack
Docker image is an extension of Redis that adds modern data models and processing engines to provide a complete developer experience. We useport 8001
for RedisInsight — a visualization tool for understanding and optimizing Redis data. - The frontend, accessible via
port 3000
- The
depends_on
parameter, letting you create your backend service before the frontend service starts - One persistent volume, attached to the backend
- The environmental variables for your Redis database
Once you’ve stopped the frontend and backend services that we ran in the previous section, let’s build and start our services using the docker-compose up
command:
docker compose up -d –build
Note: If you’re using Docker Compose v1, the command line syntax is docker-compose
with a hyphen. If you’re using v2, which ships with Docker Desktop, the hyphen is omitted and docker compose
is correct.
docker compose ps NAME COMMAND SERVICE STATUS PORTS link-shortener-js-dev-1 "docker-entrypoint.s…" dev running 0.0.0.0:3000-&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;3000/tcp link-shortener-js-redis-1 "/entrypoint.sh" redis running 0.0.0.0:6379-&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;6379/tcp, 0.0.0.0:8001-&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;8001/tcp
Just like that, you’ve created and deployed your TypeScript URL shortener! You can use this in your browser like before. If you visit the application at https://localhost:3000, you should see a friendly “Hello World!” message. Use the following curl
command to shorten a new link:
curl -XPOST -d "url=https://docker.com" localhost:3000/shorten
Here’s your response:
{"hash":"l6r71d"}
This hash may differ on your machine. You can use it to redirect to the original link. Open any web browser and visit https://localhost:3000/l6r71d to access Docker’s website.
Viewing the Redis Keys
You can view the Redis keys with the RedisInsight tool by visiting https://localhost:8001.
Viewing the Compose Logs
You can use docker compose logs -f
to check and view your Compose logs:
[6:17:19 AM] Starting compilation in watch mode... link-shortener-js-dev-1 | link-shortener-js-dev-1 | [6:17:22 AM] Found 0 errors. Watching for file changes. link-shortener-js-dev-1 | link-shortener-js-dev-1 | [Nest] 31 - 06/18/2022, 6:17:23 AM LOG [NestFactory] Starting Nest application... link-shortener-js-dev-1 | [Nest] 31 - 06/18/2022, 6:17:23 AM LOG [InstanceLoader] AppModule dependencies initialized +21ms link-shortener-js-dev-1 | [Nest] 31 - 06/18/2022, 6:17:23 AM LOG [RoutesResolver] AppController {/}: +3ms link-shortener-js-dev-1 | [Nest] 31 - 06/18/2022, 6:17:23 AM LOG [RouterExplorer] Mapped {/, GET} route +1ms link-shortener-js-dev-1 | [Nest] 31 - 06/18/2022, 6:17:23 AM LOG [RouterExplorer] Mapped {/shorten, POST} route +0ms link-shortener-js-dev-1 | [Nest] 31 - 06/18/2022, 6:17:23 AM LOG [RouterExplorer] Mapped {/:hash, GET} route +1ms link-shortener-js-dev-1 | [Nest] 31 - 06/18/2022, 6:17:23 AM LOG [NestApplication] Nest application successfully started +1ms link-shortener-js-dev-1 | Redis connected
You can also leverage the Docker Dashboard to view your container’s ID and easily access or manage your application:
You can also inspect important logs via the Docker Dashboard:
Conclusion
Congratulations! You’ve successfully learned how to build and deploy a URL shortener with TypeScript and Nest. Using a single YAML file, we demonstrated how Docker Compose helps you easily build and deploy a TypeScript-based URL shortener app in seconds. With just a few extra steps, you can apply this tutorial while building applications with much greater complexity.
Happy coding.