# 後端體驗營 Day-2 教材
## 認識 API
API(Application Programming Interface)是一種讓「不同系統」彼此溝通的方式。
在後端開發中,最常見的是 **RESTful API**:
* **GET**:取得資料
* **POST**:新增資料
* **PATCH**:更新資料
* **DELETE**:刪除資料
---
## 初探 Spring Boot
Spring Boot 可以讓我們快速建立後端專案,不用自己設定一堆複雜的設定。
只要建立一個專案,寫上 Controller,就能跑出 API。
啟動後,你就可以透過 `http://localhost:8080` 測試 API。
---
## 建立&開啟專案
進入[Spring Initializr](https://start.spring.io/)
依照下圖會得到一個壓縮檔,解壓縮後將demo資料夾用IDEA開啟。

(目前可能最高版本為Java 25,但為了避免新版本的衝突,選擇Java 21就好)
### 資料夾結構
開啟後的資料夾結構會如下圖
開啟DemoApplication.java會出現黃色提示,點Setup JDK並選擇版本(要選與專案相同的版本,就是Spring Initializr的Java版本)

> 如果沒有版本可以選的話,點Download JDK並選擇21.x開頭的Download就好!!!
> 下載完後點開最右方的Gradle大象並按重整
> 
---
## 實作 API
我們這裡要做 **註冊 / 登入 API**:
1. **註冊 API**:使用者傳送帳號、密碼 → 後端儲存資料庫
2. **登入 API**:使用者傳送帳號、密碼 → 後端檢查是否正確 → 回傳登入成功或失敗
由於不連結資料庫,我們就用 `List` 來暫存使用者。
---
## 完整程式碼
建立一個簡單的 Spring Boot 專案,在與`DemoApplication`同層資料夾下新增:
### `User`
```java
public class User {
private String username;
private String password;
public User() {}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
```
---
### `UserController` (Controller)
```java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/user")
@CrossOrigin
public class UserController {
// 暫存使用者 (假裝是資料庫)
private List<User> users = new ArrayList<>();
// 註冊 API
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody User user) {
// 檢查帳號是否已存在
for (int i = 0; i < users.size(); i++) {
if (users.get(i).getUsername().equals(user.getUsername())) {
return ResponseEntity.badRequest().body("帳號已存在");
}
}
// 加入使用者
users.add(user);
return ResponseEntity.ok("註冊成功");
}
// 登入 API
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody User user) {
for (int i = 0; i < users.size(); i++) {
User userInDb = users.get(i);
if (userInDb.getUsername().equals(user.getUsername()) &&
userInDb.getPassword().equals(user.getPassword())) {
return ResponseEntity.ok("登入成功,歡迎 " + userInDb.getUsername());
}
}
return ResponseEntity.status(401).body("帳號或密碼錯誤");
}
}
```
---
### `DemoApplication` (啟動程式)
```java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
---
## 測試方式
啟動專案後,可以用 **Postman** 或其他 **http client** 測試:
1. 在API URL旁邊有一個地球,點下去後再選擇**Generate request in HTTP Client**

2. 接著會跑出一個視窗,會有一個下拉可以選擇你的端口,選擇第一個就好直接按Enter

3. 綠色箭頭是發送請求的按鈕,用`{}`包起來的是`請求體`,就是你要傳入的帳號密碼,將值輸入在`""`裡面就可以了

4. 發送成功的結果

5. 接著再試試看登入吧!

***
使用已經寫好的前端測試
在resources/static下新增`index.html`,並複製以下程式碼
透過下圖方式開啟網頁

<details>
<summary>前端程式碼</summary>
```html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Instagram</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #fafafa;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.main-container {
display: flex;
max-width: 935px;
width: 100%;
align-items: center;
gap: 32px;
}
.phone-mockup {
flex: 1;
max-width: 380px;
position: relative;
display: none;
}
.phone-image {
width: 100%;
height: 580px;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 580"><rect width="380" height="580" fill="%23000" rx="40"/><rect x="20" y="40" width="340" height="500" fill="%23fff" rx="15"/><rect x="40" y="60" width="300" height="40" fill="%23f7f7f7" rx="20"/><rect x="40" y="120" width="300" height="300" fill="%23e1306c" rx="10"/><circle cx="320" cy="80" r="15" fill="%23e1306c"/><rect x="40" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/><rect x="120" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/><rect x="200" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/><rect x="280" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/></svg>') no-repeat center;
background-size: contain;
}
.form-container {
flex: 1;
max-width: 350px;
}
.form-box {
background: white;
border: 1px solid #dbdbdb;
border-radius: 1px;
padding: 40px 40px 20px;
margin-bottom: 10px;
}
.instagram-logo {
text-align: center;
margin-bottom: 32px;
}
.instagram-logo h1 {
font-family: 'Billabong', cursive;
font-size: 51px;
font-weight: normal;
color: #262626;
margin: 0;
text-decoration: none;
background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.form-group {
margin-bottom: 6px;
}
.form-group input {
width: 100%;
padding: 9px 0 7px 8px;
border: 1px solid #dbdbdb;
border-radius: 3px;
background: #fafafa;
font-size: 12px;
color: #262626;
outline: none;
transition: border-color 0.2s ease;
}
.form-group input:focus {
border-color: #a8a8a8;
background: white;
}
.form-group input::placeholder {
color: #8e8e8e;
}
.submit-btn {
width: 100%;
padding: 5px 9px;
background: #0095f6;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
margin: 8px 0;
height: 32px;
transition: background-color 0.2s ease;
position: relative;
overflow: hidden;
}
.submit-btn:hover {
background: #1877f2;
}
.submit-btn:disabled {
background: #b2dffc;
cursor: not-allowed;
}
.submit-btn.loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.submit-btn.loading .btn-text {
opacity: 0;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
.divider {
display: flex;
align-items: center;
margin: 20px 0;
color: #8e8e8e;
font-size: 13px;
font-weight: 600;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: #dbdbdb;
}
.divider span {
padding: 0 18px;
}
.facebook-login {
display: flex;
align-items: center;
justify-content: center;
color: #385185;
font-size: 14px;
font-weight: 600;
text-decoration: none;
margin-bottom: 20px;
}
.facebook-login::before {
content: "📘";
margin-right: 8px;
font-size: 16px;
}
.forgot-password {
text-align: center;
margin-top: 12px;
}
.forgot-password a {
color: #0095f6;
font-size: 12px;
text-decoration: none;
}
.signup-box {
background: white;
border: 1px solid #dbdbdb;
border-radius: 1px;
padding: 20px;
text-align: center;
font-size: 14px;
color: #262626;
}
.signup-box a {
color: #0095f6;
font-weight: 600;
text-decoration: none;
}
.message {
padding: 10px;
margin-bottom: 15px;
border-radius: 3px;
font-size: 12px;
text-align: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.message.show {
opacity: 1;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.app-download {
text-align: center;
margin-top: 20px;
}
.app-download p {
font-size: 14px;
color: #262626;
margin-bottom: 20px;
}
.download-buttons {
display: flex;
justify-content: center;
gap: 8px;
}
.download-btn {
height: 40px;
border-radius: 3px;
overflow: hidden;
}
.download-btn img {
height: 100%;
width: auto;
}
.api-config {
position: fixed;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.95);
padding: 8px 12px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-size: 11px;
color: #666;
border: 1px solid #dbdbdb;
}
.api-config input {
border: 1px solid #dbdbdb;
padding: 4px 6px;
border-radius: 3px;
width: 180px;
font-size: 11px;
margin-left: 5px;
}
.hidden {
display: none;
}
@media (min-width: 768px) {
.phone-mockup {
display: block;
}
}
@media (max-width: 767px) {
.main-container {
max-width: 350px;
}
.form-box {
border: none;
background: transparent;
padding: 20px 0;
}
.signup-box {
border: none;
background: transparent;
}
.api-config {
position: relative;
top: auto;
right: auto;
margin-bottom: 20px;
text-align: center;
}
}
/* 載入Billabong字體 */
@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
.instagram-logo h1 {
font-family: 'Lobster', cursive;
}
</style>
</head>
<body>
<!-- API 設定 -->
<div class="api-config">
API: <input type="text" id="apiUrl" value="http://localhost:8080" placeholder="http://localhost:8080">
</div>
<div class="main-container">
<!-- 手機模擬圖 -->
<div class="phone-mockup">
<div class="phone-image"></div>
</div>
<!-- 表單容器 -->
<div class="form-container">
<!-- 登入表單 -->
<div id="loginForm">
<div class="form-box">
<div class="instagram-logo">
<h1>Instagram</h1>
</div>
<!-- 訊息顯示 -->
<div id="message" class="message"></div>
<div class="form-group">
<input type="text" id="loginUsername" placeholder="電話號碼、使用者名稱或電子郵件" required>
</div>
<div class="form-group">
<input type="password" id="loginPassword" placeholder="密碼" required>
</div>
<button class="submit-btn" id="loginBtn" onclick="handleLogin()">
<span class="btn-text">登入</span>
</button>
<div class="divider">
<span>或</span>
</div>
<a href="#" class="facebook-login">使用 Facebook 登入</a>
<div class="forgot-password">
<a href="#">忘記密碼?</a>
</div>
</div>
<div class="signup-box">
沒有帳號嗎?<a href="#" onclick="toggleForm()">註冊</a>
</div>
</div>
<!-- 註冊表單 -->
<div id="registerForm" class="hidden">
<div class="form-box">
<div class="instagram-logo">
<h1>Instagram</h1>
</div>
<p style="text-align: center; color: #8e8e8e; font-size: 17px; font-weight: 600; margin-bottom: 20px;">
註冊即可查看朋友的相片和影片。
</p>
<!-- 訊息顯示 -->
<div id="registerMessage" class="message"></div>
<a href="#" class="facebook-login" style="background: #0095f6; color: white; padding: 8px; border-radius: 4px; margin-bottom: 20px; text-decoration: none;">
使用 Facebook 登入
</a>
<div class="divider">
<span>或</span>
</div>
<div class="form-group">
<input type="text" id="registerUsername" placeholder="使用者名稱" required>
</div>
<div class="form-group">
<input type="password" id="registerPassword" placeholder="密碼" required>
</div>
<p style="font-size: 12px; color: #8e8e8e; text-align: center; margin: 15px 0;">
註冊即表示你同意我們的<a href="#" style="color: #0095f6;">服務條款</a>、<a href="#" style="color: #0095f6;">隱私權政策</a>和<a href="#" style="color: #0095f6;">Cookie 政策</a>。
</p>
<button class="submit-btn" id="registerBtn" onclick="handleRegister()">
<span class="btn-text">註冊</span>
</button>
</div>
<div class="signup-box">
有帳號了嗎?<a href="#" onclick="toggleForm()">登入</a>
</div>
</div>
<!-- 下載App -->
<div class="app-download">
<p>取得應用程式。</p>
<div class="download-buttons">
<div class="download-btn">
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 135 40'><rect width='135' height='40' fill='%23000' rx='5'/><text x='67.5' y='25' fill='white' text-anchor='middle' font-size='12'>App Store</text></svg>" alt="App Store">
</div>
<div class="download-btn">
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 135 40'><rect width='135' height='40' fill='%23000' rx='5'/><text x='67.5' y='25' fill='white' text-anchor='middle' font-size='12'>Google Play</text></svg>" alt="Google Play">
</div>
</div>
</div>
</div>
</div>
<script>
let isLoginForm = true;
// 獲取API基礎URL
function getApiUrl() {
return document.getElementById('apiUrl').value.trim() || 'http://localhost:8080';
}
// 切換表單
function toggleForm() {
const loginForm = document.getElementById('loginForm');
const registerForm = document.getElementById('registerForm');
if (isLoginForm) {
loginForm.classList.add('hidden');
registerForm.classList.remove('hidden');
} else {
loginForm.classList.remove('hidden');
registerForm.classList.add('hidden');
}
isLoginForm = !isLoginForm;
clearMessages();
clearInputs();
}
// 設定按鈕載入狀態
function setButtonLoading(buttonId, isLoading) {
const button = document.getElementById(buttonId);
if (isLoading) {
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
}
}
// 處理登入
async function handleLogin() {
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value.trim();
if (!username || !password) {
showMessage('message', '請填寫所有欄位', 'error');
return;
}
setButtonLoading('loginBtn', true);
try {
const response = await fetch(`${getApiUrl()}/user/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
password: password
})
});
const message = await response.text();
if (response.ok) {
showMessage('message', message, 'success');
clearInputs();
} else {
showMessage('message', message, 'error');
}
} catch (error) {
console.error('登入錯誤:', error);
showMessage('message', '連線失敗,請稍後再試', 'error');
} finally {
setButtonLoading('loginBtn', false);
}
}
// 處理註冊
async function handleRegister() {
const username = document.getElementById('registerUsername').value.trim();
const password = document.getElementById('registerPassword').value.trim();
if (!username || !password) {
showMessage('registerMessage', '請填寫所有欄位', 'error');
return;
}
if (password.length < 6) {
showMessage('registerMessage', '密碼至少需要 6 個字元', 'error');
return;
}
setButtonLoading('registerBtn', true);
try {
const response = await fetch(`${getApiUrl()}/user/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
password: password
})
});
const message = await response.text();
if (response.ok) {
showMessage('registerMessage', message, 'success');
clearInputs();
// 註冊成功後自動切換到登入頁面
setTimeout(() => {
if (!isLoginForm) {
toggleForm();
}
}, 2000);
} else {
showMessage('registerMessage', message, 'error');
}
} catch (error) {
console.error('註冊錯誤:', error);
showMessage('registerMessage', '連線失敗,請稍後再試', 'error');
} finally {
setButtonLoading('registerBtn', false);
}
}
// 顯示訊息
function showMessage(elementId, text, type) {
const message = document.getElementById(elementId);
message.textContent = text;
message.className = `message ${type} show`;
setTimeout(() => {
message.classList.remove('show');
}, 5000);
}
// 清除訊息
function clearMessages() {
document.getElementById('message').classList.remove('show');
document.getElementById('registerMessage').classList.remove('show');
}
// 清空輸入框
function clearInputs() {
document.getElementById('loginUsername').value = '';
document.getElementById('loginPassword').value = '';
document.getElementById('registerUsername').value = '';
document.getElementById('registerPassword').value = '';
}
// 按Enter鍵提交表單
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const activeBtn = isLoginForm ?
document.getElementById('loginBtn') :
document.getElementById('registerBtn');
if (!activeBtn.disabled) {
if (isLoginForm) {
handleLogin();
} else {
handleRegister();
}
}
}
});
// 防止Facebook登入連結跳轉
document.addEventListener('click', function(e) {
if (e.target.classList.contains('facebook-login') ||
e.target.closest('.facebook-login')) {
e.preventDefault();
}
});
// 頁面載入完成後檢查API連線
window.addEventListener('load', async function() {
try {
const response = await fetch(`${getApiUrl()}/user/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: '', password: '' })
});
console.log('API 伺服器連線正常');
} catch (error) {
console.warn('無法連接到API伺服器:', error);
}
});
</script>
</body>
</html>
```
</details>