├── .gitignore ├── README.md ├── encore.app ├── greeting ├── greeting.ts └── search.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── url ├── migrations └── 1_create_tables.up.sql └── url.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .encore 2 | encore.gen.go 3 | encore.gen.cue 4 | /.encore 5 | node_modules 6 | /encore.gen 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Encore URL Shortener 2 | 3 | This is a simple service that takes a URL and returns an ID to create a short URL. It is written in TypeScript using the Encore framework. This is the code for my [YouTube tutorial](https://youtu.be/tL01EzN2-xA). 4 | 5 | The "greeting" folder is just to show you how to create endpoints with Encore.ts. You can delete this if you want. 6 | 7 | ## Usage 8 | 9 | Install the dependencies 10 | 11 | ```bash 12 | npm install 13 | ``` 14 | 15 | Run the server 16 | 17 | ```bash 18 | encore run 19 | ``` 20 | 21 | This will start up the server and you can visit the following: 22 | 23 | - http://127.0.0.1:9400 - Developer Dashboard 24 | - http://127.0.0.1:4000 - API/Service 25 | 26 | ## API Usage 27 | 28 | To create a short ID from a URL: 29 | 30 | - Request - POST /url 31 | - Body - {"url": "http://google.com"} 32 | - Response - {"id": "34h5y4", "url": "https://google.com"} 33 | 34 | To get the ID of a submitted URL: 35 | 36 | - Request - GET /url/34h5y4 37 | - Response - {"id": "34h5y4", "url": "https://google.com"} 38 | -------------------------------------------------------------------------------- /encore.app: -------------------------------------------------------------------------------- 1 | { 2 | "id": "encore-url-shortener-a4n2", 3 | "lang": "typescript" 4 | } 5 | -------------------------------------------------------------------------------- /greeting/greeting.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is just to show you how to create endpoints. You can delete the "greeting" folder if you want. 3 | * 4 | */ 5 | 6 | import { api } from 'encore.dev/api'; 7 | 8 | interface Response { 9 | data: string; 10 | } 11 | 12 | export const greeting = api( 13 | { 14 | method: 'GET', 15 | path: '/greeting/:name', 16 | expose: true, 17 | }, 18 | async ({ name }: { name: string }): Promise => { 19 | return { data: `Hello ${name}!` }; 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /greeting/search.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is just to show you how to create endpoints with a query param. You can delete the "greeting" folder if you want. 3 | * 4 | */ 5 | 6 | import { api, Query } from 'encore.dev/api'; 7 | 8 | interface SearchParams { 9 | filter: Query; 10 | } 11 | 12 | interface SearchResponse { 13 | matches: string; 14 | } 15 | 16 | export const search = api( 17 | { 18 | method: 'GET', 19 | path: '/greeting/search', 20 | expose: true, 21 | }, 22 | async ({ filter }) => { 23 | return { matches: `Matched ${filter}` }; 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encore-ts-starter", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "encore-ts-starter", 9 | "version": "0.0.1", 10 | "license": "MPL-2.0", 11 | "dependencies": { 12 | "encore.dev": "^1.39.8" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^20.5.7", 16 | "typescript": "^5.2.2" 17 | } 18 | }, 19 | "node_modules/@types/node": { 20 | "version": "20.12.7", 21 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", 22 | "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", 23 | "dev": true, 24 | "dependencies": { 25 | "undici-types": "~5.26.4" 26 | } 27 | }, 28 | "node_modules/encore.dev": { 29 | "version": "1.39.8", 30 | "resolved": "https://registry.npmjs.org/encore.dev/-/encore.dev-1.39.8.tgz", 31 | "integrity": "sha512-9oS0zs5C1H0Jfmps9Sz6DARbK9Hy8GyugM0EWeo23926Amw4jr0qQhxBDtnBz1CP/qMTIzW0bzWcOCGZJpSoAA==", 32 | "license": "MPL-2.0", 33 | "engines": { 34 | "node": ">=18.0.0" 35 | } 36 | }, 37 | "node_modules/typescript": { 38 | "version": "5.4.5", 39 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 40 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 41 | "dev": true, 42 | "bin": { 43 | "tsc": "bin/tsc", 44 | "tsserver": "bin/tsserver" 45 | }, 46 | "engines": { 47 | "node": ">=14.17" 48 | } 49 | }, 50 | "node_modules/undici-types": { 51 | "version": "5.26.5", 52 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 53 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 54 | "dev": true 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "encore-ts-starter", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Encore Typescript Starter", 6 | "license": "MPL-2.0", 7 | "type": "module", 8 | "devDependencies": { 9 | "@types/node": "^20.5.7", 10 | "typescript": "^5.2.2" 11 | }, 12 | "dependencies": { 13 | "encore.dev": "^1.39.8" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "lib": ["ES2022"], 6 | "target": "ES2022", 7 | "module": "ES2022", 8 | "types": ["node"], 9 | "paths": { 10 | "~encore/*": ["./encore.gen/*"] 11 | }, 12 | 13 | /* Workspace Settings */ 14 | "composite": true, 15 | 16 | /* Strict Type-Checking Options */ 17 | "strict": true, 18 | 19 | /* Module Resolution Options */ 20 | "moduleResolution": "bundler", 21 | "allowSyntheticDefaultImports": true, 22 | "isolatedModules": true, 23 | "sourceMap": true, 24 | 25 | "declaration": true, 26 | 27 | /* Advanced Options */ 28 | "forceConsistentCasingInFileNames": true, 29 | "skipLibCheck": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /url/migrations/1_create_tables.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE url ( 2 | id TEXT PRIMARY KEY, 3 | original_url TEXT NOT NULL 4 | ); -------------------------------------------------------------------------------- /url/url.ts: -------------------------------------------------------------------------------- 1 | import { api, APIError } from 'encore.dev/api'; 2 | import { SQLDatabase } from 'encore.dev/storage/sqldb'; 3 | import { randomBytes } from 'node:crypto'; 4 | 5 | const db = new SQLDatabase('url', { migrations: './migrations' }); 6 | 7 | interface UrlResponse { 8 | id: string; 9 | url: string; 10 | } 11 | 12 | interface UrlParams { 13 | url: string; 14 | } 15 | 16 | export const shorten = api( 17 | { 18 | method: 'POST', 19 | path: '/url', 20 | expose: true, 21 | }, 22 | async ({ url }: UrlParams): Promise => { 23 | const id = randomBytes(6).toString('base64url'); 24 | await db.exec`INSERT INTO url (id, original_url) 25 | VALUES (${id}, ${url})`; 26 | return { id, url }; 27 | } 28 | ); 29 | 30 | export const getShortenedUrl = api( 31 | { 32 | method: 'GET', 33 | path: '/url/:id', 34 | expose: true, 35 | }, 36 | async ({ id }: { id: string }): Promise => { 37 | const row = await db.queryRow` 38 | SELECT original_url FROM url WHERE id = ${id} 39 | `; 40 | if (!row) throw APIError.notFound('URL not found'); 41 | return { id, url: row.original_url }; 42 | } 43 | ); 44 | --------------------------------------------------------------------------------