Modern web development demands robust, secure, and accessible applications. This post demonstrates how to integrate SolidJS for a reactive frontend, AWS serverless for a scalable backend, and Firebase for streamlined authentication, all while prioritizing accessibility and security with TypeScript.
Frontend: SolidJS, A11y & Firebase Authentication
SolidJS offers high-performance reactivity with a small bundle size, making it ideal for dynamic user interfaces. We'll use it to create an accessible form that integrates with Firebase Authentication.
// src/components/ItemForm.tsx
import { createSignal, onMount } from 'solid-js';
import { auth } from '../firebaseConfig'; // Firebase Auth instance
import { signInWithPopup, GoogleAuthProvider, signOut, User } from 'firebase/auth';
interface ItemFormProps {
onAddItem: (item: { name: string }) => Promise<void>;
}
export default function ItemForm(props: ItemFormProps) {
const [itemName, setItemName] = createSignal('');
const [user, setUser] = createSignal<User | null>(null);
const [error, setError] = createSignal('');
onMount(() => {
auth.onAuthStateChanged(currentUser => {
setUser(currentUser);
});
});
const handleLogin = async () => {
try {
await signInWithPopup(auth, new GoogleAuthProvider());
setError('');
} catch (e: any) {
setError(e.message);
console.error("Login error:", e);
}
};
const handleLogout = async () => {
try {
await signOut(auth);
setError('');
} catch (e: any) {
setError(e.message);
console.error("Logout error:", e);
}
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
if (!user()) {
setError('Please log in to add items.');
return;
}
if (itemName().trim() === '') {
setError('Item name cannot be empty.');
return;
}
try {
await props.onAddItem({ name: itemName() });
setItemName('');
setError('');
} catch (e: any) {
setError('Failed to add item: ' + e.message);
console.error("Add item error:", e);
}
};
return (
<form onSubmit={handleSubmit} aria-labelledby="item-form-title">
<h2 id="item-form-title">Manage Your Items</h2>
{user() ? (
<p>Welcome, {user()!.displayName} (<button type="button" onClick={handleLogout} aria-label="Log out">Log Out</button>)</p>
) : (
<p><button type="button" onClick={handleLogin} aria-label="Log in with Google">Log In with Google</button></p>
)}
{error() && <p role="alert" style="color: red;">{error()}</p>}
<label for="item-name">Item Name:</label>
<input
id="item-name"
type="text"
value={itemName()}
onInput={(e) => setItemName(e.currentTarget.value)}
aria-required="true"
placeholder="e.g., Buy groceries"
disabled={!user()}
/>
<button type="submit" disabled={!user() || itemName().trim() === ''}>
Add Item
</button>
</form>
);
}
// src/firebaseConfig.ts (example)
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
Accessibility (A11y): Notice the use of aria-labelledby, aria-required, placeholder, and semantic <button> elements with aria-label. These enhance usability for screen readers and keyboard navigation.
Backend: AWS Serverless API with TypeScript
AWS Lambda functions provide a scalable, cost-effective backend without managing servers. We'll use API Gateway as the HTTP endpoint and DynamoDB as our NoSQL database.
// src/lambda/createItem.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { v4 as uuidv4 } from 'uuid';
const client = new DynamoDBClient({});
const ddbDocClient = DynamoDBDocumentClient.from(client);
export const handler = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> => {
if (!event.body) {
return { statusCode: 400, body: JSON.stringify({ message: "Missing request body" }) };
}
let parsedBody: { name: string; userId?: string };
try {
parsedBody = JSON.parse(event.body);
} catch (error) {
return { statusCode: 400, body: JSON.stringify({ message: "Invalid JSON body" }) };
}
// Extract userId from authenticated context (set by Lambda Authorizer)
const userId = event.requestContext.authorizer?.lambda?.userId || 'anonymous'; // Fallback for testing
if (userId === 'anonymous') {
return { statusCode: 401, body: JSON.stringify({ message: "Unauthorized" }) };
}
const { name } = parsedBody;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return { statusCode: 400, body: JSON.stringify({ message: "Item name is required and must be a non-empty string" }) };
}
const itemId = uuidv4();
const item = { itemId, userId, name, createdAt: new Date().toISOString() };
try {
await ddbDocClient.send(new PutCommand({
TableName: process.env.TABLE_NAME,
Item: item,
}));
return { statusCode: 201, body: JSON.stringify(item) };
} catch (error) {
console.error("DynamoDB put error:", error);
return { statusCode: 500, body: JSON.stringify({ message: "Failed to create item" }) };
}
};
This TypeScript Lambda function handles requests to create an item, validates input, and stores it in DynamoDB. The userId is extracted from the requestContext after a successful authorization.
Web Security & Authorization with Firebase
Securing our API involves validating the user's identity. Firebase Authentication provides ID tokens that can be verified on the backend.
- Frontend: After successful Firebase login, the SolidJS app obtains the Firebase ID token (
await user.getIdToken()). This token is then sent in theAuthorization: Bearer <token>header with every API request to AWS. - AWS API Gateway Lambda Authorizer: This dedicated Lambda function intercepts incoming requests, verifies the Firebase ID token's signature, expiry, and issuer. If valid, it returns an IAM policy allowing access to the target Lambda function and injects decoded user information (like
userId) into theevent.requestContext.authorizerobject for the main Lambda.
Security Best Practices: Implement CORS on API Gateway, perform rigorous input validation (as shown in the Lambda), and adhere to the principle of least privilege for AWS IAM roles. Always use environment variables for sensitive configurations, never hardcode them.
Integration and Deployment
The SolidJS frontend is typically deployed to AWS S3 and served via CloudFront for global low-latency access. The frontend fetches the Firebase ID token and includes it in fetch requests to the API Gateway endpoint. The API Gateway then routes and authorizes these requests before they reach the Lambda function.
Conclusion
By carefully integrating SolidJS, AWS serverless, and Firebase, we've built a powerful, scalable, and maintainable application. Prioritizing accessibility and implementing robust security measures from the start ensures a high-quality user experience and protects your application from common vulnerabilities. This TypeScript-first approach delivers clarity and confidence across the full stack.